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;
}