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/04/12 13:13:53 UTC

[superset] branch master updated: feat(plugin-chart-echarts): add x-axis sort to multi series (#23644)

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 f49702feff feat(plugin-chart-echarts): add x-axis sort to multi series (#23644)
f49702feff is described below

commit f49702feffb3b08476c22916e185c0ce2c64e7f1
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Wed Apr 12 16:13:41 2023 +0300

    feat(plugin-chart-echarts): add x-axis sort to multi series (#23644)
---
 .../superset-ui-chart-controls/src/constants.ts    |  20 ++-
 .../src/sections/echartsTimeSeriesQuery.tsx        |   6 +-
 .../src/shared-controls/customControls.tsx         |  49 ++++++++
 .../superset-ui-chart-controls/src/types.ts        |  13 ++
 .../src/Timeseries/constants.ts                    |   6 +-
 .../src/Timeseries/transformProps.ts               |   6 +
 .../plugins/plugin-chart-echarts/src/constants.ts  |   7 --
 .../plugins/plugin-chart-echarts/src/controls.tsx  |  17 +--
 .../plugins/plugin-chart-echarts/src/types.ts      |  13 --
 .../plugin-chart-echarts/src/utils/series.ts       | 106 ++++++++++++++--
 .../plugin-chart-echarts/test/utils/series.test.ts | 137 ++++++++++++++++++---
 11 files changed, 317 insertions(+), 63 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
index f410c4479a..8ae89efbf6 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts
@@ -24,7 +24,7 @@ import {
   QueryColumn,
   DatasourceType,
 } from '@superset-ui/core';
-import { ColumnMeta } from './types';
+import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
 
 // eslint-disable-next-line import/prefer-default-export
 export const TIME_FILTER_LABELS = {
@@ -57,3 +57,21 @@ export const QueryModeLabel = {
   [QueryMode.aggregate]: t('Aggregate'),
   [QueryMode.raw]: t('Raw records'),
 };
+
+export const DEFAULT_SORT_SERIES_DATA: SortSeriesData = {
+  sort_series_type: SortSeriesType.Sum,
+  sort_series_ascending: false,
+};
+
+export const SORT_SERIES_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')],
+];
+
+export const DEFAULT_XAXIS_SORT_SERIES_DATA: SortSeriesData = {
+  sort_series_type: SortSeriesType.Name,
+  sort_series_ascending: true,
+};
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx
index cd58780d89..53c9aa7447 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx
@@ -20,8 +20,10 @@ import { hasGenericChartAxes, t } from '@superset-ui/core';
 import { ControlPanelSectionConfig, ControlSetRow } from '../types';
 import {
   contributionModeControl,
-  xAxisSortControl,
   xAxisSortAscControl,
+  xAxisSortControl,
+  xAxisSortSeriesAscendingControl,
+  xAxisSortSeriesControl,
 } from '../shared-controls';
 
 const controlsWithoutXAxis: ControlSetRow[] = [
@@ -55,6 +57,8 @@ export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
     [hasGenericChartAxes ? 'time_grain_sqla' : null],
     [hasGenericChartAxes ? xAxisSortControl : null],
     [hasGenericChartAxes ? xAxisSortAscControl : null],
+    [hasGenericChartAxes ? xAxisSortSeriesControl : null],
+    [hasGenericChartAxes ? xAxisSortSeriesAscendingControl : null],
     ...controlsWithoutXAxis,
   ],
 };
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx
index 5ac303f54d..28fbfb876c 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx
@@ -34,6 +34,10 @@ import {
   isDataset,
 } from '../types';
 import { isTemporalColumn } from '../utils';
+import {
+  DEFAULT_XAXIS_SORT_SERIES_DATA,
+  SORT_SERIES_CHOICES,
+} from '../constants';
 
 export const contributionModeControl = {
   name: 'contributionMode',
@@ -59,6 +63,19 @@ const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) =>
   Array.isArray(controls?.groupby?.value) &&
   controls.groupby.value.length === 0;
 
+const xAxisMultiSortVisibility = ({
+  controls,
+}: {
+  controls: ControlStateMapping;
+}) =>
+  isDefined(controls?.x_axis?.value) &&
+  !isTemporalColumn(
+    getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
+    controls?.datasource?.datasource,
+  ) &&
+  Array.isArray(controls?.groupby?.value) &&
+  !!controls.groupby.value.length;
+
 export const xAxisSortControl = {
   name: 'x_axis_sort',
   config: {
@@ -125,3 +142,35 @@ export const xAxisSortAscControl = {
     visibility: xAxisSortVisibility,
   },
 };
+
+export const xAxisSortSeriesControl = {
+  name: 'x_axis_sort_series',
+  config: {
+    type: 'SelectControl',
+    freeForm: false,
+    label: (state: ControlPanelState) =>
+      state.form_data?.orientation === 'horizontal'
+        ? t('Y-Axis Sort By')
+        : t('X-Axis Sort By'),
+    choices: SORT_SERIES_CHOICES,
+    default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_type,
+    renderTrigger: true,
+    description: t('Decides which measure to sort the base axis by.'),
+    visibility: xAxisMultiSortVisibility,
+  },
+};
+
+export const xAxisSortSeriesAscendingControl = {
+  name: 'x_axis_sort_series_ascending',
+  config: {
+    type: 'CheckboxControl',
+    label: (state: ControlPanelState) =>
+      state.form_data?.orientation === 'horizontal'
+        ? t('Y-Axis Sort Ascending')
+        : t('X-Axis Sort Ascending'),
+    default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_ascending,
+    description: t('Whether to sort ascending or descending on the base Axis.'),
+    renderTrigger: true,
+    visibility: xAxisMultiSortVisibility,
+  },
+};
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index d4e91246ab..67582523bc 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -481,3 +481,16 @@ export function isQueryResponse(
 ): datasource is QueryResponse {
   return !!datasource && 'results' in datasource && 'sql' in datasource;
 }
+
+export enum SortSeriesType {
+  Name = 'name',
+  Max = 'max',
+  Min = 'min',
+  Sum = 'sum',
+  Avg = 'avg',
+}
+
+export type SortSeriesData = {
+  sort_series_type: SortSeriesType;
+  sort_series_ascending: boolean;
+};
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 e0b41f9f68..17629c0996 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
@@ -16,7 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { sections } from '@superset-ui/chart-controls';
+import {
+  DEFAULT_SORT_SERIES_DATA,
+  sections,
+} from '@superset-ui/chart-controls';
 import { t } from '@superset-ui/core';
 import {
   OrientationType,
@@ -25,7 +28,6 @@ import {
 } from './types';
 import {
   DEFAULT_LEGEND_FORM_DATA,
-  DEFAULT_SORT_SERIES_DATA,
   DEFAULT_TITLE_FORM_DATA,
 } from '../constants';
 
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 d00f74a0f2..5565fee6a2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -146,6 +146,8 @@ export default function transformProps(
     truncateYAxis,
     xAxis: xAxisOrig,
     xAxisLabelRotation,
+    xAxisSortSeries,
+    xAxisSortSeriesAscending,
     xAxisTimeFormat,
     xAxisTitle,
     xAxisTitleMargin,
@@ -200,6 +202,10 @@ export default function transformProps(
     isHorizontal,
     sortSeriesType,
     sortSeriesAscending,
+    xAxisSortSeries: groupby.length ? xAxisSortSeries : undefined,
+    xAxisSortSeriesAscending: groupby.length
+      ? xAxisSortSeriesAscending
+      : undefined,
   });
   const showValueIndexes = extractShowValueIndexes(rawSeries, {
     stack,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
index bfc6c98fa5..b0b87bd188 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
@@ -24,8 +24,6 @@ import {
   LegendFormData,
   LegendOrientation,
   LegendType,
-  SortSeriesData,
-  SortSeriesType,
   TitleFormData,
 } from './types';
 
@@ -122,8 +120,3 @@ 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 26f10e0fe4..bfd2634ac9 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
@@ -22,15 +22,12 @@ import {
   ControlPanelsContainerProps,
   ControlSetItem,
   ControlSetRow,
+  DEFAULT_SORT_SERIES_DATA,
+  SORT_SERIES_CHOICES,
   sharedControls,
 } from '@superset-ui/chart-controls';
-import {
-  DEFAULT_LEGEND_FORM_DATA,
-  DEFAULT_SORT_SERIES_DATA,
-  StackControlOptions,
-} from './constants';
+import { DEFAULT_LEGEND_FORM_DATA, StackControlOptions } from './constants';
 import { DEFAULT_FORM_DATA } from './Timeseries/constants';
-import { SortSeriesType } from './types';
 
 const { legendMargin, legendOrientation, legendType, showLegend } =
   DEFAULT_LEGEND_FORM_DATA;
@@ -225,13 +222,7 @@ const sortSeriesType: ControlSetItem = {
     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')],
-    ],
+    choices: SORT_SERIES_CHOICES,
     default: DEFAULT_SORT_SERIES_DATA.sort_series_type,
     renderTrigger: true,
     description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index cb44f17ed3..142c41c17d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -167,17 +167,4 @@ 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 2cfd7e831d..ea1691d11c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -18,6 +18,7 @@
  * under the License.
  */
 import {
+  AxisType,
   ChartDataResponseResult,
   DataRecord,
   DataRecordValue,
@@ -27,22 +28,17 @@ import {
   NumberFormats,
   NumberFormatter,
   TimeFormatter,
-  AxisType,
   SupersetTheme,
 } from '@superset-ui/core';
+import { SortSeriesType } from '@superset-ui/chart-controls';
 import { format, LegendComponentOption, SeriesOption } from 'echarts';
-import { sumBy, meanBy, minBy, maxBy, orderBy } from 'lodash';
+import { maxBy, meanBy, minBy, orderBy, sumBy } from 'lodash';
 import {
-  StackControlsValue,
   NULL_STRING,
+  StackControlsValue,
   TIMESERIES_CONSTANTS,
 } from '../constants';
-import {
-  LegendOrientation,
-  LegendType,
-  SortSeriesType,
-  StackType,
-} from '../types';
+import { LegendOrientation, LegendType, StackType } from '../types';
 import { defaultLegendPadding } from '../defaults';
 
 function isDefined<T>(value: T | undefined | null): boolean {
@@ -155,6 +151,84 @@ export function sortAndFilterSeries(
   ).map(({ name }) => name);
 }
 
+export function sortRows(
+  rows: DataRecord[],
+  xAxis: string,
+  xAxisSortSeries: SortSeriesType,
+  xAxisSortSeriesAscending: boolean,
+) {
+  const sortedRows = rows.map(row => {
+    let sortKey: DataRecordValue = '';
+    let aggregate: number | undefined;
+    let entries = 0;
+    Object.entries(row).forEach(([key, value]) => {
+      const isValueDefined = isDefined(value);
+      if (key === xAxis) {
+        sortKey = value;
+      }
+      if (
+        xAxisSortSeries === SortSeriesType.Name ||
+        typeof value !== 'number'
+      ) {
+        return;
+      }
+
+      if (!(xAxisSortSeries === SortSeriesType.Avg && !isValueDefined)) {
+        entries += 1;
+      }
+
+      switch (xAxisSortSeries) {
+        case SortSeriesType.Avg:
+        case SortSeriesType.Sum:
+          if (aggregate === undefined) {
+            aggregate = value;
+          } else {
+            aggregate += value;
+          }
+          break;
+        case SortSeriesType.Min:
+          aggregate =
+            aggregate === undefined || (isValueDefined && value < aggregate)
+              ? value
+              : aggregate;
+          break;
+        case SortSeriesType.Max:
+          aggregate =
+            aggregate === undefined || (isValueDefined && value > aggregate)
+              ? value
+              : aggregate;
+          break;
+        default:
+          break;
+      }
+    });
+    if (
+      xAxisSortSeries === SortSeriesType.Avg &&
+      entries > 0 &&
+      aggregate !== undefined
+    ) {
+      aggregate /= entries;
+    }
+
+    const value =
+      xAxisSortSeries === SortSeriesType.Name && typeof sortKey === 'string'
+        ? sortKey.toLowerCase()
+        : aggregate;
+
+    return {
+      key: sortKey,
+      value,
+      row,
+    };
+  });
+
+  return orderBy(
+    sortedRows,
+    ['value'],
+    [xAxisSortSeriesAscending ? 'asc' : 'desc'],
+  ).map(({ row }) => row);
+}
+
 export function extractSeries(
   data: DataRecord[],
   opts: {
@@ -167,6 +241,8 @@ export function extractSeries(
     isHorizontal?: boolean;
     sortSeriesType?: SortSeriesType;
     sortSeriesAscending?: boolean;
+    xAxisSortSeries?: SortSeriesType;
+    xAxisSortSeriesAscending?: boolean;
   } = {},
 ): SeriesOption[] {
   const {
@@ -179,24 +255,30 @@ export function extractSeries(
     isHorizontal = false,
     sortSeriesType,
     sortSeriesAscending,
+    xAxisSortSeries,
+    xAxisSortSeriesAscending,
   } = opts;
   if (data.length === 0) return [];
   const rows: DataRecord[] = data.map(datum => ({
     ...datum,
     [xAxis]: datum[xAxis],
   }));
-  const series = sortAndFilterSeries(
+  const sortedSeries = sortAndFilterSeries(
     rows,
     xAxis,
     extraMetricLabels,
     sortSeriesType,
     sortSeriesAscending,
   );
+  const sortedRows =
+    isDefined(xAxisSortSeries) && isDefined(xAxisSortSeriesAscending)
+      ? sortRows(rows, xAxis, xAxisSortSeries!, xAxisSortSeriesAscending!)
+      : rows;
 
-  return series.map(name => ({
+  return sortedSeries.map(name => ({
     id: name,
     name,
-    data: rows
+    data: sortedRows
       .map((row, idx) => {
         const isNextToDefinedValue =
           isDefined(rows[idx - 1]?.[name]) || isDefined(rows[idx + 1]?.[name]);
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 4daeac6a86..69570ff007 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,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { SortSeriesType } from '@superset-ui/chart-controls';
 import {
   DataRecord,
   getNumberFormatter,
@@ -33,8 +34,9 @@ import {
   getOverMaxHiddenFormatter,
   sanitizeHtml,
   sortAndFilterSeries,
+  sortRows,
 } from '../../src/utils/series';
-import { LegendOrientation, LegendType, SortSeriesType } from '../../src/types';
+import { LegendOrientation, LegendType } from '../../src/types';
 import { defaultLegendPadding } from '../../src/defaults';
 import { NULL_STRING } from '../../src/constants';
 
@@ -48,42 +50,149 @@ const expectedThemeProps = {
   },
 };
 
-test('sortAndFilterSeries', () => {
-  const data: DataRecord[] = [
+const sortData: 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 },
+];
+
+test('sortRows by name ascending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Name, true)).toEqual([
     { 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 },
-  ];
+  ]);
+});
+
+test('sortRows by name descending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Name, false)).toEqual([
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+  ]);
+});
+
+test('sortRows by sum ascending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Sum, true)).toEqual([
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+  ]);
+});
+
+test('sortRows by sum descending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Sum, false)).toEqual([
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+  ]);
+});
 
+test('sortRows by avg ascending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Avg, true)).toEqual([
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+  ]);
+});
+
+test('sortRows by avg descending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Avg, false)).toEqual([
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+  ]);
+});
+
+test('sortRows by min ascending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, true)).toEqual([
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+  ]);
+});
+
+test('sortRows by min descending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, false)).toEqual([
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+  ]);
+});
+
+test('sortRows by max ascending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, true)).toEqual([
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+  ]);
+});
+
+test('sortRows by max descending', () => {
+  expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, false)).toEqual([
+    { my_x_axis: 'foo', x: null, y: 10, z: 5 },
+    { my_x_axis: null, x: 4, y: 3, z: 7 },
+    { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
+  ]);
+});
+
+test('sortAndFilterSeries by min ascending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Min, true),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Min, true),
   ).toEqual(['y', 'x', 'z']);
