mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat(echarts-funnel): Implement % calculation type (#26290)
This commit is contained in:
committed by
GitHub
parent
39ac45351b
commit
5400d30b20
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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>');
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user