bar percentage mode updates 🪿

This commit is contained in:
Nate Kelley 2025-04-10 22:26:50 -06:00
parent 4e068074c7
commit 1f15d0bb6a
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
14 changed files with 152 additions and 30 deletions

View File

@ -3,8 +3,6 @@ import { queryKeys } from '@/api/query_keys';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
export const useGetCurrencies = () => { export const useGetCurrencies = () => {
nextApi;
return useQuery({ return useQuery({
...queryKeys.getCurrencies, ...queryKeys.getCurrencies,
queryFn: async () => { queryFn: async () => {

View File

@ -95,7 +95,8 @@ export const BusterChartJSComponent = React.memo(
scatterDotSize, scatterDotSize,
lineGroupType, lineGroupType,
categoryKeys: (selectedAxis as ScatterAxis).category, categoryKeys: (selectedAxis as ScatterAxis).category,
trendlineSeries trendlineSeries,
barGroupType
}); });
const { chartPlugins, chartOptions } = useChartSpecificOptions({ const { chartPlugins, chartOptions } = useChartSpecificOptions({

View File

@ -5,10 +5,10 @@ import type { Context } from 'chartjs-plugin-datalabels';
export const formatBarAndLineDataLabel = ( export const formatBarAndLineDataLabel = (
value: number, value: number,
context: Context, context: Context,
usePercentage: boolean | undefined, percentageMode: false | 'stacked' | 'data-label',
columnLabelFormat: ColumnLabelFormat columnLabelFormat: ColumnLabelFormat
) => { ) => {
if (!usePercentage) { if (!percentageMode) {
return formatLabel(value, columnLabelFormat); return formatLabel(value, columnLabelFormat);
} }
@ -16,7 +16,10 @@ export const formatBarAndLineDataLabel = (
(dataset) => !dataset.hidden && !dataset.isTrendline (dataset) => !dataset.hidden && !dataset.isTrendline
); );
const hasMultipleDatasets = shownDatasets.length > 1; 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.stackTotals[context.dataIndex]
: context.chart.$totalizer.seriesTotals[context.datasetIndex]; : context.chart.$totalizer.seriesTotals[context.datasetIndex];
const percentage = ((value as number) / total) * 100; const percentage = ((value as number) / total) * 100;

View File

@ -16,6 +16,8 @@ export const BusterChartJSTooltip: React.FC<{
hasCategoryAxis: boolean; hasCategoryAxis: boolean;
hasMultipleMeasures: boolean; hasMultipleMeasures: boolean;
keyToUsePercentage: string[]; keyToUsePercentage: string[];
lineGroupType: BusterChartProps['lineGroupType'];
barGroupType: BusterChartProps['barGroupType'];
}> = ({ }> = ({
chart, chart,
dataPoints: dataPointsProp, dataPoints: dataPointsProp,
@ -23,7 +25,9 @@ export const BusterChartJSTooltip: React.FC<{
selectedChartType, selectedChartType,
hasCategoryAxis, hasCategoryAxis,
keyToUsePercentage, keyToUsePercentage,
hasMultipleMeasures hasMultipleMeasures,
lineGroupType,
barGroupType
}) => { }) => {
const isPieChart = selectedChartType === ChartType.Pie; const isPieChart = selectedChartType === ChartType.Pie;
const isScatter = selectedChartType === ChartType.Scatter; const isScatter = selectedChartType === ChartType.Scatter;
@ -34,6 +38,16 @@ export const BusterChartJSTooltip: React.FC<{
const datasets = chart.data.datasets; const datasets = chart.data.datasets;
const dataPoints = dataPointsProp.filter((item) => !item.dataset.isTrendline); 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(() => { const tooltipItems: ITooltipItem[] = useMemo(() => {
if (isBar || isLine || isComboChart) { if (isBar || isLine || isComboChart) {
const hasMultipleShownDatasets = const hasMultipleShownDatasets =
@ -47,7 +61,8 @@ export const BusterChartJSTooltip: React.FC<{
hasMultipleMeasures, hasMultipleMeasures,
keyToUsePercentage, keyToUsePercentage,
hasCategoryAxis, hasCategoryAxis,
hasMultipleShownDatasets hasMultipleShownDatasets,
percentageMode
); );
} }

View File

@ -13,7 +13,8 @@ export const barAndLineTooltipHelper = (
hasMultipleMeasures: boolean, hasMultipleMeasures: boolean,
keyToUsePercentage: string[], keyToUsePercentage: string[],
hasCategoryAxis: boolean, hasCategoryAxis: boolean,
hasMultipleShownDatasets: boolean hasMultipleShownDatasets: boolean,
percentageMode: undefined | 'stacked'
): ITooltipItem[] => { ): ITooltipItem[] => {
const dataPoint = dataPoints[0]; const dataPoint = dataPoints[0];
const dataPointDataset = dataPoint.dataset; const dataPointDataset = dataPoint.dataset;
@ -28,7 +29,8 @@ export const barAndLineTooltipHelper = (
// } // }
const tooltipItems = tooltipDatasets.map<ITooltipItem>((tooltipDataset) => { const tooltipItems = tooltipDatasets.map<ITooltipItem>((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 assosciatedData = datasets.find((dataset) => dataset.label === tooltipDataset.label);
const colorItem = assosciatedData?.backgroundColor as string; const colorItem = assosciatedData?.backgroundColor as string;
const color = assosciatedData const color = assosciatedData
@ -47,7 +49,8 @@ export const barAndLineTooltipHelper = (
tooltipDataset.label as string, tooltipDataset.label as string,
columnLabelFormats, columnLabelFormats,
chart, chart,
hasMultipleShownDatasets hasMultipleShownDatasets,
percentageMode
) )
: undefined; : undefined;

View File

@ -10,9 +10,10 @@ export const getPercentage = (
datasetKey: string, datasetKey: string,
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>, columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>,
chart: Chart, chart: Chart,
hasMultipleShownDatasets: boolean hasMultipleShownDatasets: boolean,
percentageMode: undefined | 'stacked'
) => { ) => {
if (hasMultipleShownDatasets) { if (hasMultipleShownDatasets || percentageMode === 'stacked') {
return getStackedPercentage(rawValue, dataIndex, datasetKey, columnLabelFormats, chart); return getStackedPercentage(rawValue, dataIndex, datasetKey, columnLabelFormats, chart);
} }

View File

@ -131,7 +131,9 @@ export const useTooltipOptions = ({
keyToUsePercentage, keyToUsePercentage,
columnSettings, columnSettings,
hasCategoryAxis, hasCategoryAxis,
hasMultipleMeasures hasMultipleMeasures,
barGroupType,
lineGroupType
); );
if (result) { if (result) {
@ -225,7 +227,9 @@ const externalTooltip = (
keyToUsePercentage: string[], keyToUsePercentage: string[],
columnSettings: NonNullable<BusterChartProps['columnSettings']>, columnSettings: NonNullable<BusterChartProps['columnSettings']>,
hasCategoryAxis: boolean, hasCategoryAxis: boolean,
hasMultipleMeasures: boolean hasMultipleMeasures: boolean,
barGroupType: BusterChartProps['barGroupType'],
lineGroupType: BusterChartProps['lineGroupType']
) => { ) => {
const { chart, tooltip } = context; const { chart, tooltip } = context;
const tooltipEl = getOrCreateInitialTooltipContainer(chart)!; const tooltipEl = getOrCreateInitialTooltipContainer(chart)!;
@ -251,6 +255,8 @@ const externalTooltip = (
chart={chart} chart={chart}
hasCategoryAxis={hasCategoryAxis} hasCategoryAxis={hasCategoryAxis}
hasMultipleMeasures={hasMultipleMeasures} hasMultipleMeasures={hasMultipleMeasures}
barGroupType={barGroupType}
lineGroupType={lineGroupType}
/> />
); );
} }

View File

@ -72,7 +72,7 @@ export const useXAxis = ({
return { return {
display: useGrid && gridLines, display: useGrid && gridLines,
offset: true offset: true
}; } satisfies DeepPartial<GridLineOptions>;
}, [gridLines, useGrid]); }, [gridLines, useGrid]);
const type: DeepPartial<ScaleChartOptions<'bar'>['scales']['x']['type']> = useMemo(() => { const type: DeepPartial<ScaleChartOptions<'bar'>['scales']['x']['type']> = useMemo(() => {
@ -168,12 +168,18 @@ export const useXAxis = ({
return false; return false;
}, [type, xAxisTimeInterval]); }, [type, xAxisTimeInterval]);
const offset = useMemo(() => {
if (isScatterChart) return false;
if (isLineChart) return lineGroupType !== 'percentage-stack';
return true;
}, [isScatterChart, isLineChart, lineGroupType]);
const memoizedXAxisOptions: DeepPartial<ScaleChartOptions<'bar'>['scales']['x']> | undefined = const memoizedXAxisOptions: DeepPartial<ScaleChartOptions<'bar'>['scales']['x']> | undefined =
useMemo(() => { useMemo(() => {
if (isPieChart) return undefined; if (isPieChart) return undefined;
return { return {
type, type,
offset: !isScatterChart, offset,
title: { title: {
display: !!title, display: !!title,
text: title text: title
@ -196,6 +202,7 @@ export const useXAxis = ({
} satisfies DeepPartial<ScaleChartOptions<'bar'>['scales']['x']>; } satisfies DeepPartial<ScaleChartOptions<'bar'>['scales']['x']>;
}, [ }, [
timeUnit, timeUnit,
offset,
title, title,
isScatterChart, isScatterChart,
isPieChart, isPieChart,

View File

@ -9,7 +9,11 @@ import { defaultLabelOptionConfig } from '../useChartSpecificOptions/labelOption
import type { Options } from 'chartjs-plugin-datalabels/types/options'; import type { Options } from 'chartjs-plugin-datalabels/types/options';
import { DEFAULT_CHART_LAYOUT } from '../../ChartJSTheme'; import { DEFAULT_CHART_LAYOUT } from '../../ChartJSTheme';
import { extractFieldsFromChain } from '../../../chartHooks'; 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 = ({ export const barSeriesBuilder = ({
selectedDataset, selectedDataset,
@ -20,6 +24,7 @@ export const barSeriesBuilder = ({
xAxisKeys, xAxisKeys,
barShowTotalAtTop, barShowTotalAtTop,
allY2AxisKeysIndexes, allY2AxisKeysIndexes,
barGroupType,
...rest ...rest
}: SeriesBuilderProps): ChartProps<'bar'>['data']['datasets'] => { }: SeriesBuilderProps): ChartProps<'bar'>['data']['datasets'] => {
const dataLabelOptions: Options['labels'] = {}; const dataLabelOptions: Options['labels'] = {};
@ -85,7 +90,8 @@ export const barSeriesBuilder = ({
yAxisItem, yAxisItem,
index, index,
xAxisKeys, xAxisKeys,
dataLabelOptions dataLabelOptions,
barGroupType
}); });
} }
); );
@ -116,7 +122,8 @@ export const barBuilder = ({
yAxisID, yAxisID,
order, order,
xAxisKeys, xAxisKeys,
dataLabelOptions dataLabelOptions,
barGroupType
}: Pick< }: Pick<
SeriesBuilderProps, SeriesBuilderProps,
'selectedDataset' | 'colors' | 'columnSettings' | 'columnLabelFormats' 'selectedDataset' | 'colors' | 'columnSettings' | 'columnLabelFormats'
@ -127,12 +134,19 @@ export const barBuilder = ({
order?: number; order?: number;
xAxisKeys: string[]; xAxisKeys: string[];
dataLabelOptions?: Options['labels']; dataLabelOptions?: Options['labels'];
barGroupType: BusterChartProps['barGroupType'];
}): ChartProps<'bar'>['data']['datasets'][number] => { }): ChartProps<'bar'>['data']['datasets'][number] => {
const yKey = extractFieldsFromChain(yAxisItem.name).at(-1)?.key!; const yKey = extractFieldsFromChain(yAxisItem.name).at(-1)?.key!;
const columnSetting = columnSettings[yKey]; const columnSetting = columnSettings[yKey];
const columnLabelFormat = columnLabelFormats[yKey]; const columnLabelFormat = columnLabelFormats[yKey];
const usePercentage = !!columnSetting?.showDataLabelsAsPercentage;
const showLabels = !!columnSetting?.showDataLabels; const showLabels = !!columnSetting?.showDataLabels;
const isPercentageStackedBar = barGroupType === 'percentage-stack';
const percentageMode = isPercentageStackedBar
? 'stacked'
: columnSetting?.showDataLabelsAsPercentage
? 'data-label'
: false;
return { return {
type: 'bar', type: 'bar',
@ -170,7 +184,7 @@ export const barBuilder = ({
if (barWidth < MAX_BAR_WIDTH) return false; if (barWidth < MAX_BAR_WIDTH) return false;
const formattedValue = getFormattedValue(context, { const formattedValue = getFormattedValue(context, {
usePercentage, percentageMode,
columnLabelFormat: columnLabelFormat || DEFAULT_COLUMN_LABEL_FORMAT columnLabelFormat: columnLabelFormat || DEFAULT_COLUMN_LABEL_FORMAT
}); });
@ -286,10 +300,10 @@ const setGlobalRotation = (context: Context) => {
const getFormattedValue = ( const getFormattedValue = (
context: Context, context: Context,
{ {
usePercentage, percentageMode,
columnLabelFormat columnLabelFormat
}: { }: {
usePercentage: boolean; percentageMode: false | 'stacked' | 'data-label';
columnLabelFormat: IColumnLabelFormat; columnLabelFormat: IColumnLabelFormat;
} }
) => { ) => {
@ -297,7 +311,7 @@ const getFormattedValue = (
const currentValue = const currentValue =
context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || ''; context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || '';
const formattedValue = const formattedValue =
currentValue || formatBarAndLineDataLabel(rawValue, context, usePercentage, columnLabelFormat); currentValue || formatBarAndLineDataLabel(rawValue, context, percentageMode, columnLabelFormat);
// Store only the formatted value, rotation is handled globally // Store only the formatted value, rotation is handled globally
setBarDataLabelsManager(context, formattedValue); setBarDataLabelsManager(context, formattedValue);

View File

@ -30,4 +30,5 @@ export interface SeriesBuilderProps {
lineGroupType: BusterChartProps['lineGroupType']; lineGroupType: BusterChartProps['lineGroupType'];
selectedChartType: BusterChartProps['selectedChartType']; selectedChartType: BusterChartProps['selectedChartType'];
barShowTotalAtTop: BusterChartProps['barShowTotalAtTop']; barShowTotalAtTop: BusterChartProps['barShowTotalAtTop'];
barGroupType: BusterChartProps['barGroupType'];
} }

View File

@ -89,7 +89,11 @@ export const lineBuilder = (
const isStackedArea = lineGroupType === 'percentage-stack'; const isStackedArea = lineGroupType === 'percentage-stack';
const isArea = lineStyle === 'area' || isStackedArea; const isArea = lineStyle === 'area' || isStackedArea;
const fill = isArea ? (index === 0 ? 'origin' : '-1') : false; const fill = isArea ? (index === 0 ? 'origin' : '-1') : false;
const usePercentage = isStackedArea; const percentageMode = isStackedArea
? 'stacked'
: columnSetting.showDataLabelsAsPercentage
? 'data-label'
: false;
return { return {
type: 'line', type: 'line',
@ -127,7 +131,7 @@ export const lineBuilder = (
} }
: false, : false,
formatter: (value, context) => formatter: (value, context) =>
formatBarAndLineDataLabel(value, context, usePercentage, columnLabelFormat), formatBarAndLineDataLabel(value, context, percentageMode, columnLabelFormat),
...getLabelPosition(isStackedArea), ...getLabelPosition(isStackedArea),
...defaultLabelOptionConfig ...defaultLabelOptionConfig
} }

View File

@ -33,6 +33,7 @@ export interface UseSeriesOptionsProps {
scatterDotSize: BusterChartProps['scatterDotSize']; scatterDotSize: BusterChartProps['scatterDotSize'];
columnMetadata: ColumnMetaData[]; columnMetadata: ColumnMetaData[];
lineGroupType: BusterChartProps['lineGroupType']; lineGroupType: BusterChartProps['lineGroupType'];
barGroupType: BusterChartProps['barGroupType'];
trendlineSeries: ChartProps<'line'>['data']['datasets'][number][]; trendlineSeries: ChartProps<'line'>['data']['datasets'][number][];
barShowTotalAtTop: BusterChartProps['barShowTotalAtTop']; barShowTotalAtTop: BusterChartProps['barShowTotalAtTop'];
} }
@ -53,7 +54,8 @@ export const useSeriesOptions = ({
scatterDotSize, scatterDotSize,
lineGroupType, lineGroupType,
categoryKeys, categoryKeys,
barShowTotalAtTop barShowTotalAtTop,
barGroupType
}: UseSeriesOptionsProps): ChartProps<ChartJSChartType>['data'] => { }: UseSeriesOptionsProps): ChartProps<ChartJSChartType>['data'] => {
const selectedDataset = useMemo(() => { const selectedDataset = useMemo(() => {
return datasetOptions[datasetOptions.length - 1]; return datasetOptions[datasetOptions.length - 1];
@ -134,7 +136,8 @@ export const useSeriesOptions = ({
scatterDotSize, scatterDotSize,
lineGroupType, lineGroupType,
categoryKeys, categoryKeys,
selectedChartType selectedChartType,
barGroupType
}); });
}, [ }, [
selectedDataset, selectedDataset,

View File

@ -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
}
}
};

View File

@ -315,7 +315,7 @@ const LabelSettings: React.FC<{
return formatLabel(id, columnLabelFormat, true); return formatLabel(id, columnLabelFormat, true);
}, [displayName]); }, [displayName]);
//THIS IS HERE JUST TO PREFETCH THE CURRENCIES //THIS IS HERE JUST TO PREFETCH THE CURRENCIES, I guess I could use prefetch...
useGetCurrencies(); useGetCurrencies();
const ComponentsLoop = [ const ComponentsLoop = [