[AI] Only show stacked bar tooltip truncation indicator when categories are hidden (#8917)

* [AI] Only show stacked bar tooltip truncation indicator when categories are hidden

* [AI] Cover the non-compact tooltip case and sharpen the release note
This commit is contained in:
Saurabh Mishra
2026-09-29 00:52:52 +00:00
committed by GitHub
parent 967684afa4
commit 2420942e88
3 changed files with 170 additions and 8 deletions
@@ -0,0 +1,150 @@
import React, { cloneElement } from 'react';
import type { ReactElement, ReactNode } from 'react';
import { MemoryRouter } from 'react-router';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { TestProviders } from '#mocks';
type TooltipEntry = {
name: string;
value: number;
color: string;
payload: { name: string; color: string };
};
// The tooltip is only reachable through recharts, which cannot measure or
// hover a chart in jsdom. Stubbing the chart primitives renders the tooltip
// content directly with the payload recharts would have handed it.
let tooltipPayload: TooltipEntry[] = [];
vi.mock('recharts', () => ({
Bar: () => null,
BarChart: ({ children }: { children: ReactNode }) => <div>{children}</div>,
CartesianGrid: () => null,
LabelList: () => null,
XAxis: () => null,
YAxis: () => null,
Tooltip: ({
content,
}: {
content: ReactElement<{
active?: boolean;
label?: string;
payload?: TooltipEntry[];
}>;
}) =>
cloneElement(content, {
active: true,
label: 'Jan 2024',
payload: tooltipPayload,
}),
}));
// jsdom does not implement matchMedia, which the animation hook uses for
// reduced-motion detection
window.matchMedia = (query: string): MediaQueryList => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(() => false),
});
const { StackedBarGraph } = await import('./StackedBarGraph');
function makeEntry(name: string, value: number): TooltipEntry {
return { name, value, color: 'red', payload: { name, color: 'red' } };
}
// recharts hands the payload over in reverse order, and the tooltip reverses
// it back, so the payload here is written last-category-first
function renderTooltip(entries: TooltipEntry[], { compact = true } = {}) {
tooltipPayload = entries.slice(0).reverse();
render(
<TestProviders>
<MemoryRouter>
<StackedBarGraph
data={
{
intervalData: [{ date: 'Jan 2024' }],
legend: entries.map(entry => ({
name: entry.name,
dataKey: entry.name,
color: entry.color,
})),
} as never
}
filters={[]}
groupBy="Category"
compact={compact}
viewLabels={false}
balanceTypeOp="totalDebts"
/>
</MemoryRouter>
</TestProviders>,
);
}
describe('StackedBarGraph tooltip', () => {
it('leaves out categories with no value for the interval', () => {
renderTooltip([
makeEntry('Food', 1000),
makeEntry('Bills', 0),
makeEntry('Fun', 2000),
]);
expect(screen.getByText('Food')).toBeInTheDocument();
expect(screen.getByText('Fun')).toBeInTheDocument();
expect(screen.queryByText('Bills')).not.toBeInTheDocument();
});
it('does not show the truncation indicator when every non-zero category is visible', () => {
renderTooltip([
makeEntry('Food', 1000),
makeEntry('Bills', 0),
makeEntry('Fun', 2000),
makeEntry('Rent', 0),
makeEntry('Travel', 3000),
makeEntry('Gifts', 0),
]);
expect(document.body).not.toHaveTextContent('...');
});
it('shows the truncation indicator when there are more non-zero categories than fit', () => {
renderTooltip([
makeEntry('Food', 1000),
makeEntry('Bills', 2000),
makeEntry('Fun', 3000),
makeEntry('Rent', 4000),
makeEntry('Travel', 5000),
makeEntry('Gifts', 6000),
]);
expect(screen.queryByText('Gifts')).not.toBeInTheDocument();
expect(document.body).toHaveTextContent('...');
});
it('never truncates outside compact mode', () => {
renderTooltip(
[
makeEntry('Food', 1000),
makeEntry('Bills', 2000),
makeEntry('Fun', 3000),
makeEntry('Rent', 4000),
makeEntry('Travel', 5000),
makeEntry('Gifts', 6000),
],
{ compact: false },
);
expect(screen.getByText('Gifts')).toBeInTheDocument();
expect(document.body).not.toHaveTextContent('...');
});
});
@@ -91,16 +91,22 @@ const CustomTooltip = ({
const maxTooltipItems = 5;
const visibleItems = useMemo(() => {
const nonZero = items.filter(p => p.value !== 0);
if (!compact || nonZero.length <= maxTooltipItems) return nonZero;
const nonZeroItems = useMemo(() => {
return items.filter(p => p.value !== 0);
}, [items]);
const hoveredIndex = nonZero.findIndex(p => tooltip === p.name);
const visibleItems = useMemo(() => {
if (!compact || nonZeroItems.length <= maxTooltipItems) return nonZeroItems;
const hoveredIndex = nonZeroItems.findIndex(p => tooltip === p.name);
if (hoveredIndex >= maxTooltipItems) {
return [...nonZero.slice(0, maxTooltipItems - 1), nonZero[hoveredIndex]];
return [
...nonZeroItems.slice(0, maxTooltipItems - 1),
nonZeroItems[hoveredIndex],
];
}
return nonZero.slice(0, maxTooltipItems);
}, [compact, items, tooltip]);
return nonZeroItems.slice(0, maxTooltipItems);
}, [compact, nonZeroItems, tooltip]);
if (active && items.length) {
return (
@@ -139,7 +145,7 @@ const CustomTooltip = ({
/>
);
})}
{compact && items.length > visibleItems.length && '...'}
{compact && nonZeroItems.length > visibleItems.length && '...'}
<AlignedText
left={t('Total')}
right={
@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [bean-1129]
---
Fix the stacked bar report showing an unnecessary "..." when all spending categories were visible