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';
export const useGetCurrencies = () => {
nextApi;
return useQuery({
...queryKeys.getCurrencies,
queryFn: async () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<BusterChartProps['columnSettings']>,
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}
/>
);
}

View File

@ -72,7 +72,7 @@ export const useXAxis = ({
return {
display: useGrid && gridLines,
offset: true
};
} satisfies DeepPartial<GridLineOptions>;
}, [gridLines, useGrid]);
const type: DeepPartial<ScaleChartOptions<'bar'>['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<ScaleChartOptions<'bar'>['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<ScaleChartOptions<'bar'>['scales']['x']>;
}, [
timeUnit,
offset,
title,
isScatterChart,
isPieChart,

View File

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

View File

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

View File

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

View File

@ -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<ChartJSChartType>['data'] => {
const selectedDataset = useMemo(() => {
return datasetOptions[datasetOptions.length - 1];
@ -134,7 +136,8 @@ export const useSeriesOptions = ({
scatterDotSize,
lineGroupType,
categoryKeys,
selectedChartType
selectedChartType,
barGroupType
});
}, [
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);
}, [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 = [