You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2023/05/25 22:26:12 UTC

[superset] 07/07: fix(plugin-chart-echarts): reorder totals and support multimetric sort (#23675)

This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch 2.1
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 661a559cb2fe4cad1c384203224e2622e2e309c8
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Fri Apr 14 20:43:15 2023 +0300

    fix(plugin-chart-echarts): reorder totals and support multimetric sort (#23675)
---
 .../src/shared-controls/customControls.tsx         |  29 +-
 .../src/MixedTimeseries/transformProps.ts          |   4 +-
 .../src/Timeseries/transformProps.ts               |  13 +-
 .../plugin-chart-echarts/src/Timeseries/types.ts   |   2 +
 .../plugins/plugin-chart-echarts/src/constants.ts  |   6 +
 .../plugin-chart-echarts/src/utils/series.ts       | 134 ++++++++-
 .../plugin-chart-echarts/test/utils/series.test.ts | 300 +++++++++++++++++----
 7 files changed, 416 insertions(+), 72 deletions(-)

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 8e8f4d8400..0564745eae 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
@@ -50,14 +50,29 @@ export const contributionModeControl = {
   },
 };
 
+function isTemporal(controls: ControlStateMapping): boolean {
+  return !(
+    isDefined(controls?.x_axis?.value) &&
+    !isTemporalColumn(
+      getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
+      controls?.datasource?.datasource,
+    )
+  );
+}
+
 const xAxisSortVisibility = ({ 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 === 0;
+  !isTemporal(controls) &&
+  ensureIsArray(controls?.groupby?.value).length === 0 &&
+  ensureIsArray(controls?.metrics?.value).length === 1;
+
+const xAxisMultiSortVisibility = ({
+  controls,
+}: {
+  controls: ControlStateMapping;
+}) =>
+  !isTemporal(controls) &&
+  (!!ensureIsArray(controls?.groupby?.value).length ||
+    ensureIsArray(controls?.metrics?.value).length > 1);
 
 export const xAxisSortControl = {
   name: 'x_axis_sort',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index 98367daf9a..da8ac01f4e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -170,12 +170,12 @@ export default function transformProps(
   }
 
   const rebasedDataA = rebaseForecastDatum(data1, verboseMap);
-  const rawSeriesA = extractSeries(rebasedDataA, {
+  const [rawSeriesA] = extractSeries(rebasedDataA, {
     fillNeighborValue: stack ? 0 : undefined,
     xAxis: xAxisLabel,
   });
   const rebasedDataB = rebaseForecastDatum(data2, verboseMap);
-  const rawSeriesB = extractSeries(rebasedDataB, {
+  const [rawSeriesB] = extractSeries(rebasedDataB, {
     fillNeighborValue: stackB ? 0 : undefined,
     xAxis: xAxisLabel,
   });
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 4398f7e555..afd54e552c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -125,6 +125,7 @@ export default function transformProps(
     logAxis,
     markerEnabled,
     markerSize,
+    metrics,
     minorSplitLine,
     onlyTotal,
     opacity,
@@ -144,6 +145,8 @@ export default function transformProps(
     truncateYAxis,
     xAxis: xAxisOrig,
     xAxisLabelRotation,
+    xAxisSortSeries,
+    xAxisSortSeriesAscending,
     xAxisTimeFormat,
     xAxisTitle,
     xAxisTitleMargin,
@@ -178,7 +181,9 @@ export default function transformProps(
     getMetricLabel,
   );
 
-  const rawSeries = extractSeries(rebasedData, {
+  const isMultiSeries = groupby.length || metrics.length > 1;
+
+  const [rawSeries, sortedTotalValues] = extractSeries(rebasedData, {
     fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
     xAxis: xAxisLabel,
     extraMetricLabels,
@@ -188,6 +193,10 @@ export default function transformProps(
     isHorizontal,
     sortSeriesType,
     sortSeriesAscending,
+    xAxisSortSeries: isMultiSeries ? xAxisSortSeries : undefined,
+    xAxisSortSeriesAscending: isMultiSeries
+      ? xAxisSortSeriesAscending
+      : undefined,
   });
   const showValueIndexes = extractShowValueIndexes(rawSeries, {
     stack,
@@ -222,7 +231,7 @@ export default function transformProps(
       formatter,
       showValue,
       onlyTotal,
-      totalStackedValues,
+      totalStackedValues: sortedTotalValues,
       showValueIndexes,
       thresholdValues,
       richTooltip,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
index 56527ebd63..b5a67d977d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
@@ -23,6 +23,7 @@ import {
   ContributionType,
   QueryFormColumn,
   QueryFormData,
+  QueryFormMetric,
   TimeFormatter,
   TimeGranularity,
 } from '@superset-ui/core';
@@ -65,6 +66,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
   logAxis: boolean;
   markerEnabled: boolean;
   markerSize: number;
+  metrics: QueryFormMetric[];
   minorSplitLine: boolean;
   opacity: number;
   orderDesc: boolean;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
index 3fc3fc999b..6be6706886 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
@@ -49,6 +49,12 @@ export const TIMESERIES_CONSTANTS = {
   extraControlsOffset: 22,
 };
 
+export enum StackControlsValue {
+  Stack = 'Stack',
+  Stream = 'Stream',
+  Expand = 'Expand',
+}
+
 export const LABEL_POSITION: [LabelPositionEnum, string][] = [
   [LabelPositionEnum.Top, 'Top'],
   [LabelPositionEnum.Left, 'Left'],
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 6d1396afc2..5bb026dc77 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -35,6 +35,7 @@ import {
   AreaChartExtraControlsValue,
   NULL_STRING,
   TIMESERIES_CONSTANTS,
+  StackControlsValue
 } from '../constants';
 import {
   LegendOrientation,
@@ -154,6 +155,86 @@ export function sortAndFilterSeries(
   ).map(({ name }) => name);
 }
 
+export function sortRows(
+  rows: DataRecord[],
+  totalStackedValues: number[],
+  xAxis: string,
+  xAxisSortSeries: SortSeriesType,
+  xAxisSortSeriesAscending: boolean,
+) {
+  const sortedRows = rows.map((row, idx) => {
+    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,
+      totalStackedValue: totalStackedValues[idx],
+    };
+  });
+
+  return orderBy(
+    sortedRows,
+    ['value'],
+    [xAxisSortSeriesAscending ? 'asc' : 'desc'],
+  ).map(({ row, totalStackedValue }) => ({ row, totalStackedValue }));
+}
+
 export function extractSeries(
   data: DataRecord[],
   opts: {
@@ -166,8 +247,10 @@ export function extractSeries(
     isHorizontal?: boolean;
     sortSeriesType?: SortSeriesType;
     sortSeriesAscending?: boolean;
+    xAxisSortSeries?: SortSeriesType;
+    xAxisSortSeriesAscending?: boolean;
   } = {},
-): SeriesOption[] {
+): [SeriesOption[], number[], number | undefined] {
   const {
     fillNeighborValue,
     xAxis = DTTM_ALIAS,
@@ -178,45 +261,74 @@ export function extractSeries(
     isHorizontal = false,
     sortSeriesType,
     sortSeriesAscending,
+    xAxisSortSeries,
+    xAxisSortSeriesAscending,
   } = opts;
-  if (data.length === 0) return [];
+  if (data.length === 0) return [[], [], undefined];
   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,
+          totalStackedValues,
+          xAxis,
+          xAxisSortSeries!,
+          xAxisSortSeriesAscending!,
+        )
+      : rows.map((row, idx) => ({
+          row,
+          totalStackedValue: totalStackedValues[idx],
+        }));
 
-  return series.map(name => ({
+  let minPositiveValue: number | undefined;
+  const finalSeries = sortedSeries.map(name => ({
     id: name,
     name,
-    data: rows
-      .map((row, idx) => {
+    data: sortedRows
+      .map(({ row, totalStackedValue }, idx) => {
+        const currentValue = row[name];
+        if (
+          typeof currentValue === 'number' &&
+          currentValue > 0 &&
+          (minPositiveValue === undefined || minPositiveValue > currentValue)
+        ) {
+          minPositiveValue = currentValue;
+        }
         const isNextToDefinedValue =
           isDefined(rows[idx - 1]?.[name]) || isDefined(rows[idx + 1]?.[name]);
         const isFillNeighborValue =
-          !isDefined(row[name]) &&
+          !isDefined(currentValue) &&
           isNextToDefinedValue &&
           fillNeighborValue !== undefined;
-        let value: DataRecordValue | undefined = row[name];
+        let value: DataRecordValue | undefined = currentValue;
         if (isFillNeighborValue) {
           value = fillNeighborValue;
         } else if (
-          stack === AreaChartExtraControlsValue.Expand &&
-          totalStackedValues.length > 0
+          stack === StackControlsValue.Expand &&
+          totalStackedValue !== undefined
         ) {
-          value = ((value || 0) as number) / totalStackedValues[idx];
+          value = ((value || 0) as number) / totalStackedValue;
         }
         return [row[xAxis], value];
       })
       .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
       .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),
   }));
+  return [
+    finalSeries,
+    sortedRows.map(({ totalStackedValue }) => totalStackedValue),
+    minPositiveValue,
+  ];
 }
 
 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 51b8b72f06..89e37b0856 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
@@ -32,11 +32,30 @@ import {
   getOverMaxHiddenFormatter,
   sanitizeHtml,
   sortAndFilterSeries,
+  sortRows,
 } from '../../src/utils/series';
 import { LegendOrientation, LegendType, SortSeriesType } from '../../src/types';
 import { defaultLegendPadding } from '../../src/defaults';
 import { NULL_STRING } from '../../src/constants';
 
+const expectedThemeProps = {
+  selector: ['all', 'inverse'],
+  selectorLabel: {
+    fontFamily: theme.typography.families.sansSerif,
+    fontSize: theme.typography.sizes.s,
+    color: theme.colors.grayscale.base,
+    borderColor: theme.colors.grayscale.base,
+  },
+};
+
+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 },
+];
+
+const totalStackedValues = [3, 15, 14];
+
 test('sortAndFilterSeries', () => {
   const data: DataRecord[] = [
     { my_x_axis: 'abc', x: 1, y: 0, z: 2 },
@@ -44,6 +63,167 @@ test('sortAndFilterSeries', () => {
     { my_x_axis: null, x: 4, y: 3, z: 7 },
   ];
 
+
+test('sortRows by name ascending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Name,
+      true,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+  ]);
+});
+
+test('sortRows by name descending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Name,
+      false,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+  ]);
+});
+
+test('sortRows by sum ascending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Sum,
+      true,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+  ]);
+});
+
+test('sortRows by sum descending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Sum,
+      false,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+  ]);
+});
+
+test('sortRows by avg ascending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Avg,
+      true,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+  ]);
+});
+
+test('sortRows by avg descending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Avg,
+      false,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+  ]);
+});
+
+test('sortRows by min ascending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Min,
+      true,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+  ]);
+});
+
+test('sortRows by min descending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Min,
+      false,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+  ]);
+});
+
+test('sortRows by max ascending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Min,
+      true,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+  ]);
+});
+
+test('sortRows by max descending', () => {
+  expect(
+    sortRows(
+      sortData,
+      totalStackedValues,
+      'my_x_axis',
+      SortSeriesType.Min,
+      false,
+    ),
+  ).toEqual([
+    { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 },
+    { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 },
+    { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 },
+  ]);
+});
+
   expect(
     sortAndFilterSeries(data, 'my_x_axis', [], SortSeriesType.Min, true),
   ).toEqual(['y', 'x', 'z']);
