mirror of https://github.com/buster-so/buster.git
tooltip updates
This commit is contained in:
parent
79c00c4906
commit
12e5347927
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -33,4 +33,5 @@ export interface UseChartLengendReturnValues {
|
|||
showLegend: boolean;
|
||||
renderLegend: boolean;
|
||||
inactiveDatasets: Record<string, boolean>;
|
||||
isUpdatingChart?: boolean;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue