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 d3ae3ee8e..59c118e28 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/barSeriesBuilder.ts @@ -9,6 +9,7 @@ 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 { IColumnLabelFormat } from '@/api/asset_interfaces/metric'; export const barSeriesBuilder = ({ selectedDataset, @@ -33,12 +34,6 @@ 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) => @@ -52,18 +47,8 @@ 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(() => { - 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); - } + context.chart.update(); //this is hack because the chart data label almost always overflows }); hasBeenDrawn = true; } @@ -115,6 +100,9 @@ declare module 'chart.js' { } const TEXT_WIDTH_BUFFER = 4; +const MAX_BAR_HEIGHT = 16; +const MAX_BAR_WIDTH = 13; +const FULL_ROTATION_ANGLE = -90; export const barBuilder = ({ selectedDataset, @@ -166,91 +154,37 @@ export const barBuilder = ({ } // 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(); + if (index === 0 && context.datasetIndex === 0) { + setGlobalRotation(context); } const rawValue = context.dataset.data[context.dataIndex] as number; + if (!showLabels || !rawValue) return false; const { barWidth, barHeight } = getBarDimensions(context); - if (barWidth < 13) return false; - const formattedValue = formatBarAndLineDataLabel( - rawValue, - context, + if (barWidth < MAX_BAR_WIDTH) return false; + + const formattedValue = getFormattedValue(context, { usePercentage, columnLabelFormat - ); + }); // 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; + const rotation = context.chart.$barDataLabelsGlobalRotation ? FULL_ROTATION_ANGLE : 0; // Check if this label can be displayed even with rotation if (rotation === -90 && textWidth > barHeight - TEXT_WIDTH_BUFFER) { return false; } - // Store only the formatted value, rotation is handled globally - setBarDataLabelsManager(context, formattedValue); + // Check if the bar height is too small to display the label + if (barHeight < MAX_BAR_HEIGHT) return false; - if (barHeight < 16) return false; return 'auto'; }, formatter: (_, context) => { @@ -258,7 +192,7 @@ export const barBuilder = ({ }, rotation: (context) => { // Always use the global rotation setting - return context.chart.$barDataLabelsGlobalRotation ? -90 : 0; + return context.chart.$barDataLabelsGlobalRotation ? FULL_ROTATION_ANGLE : 0; }, color: dataLabelFontColorContrast, borderWidth: 0, @@ -299,6 +233,50 @@ const getBarDimensions = (context: Context) => { return { barWidth, barHeight }; }; +const setGlobalRotation = (context: Context) => { + context.chart.$barDataLabelsGlobalRotation = false; + + const labels = context.chart.data.datasets + .filter((d) => !d.hidden) + .flatMap((dataset, datasetIndex) => { + return dataset.data.map((value, dataIndex) => { + const currentValue = context.chart.$barDataLabels?.[datasetIndex]?.[dataIndex] || ''; + return currentValue || ''; + }); + }); + + const labelNeedsToBeRotated = labels.some((label) => { + const { width: textWidth } = context.chart.ctx.measureText(label); + const { barWidth, barHeight } = getBarDimensions(context); + return textWidth > barWidth - TEXT_WIDTH_BUFFER; + }); + + if (labelNeedsToBeRotated) { + context.chart.$barDataLabelsGlobalRotation = true; + } +}; + +const getFormattedValue = ( + context: Context, + { + usePercentage, + columnLabelFormat + }: { + usePercentage: boolean; + columnLabelFormat: IColumnLabelFormat; + } +) => { + const rawValue = context.dataset.data[context.dataIndex] as number; + const currentValue = + context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || ''; + const formattedValue = + currentValue || formatBarAndLineDataLabel(rawValue, context, usePercentage, columnLabelFormat); + // Store only the formatted value, rotation is handled globally + setBarDataLabelsManager(context, formattedValue); + + return formattedValue; +}; + export const barSeriesBuilder_labels = (props: LabelBuilderProps) => { const { dataset, columnLabelFormats } = props; diff --git a/web/src/components/ui/charts/stories/BusterChart.BarChart.stories.tsx b/web/src/components/ui/charts/stories/BusterChart.BarChart.stories.tsx index ae3481d1b..410ae21e4 100644 --- a/web/src/components/ui/charts/stories/BusterChart.BarChart.stories.tsx +++ b/web/src/components/ui/charts/stories/BusterChart.BarChart.stories.tsx @@ -297,7 +297,7 @@ export const WithDataLabelsAndStackTotal: Story = { showDataLabelsAsPercentage: false }, units: { - showDataLabels: false, + showDataLabels: true, showDataLabelsAsPercentage: false } },