@@ -95,25 +275,29 @@ describe('extractSeries', () => {
         abc: 5,
       },
     ];
-    expect(extractSeries(data)).toEqual([
-      {
-        id: 'Hulk',
-        name: 'Hulk',
-        data: [
-          ['2000-01-01', null],
-          ['2000-02-01', 2],
-          ['2000-03-01', 1],
-        ],
-      },
-      {
-        id: 'abc',
-        name: 'abc',
-        data: [
-          ['2000-01-01', 2],
-          ['2000-02-01', 10],
-          ['2000-03-01', 5],
-        ],
-      },
+    const totalStackedValues = [2, 12, 6];
+    expect(extractSeries(data, { totalStackedValues })).toEqual([
+      [
+        {
+          id: 'Hulk',
+          name: 'Hulk',
+          data: [
+            ['2000-01-01', null],
+            ['2000-02-01', 2],
+            ['2000-03-01', 1],
+          ],
+        },
+        {
+          id: 'abc',
+          name: 'abc',
+          data: [
+            ['2000-01-01', 2],
+            ['2000-02-01', 10],
+            ['2000-03-01', 5],
+          ],
+        },
+      ],
+      totalStackedValues,
     ]);
   });
 
@@ -135,20 +319,30 @@ describe('extractSeries', () => {
         abc: 5,
       },
     ];
