diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx index b4ae075b0c878..7e700c18c92f6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx @@ -123,11 +123,31 @@ const config: ControlPanelConfig = { ['key_percent', t('Category and Percentage')], ['key_value_percent', t('Category, Value and Percentage')], ['value_percent', t('Value and Percentage')], + ['template', t('Template')], ], description: t('What should be shown on the label?'), }, }, ], + [ + { + name: 'label_template', + config: { + type: 'TextControl', + label: t('Label Template'), + renderTrigger: true, + description: t( + 'Format data labels. ' + + 'Use variables: {name}, {value}, {percent}. ' + + '\\n represents a new line. ' + + 'ECharts compatibility:\n' + + '{a} (series), {b} (name), {c} (value), {d} (percentage)', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + controls?.label_type?.value === 'template', + }, + }, + ], [ { name: 'number_format', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index d6400d4022f5c..40d49908e9f98 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -143,6 +143,7 @@ export default function transformProps( labelsOutside, labelLine, labelType, + labelTemplate, legendMargin, legendOrientation, legendType, @@ -242,6 +243,38 @@ export default function transformProps( {}, ); + const formatTemplate = ( + template: string, + formattedParams: { + name: string; + value: string; + percent: string; + }, + rawParams: CallbackDataParams, + ) => { + // This function supports two forms of template variables: + // 1. {name}, {value}, {percent}, for values formatted by number formatter. + // 2. {a}, {b}, {c}, {d}, compatible with ECharts formatter. + // + // \n is supported to represent a new line. + + const items = { + '{name}': formattedParams.name, + '{value}': formattedParams.value, + '{percent}': formattedParams.percent, + '{a}': rawParams.seriesName || '', + '{b}': rawParams.name, + '{c}': `${rawParams.value}`, + '{d}': `${rawParams.percent}`, + '\\n': '\n', + }; + + return Object.entries(items).reduce( + (acc, [key, value]) => acc.replaceAll(key, value), + template, + ); + }; + const formatter = (params: CallbackDataParams) => { const [name, formattedValue, formattedPercent] = parseParams({ params, @@ -262,6 +295,19 @@ export default function transformProps( return `${name}: ${formattedPercent}`; case EchartsPieLabelType.ValuePercent: return `${formattedValue} (${formattedPercent})`; + case EchartsPieLabelType.Template: + if (!labelTemplate) { + return ''; + } + return formatTemplate( + labelTemplate, + { + name, + value: formattedValue, + percent: formattedPercent, + }, + params, + ); default: return name; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index c72da1feed07f..06ff2bc41af73 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -38,6 +38,7 @@ export type EchartsPieFormData = QueryFormData & innerRadius: number; labelLine: boolean; labelType: EchartsPieLabelType; + labelTemplate: string | null; labelsOutside: boolean; metric?: string; outerRadius: number; @@ -56,6 +57,7 @@ export enum EchartsPieLabelType { KeyPercent = 'key_percent', KeyValuePercent = 'key_value_percent', ValuePercent = 'value_percent', + Template = 'template', } export interface EchartsPieChartProps diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts index 1add75d40151f..e0c1992574290 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts @@ -22,6 +22,8 @@ import { SqlaFormData, supersetTheme, } from '@superset-ui/core'; +import { LabelFormatterCallback, PieSeriesOption } from 'echarts'; +import { CallbackDataParams } from 'echarts/types/src/util/types'; import transformProps, { parseParams } from '../../src/Pie/transformProps'; import { EchartsPieChartProps } from '../../src/Pie/types'; @@ -101,3 +103,112 @@ describe('formatPieLabel', () => { ).toEqual(['<NULL>', '1.23k', '12.34%']); }); }); + +describe('Pie label string template', () => { + const params: CallbackDataParams = { + componentType: '', + componentSubType: '', + componentIndex: 0, + seriesType: 'pie', + seriesIndex: 0, + seriesId: 'seriesId', + seriesName: 'test', + name: 'Tablet', + dataIndex: 0, + data: {}, + value: 123456, + percent: 55.5, + $vars: [], + }; + + const getChartProps = (form: Partial): EchartsPieChartProps => { + const formData: SqlaFormData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'sum__num', + groupby: ['foo', 'bar'], + viz_type: 'my_viz', + ...form, + }; + + return new ChartProps({ + formData, + width: 800, + height: 600, + queriesData: [ + { + data: [ + { foo: 'Sylvester', bar: 1, sum__num: 10 }, + { foo: 'Arnold', bar: 2, sum__num: 2.5 }, + ], + }, + ], + theme: supersetTheme, + }) as EchartsPieChartProps; + }; + + const format = (form: Partial) => { + const props = transformProps(getChartProps(form)); + expect(props).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: [ + expect.objectContaining({ + avoidLabelOverlap: true, + data: expect.arrayContaining([ + expect.objectContaining({ + name: 'Arnold, 2', + value: 2.5, + }), + expect.objectContaining({ + name: 'Sylvester, 1', + value: 10, + }), + ]), + label: expect.objectContaining({ + formatter: expect.any(Function), + }), + }), + ], + }), + }), + ); + + const formatter = (props.echartOptions.series as PieSeriesOption[])[0]! + .label?.formatter; + + return (formatter as LabelFormatterCallback)(params); + }; + + it('should generate a valid pie chart label with template', () => { + expect( + format({ + label_type: 'template', + label_template: '{name}:{value}\n{percent}', + }), + ).toEqual('Tablet:123k\n55.50%'); + }); + + it('should be formatted using the number formatter', () => { + expect( + format({ + label_type: 'template', + label_template: '{name}:{value}\n{percent}', + number_format: ',d', + }), + ).toEqual('Tablet:123,456\n55.50%'); + }); + + it('should be compatible with ECharts raw variable syntax', () => { + expect( + format({ + label_type: 'template', + label_template: '{b}:{c}\n{d}', + number_format: ',d', + }), + ).toEqual('Tablet:123456\n55.5'); + }); +});