Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c7b89ab
feat: wip chart highlight functionality
hcopp Dec 9, 2025
2cc4c1c
Continue migration
hcopp Dec 9, 2025
94072ab
Merge branch 'hunter/legend-tooltip-2' into hunter/chart-highlight
hcopp Dec 9, 2025
400accf
Drop unused default
hcopp Dec 9, 2025
fbc141a
Fix lint
hcopp Dec 9, 2025
fb8f8e8
Fix lint
hcopp Dec 9, 2025
a176866
Merge branch 'hunter/legend-tooltip-2' into hunter/chart-highlight
hcopp Dec 9, 2025
b3e490c
Merge branch 'hunter/legend-tooltip-2' into hunter/chart-highlight
hcopp Dec 9, 2025
67f0348
Merge branch 'hunter/legend-tooltip-2' into hunter/chart-highlight
hcopp Dec 10, 2025
687e4be
Support PolarChart with ChartTooltip
hcopp Dec 10, 2025
107e5b1
Set default chart insets
hcopp Dec 10, 2025
e66db38
Start improving docs
hcopp Dec 10, 2025
fe710f9
Add adaptive detail example
hcopp Dec 10, 2025
6c6f7df
Update examples
hcopp Dec 10, 2025
6d0b91c
Use charttooltip and legend on barchart examples
hcopp Dec 10, 2025
4897c1c
Update legend stories
hcopp Dec 10, 2025
ae2a690
Update chart tooltip examples
hcopp Dec 10, 2025
9b267d3
Fix chart tooltip showing stacked data count
hcopp Dec 10, 2025
6aad561
Update docs
hcopp Dec 10, 2025
0545f1f
Update example
hcopp Dec 10, 2025
f8850b7
Improve bar chart examples
hcopp Dec 10, 2025
6666b40
Set default inset to 8 for polar charts
hcopp Dec 10, 2025
d96c28f
Cleanup examples for pie and donut chart
hcopp Dec 10, 2025
89ae5b8
Update web and mobile examples
hcopp Dec 10, 2025
42bf815
Improve examples and fix mobile legend
hcopp Dec 11, 2025
3960305
Add new polar example
hcopp Dec 11, 2025
2a2f011
Fix math on polar chart
hcopp Dec 11, 2025
20ee7a2
Fix alignment in storybook
hcopp Dec 15, 2025
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
41 changes: 28 additions & 13 deletions apps/docs/docs/components/graphs/BarChart/_mobileExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ You can also round the baseline of the bars by setting the `roundBaseline` prop

```jsx
function PositiveAndNegativeCashFlow() {
const theme = useTheme();
const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

const categories = Array.from({ length: 31 }, (_, i) => `3/${i + 1}`);
Expand All @@ -369,25 +370,39 @@ function PositiveAndNegativeCashFlow() {
0, 0, 0, -12, -10,
];
const series = [
{ id: 'gains', data: gains, color: 'var(--color-fgPositive)' },
{ id: 'losses', data: losses, color: 'var(--color-fgNegative)' },
{ id: 'gains', data: gains, color: theme.color.fgPositive, stackId: 'bars' },
{ id: 'losses', data: losses, color: theme.color.fgNegative, stackId: 'bars' },
];

// Custom bar component that dims non-highlighted bars
const DimmingBarComponent = memo(({ dataX, ...props }) => {
const highlightContext = useHighlightContext();

const fillOpacity = useDerivedValue(() => {
const highlightedIndex = highlightContext?.highlightedItem.value?.dataIndex;
return highlightedIndex === undefined || highlightedIndex === dataX ? 1 : 0.5;
}, [highlightContext, dataX]);

return <DefaultBar {...props} dataX={dataX} fillOpacity={fillOpacity.value} />;
});

return (
<BarChart
<CartesianChart
enableHighlighting
height={150}
inset={{ top: 8, bottom: 8, left: 0, right: 0 }}
series={series}
xAxis={{ data: categories }}
stacked
showXAxis
showYAxis
yAxis={{
showGrid: true,
GridLineComponent: ThinSolidLine,
tickLabelFormatter: (value) => `$${value}M`,
}}
/>
xAxis={{ data: categories, scaleType: 'band' }}
>
<XAxis />
<YAxis
showGrid
GridLineComponent={ThinSolidLine}
tickLabelFormatter={(value) => `$${value}M`}
/>
<BarPlot BarComponent={DimmingBarComponent} />
<ReferenceLine LineComponent={SolidLine} dataY={0} />
</CartesianChart>
);
}
```
Expand Down
117 changes: 93 additions & 24 deletions apps/docs/docs/components/graphs/BarChart/_webExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -369,25 +369,81 @@ function PositiveAndNegativeCashFlow() {
0, 0, 0, -12, -10,
];
const series = [
{ id: 'gains', data: gains, color: 'var(--color-fgPositive)' },
{ id: 'losses', data: losses, color: 'var(--color-fgNegative)' },
{ id: 'gains', data: gains, color: 'var(--color-fgPositive)', stackId: 'bars' },
{ id: 'losses', data: losses, color: 'var(--color-fgNegative)', stackId: 'bars' },
];