-    expect(extractSeries(data, { xAxis: 'x', removeNulls: true })).toEqual([
-      {
-        id: 'Hulk',
-        name: 'Hulk',
-        data: [[2, 1]],
-      },
-      {
-        id: 'abc',
-        name: 'abc',
-        data: [
-          [1, 2],
-          [2, 5],
-        ],
-      },
+    const totalStackedValues = [3, 12, 8];
+    expect(
+      extractSeries(data, {
+        totalStackedValues,
+        xAxis: 'x',
+        removeNulls: true,
+      }),
+    ).toEqual([
+      [
+        {
+          id: 'Hulk',
+          name: 'Hulk',
+          data: [[2, 1]],
+        },
+        {
+          id: 'abc',
+          name: 'abc',
+          data: [
+            [1, 2],
+            [2, 5],
+          ],
+        },
+      ],
+      totalStackedValues,
     ]);
   });
 
@@ -195,23 +389,29 @@ describe('extractSeries', () => {
         abc: null,
       },
     ];
-    expect(extractSeries(data, { fillNeighborValue: 0 })).toEqual([
-      {
-        id: 'abc',
-        name: 'abc',
-        data: [
-          ['2000-01-01', null],
-          ['2000-02-01', 0],
-          ['2000-03-01', 1],
-          ['2000-04-01', 0],
-          ['2000-05-01', null],
-          ['2000-06-01', 0],
-          ['2000-07-01', 2],
-          ['2000-08-01', 3],
-          ['2000-09-01', 0],
-          ['2000-10-01', null],
-        ],
-      },
+    const totalStackedValues = [0, 0, 1, 0, 0, 0, 2, 3, 0, 0];
+    expect(
+      extractSeries(data, { totalStackedValues, fillNeighborValue: 0 }),
+    ).toEqual([
+      [
+        {
+          id: 'abc',
+          name: 'abc',
+          data: [
+            ['2000-01-01', null],
+            ['2000-02-01', 0],
+            ['2000-03-01', 1],
+            ['2000-04-01', 0],
+            ['2000-05-01', null],
+            ['2000-06-01', 0],
+            ['2000-07-01', 2],
+            ['2000-08-01', 3],
+            ['2000-09-01', 0],
+            ['2000-10-01', null],
+          ],
+        },
+      ],
+      totalStackedValues,
     ]);
   });
 });