You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2023/03/20 11:56:41 UTC

[superset] branch master updated: feat(echarts): Implement stream graph for Echarts Timeseries (#23410)

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

kgabryje 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 b0d83e8c50 feat(echarts): Implement stream graph for Echarts Timeseries (#23410)
b0d83e8c50 is described below

commit b0d83e8c5086014492f1d11ca19c7c6871b102c7
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Mon Mar 20 12:56:15 2023 +0100

    feat(echarts): Implement stream graph for Echarts Timeseries (#23410)
---
 .../src/Timeseries/Area/controlPanel.tsx           |   6 +-
 .../src/Timeseries/Regular/Bar/controlPanel.tsx    |  12 +--
 .../src/Timeseries/transformProps.ts               |  32 ++++++-
 .../src/Timeseries/transformers.ts                 |  94 ++++++++++++++++--
 .../src/components/ExtraControls.tsx               |   4 +-
 .../plugins/plugin-chart-echarts/src/constants.ts  |  14 ++-
 .../plugins/plugin-chart-echarts/src/controls.tsx  |  10 +-
 .../plugins/plugin-chart-echarts/src/types.ts      |   4 +-
 .../plugin-chart-echarts/src/utils/series.ts       |   4 +-
 .../test/Timeseries/transformProps.test.ts         | 106 +++++++++++++++++++++
 ...7_13-24_b5ea9d343307_bar_chart_stack_options.py |  95 ++++++++++++++++++
 11 files changed, 345 insertions(+), 36 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 a1e877cf1c..99ec771d1e 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
@@ -35,8 +35,9 @@ import {
   showValueControl,
   richTooltipSection,
   seriesOrderSection,
+  percentageThresholdControl,
 } from '../../controls';
-import { AreaChartExtraControlsOptions } from '../../constants';
+import { AreaChartStackControlOptions } from '../../constants';
 
 const {
   logAxis,
@@ -109,13 +110,14 @@ const config: ControlPanelConfig = {
               type: 'SelectControl',
               label: t('Stacked Style'),
               renderTrigger: true,
-              choices: AreaChartExtraControlsOptions,
+              choices: AreaChartStackControlOptions,
               default: null,
               description: t('Stack series on top of each other'),
             },
           },
         ],
         [onlyTotalControl],
+        [percentageThresholdControl],
         [
           {
             name: 'show_extra_controls',
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 f69099866c..358c2dc949 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
@@ -29,12 +29,6 @@ import {
   sections,
   sharedControls,
 } from '@superset-ui/chart-controls';
-
-import { OrientationType } from '../../types';
-import {
-  DEFAULT_FORM_DATA,
-  TIME_SERIES_DESCRIPTION_TEXT,
-} from '../../constants';
 import {
   legendSection,
   richTooltipSection,
@@ -42,6 +36,12 @@ import {
   showValueSection,
 } from '../../../controls';
 
+import { OrientationType } from '../../types';
+import {
+  DEFAULT_FORM_DATA,
+  TIME_SERIES_DESCRIPTION_TEXT,
+} from '../../constants';
+
 const {
   logAxis,
   minorSplitLine,
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 1342e860ba..8ae67f6f31 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -43,7 +43,6 @@ import { ZRLineType } from 'echarts/types/src/util/types';
 import {
   EchartsTimeseriesChartProps,
   EchartsTimeseriesFormData,
-  EchartsTimeseriesSeriesType,
   TimeseriesChartTransformedProps,
   OrientationType,
 } from './types';
@@ -74,6 +73,7 @@ import {
 import { convertInteger } from '../utils/convertInteger';
 import { defaultGrid, defaultYAxis } from '../defaults';
 import {
+  getBaselineSeriesForStream,
   getPadding,
   getTooltipTimeFormatter,
   getXAxisFormatter,
@@ -84,7 +84,7 @@ import {
   transformTimeseriesAnnotation,
 } from './transformers';
 import {
-  AreaChartExtraControlsValue,
+  StackControlsValue,
   TIMESERIES_CONSTANTS,
   TIMEGRAIN_TO_TIMESTAMP,
 } from '../constants';
@@ -195,7 +195,6 @@ export default function transformProps(
     fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
     xAxis: xAxisLabel,
     extraMetricLabels,
-    removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
     stack,
     totalStackedValues,
     isHorizontal,
@@ -210,7 +209,7 @@ export default function transformProps(
   const seriesContexts = extractForecastSeriesContexts(
     Object.values(rawSeries).map(series => series.name as string),
   );
-  const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
+  const isAreaExpand = stack === StackControlsValue.Expand;
   const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
 
   const xAxisType = getAxisType(xAxisDataType);
@@ -243,9 +242,29 @@ export default function transformProps(
       isHorizontal,
       lineStyle,
     });
-    if (transformedSeries) series.push(transformedSeries);
+    if (transformedSeries) {
+      if (stack === StackControlsValue.Stream) {
+        // bug in Echarts - `stackStrategy: 'all'` doesn't work with nulls, so we cast them to 0
+        series.push({
+          ...transformedSeries,
+          data: (transformedSeries.data as any).map(
+            (row: [string | number, number]) => [row[0], row[1] ?? 0],
+          ),
+        });
+      } else {
+        series.push(transformedSeries);
+      }
+    }
   });
 
+  if (stack === StackControlsValue.Stream) {
+    const baselineSeries = getBaselineSeriesForStream(
+      series.map(entry => entry.data) as [string | number, number][][],
+      seriesType,
+    );
+
+    series.unshift(baselineSeries);
+  }
   const selectedValues = (filterState.selectedValues || []).reduce(
     (acc: Record<string, number>, selectedValue: string) => {
       const index = series.findIndex(({ name }) => name === selectedValue);
@@ -428,6 +447,9 @@ export default function transformProps(
 
         Object.keys(forecastValues).forEach(key => {
           const value = forecastValues[key];
+          if (value.observation === 0 && stack) {
+            return;
+          }
           const content = formatForecastTooltipSeries({
             ...value,
             seriesName: key,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index b49f9f546b..953037c3d5 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -19,6 +19,7 @@
 import {
   AnnotationData,
   AnnotationOpacity,
+  AxisType,
   CategoricalColorScale,
   EventAnnotationLayer,
   FilterState,
@@ -33,7 +34,6 @@ import {
   TimeFormatter,
   TimeseriesAnnotationLayer,
   TimeseriesDataRecord,
-  AxisType,
 } from '@superset-ui/core';
 import { SeriesOption } from 'echarts';
 import {
@@ -53,8 +53,12 @@ import {
 import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
 
 import { extractForecastSeriesContext } from '../utils/forecast';
-import { ForecastSeriesEnum, LegendOrientation, StackType } from '../types';
-import { EchartsTimeseriesSeriesType } from './types';
+import {
+  EchartsTimeseriesSeriesType,
+  ForecastSeriesEnum,
+  LegendOrientation,
+  StackType,
+} from '../types';
 
 import {
   evalFormula,
@@ -64,11 +68,79 @@ import {
 } from '../utils/annotation';
 import { currentSeries, getChartPadding } from '../utils/series';
 import {
-  AreaChartExtraControlsValue,
   OpacityEnum,
+  StackControlsValue,
   TIMESERIES_CONSTANTS,
 } from '../constants';
 
+// based on weighted wiggle algorithm
+// source: https://ieeexplore.ieee.org/document/4658136
+export const getBaselineSeriesForStream = (
+  series: [string | number, number][][],
+  seriesType: EchartsTimeseriesSeriesType,
+) => {
+  const seriesLength = series[0].length;
+  const baselineSeriesDelta = new Array(seriesLength).fill([0, 0]);
+  const getVal = (value: number | null) => value ?? 0;
+  for (let i = 0; i < seriesLength; i += 1) {
+    let seriesSum = 0;
+    let weightedSeriesSum = 0;
+    for (let j = 0; j < series.length; j += 1) {
+      const delta =
+        i > 0
+          ? getVal(series[j][i][1]) - getVal(series[j][i - 1][1])
+          : getVal(series[j][i][1]);
+      let deltaPrev = 0;
+      for (let k = 1; k < j - 1; k += 1) {
+        deltaPrev +=
+          i > 0
+            ? getVal(series[k][i][1]) - getVal(series[k][i - 1][1])
+            : getVal(series[k][i][1]);
+      }
+      weightedSeriesSum += (0.5 * delta + deltaPrev) * getVal(series[j][i][1]);
+      seriesSum += getVal(series[j][i][1]);
+    }
+    baselineSeriesDelta[i] = [series[0][i][0], -weightedSeriesSum / seriesSum];
+  }
+  const baselineSeries = baselineSeriesDelta.reduce((acc, curr, i) => {
+    if (i === 0) {
+      acc.push(curr);
+    } else {
+      acc.push([curr[0], acc[i - 1][1] + curr[1]]);
+    }
+    return acc;
+  }, []);
+  return {
+    data: baselineSeries,
+    name: 'baseline',
+    stack: 'obs',
+    stackStrategy: 'all' as const,
+    type: 'line' as const,
+    lineStyle: {
+      opacity: 0,
+    },
+    tooltip: {
+      show: false,
+    },
+    silent: true,
+    showSymbol: false,
+    areaStyle: {
+      opacity: 0,
+    },
+    step: [
+      EchartsTimeseriesSeriesType.Start,
+      EchartsTimeseriesSeriesType.Middle,
+      EchartsTimeseriesSeriesType.End,
+    ].includes(seriesType)
+      ? (seriesType as
+          | EchartsTimeseriesSeriesType.Start
+          | EchartsTimeseriesSeriesType.Middle
+          | EchartsTimeseriesSeriesType.End)
+      : undefined,
+    smooth: seriesType === EchartsTimeseriesSeriesType.Smooth,
+  };
+};
+
 export function transformSeries(
   series: SeriesOption,
   colorScale: CategoricalColorScale,
@@ -190,9 +262,10 @@ export function transformSeries(
       showSymbol = true;
     }
   }
-  const lineStyle = isConfidenceBand
-    ? { ...opts.lineStyle, opacity: OpacityEnum.Transparent }
-    : { ...opts.lineStyle, opacity };
+  const lineStyle =
+    isConfidenceBand || (stack === StackControlsValue.Stream && area)
+      ? { ...opts.lineStyle, opacity: OpacityEnum.Transparent }
+      : { ...opts.lineStyle, opacity };
   return {
     ...series,
     queryIndex,
@@ -208,7 +281,10 @@ export function transformSeries(
       ? seriesType
       : undefined,
     stack: stackId,
-    stackStrategy: isConfidenceBand ? 'all' : 'samesign',
+    stackStrategy:
+      isConfidenceBand || stack === StackControlsValue.Stream
+        ? 'all'
+        : 'samesign',
     lineStyle,
     areaStyle:
       area || forecastSeries.type === ForecastSeriesEnum.ForecastUpper
@@ -234,7 +310,7 @@ export function transformSeries(
         const { value, dataIndex, seriesIndex, seriesName } = params;
         const numericValue = isHorizontal ? value[0] : value[1];
         const isSelectedLegend = currentSeries.legend === seriesName;
-        const isAreaExpand = stack === AreaChartExtraControlsValue.Expand;
+        const isAreaExpand = stack === StackControlsValue.Expand;
         if (!formatter) return numericValue;
         if (!stack || isSelectedLegend) return formatter(numericValue);
         if (!onlyTotal) {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx
index 10217b3add..33e9ab016e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx
@@ -22,7 +22,7 @@ import {
   RadioButtonOption,
   sharedControlComponents,
 } from '@superset-ui/chart-controls';
-import { AreaChartExtraControlsOptions } from '../constants';
+import { AreaChartStackControlOptions } from '../constants';
 
 const { RadioButtonControl } = sharedControlComponents;
 
@@ -53,7 +53,7 @@ export function useExtraControl<
 
   const extraControlsOptions = useMemo(() => {
     if (area) {
-      return AreaChartExtraControlsOptions;
+      return AreaChartStackControlOptions;
     }
     return [];
   }, [area]);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
index 3fc3fc999b..bfc6c98fa5 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts
@@ -71,20 +71,26 @@ export enum OpacityEnum {
   NonTransparent = 1,
 }
 
-export enum AreaChartExtraControlsValue {
+export enum StackControlsValue {
   Stack = 'Stack',
+  Stream = 'Stream',
   Expand = 'Expand',
 }
 
-export const AreaChartExtraControlsOptions: [
+export const StackControlOptions: [
   JsonValue,
   Exclude<ReactNode, null | undefined | boolean>,
 ][] = [
   [null, t('None')],
-  [AreaChartExtraControlsValue.Stack, t('Stack')],
-  [AreaChartExtraControlsValue.Expand, t('Expand')],
+  [StackControlsValue.Stack, t('Stack')],
+  [StackControlsValue.Stream, t('Stream')],
 ];
 
+export const AreaChartStackControlOptions: [
+  JsonValue,
+  Exclude<ReactNode, null | undefined | boolean>,
+][] = [...StackControlOptions, [StackControlsValue.Expand, t('Expand')]];
+
 export const TIMEGRAIN_TO_TIMESTAMP = {
   [TimeGranularity.HOUR]: 3600 * 1000,
   [TimeGranularity.DAY]: 3600 * 1000 * 24,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
index 0733721091..26f10e0fe4 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
@@ -27,6 +27,7 @@ import {
 import {
   DEFAULT_LEGEND_FORM_DATA,
   DEFAULT_SORT_SERIES_DATA,
+  StackControlOptions,
 } from './constants';
 import { DEFAULT_FORM_DATA } from './Timeseries/constants';
 import { SortSeriesType } from './types';
@@ -119,10 +120,11 @@ export const showValueControl: ControlSetItem = {
 export const stackControl: ControlSetItem = {
   name: 'stack',
   config: {
-    type: 'CheckboxControl',
-    label: t('Stack series'),
+    type: 'SelectControl',
+    label: t('Stacked Style'),
     renderTrigger: true,
-    default: false,
+    choices: StackControlOptions,
+    default: null,
     description: t('Stack series on top of each other'),
   },
 };
@@ -142,7 +144,7 @@ export const onlyTotalControl: ControlSetItem = {
   },
 };
 
-const percentageThresholdControl: ControlSetItem = {
+export const percentageThresholdControl: ControlSetItem = {
   name: 'percentage_threshold',
   config: {
     type: 'TextControl',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index 7408d0a112..cb44f17ed3 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -29,7 +29,7 @@ import {
 } from '@superset-ui/core';
 import { EChartsCoreOption, ECharts } from 'echarts';
 import { TooltipMarker } from 'echarts/types/src/util/format';
-import { AreaChartExtraControlsValue } from './constants';
+import { StackControlsValue } from './constants';
 
 export type EchartsStylesProps = {
   height: number;
@@ -159,7 +159,7 @@ export interface TitleFormData {
   yAxisTitlePosition: string;
 }
 
-export type StackType = boolean | null | Partial<AreaChartExtraControlsValue>;
+export type StackType = boolean | null | Partial<StackControlsValue>;
 
 export interface TreePathInfo {
   name: string;
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..3e6e7827a6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -32,7 +32,7 @@ import {
 import { format, LegendComponentOption, SeriesOption } from 'echarts';
 import { sumBy, meanBy, minBy, maxBy, orderBy } from 'lodash';
 import {
-  AreaChartExtraControlsValue,
+  StackControlsValue,
   NULL_STRING,
   TIMESERIES_CONSTANTS,
 } from '../constants';
@@ -207,7 +207,7 @@ export function extractSeries(
         if (isFillNeighborValue) {
           value = fillNeighborValue;
         } else if (
-          stack === AreaChartExtraControlsValue.Expand &&
+          stack === StackControlsValue.Expand &&
           totalStackedValues.length > 0
         ) {
           value = ((value || 0) as number) / totalStackedValues[idx];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
index 63ca50449e..cda213d72b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
@@ -295,6 +295,112 @@ describe('EchartsTimeseries transformProps', () => {
       }),
     );
   });
+
+  it('Should add a baseline series for stream graph', () => {
+    const streamQueriesData = [
+      {
+        data: [
+          {
+            'San Francisco': 120,
+            'New York': 220,
+            Boston: 150,
+            Miami: 270,
+            Denver: 800,
+            __timestamp: 599616000000,
+          },
+          {
+            'San Francisco': 150,
+            'New York': 190,
+            Boston: 240,
+            Miami: 350,
+            Denver: 700,
+            __timestamp: 599616000001,
+          },
+          {
+            'San Francisco': 130,
+            'New York': 300,
+            Boston: 250,
+            Miami: 410,
+            Denver: 650,
+            __timestamp: 599616000002,
+          },
+          {
+            'San Francisco': 90,
+            'New York': 340,
+            Boston: 300,
+            Miami: 480,
+            Denver: 590,
+            __timestamp: 599616000003,
+          },
+          {
+            'San Francisco': 260,
+            'New York': 200,
+            Boston: 420,
+            Miami: 490,
+            Denver: 760,
+            __timestamp: 599616000004,
+          },
+          {
+            'San Francisco': 250,
+            'New York': 250,
+            Boston: 380,
+            Miami: 360,
+            Denver: 400,
+            __timestamp: 599616000005,
+          },
+          {
+            'San Francisco': 160,
+            'New York': 210,
+            Boston: 330,
+            Miami: 440,
+            Denver: 580,
+            __timestamp: 599616000006,
+          },
+        ],
+      },
+    ];
+    const streamFormData = { ...formData, stack: 'Stream' };
+    const props = {
+      ...chartPropsConfig,
+      formData: streamFormData,
+      queriesData: streamQueriesData,
+    };
+
+    const chartProps = new ChartProps(props);
+    expect(
+      (
+        transformProps(chartProps as EchartsTimeseriesChartProps).echartOptions
+          .series as any[]
+      )[0],
+    ).toEqual({
+      areaStyle: {
+        opacity: 0,
+      },
+      lineStyle: {
+        opacity: 0,
+      },
+      name: 'baseline',
+      showSymbol: false,
+      silent: true,
+      smooth: false,
+      stack: 'obs',
+      stackStrategy: 'all',
+      step: undefined,
+      tooltip: {
+        show: false,
+      },
+      type: 'line',
+      data: [
+        [599616000000, -415.7692307692308],
+        [599616000001, -403.6219915054271],
+        [599616000002, -476.32314093071443],
+        [599616000003, -514.2120298196033],
+        [599616000004, -485.7378514158475],
+        [599616000005, -419.6402904402378],
+        [599616000006, -442.9833136960517],
+      ],
+    });
+  });
 });
 
 describe('Does transformProps transform series correctly', () => {
diff --git a/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py b/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py
new file mode 100644
index 0000000000..49844cda11
--- /dev/null
+++ b/superset/migrations/versions/2023-03-17_13-24_b5ea9d343307_bar_chart_stack_options.py
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""bar_chart_stack_options
+
+Revision ID: b5ea9d343307
+Revises: d0ac08bb5b83
+Create Date: 2023-03-17 13:24:54.662754
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "b5ea9d343307"
+down_revision = "d0ac08bb5b83"
+
+import json
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy import and_, Column, Integer, String, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+from superset import db
+
+Base = declarative_base()
+
+CHART_TYPE = "%echarts_timeseries%"
+
+
+class Slice(Base):
+    """Declarative class to do query in upgrade"""
+
+    __tablename__ = "slices"
+    id = Column(Integer, primary_key=True)
+    viz_type = Column(String(250))
+    params = Column(Text)
+
+
+def upgrade():
+    bind = op.get_bind()
+    session = db.Session(bind=bind)
+
+    slices = session.query(Slice).filter(Slice.viz_type.like(CHART_TYPE)).all()
+    for slc in slices:
+        try:
+            params = json.loads(slc.params)
+            stack = params.get("stack", None)
+            if stack:
+                params["stack"] = "Stack"
+            else:
+                params["stack"] = None
+            slc.params = json.dumps(params, sort_keys=True)
+        except Exception as e:
+            print(e)
+            print(f"Parsing params for slice {slc.id} failed.")
+            pass
+
+    session.commit()
+    session.close()
+
+
+def downgrade():
+    bind = op.get_bind()
+    session = db.Session(bind=bind)
+
+    slices = session.query(Slice).filter(Slice.viz_type.like(CHART_TYPE)).all()
+    for slc in slices:
+        try:
+            params = json.loads(slc.params)
+            stack = params.get("stack", None)
+            if stack == "Stack" or stack == "Stream":
+                params["stack"] = True
+            else:
+                params["stack"] = False
+            slc.params = json.dumps(params, sort_keys=True)
+        except Exception as e:
+            print(e)
+            print(f"Parsing params for slice {slc.id} failed.")
+            pass
+
+    session.commit()
+    session.close()