From 1f15d0bb6a602206df9b7ad824a0795ee9085dc2 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 10 Apr 2025 22:26:50 -0600 Subject: [PATCH] =?UTF-8?q?bar=20percentage=20mode=20updates=20?= =?UTF-8?q?=F0=9F=AA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/buster_rest/nextjs/currency.ts | 2 - .../BusterChartJS/BusterChartJSComponent.tsx | 3 +- .../helpers/formatBarAndLineDataLabel.ts | 9 ++- .../BusterChartJSTooltip.tsx | 19 +++++- .../barAndLineTooltipHelper.ts | 9 ++- .../useOptions/useTooltipOptions.ts/helper.ts | 5 +- .../useTooltipOptions.tsx | 10 ++- .../hooks/useOptions/useXAxis.ts | 11 +++- .../useSeriesOptions/barSeriesBuilder.ts | 30 ++++++--- .../hooks/useSeriesOptions/interfaces.ts | 1 + .../useSeriesOptions/lineSeriesBuilder.ts | 8 ++- .../useSeriesOptions/useSeriesOptions.ts | 7 +- .../stories/BusterChart.LineChart.stories.tsx | 66 +++++++++++++++++++ .../SelectAxisDropdownContent.tsx | 2 +- 14 files changed, 152 insertions(+), 30 deletions(-) diff --git a/web/src/api/buster_rest/nextjs/currency.ts b/web/src/api/buster_rest/nextjs/currency.ts index aa04a7fa5..e335620ba 100644 --- a/web/src/api/buster_rest/nextjs/currency.ts +++ b/web/src/api/buster_rest/nextjs/currency.ts @@ -3,8 +3,6 @@ import { queryKeys } from '@/api/query_keys'; import { useQuery } from '@tanstack/react-query'; export const useGetCurrencies = () => { - nextApi; - return useQuery({ ...queryKeys.getCurrencies, queryFn: async () => { diff --git a/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx b/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx index 069e4292e..f16bb48a2 100644 --- a/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx +++ b/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx @@ -95,7 +95,8 @@ export const BusterChartJSComponent = React.memo( scatterDotSize, lineGroupType, categoryKeys: (selectedAxis as ScatterAxis).category, - trendlineSeries + trendlineSeries, + barGroupType }); const { chartPlugins, chartOptions } = useChartSpecificOptions({ diff --git a/web/src/components/ui/charts/BusterChartJS/helpers/formatBarAndLineDataLabel.ts b/web/src/components/ui/charts/BusterChartJS/helpers/formatBarAndLineDataLabel.ts index a226d135c..34e4359e7 100644 --- a/web/src/components/ui/charts/BusterChartJS/helpers/formatBarAndLineDataLabel.ts +++ b/web/src/components/ui/charts/BusterChartJS/helpers/formatBarAndLineDataLabel.ts @@ -5,10 +5,10 @@ import type { Context } from 'chartjs-plugin-datalabels'; export const formatBarAndLineDataLabel = ( value: number, context: Context, - usePercentage: boolean | undefined, + percentageMode: false | 'stacked' | 'data-label', columnLabelFormat: ColumnLabelFormat ) => { - if (!usePercentage) { + if (!percentageMode) { return formatLabel(value, columnLabelFormat); } @@ -16,7 +16,10 @@ export const formatBarAndLineDataLabel = ( (dataset) => !dataset.hidden && !dataset.isTrendline ); const hasMultipleDatasets = shownDatasets.length > 1; - const total: number = hasMultipleDatasets + + const useStackTotal = !hasMultipleDatasets || percentageMode === 'stacked'; + + const total: number = useStackTotal ? context.chart.$totalizer.stackTotals[context.dataIndex] : context.chart.$totalizer.seriesTotals[context.datasetIndex]; const percentage = ((value as number) / total) * 100; diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/BusterChartJSTooltip.tsx b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/BusterChartJSTooltip.tsx index 8cc756137..33c7ecd20 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/BusterChartJSTooltip.tsx +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/BusterChartJSTooltip.tsx @@ -16,6 +16,8 @@ export const BusterChartJSTooltip: React.FC<{ hasCategoryAxis: boolean; hasMultipleMeasures: boolean; keyToUsePercentage: string[]; + lineGroupType: BusterChartProps['lineGroupType']; + barGroupType: BusterChartProps['barGroupType']; }> = ({ chart, dataPoints: dataPointsProp, @@ -23,7 +25,9 @@ export const BusterChartJSTooltip: React.FC<{ selectedChartType, hasCategoryAxis, keyToUsePercentage, - hasMultipleMeasures + hasMultipleMeasures, + lineGroupType, + barGroupType }) => { const isPieChart = selectedChartType === ChartType.Pie; const isScatter = selectedChartType === ChartType.Scatter; @@ -34,6 +38,16 @@ export const BusterChartJSTooltip: React.FC<{ const datasets = chart.data.datasets; const dataPoints = dataPointsProp.filter((item) => !item.dataset.isTrendline); + const percentageMode: undefined | 'stacked' = useMemo(() => { + if (isBar) { + return barGroupType === 'percentage-stack' ? 'stacked' : undefined; + } + if (isLine) { + return lineGroupType === 'percentage-stack' ? 'stacked' : undefined; + } + return undefined; + }, [isBar, barGroupType, isLine, lineGroupType]); + const tooltipItems: ITooltipItem[] = useMemo(() => { if (isBar || isLine || isComboChart) { const hasMultipleShownDatasets = @@ -47,7 +61,8 @@ export const BusterChartJSTooltip: React.FC<{ hasMultipleMeasures, keyToUsePercentage, hasCategoryAxis, - hasMultipleShownDatasets + hasMultipleShownDatasets, + percentageMode ); } diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/barAndLineTooltipHelper.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/barAndLineTooltipHelper.ts index 8807d2543..bb7239145 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/barAndLineTooltipHelper.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/barAndLineTooltipHelper.ts @@ -13,7 +13,8 @@ export const barAndLineTooltipHelper = ( hasMultipleMeasures: boolean, keyToUsePercentage: string[], hasCategoryAxis: boolean, - hasMultipleShownDatasets: boolean + hasMultipleShownDatasets: boolean, + percentageMode: undefined | 'stacked' ): ITooltipItem[] => { const dataPoint = dataPoints[0]; const dataPointDataset = dataPoint.dataset; @@ -28,7 +29,8 @@ export const barAndLineTooltipHelper = ( // } const tooltipItems = tooltipDatasets.map((tooltipDataset) => { - const usePercentage = keyToUsePercentage.includes(tooltipDataset.label as string); + const usePercentage = + !!percentageMode || keyToUsePercentage.includes(tooltipDataset.label as string); const assosciatedData = datasets.find((dataset) => dataset.label === tooltipDataset.label); const colorItem = assosciatedData?.backgroundColor as string; const color = assosciatedData @@ -47,7 +49,8 @@ export const barAndLineTooltipHelper = ( tooltipDataset.label as string, columnLabelFormats, chart, - hasMultipleShownDatasets + hasMultipleShownDatasets, + percentageMode ) : undefined; diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/helper.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/helper.ts index 79efe5dcc..ecd58fcf7 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/helper.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/helper.ts @@ -10,9 +10,10 @@ export const getPercentage = ( datasetKey: string, columnLabelFormats: NonNullable, chart: Chart, - hasMultipleShownDatasets: boolean + hasMultipleShownDatasets: boolean, + percentageMode: undefined | 'stacked' ) => { - if (hasMultipleShownDatasets) { + if (hasMultipleShownDatasets || percentageMode === 'stacked') { return getStackedPercentage(rawValue, dataIndex, datasetKey, columnLabelFormats, chart); } diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/useTooltipOptions.tsx b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/useTooltipOptions.tsx index a6e5349c0..aa8ad65dc 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/useTooltipOptions.tsx +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useTooltipOptions.ts/useTooltipOptions.tsx @@ -131,7 +131,9 @@ export const useTooltipOptions = ({ keyToUsePercentage, columnSettings, hasCategoryAxis, - hasMultipleMeasures + hasMultipleMeasures, + barGroupType, + lineGroupType ); if (result) { @@ -225,7 +227,9 @@ const externalTooltip = ( keyToUsePercentage: string[], columnSettings: NonNullable, hasCategoryAxis: boolean, - hasMultipleMeasures: boolean + hasMultipleMeasures: boolean, + barGroupType: BusterChartProps['barGroupType'], + lineGroupType: BusterChartProps['lineGroupType'] ) => { const { chart, tooltip } = context; const tooltipEl = getOrCreateInitialTooltipContainer(chart)!; @@ -251,6 +255,8 @@ const externalTooltip = ( chart={chart} hasCategoryAxis={hasCategoryAxis} hasMultipleMeasures={hasMultipleMeasures} + barGroupType={barGroupType} + lineGroupType={lineGroupType} /> ); } diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis.ts index 4b7490a2d..dbb4e949e 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis.ts @@ -72,7 +72,7 @@ export const useXAxis = ({ return { display: useGrid && gridLines, offset: true - }; + } satisfies DeepPartial; }, [gridLines, useGrid]); const type: DeepPartial['scales']['x']['type']> = useMemo(() => { @@ -168,12 +168,18 @@ export const useXAxis = ({ return false; }, [type, xAxisTimeInterval]); + const offset = useMemo(() => { + if (isScatterChart) return false; + if (isLineChart) return lineGroupType !== 'percentage-stack'; + return true; + }, [isScatterChart, isLineChart, lineGroupType]); + const memoizedXAxisOptions: DeepPartial['scales']['x']> | undefined = useMemo(() => { if (isPieChart) return undefined; return { type, - offset: !isScatterChart, + offset, title: { display: !!title, text: title @@ -196,6 +202,7 @@ export const useXAxis = ({ } satisfies DeepPartial['scales']['x']>; }, [ timeUnit, + offset, title, isScatterChart, isPieChart, diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts index 9e5d00592..139962edc 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts @@ -9,7 +9,11 @@ import { defaultLabelOptionConfig } from '../useChartSpecificOptions/labelOption import type { Options } from 'chartjs-plugin-datalabels/types/options'; import { DEFAULT_CHART_LAYOUT } from '../../ChartJSTheme'; import { extractFieldsFromChain } from '../../../chartHooks'; -import { DEFAULT_COLUMN_LABEL_FORMAT, IColumnLabelFormat } from '@/api/asset_interfaces/metric'; +import { + BusterChartProps, + DEFAULT_COLUMN_LABEL_FORMAT, + IColumnLabelFormat +} from '@/api/asset_interfaces/metric'; export const barSeriesBuilder = ({ selectedDataset, @@ -20,6 +24,7 @@ export const barSeriesBuilder = ({ xAxisKeys, barShowTotalAtTop, allY2AxisKeysIndexes, + barGroupType, ...rest }: SeriesBuilderProps): ChartProps<'bar'>['data']['datasets'] => { const dataLabelOptions: Options['labels'] = {}; @@ -85,7 +90,8 @@ export const barSeriesBuilder = ({ yAxisItem, index, xAxisKeys, - dataLabelOptions + dataLabelOptions, + barGroupType }); } ); @@ -116,7 +122,8 @@ export const barBuilder = ({ yAxisID, order, xAxisKeys, - dataLabelOptions + dataLabelOptions, + barGroupType }: Pick< SeriesBuilderProps, 'selectedDataset' | 'colors' | 'columnSettings' | 'columnLabelFormats' @@ -127,12 +134,19 @@ export const barBuilder = ({ order?: number; xAxisKeys: string[]; dataLabelOptions?: Options['labels']; + barGroupType: BusterChartProps['barGroupType']; }): ChartProps<'bar'>['data']['datasets'][number] => { const yKey = extractFieldsFromChain(yAxisItem.name).at(-1)?.key!; const columnSetting = columnSettings[yKey]; const columnLabelFormat = columnLabelFormats[yKey]; - const usePercentage = !!columnSetting?.showDataLabelsAsPercentage; const showLabels = !!columnSetting?.showDataLabels; + const isPercentageStackedBar = barGroupType === 'percentage-stack'; + + const percentageMode = isPercentageStackedBar + ? 'stacked' + : columnSetting?.showDataLabelsAsPercentage + ? 'data-label' + : false; return { type: 'bar', @@ -170,7 +184,7 @@ export const barBuilder = ({ if (barWidth < MAX_BAR_WIDTH) return false; const formattedValue = getFormattedValue(context, { - usePercentage, + percentageMode, columnLabelFormat: columnLabelFormat || DEFAULT_COLUMN_LABEL_FORMAT }); @@ -286,10 +300,10 @@ const setGlobalRotation = (context: Context) => { const getFormattedValue = ( context: Context, { - usePercentage, + percentageMode, columnLabelFormat }: { - usePercentage: boolean; + percentageMode: false | 'stacked' | 'data-label'; columnLabelFormat: IColumnLabelFormat; } ) => { @@ -297,7 +311,7 @@ const getFormattedValue = ( const currentValue = context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || ''; const formattedValue = - currentValue || formatBarAndLineDataLabel(rawValue, context, usePercentage, columnLabelFormat); + currentValue || formatBarAndLineDataLabel(rawValue, context, percentageMode, columnLabelFormat); // Store only the formatted value, rotation is handled globally setBarDataLabelsManager(context, formattedValue); diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/interfaces.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/interfaces.ts index 1c57715f8..d392d81bc 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/interfaces.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/interfaces.ts @@ -30,4 +30,5 @@ export interface SeriesBuilderProps { lineGroupType: BusterChartProps['lineGroupType']; selectedChartType: BusterChartProps['selectedChartType']; barShowTotalAtTop: BusterChartProps['barShowTotalAtTop']; + barGroupType: BusterChartProps['barGroupType']; } diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/lineSeriesBuilder.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/lineSeriesBuilder.ts index d3583794f..db9601a9c 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/lineSeriesBuilder.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/lineSeriesBuilder.ts @@ -89,7 +89,11 @@ export const lineBuilder = ( const isStackedArea = lineGroupType === 'percentage-stack'; const isArea = lineStyle === 'area' || isStackedArea; const fill = isArea ? (index === 0 ? 'origin' : '-1') : false; - const usePercentage = isStackedArea; + const percentageMode = isStackedArea + ? 'stacked' + : columnSetting.showDataLabelsAsPercentage + ? 'data-label' + : false; return { type: 'line', @@ -127,7 +131,7 @@ export const lineBuilder = ( } : false, formatter: (value, context) => - formatBarAndLineDataLabel(value, context, usePercentage, columnLabelFormat), + formatBarAndLineDataLabel(value, context, percentageMode, columnLabelFormat), ...getLabelPosition(isStackedArea), ...defaultLabelOptionConfig } diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/useSeriesOptions.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/useSeriesOptions.ts index 1f774c685..a777acb19 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/useSeriesOptions.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/useSeriesOptions.ts @@ -33,6 +33,7 @@ export interface UseSeriesOptionsProps { scatterDotSize: BusterChartProps['scatterDotSize']; columnMetadata: ColumnMetaData[]; lineGroupType: BusterChartProps['lineGroupType']; + barGroupType: BusterChartProps['barGroupType']; trendlineSeries: ChartProps<'line'>['data']['datasets'][number][]; barShowTotalAtTop: BusterChartProps['barShowTotalAtTop']; } @@ -53,7 +54,8 @@ export const useSeriesOptions = ({ scatterDotSize, lineGroupType, categoryKeys, - barShowTotalAtTop + barShowTotalAtTop, + barGroupType }: UseSeriesOptionsProps): ChartProps['data'] => { const selectedDataset = useMemo(() => { return datasetOptions[datasetOptions.length - 1]; @@ -134,7 +136,8 @@ export const useSeriesOptions = ({ scatterDotSize, lineGroupType, categoryKeys, - selectedChartType + selectedChartType, + barGroupType }); }, [ selectedDataset, diff --git a/web/src/components/ui/charts/stories/BusterChart.LineChart.stories.tsx b/web/src/components/ui/charts/stories/BusterChart.LineChart.stories.tsx index 6c0390138..e1cfe9ed1 100644 --- a/web/src/components/ui/charts/stories/BusterChart.LineChart.stories.tsx +++ b/web/src/components/ui/charts/stories/BusterChart.LineChart.stories.tsx @@ -371,3 +371,69 @@ export const NumericMonthX: Story = { } } }; + +export const PercentageStackedLineSingle: Story = { + args: { + selectedChartType: ChartType.Line, + data: generateLineChartData(), + lineGroupType: 'percentage-stack', + barAndLineAxis: { + x: ['date'], + y: ['revenue'], + category: [] + }, + className: 'w-[800px] h-[400px]', + columnLabelFormats: { + month: { + columnType: 'number', + style: 'date', + dateFormat: 'MMM', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + } satisfies IColumnLabelFormat, + sales: { + columnType: 'number', + style: 'currency', + currency: 'USD' + } satisfies IColumnLabelFormat, + customers: { + columnType: 'number', + style: 'number', + numberSeparatorStyle: ',' + } satisfies IColumnLabelFormat + } + } +}; + +export const PercentageStackedLineMultiple: Story = { + args: { + selectedChartType: ChartType.Line, + data: generateLineChartData(), + lineGroupType: 'percentage-stack', + barAndLineAxis: { + x: ['date'], + y: ['revenue', 'profit', 'customers'], + category: [] + }, + className: 'w-[800px] h-[400px]', + columnLabelFormats: { + date: { + columnType: 'number', + style: 'date', + dateFormat: 'll', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + } satisfies IColumnLabelFormat, + sales: { + columnType: 'number', + style: 'currency', + currency: 'USD' + } satisfies IColumnLabelFormat, + customers: { + columnType: 'number', + style: 'number', + numberSeparatorStyle: ',' + } satisfies IColumnLabelFormat + } + } +}; diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/SelectAxisDropdownContent.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/SelectAxisDropdownContent.tsx index 3a52e1e33..5548a9b05 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/SelectAxisDropdownContent.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/SelectAxisDropdownContent.tsx @@ -315,7 +315,7 @@ const LabelSettings: React.FC<{ return formatLabel(id, columnLabelFormat, true); }, [displayName]); - //THIS IS HERE JUST TO PREFETCH THE CURRENCIES + //THIS IS HERE JUST TO PREFETCH THE CURRENCIES, I guess I could use prefetch... useGetCurrencies(); const ComponentsLoop = [