const DimmingBarComponent = memo(({ seriesId, dataX, ...props }) => {
const highlightContext = useHighlightContext();
const highlightedItem = highlightContext?.highlightedItem;
return (
<DefaultBar
{...props}
dataX={dataX}
fillOpacity={
highlightedItem?.dataIndex === undefined || highlightedItem.dataIndex === dataX ? 1 : 0.5
}
seriesId={seriesId}
/>
);
});

// Custom line component that renders a rect to highlight the entire bandwidth including gaps
const BandwidthHighlight = memo(({ stroke }) => {
const { getXScale, drawingArea } = useCartesianChartContext();
const highlightContext = useHighlightContext();
const scrubberPosition = highlightContext?.highlightedItem?.dataIndex;
const xScale = getXScale();

if (!xScale || scrubberPosition === undefined) return null;

const xPos = xScale(scrubberPosition);

if (xPos === undefined) return null;

const bandwidth = 'bandwidth' in xScale ? xScale.bandwidth() : 0;
const step = 'step' in xScale ? xScale.step() : bandwidth;
const gap = step - bandwidth;

// Expand the highlight to include half the gap on each side
const highlightWidth = bandwidth + gap;
const highlightX = xPos - gap / 2;

return (
<rect
fill={stroke}
height={drawingArea.height}
width={highlightWidth}
x={highlightX}
y={drawingArea.y}
/>
);
});

