tooltip updates

This commit is contained in:
Nate Kelley 2025-03-31 12:27:57 -06:00
parent 79c00c4906
commit 12e5347927
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 143 additions and 83 deletions

View File

@ -16,6 +16,8 @@ export const BusterChartComponent: React.FC<BusterChartRenderComponentProps> = (
const { barGroupType, lineGroupType, columnLabelFormats, selectedChartType, selectedAxis } =
props;
console.log('original data', dataProp);
const {
datasetOptions,
dataTrendlineOptions,

View File

@ -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 (
<Chart

View File

@ -54,7 +54,8 @@ export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperP
onHoverItem,
onLegendItemClick,
onLegendItemFocus,
showLegend
showLegend,
isUpdatingChart
} = useBusterChartJSLegend({
selectedAxis,
columnLabelFormats,
@ -85,7 +86,12 @@ export const BusterChartJSLegendWrapper = React.memo<BusterChartJSLegendWrapperP
onHoverItem={onHoverItem}
onLegendItemClick={onLegendItemClick}
onLegendItemFocus={onLegendItemFocus}>
<div className="flex h-full w-full items-center justify-center overflow-hidden">
<div className="relative flex h-full w-full items-center justify-center overflow-hidden">
{isUpdatingChart && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/50 dark:bg-gray-900/50">
<div className="border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent"></div>
</div>
)}
{children}
</div>
</BusterChartLegendWrapper>

View File

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

View File

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

View File

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

View File

@ -33,4 +33,5 @@ export interface UseChartLengendReturnValues {
showLegend: boolean;
renderLegend: boolean;
inactiveDatasets: Record<string, boolean>;
isUpdatingChart?: boolean;
}

View File

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

View File

@ -15,20 +15,11 @@ export const TooltipItem: React.FC<ITooltipItem> = ({
return (
<>
{formattedLabel && (
<>
<div className="flex items-center space-x-1.5 overflow-hidden pr-3 pl-3">
<LegendItemDot color={color} type={seriesType as ChartType} inactive={false} />
<span
className={cn('truncate text-base', {
'text-foreground font-medium': isScatter
})}>
{formattedLabel}
</span>
</div>
{isScatter && <div className="bg-border h-[0.5px] w-full" />}
</>
{!isScatter && (
<div className="flex items-center space-x-1.5 overflow-hidden pr-3 pl-3">
<LegendItemDot color={color} type={seriesType as ChartType} inactive={false} />
<span className={cn('truncate text-base')}>{formattedLabel}</span>
</div>
)}
<TooltipItemValue values={values} usePercentage={usePercentage} isScatter={isScatter} />

View File

@ -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 (
<div className={cn('border-b', 'px-3 py-1.5')}>
<div className={'flex items-center space-x-1.5 border-b px-3 py-1.5'}>
{seriesType && (
<LegendItemDot color={color} type={seriesType as ChartType} inactive={false} />
)}
<span className="text-foreground text-base font-medium">{title}</span>
</div>
);

View File

@ -21,7 +21,7 @@ type Story = StoryObj<typeof BusterChart>;
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,