Synchronized Charts With Different Data
Two synchronized charts whose data arrays do not line up, using a custom syncMethod function to snap the tooltip to the nearest date.
import { CartesianGrid, createHorizontalChart, Line, SyncMethod, Tooltip, XAxis, YAxis } from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';
type DailyPoint = { date: string; value: number };
/*
* Two charts that share a syncId but do NOT share the same data:
* - the first chart has a reading for every day
* - the second chart only has a reading every fifth day
*
* With syncMethod="value" the second chart would only show a tooltip when the hovered
* date exactly matches one of its own dates. With syncMethod="index" the charts would
* line up by array position, which is wrong because the arrays have different lengths.
*
* A custom syncMethod function solves this: it receives the receiving chart's ticks and
* the hovered label from the source chart, and returns the index of the tick to activate.
*/
function isoDate(dayOffset: number): string {
const d = new Date(Date.UTC(2024, 0, 1 + dayOffset));
return d.toISOString().slice(0, 10);
}
const dailyData: DailyPoint[] = Array.from({ length: 30 }, (_, i) => ({
date: isoDate(i),
value: Math.round(50 + 30 * Math.sin(i / 3) + (i % 7) * 2),
}));
const weeklyData: DailyPoint[] = dailyData.filter((_, i) => i % 5 === 0);
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const MAX_DISTANCE_MS = 2 * ONE_DAY_MS;
/**
* Snap to the nearest date in this chart, but only if it is within two days.
* Returning an index outside the ticks array hides the tooltip in the receiving chart,
* which is what we want when there is no nearby data point.
*/
export const closestDate: SyncMethod = (ticks, data) => {
const hovered = Date.parse(String(data.activeLabel));
if (Number.isNaN(hovered)) {
return -1;
}
let bestIndex = -1;
let bestDistance = Infinity;
ticks.forEach((tick, index) => {
const distance = Math.abs(Date.parse(String(tick.value)) - hovered);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = index;
}
});
return bestDistance <= MAX_DISTANCE_MS ? bestIndex : -1;
};
const Typed = createHorizontalChart<DailyPoint, string, number>()({ XAxis, YAxis, Tooltip, Line });
const chartStyle = { width: '100%', maxWidth: '700px', maxHeight: '25vh', aspectRatio: 1.618 };
const margin = { top: 10, right: 30, left: 0, bottom: 0 };
// #endregion
export default function Example() {
return (
<>
<Typed.LineChart
style={chartStyle}
responsive
data={dailyData}
syncId="differentData"
syncMethod={closestDate}
margin={margin}
>
<CartesianGrid />
<Typed.XAxis dataKey="date" />
<Typed.YAxis />
<Tooltip />
<Typed.Line type="monotone" dataKey="value" name="Daily reading" dot={false} />
<RechartsDevtools />
</Typed.LineChart>
<Typed.LineChart
style={chartStyle}
responsive
data={weeklyData}
syncId="differentData"
syncMethod={closestDate}
margin={margin}
>
<CartesianGrid />
<Typed.XAxis dataKey="date" />
<Typed.YAxis />
<Tooltip />
<Typed.Line type="monotone" dataKey="value" name="Every fifth day" />
<RechartsDevtools />
</Typed.LineChart>
</>
);
}