You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2023/03/16 14:26:14 UTC
[superset] branch master updated: feat(plugin-chart-echarts): add series sorting (#23392)
This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 0c454c6442 feat(plugin-chart-echarts): add series sorting (#23392)
0c454c6442 is described below
commit 0c454c64426376d7fb209a8b16d15c580be811f4
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Thu Mar 16 16:26:01 2023 +0200
feat(plugin-chart-echarts): add series sorting (#23392)
---
.../src/Timeseries/Area/controlPanel.tsx | 2 +
.../src/Timeseries/Regular/Bar/controlPanel.tsx | 2 +
.../src/Timeseries/Regular/Line/controlPanel.tsx | 2 +
.../Timeseries/Regular/Scatter/controlPanel.tsx | 2 +
.../Timeseries/Regular/SmoothLine/controlPanel.tsx | 2 +
.../src/Timeseries/Step/controlPanel.tsx | 2 +
.../src/Timeseries/constants.ts | 4 +
.../src/Timeseries/transformProps.ts | 10 +-
.../plugins/plugin-chart-echarts/src/constants.ts | 11 +-
.../plugins/plugin-chart-echarts/src/controls.tsx | 44 +++++++-
.../plugins/plugin-chart-echarts/src/types.ts | 13 +++
.../plugin-chart-echarts/src/utils/series.ts | 112 +++++++++++++++------
.../plugin-chart-echarts/test/utils/series.test.ts | 52 +++++++++-
13 files changed, 221 insertions(+), 37 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
index 82ca0b585d..a1e877cf1c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
@@ -34,6 +34,7 @@ import {
onlyTotalControl,
showValueControl,
richTooltipSection,
+ seriesOrderSection,
} from '../../controls';
import { AreaChartExtraControlsOptions } from '../../constants';
@@ -62,6 +63,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
[
{
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
index 509dc6c815..f69099866c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
@@ -38,6 +38,7 @@ import {
import {
legendSection,
richTooltipSection,
+ seriesOrderSection,
showValueSection,
} from '../../../controls';
@@ -301,6 +302,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
...showValueSection,
[
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
index 0ceb518b88..a2c9648ea0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
@@ -35,6 +35,7 @@ import {
import {
legendSection,
richTooltipSection,
+ seriesOrderSection,
showValueSection,
} from '../../../controls';
@@ -64,6 +65,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
[
{
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
index 9e36db0d3b..d8ad857129 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
@@ -34,6 +34,7 @@ import {
import {
legendSection,
richTooltipSection,
+ seriesOrderSection,
showValueSection,
} from '../../../controls';
@@ -60,6 +61,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
...showValueSection,
[
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
index bfb7671ddb..a45a790883 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
@@ -34,6 +34,7 @@ import {
import {
legendSection,
richTooltipSection,
+ seriesOrderSection,
showValueSectionWithoutStack,
} from '../../../controls';
@@ -60,6 +61,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
...showValueSectionWithoutStack,
[
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
index 6a8e6eef17..2f65c4d151 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
@@ -32,6 +32,7 @@ import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants';
import {
legendSection,
richTooltipSection,
+ seriesOrderSection,
showValueSection,
} from '../../controls';
@@ -60,6 +61,7 @@ const config: ControlPanelConfig = {
label: t('Chart Options'),
expanded: true,
controlSetRows: [
+ ...seriesOrderSection,
['color_scheme'],
[
{
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
index 1d7b871944..e0b41f9f68 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
@@ -25,6 +25,7 @@ import {
} from './types';
import {
DEFAULT_LEGEND_FORM_DATA,
+ DEFAULT_SORT_SERIES_DATA,
DEFAULT_TITLE_FORM_DATA,
} from '../constants';
@@ -32,6 +33,7 @@ import {
export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_TITLE_FORM_DATA,
+ ...DEFAULT_SORT_SERIES_DATA,
annotationLayers: sections.annotationLayers,
area: false,
forecastEnabled: sections.FORECAST_DEFAULT_DATA.forecastEnabled,
@@ -63,6 +65,8 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
onlyTotal: false,
percentageThreshold: 0,
orientation: OrientationType.vertical,
+ sort_series_type: 'sum',
+ sort_series_ascending: false,
};
export const TIME_SERIES_DESCRIPTION_TEXT: string = t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index a853c4b869..1342e860ba 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -136,6 +136,8 @@ export default function transformProps(
showLegend,
showValue,
sliceId,
+ sortSeriesType,
+ sortSeriesAscending,
timeGrainSqla,
timeCompare,
stack,
@@ -197,6 +199,8 @@ export default function transformProps(
stack,
totalStackedValues,
isHorizontal,
+ sortSeriesType,
+ sortSeriesAscending,
});
const showValueIndexes = extractShowValueIndexes(rawSeries, {
stack,
@@ -418,7 +422,7 @@ export default function transformProps(
forecastValue.sort((a, b) => b.data[yIndex] - a.data[yIndex]);
}
- const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
+ const rows: string[] = [];
const forecastValues: Record<string, ForecastValue> =
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
@@ -435,6 +439,10 @@ export default function transformProps(
rows.push(`<span style="opacity: 0.7">${content}</span>`);
}
});
+ if (stack) {
+ rows.reverse();
+ }
+ rows.unshift(`${tooltipFormatter(xValue)}`);
return rows.join('<br />');
},
},
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
index 1c20128b67..3fc3fc999b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
@@ -20,11 +20,13 @@
import { JsonValue, t, TimeGranularity } from '@superset-ui/core';
import { ReactNode } from 'react';
import {
- LegendFormData,
- TitleFormData,
LabelPositionEnum,
+ LegendFormData,
LegendOrientation,
LegendType,
+ SortSeriesData,
+ SortSeriesType,
+ TitleFormData,
} from './types';
// eslint-disable-next-line import/prefer-default-export
@@ -114,3 +116,8 @@ export const TOOLTIP_POINTER_MARGIN = 10;
// If no satisfactory position can be found, how far away
// from the edge of the window should the tooltip be kept
export const TOOLTIP_OVERFLOW_MARGIN = 5;
+
+export const DEFAULT_SORT_SERIES_DATA: SortSeriesData = {
+ sort_series_type: SortSeriesType.Sum,
+ sort_series_ascending: false,
+};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
index ff74cd171d..0733721091 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
@@ -24,8 +24,12 @@ import {
ControlSetRow,
sharedControls,
} from '@superset-ui/chart-controls';
-import { DEFAULT_LEGEND_FORM_DATA } from './constants';
+import {
+ DEFAULT_LEGEND_FORM_DATA,
+ DEFAULT_SORT_SERIES_DATA,
+} from './constants';
import { DEFAULT_FORM_DATA } from './Timeseries/constants';
+import { SortSeriesType } from './types';
const { legendMargin, legendOrientation, legendType, showLegend } =
DEFAULT_LEGEND_FORM_DATA;
@@ -212,3 +216,41 @@ export const richTooltipSection: ControlSetRow[] = [
[tooltipSortByMetricControl],
[tooltipTimeFormatControl],
];
+
+const sortSeriesType: ControlSetItem = {
+ name: 'sort_series_type',
+ config: {
+ type: 'SelectControl',
+ freeForm: false,
+ label: t('Sort Series By'),
+ choices: [
+ [SortSeriesType.Name, t('Category name')],
+ [SortSeriesType.Sum, t('Total value')],
+ [SortSeriesType.Min, t('Minimum value')],
+ [SortSeriesType.Max, t('Maximum value')],
+ [SortSeriesType.Avg, t('Average value')],
+ ],
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_type,
+ renderTrigger: true,
+ description: t(
+ 'Based on what should series be ordered on the chart and legend',
+ ),
+ },
+};
+
+const sortSeriesAscending: ControlSetItem = {
+ name: 'sort_series_ascending',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Sort Series Ascending'),
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending,
+ renderTrigger: true,
+ description: t('Sort series in ascending order'),
+ },
+};
+
+export const seriesOrderSection: ControlSetRow[] = [
+ [<div className="section-header">{t('Series Order')}</div>],
+ [sortSeriesType],
+ [sortSeriesAscending],
+];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index d51102439f..7408d0a112 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -167,4 +167,17 @@ export interface TreePathInfo {
value: number | number[];
}
+export enum SortSeriesType {
+ Name = 'name',
+ Max = 'max',
+ Min = 'min',
+ Sum = 'sum',
+ Avg = 'avg',
+}
+
+export type SortSeriesData = {
+ sort_series_type: SortSeriesType;
+ sort_series_ascending: boolean;
+};
+
export * from './Timeseries/types';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 649dedd680..6d1396afc2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -30,12 +30,18 @@ import {
AxisType,
} from '@superset-ui/core';
import { format, LegendComponentOption, SeriesOption } from 'echarts';
+import { sumBy, meanBy, minBy, maxBy, orderBy } from 'lodash';
import {
AreaChartExtraControlsValue,
NULL_STRING,
TIMESERIES_CONSTANTS,
} from '../constants';
-import { LegendOrientation, LegendType, StackType } from '../types';
+import {
+ LegendOrientation,
+ LegendType,
+ SortSeriesType,
+ StackType,
+} from '../types';
import { defaultLegendPadding } from '../defaults';
function isDefined<T>(value: T | undefined | null): boolean {
@@ -108,6 +114,46 @@ export function extractShowValueIndexes(
return showValueIndexes;
}
+export function sortAndFilterSeries(
+ rows: DataRecord[],
+ xAxis: string,
+ extraMetricLabels: any[],
+ sortSeriesType?: SortSeriesType,
+ sortSeriesAscending?: boolean,
+): string[] {
+ const seriesNames = Object.keys(rows[0])
+ .filter(key => key !== xAxis)
+ .filter(key => !extraMetricLabels.includes(key));
+
+ let aggregator: (name: string) => { name: string; value: any };
+
+ switch (sortSeriesType) {
+ case SortSeriesType.Sum:
+ aggregator = name => ({ name, value: sumBy(rows, name) });
+ break;
+ case SortSeriesType.Min:
+ aggregator = name => ({ name, value: minBy(rows, name)?.[name] });
+ break;
+ case SortSeriesType.Max:
+ aggregator = name => ({ name, value: maxBy(rows, name)?.[name] });
+ break;
+ case SortSeriesType.Avg:
+ aggregator = name => ({ name, value: meanBy(rows, name) });
+ break;
+ default:
+ aggregator = name => ({ name, value: name.toLowerCase() });
+ break;
+ }
+
+ const sortedValues = seriesNames.map(aggregator);
+
+ return orderBy(
+ sortedValues,
+ ['value'],
+ [sortSeriesAscending ? 'asc' : 'desc'],
+ ).map(({ name }) => name);
+}
+
export function extractSeries(
data: DataRecord[],
opts: {
@@ -118,6 +164,8 @@ export function extractSeries(
stack?: StackType;
totalStackedValues?: number[];
isHorizontal?: boolean;
+ sortSeriesType?: SortSeriesType;
+ sortSeriesAscending?: boolean;
} = {},
): SeriesOption[] {
const {
@@ -128,41 +176,47 @@ export function extractSeries(
stack = false,
totalStackedValues = [],
isHorizontal = false,
+ sortSeriesType,
+ sortSeriesAscending,
} = opts;
if (data.length === 0) return [];
const rows: DataRecord[] = data.map(datum => ({
...datum,
[xAxis]: datum[xAxis],
}));
+ const series = sortAndFilterSeries(
+ rows,
+ xAxis,
+ extraMetricLabels,
+ sortSeriesType,
+ sortSeriesAscending,
+ );
- return Object.keys(rows[0])
- .filter(key => key !== xAxis && key !== DTTM_ALIAS)
- .filter(key => !extraMetricLabels.includes(key))
- .map(key => ({
- id: key,
- name: key,
- data: rows
- .map((row, idx) => {
- const isNextToDefinedValue =
- isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]);
- const isFillNeighborValue =
- !isDefined(row[key]) &&
- isNextToDefinedValue &&
- fillNeighborValue !== undefined;
- let value: DataRecordValue | undefined = row[key];
- if (isFillNeighborValue) {
- value = fillNeighborValue;
- } else if (
- stack === AreaChartExtraControlsValue.Expand &&
- totalStackedValues.length > 0
- ) {
- value = ((value || 0) as number) / totalStackedValues[idx];
- }
- return [row[xAxis], value];
- })
- .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
- .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),
- }));
+ return series.map(name => ({
+ id: name,
+ name,
+ data: rows
+ .map((row, idx) => {
+ const isNextToDefinedValue =
+ isDefined(rows[idx - 1]?.[name]) || isDefined(rows[idx + 1]?.[name]);
+ const isFillNeighborValue =
+ !isDefined(row[name]) &&
+ isNextToDefinedValue &&
+ fillNeighborValue !== undefined;
+ let value: DataRecordValue | undefined = row[name];
+ if (isFillNeighborValue) {
+ value = fillNeighborValue;
+ } else if (
+ stack === AreaChartExtraControlsValue.Expand &&
+ totalStackedValues.length > 0
+ ) {
+ value = ((value || 0) as number) / totalStackedValues[idx];
+ }
+ return [row[xAxis], value];
+ })
+ .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
+ .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),
+ }));
}
export function formatSeriesName(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
index 3bd949d8ad..51b8b72f06 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
@@ -16,22 +16,66 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { getNumberFormatter, getTimeFormatter } from '@superset-ui/core';
+import {
+ DataRecord,
+ getNumberFormatter,
+ getTimeFormatter,
+} from '@superset-ui/core';
import {
dedupSeries,
extractGroupbyLabel,
extractSeries,
+ extractShowValueIndexes,
formatSeriesName,
getChartPadding,
getLegendProps,
- sanitizeHtml,
- extractShowValueIndexes,
getOverMaxHiddenFormatter,
+ sanitizeHtml,
+ sortAndFilterSeries,
} from '../../src/utils/series';
-import { LegendOrientation, LegendType } from '../../src/types';
+import { LegendOrientation, LegendType, SortSeriesType } from '../../src/types';
import { defaultLegendPadding } from '../../src/defaults';
import { NULL_STRING } from '../../src/constants';
+test('sortAndFilterSeries', () => {
+ const data: DataRecord[] = [
+ { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+ { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+ { my_x_axis: null, x: 4, y: 3, z: 7 },
+ ];
+
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Min, true),
+ ).toEqual(['y', 'x', 'z']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Min, false),
+ ).toEqual(['z', 'x', 'y']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Max, true),
+ ).toEqual(['x', 'z', 'y']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Max, false),
+ ).toEqual(['y', 'z', 'x']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Avg, true),
+ ).toEqual(['x', 'y', 'z']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Avg, false),
+ ).toEqual(['z', 'y', 'x']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Sum, true),
+ ).toEqual(['x', 'y', 'z']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Sum, false),
+ ).toEqual(['z', 'y', 'x']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Name, true),
+ ).toEqual(['x', 'y', 'z']);
+ expect(
+ sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Name, false),
+ ).toEqual(['z', 'y', 'x']);
+});
+
describe('extractSeries', () => {
it('should generate a valid ECharts timeseries series object', () => {
const data = [