From 2221de1836e1ae92cadb276ff1a8ecb496b27115 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 3 Apr 2025 11:33:34 -0600 Subject: [PATCH] bar series updater --- .../useSeriesOptions/barSeriesBuilder.ts | 144 +++++++++++++----- 1 file changed, 104 insertions(+), 40 deletions(-) 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 43327076d..d3ae3ee8e 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts @@ -33,6 +33,12 @@ export const barSeriesBuilder = ({ dataLabelOptions.stackTotal = { display: function (context) { + // Reset the global rotation flag when processing the first data point + if (context.dataIndex === 0 && context.datasetIndex === 0) { + context.chart.$barDataLabelsGlobalRotation = false; + context.chart.$barDataLabelsUpdateInProgress = false; + } + const chart = context.chart; const shownDatasets = context.chart.data.datasets.filter( (dataset, index) => @@ -46,8 +52,18 @@ export const barSeriesBuilder = ({ const chartLayout = context.chart.options.layout; const padding = { ...DEFAULT_CHART_LAYOUT.padding, top: 24 }; context.chart.options.layout = { ...chartLayout, padding }; + //use setTimeout to ensure that the chart data label almost always overflows. requestAnimationFrame(() => { - context.chart.update(); //this is hack because the chart data label almost always overflows + if (!context.chart.$barDataLabelsUpdateInProgress) { + context.chart.$barDataLabelsUpdateInProgress = true; + console.log('updating'); + context.chart.update('none'); //this is hack because the chart data label almost always overflows + + // Reset the flag after the update completes + setTimeout(() => { + context.chart.$barDataLabelsUpdateInProgress = false; + }, 100); + } }); hasBeenDrawn = true; } @@ -92,19 +108,14 @@ export const barSeriesBuilder = ({ declare module 'chart.js' { interface Chart { - $barDataLabels: Record< - number, - Record< - number, - { - formattedValue: string; - rotation: number; - } - > - >; + $barDataLabels: Record>; + $barDataLabelsGlobalRotation: boolean; + $barDataLabelsUpdateInProgress: boolean; } } +const TEXT_WIDTH_BUFFER = 4; + export const barBuilder = ({ selectedDataset, colors, @@ -133,8 +144,6 @@ export const barBuilder = ({ const usePercentage = !!columnSetting?.showDataLabelsAsPercentage; const showLabels = !!columnSetting?.showDataLabels; - const textWidthBuffer = 4; - return { type: 'bar', label: yAxisItem.name, @@ -150,38 +159,106 @@ export const barBuilder = ({ labels: { barTotal: { display: (context) => { + // Initialize the global rotation flag if it doesn't exist + if (context.chart.$barDataLabelsGlobalRotation === undefined) { + context.chart.$barDataLabelsGlobalRotation = false; + context.chart.$barDataLabelsUpdateInProgress = false; + } + + // First dataset - analyze all data points to determine if any need rotation + if (index === 0 && context.dataIndex === 0) { + // Reset the rotation flag at the start of each render cycle + context.chart.$barDataLabelsGlobalRotation = false; + + // Analyze all datasets and datapoints in this first call + const checkAllLabelsForRotation = () => { + const datasets = context.chart.data.datasets; + const needsGlobalRotation = datasets.some((dataset, datasetIndex) => { + if (dataset.type !== 'bar') return false; + + return Array.from({ length: dataset.data.length }).some((_, dataIndex) => { + const value = dataset.data[dataIndex] as number; + if (!value) return false; + + // Get dimensions for this data point + const meta = context.chart.getDatasetMeta(datasetIndex); + if (!meta || !meta.data[dataIndex]) return false; + + const barElement = meta.data[dataIndex] as BarElement; + const { width: barWidth } = barElement.getProps(['width'], true); + + // Only proceed if bar is visible and has reasonable width + if (barWidth < 13) return false; + + // Check if formatted value would need rotation + const formattedValue = formatBarAndLineDataLabel( + value, + { ...context, datasetIndex, dataIndex } as Context, + false, // We're just checking width, not actual formatting + columnLabelFormat + ); + + const { width: textWidth } = context.chart.ctx.measureText(formattedValue); + return textWidth > barWidth - TEXT_WIDTH_BUFFER; + }); + }); + + if (needsGlobalRotation) { + context.chart.$barDataLabelsGlobalRotation = true; + + // Schedule update after all display calculations + if (!context.chart.$barDataLabelsUpdateInProgress) { + context.chart.$barDataLabelsUpdateInProgress = true; + setTimeout(() => { + context.chart.update('none'); + setTimeout(() => { + context.chart.$barDataLabelsUpdateInProgress = false; + }, 100); + }, 0); + } + } + }; + + // Run rotation analysis immediately + checkAllLabelsForRotation(); + } + const rawValue = context.dataset.data[context.dataIndex] as number; - if (!showLabels || !rawValue) return false; - const { barWidth, barHeight } = getBarDimensions(context); + const { barWidth, barHeight } = getBarDimensions(context); if (barWidth < 13) return false; + const formattedValue = formatBarAndLineDataLabel( rawValue, context, usePercentage, columnLabelFormat ); - const { width: widthOfFormattedValue } = context.chart.ctx.measureText(formattedValue); - const rotation = widthOfFormattedValue > barWidth - textWidthBuffer ? -90 : 0; - if (rotation === -90 && widthOfFormattedValue > barHeight - textWidthBuffer) { + // Get text width for this specific label + const { width: textWidth } = context.chart.ctx.measureText(formattedValue); + + // Use the global rotation setting + const rotation = context.chart.$barDataLabelsGlobalRotation ? -90 : 0; + + // Check if this label can be displayed even with rotation + if (rotation === -90 && textWidth > barHeight - TEXT_WIDTH_BUFFER) { return false; } - setBarDataLabelsManager(context, formattedValue, rotation); + // Store only the formatted value, rotation is handled globally + setBarDataLabelsManager(context, formattedValue); if (barHeight < 16) return false; - return 'auto'; }, formatter: (_, context) => { - const formattedValue = getBarDataLabelsManager(context).formattedValue; - return formattedValue; + return context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || ''; }, rotation: (context) => { - const { rotation } = getBarDataLabelsManager(context); - return rotation; + // Always use the global rotation setting + return context.chart.$barDataLabelsGlobalRotation ? -90 : 0; }, color: dataLabelFontColorContrast, borderWidth: 0, @@ -200,17 +277,15 @@ export const barBuilder = ({ } as ChartProps<'bar'>['data']['datasets'][number]; }; -const setBarDataLabelsManager = (context: Context, formattedValue: string, rotation: number) => { +const setBarDataLabelsManager = (context: Context, formattedValue: string) => { const dataIndex = context.dataIndex; const datasetIndex = context.datasetIndex; + context.chart.$barDataLabels = { ...context.chart.$barDataLabels, [datasetIndex]: { ...context.chart.$barDataLabels?.[datasetIndex], - [dataIndex]: { - formattedValue, - rotation - } + [dataIndex]: formattedValue } }; }; @@ -224,17 +299,6 @@ const getBarDimensions = (context: Context) => { return { barWidth, barHeight }; }; -const getBarDataLabelsManager = (context: Context) => { - const dataIndex = context.dataIndex; - const datasetIndex = context.datasetIndex; - const values = context.chart.$barDataLabels?.[datasetIndex]?.[dataIndex]; - - return { - formattedValue: values?.formattedValue, - rotation: values?.rotation - }; -}; - export const barSeriesBuilder_labels = (props: LabelBuilderProps) => { const { dataset, columnLabelFormats } = props;