Skip to content

Commit

Permalink
feat(echarts-funnel): Implement % calculation type (#26290)
Browse files Browse the repository at this point in the history
  • Loading branch information
kgabryje authored Dec 22, 2023
1 parent 39ac453 commit 5400d30
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down Expand Up @@ -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,
},
},
],
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +34,7 @@ import {
EchartsFunnelFormData,
EchartsFunnelLabelTypeType,
FunnelChartTransformedProps,
PercentCalcType,
} from './types';
import {
extractGroupbyLabel,
Expand All @@ -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';

Expand All @@ -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:
Expand Down Expand Up @@ -119,6 +135,7 @@ export default function transformProps(
showTooltipLabels,
showLegend,
sliceId,
percentCalculationType,
}: EchartsFunnelFormData = {
...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_FUNNEL_FORM_DATA,
Expand Down Expand Up @@ -154,23 +171,33 @@ 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),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,
},
firstStepPercent,
prevStepPercent,
};
});

Expand All @@ -188,7 +215,12 @@ export default function transformProps(
);

const formatter = (params: CallbackDataParams) =>
formatFunnelLabel({ params, numberFormatter, labelType });
formatFunnelLabel({
params,
numberFormatter,
labelType,
percentCalculationType,
});

const defaultLabel = {
formatter,
Expand Down Expand Up @@ -237,6 +269,7 @@ export default function transformProps(
params,
numberFormatter,
labelType: tooltipLabelType,
percentCalculationType,
}),
},
legend: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type EchartsFunnelFormData = QueryFormData &
gap: number;
sort: 'descending' | 'ascending' | 'none' | undefined;
orient: 'vertical' | 'horizontal' | undefined;
percentCalculationType: PercentCalcType;
};

export enum EchartsFunnelLabelTypeType {
Expand Down Expand Up @@ -78,3 +79,9 @@ export type FunnelChartTransformedProps =
BaseTransformedProps<EchartsFunnelFormData> &
CrossFilterTransformedProps &
ContextMenuTransformedProps;

export enum PercentCalcType {
TOTAL = 'total',
PREV_STEP = 'prev_step',
FIRST_STEP = 'first_step',
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import transformProps, {
import {
EchartsFunnelChartProps,
EchartsFunnelLabelTypeType,
PercentCalcType,
} from '../../src/Funnel/types';

describe('Funnel transformProps', () => {
Expand Down Expand Up @@ -81,61 +82,90 @@ 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(
formatFunnelLabel({
params,
numberFormatter,
labelType: EchartsFunnelLabelTypeType.Value,
percentCalculationType: PercentCalcType.TOTAL,
}),
).toEqual('1.23k');
expect(
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(
formatFunnelLabel({
params,
numberFormatter,
labelType: EchartsFunnelLabelTypeType.KeyPercent,
percentCalculationType: PercentCalcType.TOTAL,
}),
).toEqual('My Label: 12.34%');
expect(
formatFunnelLabel({
params,
numberFormatter,
labelType: EchartsFunnelLabelTypeType.KeyValuePercent,
percentCalculationType: PercentCalcType.TOTAL,
}),
).toEqual('My Label: 1.23k (12.34%)');
expect(
formatFunnelLabel({
params: { ...params, name: '<NULL>' },
numberFormatter,
labelType: EchartsFunnelLabelTypeType.Key,
percentCalculationType: PercentCalcType.TOTAL,
}),
).toEqual('<NULL>');
expect(
formatFunnelLabel({
params: { ...params, name: '<NULL>' },
numberFormatter,
labelType: EchartsFunnelLabelTypeType.Key,
percentCalculationType: PercentCalcType.TOTAL,
sanitizeName: true,
}),
).toEqual('&lt;NULL&gt;');
Expand Down
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 5400d30

Please sign in to comment.