return (
<BarChart
<CartesianChart
enableHighlighting
height={{ base: 150, tablet: 200, desktop: 250 }}
inset={{ top: 8, bottom: 8, left: 0, right: 0 }}
series={series}
xAxis={{ data: categories }}
stacked
showXAxis
showYAxis
yAxis={{
showGrid: true,
GridLineComponent: ThinSolidLine,
tickLabelFormatter: (value) => `$${value}M`,
}}
/>
xAxis={{ data: categories, scaleType: 'band' }}
>
<XAxis />
<YAxis
showGrid
GridLineComponent={ThinSolidLine}
tickLabelFormatter={(value) => `$${value}M`}
/>
<BarPlot BarComponent={DimmingBarComponent} />
<ReferenceLine LineComponent={SolidLine} dataY={0} />
<Scrubber
hideOverlay
LineComponent={BandwidthHighlight}
lineStroke="var(--color-bgLine)"
seriesIds={[]}
/>
<ChartTooltip />
</CartesianChart>
);
}
```
Expand Down Expand Up @@ -691,25 +747,33 @@ function Candlesticks() {

const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

// Custom line component that renders a rect to highlight the entire bandwidth
const BandwidthHighlight = memo(({ d, stroke }) => {
const { getXScale, drawingArea, getXAxis } = useCartesianChartContext();
const { scrubberPosition } = useScrubberContext();
// Custom line component that renders a rect to highlight the entire bandwidth including gaps
const BandwidthHighlight = memo(({ stroke }) => {
const { getXScale, drawingArea } = useCartesianChartContext();
const highlightContext = useHighlightContext();
const scrubberPosition = highlightContext?.highlightedItem?.dataIndex;
const xScale = getXScale();
const xAxis = getXAxis();

if (!xScale || scrubberPosition === undefined) return
if (!xScale || scrubberPosition === undefined) return null;

const xPos = xScale(scrubberPosition);

if (xPos === undefined) return
if (xPos === undefined) return null;

const bandwidth = 'bandwidth' in xScale ? xScale.bandwidth() : 0;
const step = 'step' in xScale ? xScale.step() : bandwidth;
const gap = step - bandwidth;

// Expand the highlight to include half the gap on each side
const highlightWidth = bandwidth + gap;
const highlightX = xPos - gap / 2;

return (
<rect
fill={stroke}
height={drawingArea.height}
width={xScale.bandwidth()}
x={xPos}
width={highlightWidth}
x={highlightX}
y={drawingArea.y}
/>
);
Expand All @@ -723,6 +787,8 @@ function Candlesticks() {
const CandlestickBarComponent = memo<BarComponentProps>(
({ x, y, width, height, originY, dataX, ...props }) => {
const { getYScale } = useCartesianChartContext();
const highlightContext = useHighlightContext();
const highlightedItem = highlightContext?.highlightedItem;
const yScale = getYScale();

const wickX = x + width / 2;
Expand All @@ -740,8 +806,11 @@ function Candlesticks() {
const bodyHeight = Math.abs(openY - closeY);
const bodyY = openY < closeY ? openY : closeY;

const fillOpacity =
highlightedItem?.dataIndex === undefined || highlightedItem.dataIndex === dataX ? 1 : 0.5;

return (
<g>
<g opacity={fillOpacity}>
<line stroke={color} strokeWidth={1} x1={wickX} x2={wickX} y1={y} y2={y + height} />
<rect fill={color} height={bodyHeight} width={width} x={x} y={bodyY} />
</g>
Expand Down Expand Up @@ -847,7 +916,7 @@ function Candlesticks() {
<Scrubber
hideOverlay
LineComponent={BandwidthHighlight}
lineStroke="var(--color-fgMuted)"
lineStroke="var(--color-bgLine)"
seriesIds={[]}
/>
</BarChart>
Expand Down
98 changes: 39 additions & 59 deletions apps/docs/docs/components/graphs/CartesianChart/_mobileExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,8 @@ You can also create your own type of cartesian chart by using `getSeriesData`, `
```jsx
function EarningsHistory() {
const theme = useTheme();
const CirclePlot = memo(({ seriesId, opacity = 1 }: { seriesId: string, opacity?: number }) => {
const { drawingArea, getSeries, getSeriesData, getXScale, getYScale } = useCartesianChartContext();
const CirclePlot = memo(({ seriesId, opacity = 1 }: { seriesId: string; opacity?: number }) => {
const { getSeries, getSeriesData, getXScale, getYScale } = useCartesianChartContext();
const series = getSeries(seriesId);
const data = getSeriesData(seriesId);
const xScale = getXScale();
Expand Down Expand Up @@ -611,74 +611,54 @@ function EarningsHistory() {
}, []);

const surprisePercentage = useCallback(
(index: number): ChartTextChildren => {
(index: number): string => {
const percentage = (actualEPS[index] - estimatedEPS[index]) / estimatedEPS[index];
const percentageString = percentage.toLocaleString('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});

return (
<tspan
style={{
fill: percentage > 0 ? theme.color.fgPositive : theme.color.fgNegative,
fontWeight: 'bold',
}}
>
{percentage > 0 ? '+' : ''}
{percentageString}
</tspan>
);
const prefix = percentage > 0 ? '+' : '';
return `${prefix}${percentageString}`;
},
[actualEPS, estimatedEPS],
);

const LegendItem = memo(({ opacity = 1, label }: { opacity?: number, label: string }) => {
return (
<Box alignItems="center" gap={0.5}>
<LegendDot opacity={opacity} />
<Text font="label2">{label}</Text>
</Box>
);
});

const LegendDot = memo((props: BoxBaseProps) => {
return <Box borderRadius={1000} width={10} height={10} background="bgPositive" {...props} />;
});

return (
<VStack gap={0.5}>
<CartesianChart
animate={false}
height={150}
padding={0}
series={[
{
id: 'estimatedEPS',
data: estimatedEPS,
color: theme.color.bgPositive,
},
{ id: 'actualEPS', data: actualEPS, color: theme.color.bgPositive },
]}
xAxis={{ scaleType: 'band', categoryPadding: 0.25 }}
>
<YAxis
showGrid
position="left"
requestedTickCount={3}
tickLabelFormatter={formatEarningAmount}
/>
<XAxis height={20} tickLabelFormatter={(index) => quarters[index]} />
<XAxis height={20} tickLabelFormatter={surprisePercentage} />
<CirclePlot opacity={0.5} seriesId="estimatedEPS" />
<CirclePlot seriesId="actualEPS" />
</CartesianChart>
<HStack justifyContent="flex-end" gap={2}>
<LegendItem opacity={0.5} label="Estimated EPS" />
<LegendItem label="Actual EPS" />
</HStack>
</VStack>
<CartesianChart
height={150}
inset={{ top: 32, bottom: 0, left: 0, right: 0 }}
legend={<Legend justifyContent="flex-end" paddingTop={1} />}
legendPosition="bottom"
series={[
{
id: 'estimatedEPS',
label: 'Estimated EPS',
data: estimatedEPS,
color: theme.color.bgPositive,
legendShape: 'circle',
},
{
id: 'actualEPS',
label: 'Actual EPS',
data: actualEPS,
color: theme.color.bgPositive,
legendShape: 'circle',
},
]}
xAxis={{ scaleType: 'band', categoryPadding: 0.25 }}
>
<YAxis
showGrid
position="left"
requestedTickCount={3}
tickLabelFormatter={formatEarningAmount}
/>
<XAxis height={20} tickLabelFormatter={(index) => quarters[index]} />
<XAxis height={20} tickLabelFormatter={surprisePercentage} />
<CirclePlot opacity={0.5} seriesId="estimatedEPS" />
<CirclePlot seriesId="actualEPS" />
</CartesianChart>
);
}
```
Expand Down
Loading