mirror of
https://github.com/getredash/redash.git
synced 2026-03-22 10:00:17 -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
129 lines
3.5 KiB
JavaScript
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);
|
|
}
|
|
}
|