Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/components/Chart/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,18 @@ export const Bar = React.forwardRef<HTMLDivElement, BarChartProps>(
if (tip) {
if (isHorizontal) {
const absY = PAD_TOP + (idx + 0.5) * slotSize;
const rightAnchor = padLeft + plotWidth + TOOLTIP_GAP;
tip.style.top = `${absY}px`;
tip.style.left = `${padLeft + plotWidth + 8}px`;
tip.style.transform = 'none';
tip.style.display = '';
const tipW = tip.offsetWidth;
const fitsRight = rightAnchor + tipW <= width;
if (fitsRight) {
tip.style.left = `${rightAnchor}px`;
tip.style.transform = 'none';
} else {
tip.style.left = `${padLeft + plotWidth - TOOLTIP_GAP}px`;
tip.style.transform = 'translateX(-100%)';
Comment on lines +270 to +272

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Left Flip Can Still Clip

When the tooltip is wider than padLeft + plotWidth - TOOLTIP_GAP, translating it left by its full width produces a negative left edge. In a narrow chart with a long unbroken label, an overflow-clipping container will crop the tooltip on the left instead of the right, so this fallback must constrain both edges.

Context Used: Chart component color and usage conventions (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/Chart/BarChart.tsx
Line: 270-272

Comment:
**Left Flip Can Still Clip**

When the tooltip is wider than `padLeft + plotWidth - TOOLTIP_GAP`, translating it left by its full width produces a negative left edge. In a narrow chart with a long unbroken label, an overflow-clipping container will crop the tooltip on the left instead of the right, so this fallback must constrain both edges.

**Context Used:** Chart component color and usage conventions ([source](https://app.greptile.com/lightspark/github/lightsparkdev/origin/-/custom-context?memory=2070e049-47f8-4317-b8d3-707fd915a94b))

How can I resolve this? If you propose a fix, please make it concise.

}
} else {
const absX = padLeft + (idx + 0.5) * slotSize;
const totalW = padLeft + plotWidth + padRight;
Expand All @@ -281,7 +290,7 @@ export const Bar = React.forwardRef<HTMLDivElement, BarChartProps>(
tip.style.display = '';
}
},
[data.length, categoryLength, padLeft, padRight, slotSize, isHorizontal, plotWidth],
[data.length, categoryLength, padLeft, padRight, slotSize, isHorizontal, plotWidth, width],
);

const handleMouseLeave = React.useCallback(() => {
Expand Down Expand Up @@ -365,9 +374,18 @@ export const Bar = React.forwardRef<HTMLDivElement, BarChartProps>(
const tip = tooltipRef.current;
if (tip) {
if (isHorizontal) {
const rightAnchor = padLeft + plotWidth + TOOLTIP_GAP;
tip.style.top = `${PAD_TOP + (next + 0.5) * slotSize}px`;
tip.style.left = `${padLeft + plotWidth + 8}px`;
tip.style.transform = 'none';
tip.style.display = '';
const tipW = tip.offsetWidth;
const fitsRight = rightAnchor + tipW <= width;
if (fitsRight) {
tip.style.left = `${rightAnchor}px`;
tip.style.transform = 'none';
} else {
tip.style.left = `${padLeft + plotWidth - TOOLTIP_GAP}px`;
tip.style.transform = 'translateX(-100%)';
}
} else {
const absX = padLeft + (next + 0.5) * slotSize;
const totalW = padLeft + plotWidth + padRight;
Expand All @@ -387,7 +405,7 @@ export const Bar = React.forwardRef<HTMLDivElement, BarChartProps>(
tip.style.display = '';
}
},
[activeIndex, data, slotSize, padLeft, padRight, plotWidth, isHorizontal, onClickDatum, trackedClick, handleMouseLeave],
[activeIndex, data, slotSize, padLeft, padRight, plotWidth, isHorizontal, onClickDatum, trackedClick, handleMouseLeave, width],
);

const interactive = interactiveProp;
Expand Down
20 changes: 20 additions & 0 deletions src/components/Chart/Chart.test-stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,26 @@ export function BarBasic() {
);
}

export function BarHorizontalNarrow() {
return (
<div style={{ width: 260 }}>
<Chart.Bar
orientation="horizontal"
data={[
{ label: 'A', value: 10 },
{ label: 'B', value: 40 },
{ label: 'C', value: 95 },
]}
series={[{ key: 'value', label: 'A very long series name for tooltip width' }]}
xKey="label"
height={200}
tooltip
data-testid="bar-chart-horizontal"
/>
</div>
);
}

export function SankeyBasic() {
return (
<Chart.Sankey
Expand Down
16 changes: 16 additions & 0 deletions src/components/Chart/Chart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SankeyBasic,
FunnelBasic,
BarBasic,
BarHorizontalNarrow,
} from './Chart.test-stories';

const axeConfig = {
Expand Down Expand Up @@ -749,6 +750,21 @@ test.describe('Keyboard interaction', () => {
await expect(tooltip).toBeHidden();
});

test('Horizontal bar chart: tooltip stays within the container on hover, not cropped off the right edge', async ({ mount, page }) => {
await mount(<BarHorizontalNarrow />);
const container = page.locator('[data-testid="bar-chart-horizontal"]');
const svg = container.locator('svg');
const svgBox = await svg.boundingBox();
if (!svgBox) throw new Error('svg not measured');
await page.mouse.move(svgBox.x + svgBox.width / 2, svgBox.y + svgBox.height - 10);
const tooltip = container.locator('> div[class*="tooltip"]').first();
await expect(tooltip).toBeVisible();
const containerBox = await container.boundingBox();
const tooltipBox = await tooltip.boundingBox();
if (!containerBox || !tooltipBox) throw new Error('boxes not measured');
expect(tooltipBox.x + tooltipBox.width).toBeLessThanOrEqual(containerBox.x + containerBox.width + 1);
});

test('Scatter chart: arrow keys show tooltip, Escape dismisses', async ({ mount, page }) => {
await mount(<ScatterBasic />);
const svg = page.locator('[data-testid="scatter-chart"] svg');
Expand Down
Loading