diff --git a/client/app/visualizations/chart/chart-editor.html b/client/app/visualizations/chart/chart-editor.html index ab472345..f5297717 100644 --- a/client/app/visualizations/chart/chart-editor.html +++ b/client/app/visualizations/chart/chart-editor.html @@ -32,6 +32,9 @@ +
+ +
diff --git a/client/app/visualizations/chart/plotly/index.js b/client/app/visualizations/chart/plotly/index.js index c05eeb15..fcd3bcb0 100644 --- a/client/app/visualizations/chart/plotly/index.js +++ b/client/app/visualizations/chart/plotly/index.js @@ -35,6 +35,12 @@ const PlotlyChart = () => ({ let layout = {}; let data = []; + const applyAutoMargins = debounce(() => { + if (applyMargins(layout.margin, calculateMargins(plotlyElement))) { + Plotly.relayout(plotlyElement, layout); + } + }, 100); + function update() { if (['normal', 'percent'].indexOf(scope.options.series.stacking) >= 0) { // Backward compatibility @@ -47,31 +53,26 @@ const PlotlyChart = () => ({ data = prepareData(scope.series, scope.options); updateStacking(data, scope.options); layout = prepareLayout(plotlyElement, scope.series, scope.options, data); + Plotly.purge(plotlyElement); Plotly.newPlot(plotlyElement, data, layout, plotlyOptions); - } - update(); - const applyAutoMargins = debounce(() => { - if (applyMargins(layout.margin, calculateMargins(plotlyElement))) { - Plotly.relayout(plotlyElement, layout); - } - }, 100); + plotlyElement.on('plotly_afterplot', () => { + applyAutoMargins(); - plotlyElement.on('plotly_afterplot', () => { - applyAutoMargins(); + plotlyElement.querySelectorAll('.legendtoggle').forEach((rectDiv, i) => { + d3.select(rectDiv).on('click', () => { + const maxIndex = scope.data.length - 1; + const itemClicked = scope.data[maxIndex - i]; - plotlyElement.querySelectorAll('.legendtoggle').forEach((rectDiv, i) => { - d3.select(rectDiv).on('click', () => { - const maxIndex = scope.data.length - 1; - const itemClicked = scope.data[maxIndex - i]; - - itemClicked.visible = (itemClicked.visible === true) ? 'legendonly' : true; - updateStacking(data, scope.options); - Plotly.redraw(plotlyElement); + itemClicked.visible = (itemClicked.visible === true) ? 'legendonly' : true; + updateStacking(data, scope.options); + Plotly.redraw(plotlyElement); + }); }); }); - }); + } + update(); scope.$watch('series', (oldValue, newValue) => { if (oldValue !== newValue) { diff --git a/client/app/visualizations/chart/plotly/utils.js b/client/app/visualizations/chart/plotly/utils.js index 1f3bf035..de3f49ef 100644 --- a/client/app/visualizations/chart/plotly/utils.js +++ b/client/app/visualizations/chart/plotly/utils.js @@ -1,5 +1,5 @@ import { - isArray, isNumber, isUndefined, contains, min, max, has, find, + isArray, isNumber, isUndefined, contains, min, max, has, find, first, last, each, values, sortBy, union, pluck, identity, filter, map, constant, } from 'underscore'; import moment from 'moment'; @@ -241,13 +241,80 @@ function prepareStackingData(seriesList) { }); }); + return seriesList; +} + +function enableAnnotations(layout, seriesList, options) { + if (options.annotations) { + if (options.globalSeriesType === 'column') { + seriesList.forEach((series) => { + series.textposition = 'auto'; + }); + } else if (['line', 'area'].indexOf(options.globalSeriesType) >= 0) { + layout.yaxis.showticklabels = false; + + delete layout.yaxis2; + layout.annotations = []; + each(seriesList, (series) => { + delete series.yaxis; + const leftAnnotation = { + xref: 'paper', + x: 0.05, + y: first(series.y), + xanchor: 'right', + yanchor: 'middle', + text: first(series.annotations), + showarrow: false, + }; + const rightAnnotation = { + xref: 'paper', + x: 0.95, + y: last(series.y), + xanchor: 'left', + yanchor: 'middle', + text: last(series.annotations), + showarrow: false, + }; + + layout.annotations.push(leftAnnotation, rightAnnotation); + }); + } + } +} + +function prepareDataLabels(seriesList, options) { seriesList.forEach((series) => { series.visible = true; series.savedY = series.y; series.hoverinfo = 'x+text+name'; series.text = map(series.y, v => `Value: ${formatNumber(v)}`); + series.annotations = map([first(series.y), last(series.y)], v => `${formatNumber(v)}`); }); + if (options.series.percentValues && (seriesList.length > 0)) { + const sumOfCorrespondingPoints = map(seriesList[0].savedY, constant(0)); + each(seriesList, (series) => { + each(series.savedY, (v, i) => { + sumOfCorrespondingPoints[i] += Math.abs(v); + }); + }); + + each(seriesList, (series) => { + series.y = map(series.savedY, (v, i) => Math.sign(v) * Math.abs(v) / sumOfCorrespondingPoints[i] * 100); + series.text = map(series.y, (v, i) => [ + `Value: ${formatNumber(series.savedY[i])}`, + '
', + `Relative: ${formatPercent(v)}%`, + ].join('')); + series.annotations = map([first(series.y), last(series.y)], (v, i) => [ + `${formatNumber(series.savedY[i])}`, + `(${formatPercent(v)}%)`, + ].join(' ')); + series.savedY = series.y; // Now we don't need absolute values, only percent values will be used + }); + } + + return seriesList; } @@ -257,25 +324,7 @@ export function prepareData(seriesList, options) { } const result = prepareStackingData(prepareChartData(seriesList, options)); - if (options.series.percentValues && (result.length > 0)) { - const sumOfCorrespondingPoints = map(result[0].savedY, constant(0)); - each(result, (series) => { - each(series.savedY, (v, i) => { - sumOfCorrespondingPoints[i] += Math.abs(v); - }); - }); - - each(result, (series) => { - series.y = map(series.savedY, (v, i) => Math.sign(v) * Math.abs(v) / sumOfCorrespondingPoints[i] * 100); - series.text = map(series.y, (v, i) => [ - `Value: ${formatNumber(series.savedY[i])}`, - '
', - `Relative: ${formatPercent(v)}%`, - ].join('')); - series.savedY = series.y; // Now we don't need absolute values, only percent values will be used - }); - } - return result; + return prepareDataLabels(result, options); } export function prepareLayout(element, seriesList, options, data) { @@ -362,6 +411,8 @@ export function prepareLayout(element, seriesList, options, data) { } } + enableAnnotations(result, data, options); + return result; } @@ -405,6 +456,16 @@ export function calculateMargins(element) { } }); + const annotations = element.querySelectorAll('.annotation-text'); + if (annotations.length > 0) { + const annotationsSize = max(map(annotations, (ann) => { + const bounds = ann.getBoundingClientRect(); + return Math.ceil(bounds.width); + })); + result.l = Math.max(result.l || 0, annotationsSize); + result.r = Math.max(result.r || 0, annotationsSize); + } + return result; }