+});
+
+test('sortAndFilterSeries by min descending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Min, false),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Min, false),
   ).toEqual(['z', 'x', 'y']);
+});
+
+test('sortAndFilterSeries by max ascending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Max, true),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Max, true),
   ).toEqual(['x', 'z', 'y']);
+});
+
+test('sortAndFilterSeries by max descending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Max, false),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Max, false),
   ).toEqual(['y', 'z', 'x']);
+});
+
+test('sortAndFilterSeries by avg ascending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Avg, true),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Avg, true),
   ).toEqual(['x', 'y', 'z']);
+});
+
+test('sortAndFilterSeries by avg descending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Avg, false),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Avg, false),
   ).toEqual(['z', 'y', 'x']);
+});
+
+test('sortAndFilterSeries by sum ascending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Sum, true),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Sum, true),
   ).toEqual(['x', 'y', 'z']);
+});
+
+test('sortAndFilterSeries by sum descending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Sum, false),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Sum, false),
   ).toEqual(['z', 'y', 'x']);
+});
+
+test('sortAndFilterSeries by name ascending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Name, true),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, true),
   ).toEqual(['x', 'y', 'z']);
+});
+
+test('sortAndFilterSeries by name descending', () => {
   expect(
-    sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Name, false),
+    sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, false),
   ).toEqual(['z', 'y', 'x']);
 });