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/12/22 12:14:59 UTC

(superset) branch master updated: feat(echarts-funnel): Implement % calculation type (#26290)

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 5400d30b20 feat(echarts-funnel): Implement % calculation type (#26290)
5400d30b20 is described below

commit 5400d30b201d5ba987dfda8ade1a157580d9cc7c
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Fri Dec 22 13:14:52 2023 +0100

    feat(echarts-funnel): Implement % calculation type (#26290)
---
 .../src/Funnel/controlPanel.tsx                    | 31 +++++++--
 .../src/Funnel/transformProps.ts                   | 51 ++++++++++++---
 .../plugin-chart-echarts/src/Funnel/types.ts       |  7 ++
 .../test/Funnel/transformProps.test.ts             | 32 +++++++++-
 ...ff00fe8_add_percent_calculation_type_funnel_.py | 74 ++++++++++++++++++++++
 5 files changed, 181 insertions(+), 14 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
index 17c73d195b..76c1465357 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx
@@ -20,16 +20,20 @@ import React from 'react';
 import { t } from '@superset-ui/core';
 import {
   ControlPanelConfig,
+  ControlStateMapping,
   ControlSubSectionHeader,
+  D3_FORMAT_DOCS,
   D3_FORMAT_OPTIONS,
   D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+  getStandardizedControls,
   sections,
   sharedControls,
-  ControlStateMapping,
-  getStandardizedControls,
-  D3_FORMAT_DOCS,
 } from '@superset-ui/chart-controls';
-import { DEFAULT_FORM_DATA, EchartsFunnelLabelTypeType } from './types';
+import {
+  DEFAULT_FORM_DATA,
+  EchartsFunnelLabelTypeType,
+  PercentCalcType,
+} from './types';
 import { legendSection } from '../controls';
 
 const { labelType, numberFormat, showLabels, defaultTooltipLabel } =
@@ -70,6 +74,25 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [
+          {
+            name: 'percent_calculation_type',
+            config: {
+              type: 'SelectControl',
+              label: t('% calculation'),
+              description: t(
+                'Display percents in the label and tooltip as the percent of the total value, from the first step of the funnel, or from the previous step in the funnel.',
+              ),
+              choices: [
+                [PercentCalcType.FIRST_STEP, t('Calculate from first step')],
+                [PercentCalcType.PREV_STEP, t('Calculate from previous step')],
+                [PercentCalcType.TOTAL, t('Percent of total')],
+              ],
+              default: PercentCalcType.FIRST_STEP,
+              renderTrigger: true,
+            },
+          },
+        ],
       ],
     },
     {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
index 6b76d16074..a8d8c9e65c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts
@@ -19,12 +19,12 @@
 import {
   CategoricalColorNamespace,
   DataRecord,
+  getColumnLabel,
   getMetricLabel,
   getNumberFormatter,
+  getValueFormatter,
   NumberFormats,
   ValueFormatter,
-  getColumnLabel,
-  getValueFormatter,
 } from '@superset-ui/core';
 import { CallbackDataParams } from 'echarts/types/src/util/types';
 import { EChartsCoreOption, FunnelSeriesOption } from 'echarts';
@@ -34,6 +34,7 @@ import {
   EchartsFunnelFormData,
   EchartsFunnelLabelTypeType,
   FunnelChartTransformedProps,
+  PercentCalcType,
 } from './types';
 import {
   extractGroupbyLabel,
@@ -43,7 +44,7 @@ import {
   sanitizeHtml,
 } from '../utils/series';
 import { defaultGrid } from '../defaults';
-import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants';
+import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { Refs } from '../types';
 
@@ -53,17 +54,32 @@ export function formatFunnelLabel({
   params,
   labelType,
   numberFormatter,
+  percentCalculationType = PercentCalcType.FIRST_STEP,
   sanitizeName = false,
 }: {
-  params: Pick<CallbackDataParams, 'name' | 'value' | 'percent'>;
+  params: Pick<CallbackDataParams, 'name' | 'value' | 'percent' | 'data'>;
   labelType: EchartsFunnelLabelTypeType;
   numberFormatter: ValueFormatter;
+  percentCalculationType?: PercentCalcType;
   sanitizeName?: boolean;
 }): string {
-  const { name: rawName = '', value, percent } = params;
+  const { name: rawName = '', value, percent: totalPercent, data } = params;
   const name = sanitizeName ? sanitizeHtml(rawName) : rawName;
   const formattedValue = numberFormatter(value as number);
-  const formattedPercent = percentFormatter((percent as number) / 100);
+  const { firstStepPercent, prevStepPercent } = data as {
+    firstStepPercent: number;
+    prevStepPercent: number;
+  };
+  let percent;
+
+  if (percentCalculationType === PercentCalcType.TOTAL) {
+    percent = (totalPercent ?? 0) / 100;
+  } else if (percentCalculationType === PercentCalcType.PREV_STEP) {
+    percent = prevStepPercent ?? 0;
+  } else {
+    percent = firstStepPercent ?? 0;
+  }
+  const formattedPercent = percentFormatter(percent);
 
   switch (labelType) {
     case EchartsFunnelLabelTypeType.Key:
@@ -119,6 +135,7 @@ export default function transformProps(
     showTooltipLabels,
     showLegend,
     sliceId,
+    percentCalculationType,
   }: EchartsFunnelFormData = {
     ...DEFAULT_LEGEND_FORM_DATA,
     ...DEFAULT_FUNNEL_FORM_DATA,
@@ -154,16 +171,24 @@ export default function transformProps(
     currencyFormat,
   );
 
-  const transformedData: FunnelSeriesOption[] = data.map(datum => {
+  const transformedData: {
+    value: number;
+    name: string;
+    itemStyle: { color: string; opacity: OpacityEnum };
+  }[] = data.map((datum, index) => {
     const name = extractGroupbyLabel({
       datum,
       groupby: groupbyLabels,
       coltypeMapping: {},
     });
+    const value = datum[metricLabel] as number;
     const isFiltered =
       filterState.selectedValues && !filterState.selectedValues.includes(name);
+    const firstStepPercent = value / (data[0][metricLabel] as number);
+    const prevStepPercent =
+      index === 0 ? 1 : value / (data[index - 1][metricLabel] as number);
     return {
-      value: datum[metricLabel],
+      value,
       name,
       itemStyle: {
         color: colorFn(name, sliceId),
@@ -171,6 +196,8 @@ export default function transformProps(
           ? OpacityEnum.SemiTransparent
           : OpacityEnum.NonTransparent,
       },
+      firstStepPercent,
+      prevStepPercent,
     };
   });
 
@@ -188,7 +215,12 @@ export default function transformProps(
   );
 
   const formatter = (params: CallbackDataParams) =>
-    formatFunnelLabel({ params, numberFormatter, labelType });
+    formatFunnelLabel({
+      params,
+      numberFormatter,
+      labelType,
+      percentCalculationType,
+    });
 
   const defaultLabel = {
     formatter,
@@ -237,6 +269,7 @@ export default function transformProps(
           params,
           numberFormatter,
           labelType: tooltipLabelType,
+          percentCalculationType,
         }),
     },
     legend: {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts
index 3c58a7e0e4..928664e223 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts
@@ -42,6 +42,7 @@ export type EchartsFunnelFormData = QueryFormData &
     gap: number;
     sort: 'descending' | 'ascending' | 'none' | undefined;
     orient: 'vertical' | 'horizontal' | undefined;
+    percentCalculationType: PercentCalcType;
   };
 
 export enum EchartsFunnelLabelTypeType {
@@ -78,3 +79,9 @@ export type FunnelChartTransformedProps =
   BaseTransformedProps<EchartsFunnelFormData> &
     CrossFilterTransformedProps &
     ContextMenuTransformedProps;
+
+export enum PercentCalcType {
+  TOTAL = 'total',
+  PREV_STEP = 'prev_step',
+  FIRST_STEP = 'first_step',
+}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts
index b71bab2ceb..9c1d35cdd3 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts
@@ -27,6 +27,7 @@ import transformProps, {
 import {
   EchartsFunnelChartProps,
   EchartsFunnelLabelTypeType,
+  PercentCalcType,
 } from '../../src/Funnel/types';
 
 describe('Funnel transformProps', () => {
@@ -81,12 +82,18 @@ describe('Funnel transformProps', () => {
 describe('formatFunnelLabel', () => {
   it('should generate a valid funnel chart label', () => {
     const numberFormatter = getNumberFormatter();
-    const params = { name: 'My Label', value: 1234, percent: 12.34 };
+    const params = {
+      name: 'My Label',
+      value: 1234,
+      percent: 12.34,
+      data: { firstStepPercent: 0.5, prevStepPercent: 0.85 },
+    };
     expect(
       formatFunnelLabel({
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.Key,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('My Label');
     expect(
@@ -94,6 +101,7 @@ describe('formatFunnelLabel', () => {
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.Value,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('1.23k');
     expect(
@@ -101,13 +109,31 @@ describe('formatFunnelLabel', () => {
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.Percent,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('12.34%');
+    expect(
+      formatFunnelLabel({
+        params,
+        numberFormatter,
+        labelType: EchartsFunnelLabelTypeType.Percent,
+        percentCalculationType: PercentCalcType.FIRST_STEP,
+      }),
+    ).toEqual('50.00%');
+    expect(
+      formatFunnelLabel({
+        params,
+        numberFormatter,
+        labelType: EchartsFunnelLabelTypeType.Percent,
+        percentCalculationType: PercentCalcType.PREV_STEP,
+      }),
+    ).toEqual('85.00%');
     expect(
       formatFunnelLabel({
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.KeyValue,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('My Label: 1.23k');
     expect(
@@ -115,6 +141,7 @@ describe('formatFunnelLabel', () => {
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.KeyPercent,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('My Label: 12.34%');
     expect(
@@ -122,6 +149,7 @@ describe('formatFunnelLabel', () => {
         params,
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.KeyValuePercent,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('My Label: 1.23k (12.34%)');
     expect(
@@ -129,6 +157,7 @@ describe('formatFunnelLabel', () => {
         params: { ...params, name: '<NULL>' },
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.Key,
+        percentCalculationType: PercentCalcType.TOTAL,
       }),
     ).toEqual('<NULL>');
     expect(
@@ -136,6 +165,7 @@ describe('formatFunnelLabel', () => {
         params: { ...params, name: '<NULL>' },
         numberFormatter,
         labelType: EchartsFunnelLabelTypeType.Key,
+        percentCalculationType: PercentCalcType.TOTAL,
         sanitizeName: true,
       }),
     ).toEqual('&lt;NULL&gt;');
diff --git a/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py b/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py
new file mode 100644
index 0000000000..22b750b761
--- /dev/null
+++ b/superset/migrations/versions/2023-12-15_17-58_06dd9ff00fe8_add_percent_calculation_type_funnel_.py
@@ -0,0 +1,74 @@
+# 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.
+"""add_percent_calculation_type_funnel_chart
+
+Revision ID: 06dd9ff00fe8
+Revises: b7851ee5522f
+Create Date: 2023-12-15 17:58:18.277951
+
+"""
+import json
+
+from alembic import op
+from sqlalchemy import Column, Integer, String, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+from superset import db
+from superset.migrations.shared.utils import paginated_update
+
+# revision identifiers, used by Alembic.
+revision = "06dd9ff00fe8"
+down_revision = "b7851ee5522f"
+
+Base = declarative_base()
+
+
+class Slice(Base):
+    __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)
+
+    for slc in paginated_update(
+        session.query(Slice).filter(Slice.viz_type == "funnel")
+    ):
+        params = json.loads(slc.params)
+        percent_calculation = params.get("percent_calculation_type")
+        if not percent_calculation:
+            params["percent_calculation_type"] = "total"
+            slc.params = json.dumps(params)
+    session.close()
+
+
+def downgrade():
+    bind = op.get_bind()
+    session = db.Session(bind=bind)
+
+    for slc in paginated_update(
+        session.query(Slice).filter(Slice.viz_type == "funnel")
+    ):
+        params = json.loads(slc.params)
+        percent_calculation = params.get("percent_calculation_type")
+        if percent_calculation:
+            del params["percent_calculation_type"]
+            slc.params = json.dumps(params)
+    session.close()