mirror of
https://github.com/getredash/redash.git
synced 2026-03-22 19:00:09 -04:00
* getredash/redash#2629 Refactor Chart visualization, add option for handling NULL values (keep/convert to 0.0) * Handle null values in line/area stacking code; some cleanup * Handle edge case: line/area stacking when last value of one of series is missing * Mjnor update to line/area stacking code * Fix line/area normalize to percents feature * Unit tests * Refine tests; add tests for prepareLayout function * Tests for prepareData (heatmap) function * Tests for prepareData (pie) function * Tests for prepareData (bar, line, area) function * Tests for prepareData (scatter, bubble) function * Tests for prepareData (box) function * Remove unused file
139 lines
4.1 KiB
JavaScript
139 lines
4.1 KiB
JavaScript
import { each, debounce, isArray, isObject } from 'lodash';
|
|
|
|
import Plotly from 'plotly.js/lib/core';
|
|
import bar from 'plotly.js/lib/bar';
|
|
import pie from 'plotly.js/lib/pie';
|
|
import histogram from 'plotly.js/lib/histogram';
|
|
import box from 'plotly.js/lib/box';
|
|
import heatmap from 'plotly.js/lib/heatmap';
|
|
|
|
import { normalizeValue } from './utils';
|
|
|
|
import prepareData from './prepareData';
|
|
import prepareLayout from './prepareLayout';
|
|
import updateData from './updateData';
|
|
import applyLayoutFixes from './applyLayoutFixes';
|
|
|
|
Plotly.register([bar, pie, histogram, box, heatmap]);
|
|
Plotly.setPlotConfig({
|
|
modeBarButtonsToRemove: ['sendDataToCloud'],
|
|
});
|
|
|
|
const PlotlyChart = () => ({
|
|
restrict: 'E',
|
|
template: '<div class="plotly-chart-container" resize-event="handleResize()"></div>',
|
|
scope: {
|
|
options: '=',
|
|
series: '=',
|
|
},
|
|
link(scope, element) {
|
|
const plotlyElement = element[0].querySelector('.plotly-chart-container');
|
|
const plotlyOptions = { showLink: false, displaylogo: false };
|
|
let layout = {};
|
|
let data = [];
|
|
|
|
function update() {
|
|
if (['normal', 'percent'].indexOf(scope.options.series.stacking) >= 0) {
|
|
// Backward compatibility
|
|
scope.options.series.percentValues = scope.options.series.stacking === 'percent';
|
|
scope.options.series.stacking = 'stack';
|
|
}
|
|
|
|
data = prepareData(scope.series, scope.options);
|
|
layout = prepareLayout(plotlyElement, scope.options, data);
|
|
|
|
// It will auto-purge previous graph
|
|
Plotly.newPlot(plotlyElement, data, layout, plotlyOptions).then(() => {
|
|
applyLayoutFixes(plotlyElement, layout, (e, u) => Plotly.relayout(e, u));
|
|
});
|
|
|
|
plotlyElement.on('plotly_restyle', (updates) => {
|
|
// This event is triggered if some plotly data/layout has changed.
|
|
// We need to catch only changes of traces visibility to update stacking
|
|
if (isArray(updates) && isObject(updates[0]) && updates[0].visible) {
|
|
updateData(data, scope.options);
|
|
Plotly.relayout(plotlyElement, layout);
|
|
}
|
|
});
|
|
}
|
|
update();
|
|
|
|
scope.$watch('series', (oldValue, newValue) => {
|
|
if (oldValue !== newValue) {
|
|
update();
|
|
}
|
|
});
|
|
scope.$watch('options', (oldValue, newValue) => {
|
|
if (oldValue !== newValue) {
|
|
update();
|
|
}
|
|
}, true);
|
|
|
|
scope.handleResize = debounce(() => {
|
|
applyLayoutFixes(plotlyElement, layout, (e, u) => Plotly.relayout(e, u));
|
|
}, 50);
|
|
},
|
|
});
|
|
|
|
const CustomPlotlyChart = clientConfig => ({
|
|
restrict: 'E',
|
|
template: '<div class="plotly-chart-container" resize-event="handleResize()"></div>',
|
|
scope: {
|
|
series: '=',
|
|
options: '=',
|
|
},
|
|
link(scope, element) {
|
|
if (!clientConfig.allowCustomJSVisualizations) {
|
|
return;
|
|
}
|
|
|
|
const refresh = () => {
|
|
// Clear existing data with blank data for succeeding codeCall adds data to existing plot.
|
|
Plotly.newPlot(element[0].firstChild);
|
|
|
|
try {
|
|
// eslint-disable-next-line no-new-func
|
|
const codeCall = new Function('x, ys, element, Plotly', scope.options.customCode);
|
|
codeCall(scope.x, scope.ys, element[0].children[0], Plotly);
|
|
} catch (err) {
|
|
if (scope.options.enableConsoleLogs) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Error while executing custom graph: ${err}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const timeSeriesToPlotlySeries = () => {
|
|
scope.x = [];
|
|
scope.ys = {};
|
|
each(scope.series, (series) => {
|
|
scope.ys[series.name] = [];
|
|
each(series.data, (point) => {
|
|
scope.x.push(normalizeValue(point.x));
|
|
scope.ys[series.name].push(normalizeValue(point.y));
|
|
});
|
|
});
|
|
};
|
|
|
|
scope.handleResize = () => {
|
|
refresh();
|
|
};
|
|
|
|
scope.$watch('[options.customCode, options.autoRedraw]', () => {
|
|
refresh();
|
|
}, true);
|
|
|
|
scope.$watch('series', () => {
|
|
timeSeriesToPlotlySeries();
|
|
refresh();
|
|
}, true);
|
|
},
|
|
});
|
|
|
|
export default function init(ngModule) {
|
|
ngModule.directive('plotlyChart', PlotlyChart);
|
|
ngModule.directive('customPlotlyChart', CustomPlotlyChart);
|
|
}
|
|
|
|
init.init = true;
|