Files
redash/client/app/visualizations/chart/plotly/prepareLayout.js
Levko Kravets 2c98f0425d Allow the user to decide how to handle null values in charts (#4071)
* 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
2019-09-09 13:00:26 +03:00

129 lines
3.5 KiB
JavaScript

import { filter, has, isNumber, isObject, isUndefined, map, max, min } from 'lodash';
import { getPieDimensions } from './preparePieData';
function getAxisTitle(axis) {
return isObject(axis.title) ? axis.title.text : null;
}
function getAxisScaleType(axis) {
switch (axis.type) {
case 'datetime': return 'date';
case 'logarithmic': return 'log';
default: return axis.type;
}
}
function calculateAxisRange(seriesList, minValue, maxValue) {
if (!isNumber(minValue)) {
minValue = Math.min(0, min(map(seriesList, series => min(series.y))));
}
if (!isNumber(maxValue)) {
maxValue = max(map(seriesList, series => max(series.y)));
}
return [minValue, maxValue];
}
function prepareXAxis(axisOptions, additionalOptions) {
const axis = {
title: getAxisTitle(axisOptions),
type: getAxisScaleType(axisOptions),
automargin: true,
};
if (additionalOptions.sortX && axis.type === 'category') {
if (additionalOptions.reverseX) {
axis.categoryorder = 'category descending';
} else {
axis.categoryorder = 'category ascending';
}
}
if (!isUndefined(axisOptions.labels)) {
axis.showticklabels = axisOptions.labels.enabled;
}
return axis;
}
function prepareYAxis(axisOptions, additionalOptions, data) {
const axis = {
title: getAxisTitle(axisOptions),
type: getAxisScaleType(axisOptions),
automargin: true,
};
if (isNumber(axisOptions.rangeMin) || isNumber(axisOptions.rangeMax)) {
axis.range = calculateAxisRange(data, axisOptions.rangeMin, axisOptions.rangeMax);
}
return axis;
}
function preparePieLayout(layout, options, data) {
const hasName = /{{\s*@@name\s*}}/.test(options.textFormat);
const { cellsInRow, cellWidth, cellHeight, xPadding } = getPieDimensions(data);
if (hasName) {
layout.annotations = [];
} else {
layout.annotations = filter(map(data, (series, index) => {
const xPosition = (index % cellsInRow) * cellWidth;
const yPosition = Math.floor(index / cellsInRow) * cellHeight;
return {
x: xPosition + ((cellWidth - xPadding) / 2),
y: yPosition + cellHeight - 0.015,
xanchor: 'center',
yanchor: 'top',
text: series.name,
showarrow: false,
};
}));
}
return layout;
}
function prepareDefaultLayout(layout, options, data) {
const ySeries = data.filter(s => s.yaxis !== 'y2');
const y2Series = data.filter(s => s.yaxis === 'y2');
layout.xaxis = prepareXAxis(options.xAxis, options);
layout.yaxis = prepareYAxis(options.yAxis[0], options, ySeries);
if (y2Series.length > 0) {
layout.yaxis2 = prepareYAxis(options.yAxis[1], options, y2Series);
layout.yaxis2.overlaying = 'y';
layout.yaxis2.side = 'right';
}
if (options.series.stacking) {
layout.barmode = 'relative';
}
return layout;
}
function prepareBoxLayout(layout, options, data) {
layout = prepareDefaultLayout(layout, options, data);
layout.boxmode = 'group';
layout.boxgroupgap = 0.50;
return layout;
}
export default function prepareLayout(element, options, data) {
const layout = {
margin: { l: 10, r: 10, b: 10, t: 25, pad: 4 },
width: Math.floor(element.offsetWidth),
height: Math.floor(element.offsetHeight),
autosize: true,
showlegend: has(options, 'legend') ? options.legend.enabled : true,
};
switch (options.globalSeriesType) {
case 'pie': return preparePieLayout(layout, options, data);
case 'box': return prepareBoxLayout(layout, options, data);
default: return prepareDefaultLayout(layout, options, data);
}
}