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 = [