From 12e5347927c7970c9effabbef456cfb40fe5122b Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 31 Mar 2025 12:27:57 -0600 Subject: [PATCH] tooltip updates --- .../ui/charts/BusterChartComponent.tsx | 2 + .../BusterChartJS/BusterChartJSComponent.tsx | 7 +- .../BusterChartJSLegendWrapper.tsx | 10 +- .../useBusterChartJSLegend.ts | 109 +++++++++++------- .../BusterChartJSTooltip.tsx | 42 ++++--- .../useSeriesOptions/scatterSeriesBuilder.ts | 1 + .../ui/charts/BusterChartLegend/interfaces.ts | 1 + .../BusterChartTooltip/BusterChartTooltip.tsx | 2 +- .../charts/BusterChartTooltip/TooltipItem.tsx | 19 +-- .../BusterChartTooltip/TooltipTitle.tsx | 19 ++- .../BusterChart.ScatterChart.stories.tsx | 14 ++- 11 files changed, 143 insertions(+), 83 deletions(-) diff --git a/web/src/components/ui/charts/BusterChartComponent.tsx b/web/src/components/ui/charts/BusterChartComponent.tsx index 6e84624c6..394be705a 100644 --- a/web/src/components/ui/charts/BusterChartComponent.tsx +++ b/web/src/components/ui/charts/BusterChartComponent.tsx @@ -16,6 +16,8 @@ export const BusterChartComponent: React.FC = ( const { barGroupType, lineGroupType, columnLabelFormats, selectedChartType, selectedAxis } = props; + console.log('original data', dataProp); + const { datasetOptions, dataTrendlineOptions, diff --git a/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx b/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx index cb11e6838..db6c17f29 100644 --- a/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx +++ b/web/src/components/ui/charts/BusterChartJS/BusterChartJSComponent.tsx @@ -180,7 +180,12 @@ export const BusterChartJSComponent = React.memo( return []; }, [selectedChartType]); - console.log(options, data); + console.log(data.datasets); + + console.log( + 'CHECK', + data.datasets.reduce((acc, dataset) => acc + dataset.data.length, 0) + ); return ( -
+
+ {isUpdatingChart && ( +
+
+
+ )} {children}
diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useBusterChartJSLegend/useBusterChartJSLegend.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useBusterChartJSLegend/useBusterChartJSLegend.ts index ea96ad04f..0416bdbeb 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useBusterChartJSLegend/useBusterChartJSLegend.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useBusterChartJSLegend/useBusterChartJSLegend.ts @@ -18,6 +18,7 @@ import { } from '../../../BusterChartLegend'; import { getLegendItems } from './helper'; import { DatasetOption } from '../../../chartHooks'; +import { ANIMATION_THRESHOLD } from '../../../config'; interface UseBusterChartJSLegendProps { chartRef: React.RefObject; @@ -78,33 +79,34 @@ export const useBusterChartJSLegend = ({ const calculateLegendItems = useMemoizedFn(() => { if (showLegend === false) return; - const items = getLegendItems({ - chartRef, - colors, - inactiveDatasets, - selectedChartType, - allYAxisColumnNames, - columnLabelFormats, - categoryAxisColumnNames, - columnSettings - }); - - if (!isStackPercentage && showLegendHeadline) { - addLegendHeadlines( - items, - datasetOptions, - showLegendHeadline, - columnMetadata, + // Defer the actual calculation to the next animation frame + requestAnimationFrame(() => { + const items = getLegendItems({ + chartRef, + colors, + inactiveDatasets, + selectedChartType, + allYAxisColumnNames, columnLabelFormats, - selectedChartType - ); - } + categoryAxisColumnNames, + columnSettings + }); - setLegendItems(items); - }); + if (!isStackPercentage && showLegendHeadline) { + addLegendHeadlines( + items, + datasetOptions, + showLegendHeadline, + columnMetadata, + columnLabelFormats, + selectedChartType + ); + } - const { run: calculateLegendItemsDebounced } = useDebounceFn(calculateLegendItems, { - wait: 125 + startTransition(() => { + setLegendItems(items); + }); + }); }); const onHoverItem = useMemoizedFn((item: BusterChartLegendItem, isHover: boolean) => { @@ -140,39 +142,57 @@ export const useBusterChartJSLegend = ({ chartjs.update(); }); + const [isUpdatingChart, setIsUpdatingChart] = React.useState(false); + const onLegendItemClick = useMemoizedFn((item: BusterChartLegendItem) => { const chartjs = chartRef.current; + if (!chartjs) return; const data = chartjs.data; + const hasAnimation = chartjs.options.animation !== false; + const numberOfPoints = data.datasets.reduce((acc, dataset) => acc + dataset.data.length, 0); + const isLargeChart = numberOfPoints > ANIMATION_THRESHOLD; + const timeoutDuration = isLargeChart && hasAnimation ? 125 : 0; + console.log(data); + console.log(numberOfPoints); + + // Set updating state + setIsUpdatingChart(true); + + // Update dataset visibility state setInactiveDatasets((prev) => ({ ...prev, [item.id]: prev[item.id] ? !prev[item.id] : true })); - console.log('hit1', performance.now()); - - if (selectedChartType === 'pie') { - const index = data.labels?.indexOf(item.id) || 0; - // Pie and doughnut charts only have a single dataset and visibility is per item - chartjs.toggleDataVisibility(index); - } else if (selectedChartType) { - const index = data.datasets?.findIndex((dataset) => dataset.label === item.id); - if (index !== -1) { - chartjs.setDatasetVisibility(index, !chartjs.isDatasetVisible(index)); + // Defer visual updates to prevent UI blocking + requestAnimationFrame(() => { + // This is a synchronous, lightweight operation that toggles visibility flags + if (selectedChartType === 'pie') { + const index = data.labels?.indexOf(item.id) || 0; + chartjs.toggleDataVisibility(index); + } else if (selectedChartType) { + const index = data.datasets?.findIndex((dataset) => dataset.label === item.id); + if (index !== -1) { + chartjs.setDatasetVisibility(index, !chartjs.isDatasetVisible(index)); + } } - } - console.log('hit2', performance.now()); + // Schedule the heavy update operation with minimal delay to allow UI to remain responsive + setTimeout(() => { + // Use React's startTransition to mark this as a non-urgent update + startTransition(() => { + chartjs.update('none'); // Use 'none' for animation mode to improve performance - //put this in a timeout and transition to avoid blocking the main thread - setTimeout(() => { - startTransition(() => { - chartjs.update(); - console.log('hit3', performance.now()); - }); - }, 125); + // Set a timeout to turn off loading state after the update is complete + requestAnimationFrame(() => { + setIsUpdatingChart(false); + }); + }); + }, timeoutDuration); + }); }); const onLegendItemFocus = useMemoizedFn((item: BusterChartLegendItem) => { @@ -234,6 +254,7 @@ export const useBusterChartJSLegend = ({ onLegendItemClick, onLegendItemFocus: selectedChartType === 'pie' ? undefined : onLegendItemFocus, showLegend, - inactiveDatasets + inactiveDatasets, + isUpdatingChart }; }; 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 6994324d7..61521e3a8 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 @@ -34,23 +34,6 @@ export const BusterChartJSTooltip: React.FC<{ const datasets = chart.data.datasets; const dataPoints = dataPointsProp.filter((item) => !item.dataset.isTrendline); - const title = useMemo(() => { - if (isScatter) return undefined; - - const dataIndex = dataPoints[0].dataIndex; - const value = chart.data.labels?.[dataIndex!]; - if (typeof value === 'string') return String(value); - - //THIS IS ONLY FOR LINE CHART WITH A TIME AXIS - const datasetIndex = dataPoints[0].datasetIndex; - const dataset = datasets[datasetIndex!]; - const xAxisKeys = (dataset as any).xAxisKeys as string[]; //hacky... TODO look into this - const key = xAxisKeys.at(0)!; - const columnLabelFormat = columnLabelFormats[key!]; - - return formatLabel(value as number | Date, columnLabelFormat); - }, [dataPoints, isPie, isScatter, chart]); - const tooltipItems: ITooltipItem[] = useMemo(() => { if (isBar || isLine || isComboChart) { const hasMultipleShownDatasets = @@ -73,6 +56,7 @@ export const BusterChartJSTooltip: React.FC<{ } if (isScatter) { + console.log({ datasets, dataPoints }); return scatterTooltipHelper( datasets, dataPoints, @@ -85,6 +69,30 @@ export const BusterChartJSTooltip: React.FC<{ return []; }, []); + const title = useMemo(() => { + if (isScatter) { + if (!hasCategoryAxis) return undefined; + return { + title: tooltipItems[0].formattedLabel, + color: tooltipItems[0].color, + seriesType: 'scatter' + }; + } + + const dataIndex = dataPoints[0].dataIndex; + const value = chart.data.labels?.[dataIndex!]; + if (typeof value === 'string') return String(value); + + //THIS IS ONLY FOR LINE CHART WITH A TIME AXIS + const datasetIndex = dataPoints[0].datasetIndex; + const dataset = datasets[datasetIndex!]; + const xAxisKeys = (dataset as any).xAxisKeys as string[]; //hacky... TODO look into this + const key = xAxisKeys.at(0)!; + const columnLabelFormat = columnLabelFormats[key!]; + + return formatLabel(value as number | Date, columnLabelFormat); + }, [dataPoints, isPie, isScatter, chart, tooltipItems[0], hasCategoryAxis]); + //use mount will not work here because the tooltip is passed to a renderString function const busterTooltipNode = document?.querySelector('#buster-chartjs-tooltip')!; if (busterTooltipNode) { diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/scatterSeriesBuilder.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/scatterSeriesBuilder.ts index 30e08b520..d8ed6c598 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/scatterSeriesBuilder.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useSeriesOptions/scatterSeriesBuilder.ts @@ -44,6 +44,7 @@ export const scatterSeriesBuilder_data = ({ y: item[yIndex] as number, originalR: sizeKeyIndex ? (item[sizeKeyIndex.index] as number) : undefined })) + // .filter((item) => item.y !== null) }; }); }; diff --git a/web/src/components/ui/charts/BusterChartLegend/interfaces.ts b/web/src/components/ui/charts/BusterChartLegend/interfaces.ts index f18aa9a6d..b0f870286 100644 --- a/web/src/components/ui/charts/BusterChartLegend/interfaces.ts +++ b/web/src/components/ui/charts/BusterChartLegend/interfaces.ts @@ -33,4 +33,5 @@ export interface UseChartLengendReturnValues { showLegend: boolean; renderLegend: boolean; inactiveDatasets: Record; + isUpdatingChart?: boolean; } diff --git a/web/src/components/ui/charts/BusterChartTooltip/BusterChartTooltip.tsx b/web/src/components/ui/charts/BusterChartTooltip/BusterChartTooltip.tsx index 6a0f44688..cf75b0ca1 100644 --- a/web/src/components/ui/charts/BusterChartTooltip/BusterChartTooltip.tsx +++ b/web/src/components/ui/charts/BusterChartTooltip/BusterChartTooltip.tsx @@ -7,7 +7,7 @@ const MAX_ITEMS_IN_TOOLTIP = 12; export const BusterChartTooltip: React.FC<{ tooltipItems: ITooltipItem[]; - title: string | undefined; + title: string | { title: string; color: string | undefined; seriesType: string } | undefined; }> = ({ tooltipItems, title }) => { const shownItems = tooltipItems.slice(0, MAX_ITEMS_IN_TOOLTIP); const hiddenItems = tooltipItems.slice(MAX_ITEMS_IN_TOOLTIP); diff --git a/web/src/components/ui/charts/BusterChartTooltip/TooltipItem.tsx b/web/src/components/ui/charts/BusterChartTooltip/TooltipItem.tsx index 5c077a6bb..54d8c3e05 100644 --- a/web/src/components/ui/charts/BusterChartTooltip/TooltipItem.tsx +++ b/web/src/components/ui/charts/BusterChartTooltip/TooltipItem.tsx @@ -15,20 +15,11 @@ export const TooltipItem: React.FC = ({ return ( <> - {formattedLabel && ( - <> -
- - - {formattedLabel} - -
- - {isScatter &&
} - + {!isScatter && ( +
+ + {formattedLabel} +
)} diff --git a/web/src/components/ui/charts/BusterChartTooltip/TooltipTitle.tsx b/web/src/components/ui/charts/BusterChartTooltip/TooltipTitle.tsx index 8fe938b2d..7d4182186 100644 --- a/web/src/components/ui/charts/BusterChartTooltip/TooltipTitle.tsx +++ b/web/src/components/ui/charts/BusterChartTooltip/TooltipTitle.tsx @@ -1,9 +1,22 @@ -import { cn } from '@/lib/classMerge'; import React from 'react'; +import { LegendItemDot } from '../BusterChartLegend'; +import { ChartType } from '@/api/asset_interfaces/metric'; + +export const TooltipTitle: React.FC<{ + title: string | { title: string; color: string | undefined; seriesType: string } | undefined; +}> = ({ title: titleProp }) => { + if (!titleProp) return null; + + const isTitleString = typeof titleProp === 'string'; + const title = isTitleString ? titleProp : titleProp.title; + const color = isTitleString ? undefined : titleProp.color; + const seriesType = isTitleString ? undefined : titleProp.seriesType; -export const TooltipTitle: React.FC<{ title: string }> = ({ title }) => { return ( -
+
+ {seriesType && ( + + )} {title}
); diff --git a/web/src/components/ui/charts/stories/BusterChart.ScatterChart.stories.tsx b/web/src/components/ui/charts/stories/BusterChart.ScatterChart.stories.tsx index 552b1cc17..0f8f6e873 100644 --- a/web/src/components/ui/charts/stories/BusterChart.ScatterChart.stories.tsx +++ b/web/src/components/ui/charts/stories/BusterChart.ScatterChart.stories.tsx @@ -21,7 +21,7 @@ type Story = StoryObj; export const Default: Story = { args: { selectedChartType: ChartType.Scatter, - data: generateScatterChartData(), + data: generateScatterChartData(50), scatterAxis: { x: ['x'], y: ['y'], @@ -60,6 +60,18 @@ export const Default: Story = { } }; +export const WithoutCategory: Story = { + args: { + ...Default.args, + scatterAxis: { + x: ['x'], + y: ['y'], + size: [], + category: [] + } + } +}; + export const LargeDataset: Story = { args: { ...Default.args,