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
224 lines
6.9 KiB
JavaScript
224 lines
6.9 KiB
JavaScript
import { isNil, each, extend, filter, identity, includes, map, sortBy } from 'lodash';
|
|
import { createNumberFormatter, formatSimpleTemplate } from '@/lib/value-format';
|
|
import { normalizeValue } from './utils';
|
|
|
|
function shouldUseUnifiedXAxis(options) {
|
|
return options.sortX && (options.xAxis.type === 'category') && (options.globalSeriesType !== 'box');
|
|
}
|
|
|
|
function defaultFormatSeriesText(item) {
|
|
let result = item['@@y'];
|
|
if (item['@@yError'] !== undefined) {
|
|
result = `${result} \u00B1 ${item['@@yError']}`;
|
|
}
|
|
if (item['@@yPercent'] !== undefined) {
|
|
result = `${item['@@yPercent']} (${result})`;
|
|
}
|
|
if (item['@@size'] !== undefined) {
|
|
result = `${result}: ${item['@@size']}`;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function defaultFormatSeriesTextForPie(item) {
|
|
return item['@@yPercent'] + ' (' + item['@@y'] + ')';
|
|
}
|
|
|
|
function createTextFormatter(options) {
|
|
if (options.textFormat === '') {
|
|
return options.globalSeriesType === 'pie' ? defaultFormatSeriesTextForPie : defaultFormatSeriesText;
|
|
}
|
|
return item => formatSimpleTemplate(options.textFormat, item);
|
|
}
|
|
|
|
function formatValue(value, axis, options) {
|
|
let axisType = null;
|
|
switch (axis) {
|
|
case 'x': axisType = options.xAxis.type; break;
|
|
case 'y': axisType = options.yAxis[0].type; break;
|
|
case 'y2': axisType = options.yAxis[1].type; break;
|
|
// no default
|
|
}
|
|
return normalizeValue(value, axisType, options.dateTimeFormat);
|
|
}
|
|
|
|
function updateSeriesText(seriesList, options) {
|
|
const formatNumber = createNumberFormatter(options.numberFormat);
|
|
const formatPercent = createNumberFormatter(options.percentFormat);
|
|
const formatText = createTextFormatter(options);
|
|
|
|
const defaultY = options.missingValuesAsZero ? 0.0 : null;
|
|
|
|
each(seriesList, (series) => {
|
|
const seriesOptions = options.seriesOptions[series.name] || { type: options.globalSeriesType };
|
|
|
|
series.text = [];
|
|
series.hover = [];
|
|
const xValues = (options.globalSeriesType === 'pie') ? series.labels : series.x;
|
|
xValues.forEach((x) => {
|
|
const text = {
|
|
'@@name': series.name,
|
|
};
|
|
const item = series.sourceData.get(x) || { x, y: defaultY, row: { x, y: defaultY } };
|
|
|
|
const yValueIsAny = includes(['bubble', 'scatter'], seriesOptions.type);
|
|
|
|
// for `formatValue` we have to use original value of `x` and `y`: `item.x`/`item.y` contains value
|
|
// already processed with `normalizeValue`, and if they were `moment` instances - they are formatted
|
|
// using default (ISO) date/time format. Here we need to use custom date/time format, so we pass original value
|
|
// to `formatValue` which will call `normalizeValue` again, but this time with different date/time format
|
|
// (if needed)
|
|
text['@@x'] = formatValue(item.row.x, 'x', options);
|
|
text['@@y'] = yValueIsAny ? formatValue(item.row.y, series.yaxis, options) : formatNumber(item.y);
|
|
if (item.yError !== undefined) {
|
|
text['@@yError'] = formatNumber(item.yError);
|
|
}
|
|
if (item.size !== undefined) {
|
|
text['@@size'] = formatNumber(item.size);
|
|
}
|
|
|
|
if (options.series.percentValues || (options.globalSeriesType === 'pie')) {
|
|
text['@@yPercent'] = formatPercent(Math.abs(item.yPercent));
|
|
}
|
|
|
|
extend(text, item.row.$raw);
|
|
|
|
series.text.push(formatText(text));
|
|
});
|
|
});
|
|
}
|
|
|
|
function updatePercentValues(seriesList, options) {
|
|
if (options.series.percentValues) {
|
|
// Some series may not have corresponding x-values;
|
|
// do calculations for each x only for series that do have that x
|
|
const sumOfCorrespondingPoints = new Map();
|
|
each(seriesList, (series) => {
|
|
series.sourceData.forEach((item) => {
|
|
const sum = sumOfCorrespondingPoints.get(item.x) || 0;
|
|
sumOfCorrespondingPoints.set(item.x, sum + Math.abs(item.y || 0.0));
|
|
});
|
|
});
|
|
|
|
each(seriesList, (series) => {
|
|
const yValues = [];
|
|
|
|
series.sourceData.forEach((item) => {
|
|
if (isNil(item.y) && !options.missingValuesAsZero) {
|
|
item.yPercent = null;
|
|
} else {
|
|
const sum = sumOfCorrespondingPoints.get(item.x);
|
|
item.yPercent = item.y / sum * 100;
|
|
}
|
|
yValues.push(item.yPercent);
|
|
});
|
|
|
|
series.y = yValues;
|
|
});
|
|
}
|
|
}
|
|
|
|
function getUnifiedXAxisValues(seriesList, sorted) {
|
|
const set = new Set();
|
|
each(seriesList, (series) => {
|
|
// `Map.forEach` will walk items in insertion order
|
|
series.sourceData.forEach((item) => {
|
|
set.add(item.x);
|
|
});
|
|
});
|
|
|
|
const result = [...set];
|
|
return sorted ? sortBy(result, identity) : result;
|
|
}
|
|
|
|
function updateUnifiedXAxisValues(seriesList, options) {
|
|
const unifiedX = getUnifiedXAxisValues(seriesList, options.sortX);
|
|
const defaultY = options.missingValuesAsZero ? 0.0 : null;
|
|
each(seriesList, (series) => {
|
|
series.x = [];
|
|
series.y = [];
|
|
series.error_y.array = [];
|
|
each(unifiedX, (x) => {
|
|
series.x.push(x);
|
|
const item = series.sourceData.get(x);
|
|
if (item) {
|
|
series.y.push(options.series.percentValues ? item.yPercent : item.y);
|
|
series.error_y.array.push(item.yError);
|
|
} else {
|
|
series.y.push(defaultY);
|
|
series.error_y.array.push(null);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function updatePieData(seriesList, options) {
|
|
updateSeriesText(seriesList, options);
|
|
}
|
|
|
|
function updateLineAreaData(seriesList, options) {
|
|
// Apply "percent values" modification
|
|
updatePercentValues(seriesList, options);
|
|
if (options.series.stacking) {
|
|
updateUnifiedXAxisValues(seriesList, options);
|
|
|
|
// Calculate cumulative value for each x tick
|
|
const cumulativeValues = {};
|
|
each(seriesList, (series) => {
|
|
series.y = map(series.y, (y, i) => {
|
|
if (isNil(y) && !options.missingValuesAsZero) {
|
|
return null;
|
|
}
|
|
const x = series.x[i];
|
|
const stackedY = y + (cumulativeValues[x] || 0.0);
|
|
cumulativeValues[x] = stackedY;
|
|
return stackedY;
|
|
});
|
|
});
|
|
} else {
|
|
if (shouldUseUnifiedXAxis(options)) {
|
|
updateUnifiedXAxisValues(seriesList, options);
|
|
}
|
|
}
|
|
|
|
// Finally - update text labels
|
|
updateSeriesText(seriesList, options);
|
|
}
|
|
|
|
function updateDefaultData(seriesList, options) {
|
|
// Apply "percent values" modification
|
|
updatePercentValues(seriesList, options);
|
|
|
|
if (!options.series.stacking) {
|
|
if (shouldUseUnifiedXAxis(options)) {
|
|
updateUnifiedXAxisValues(seriesList, options);
|
|
}
|
|
}
|
|
|
|
// Finally - update text labels
|
|
updateSeriesText(seriesList, options);
|
|
}
|
|
|
|
export default function updateData(seriesList, options) {
|
|
// Use only visible series
|
|
const visibleSeriesList = filter(seriesList, s => s.visible === true);
|
|
|
|
if (visibleSeriesList.length > 0) {
|
|
switch (options.globalSeriesType) {
|
|
case 'pie':
|
|
updatePieData(visibleSeriesList, options);
|
|
break;
|
|
case 'line':
|
|
case 'area':
|
|
updateLineAreaData(visibleSeriesList, options);
|
|
break;
|
|
case 'heatmap':
|
|
break;
|
|
default:
|
|
updateDefaultData(visibleSeriesList, options);
|
|
break;
|
|
}
|
|
}
|
|
return seriesList;
|
|
}
|