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('<NULL>');
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()