Compare Two Lines
Interactive performance comparison of two investment lines over time, with reference ranges and mouse tracking.
import { FC, useMemo, useState } from 'react';
import {
Line,
LineChart,
ReferenceLine,
Tooltip,
TooltipContentProps,
XAxis,
YAxis,
MouseHandlerDataParam,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';
// #region sample data and types
const formatUsdCompressed = (n: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumSignificantDigits: 3,
})
.format(n)
.toUpperCase();
};
interface DataPoint {
date: number;
close: number;
maxClose?: number;
reinvestClose?: number;
netDeposits?: number;
}
const getDataKeyClose = (point: DataPoint) => point.close;
const getDataKeyNetDeposits = (point: DataPoint) => point.netDeposits;
const getDataKeyReinvestClose = (point: DataPoint) => point.reinvestClose;
const CustomTooltip: FC<TooltipContentProps<any, any>> = ({ active, payload }) => {
const activePayload = payload?.[0]?.payload;
if (!active || !activePayload || !activePayload.date) {
return null;
}
return <span className="text-frecDarkGray flex -translate-x-1/2 select-none">{activePayload.date}</span>;
};
const data: DataPoint[] = [
{
date: 1754265600000,
close: 200250.28586,
maxClose: 200329.40245,
reinvestClose: 200250.28586,
netDeposits: 200000,
},
{
date: 1754352000000,
close: 199272.96485,
maxClose: 199474.10888,
reinvestClose: 199272.96485,
netDeposits: 200000,
},
{
date: 1754438400000,
close: 200912.62441,
maxClose: 200912.62441,
reinvestClose: 200912.62441,
netDeposits: 200000,
},
{
date: 1754524800000,
close: 200841.07452,
maxClose: 200841.07452,
reinvestClose: 200841.07452,
netDeposits: 200000,
},
{
date: 1754611200000,
close: 202280.1764,
maxClose: 202819.09273,
reinvestClose: 202819.09273,
netDeposits: 200000,
},
{
date: 1754697600000,
close: 202280.1764,
maxClose: 202819.12355,
reinvestClose: 202819.12355,
netDeposits: 200000,
},
{
date: 1754784000000,
close: 202280.1764,
maxClose: 202819.12355,
reinvestClose: 202819.12355,
netDeposits: 200000,
},
{
date: 1754870400000,
close: 201695.79315,
maxClose: 202233.53819000002,
reinvestClose: 202233.53819000002,
netDeposits: 200000,
},
{
date: 1754956800000,
close: 204165.11346,
maxClose: 204708.99235,
reinvestClose: 204708.99235,
netDeposits: 200000,
},
{
date: 1755043200000,
close: 204683.86015,
maxClose: 205229.85045,
reinvestClose: 205229.85045,
netDeposits: 200000,
},
{
date: 1755129600000,
close: 204386.16203,
maxClose: 204931.76703000002,
reinvestClose: 204931.76703000002,
netDeposits: 200000,
},
{
date: 1755216000000,
close: 203872.79671,
maxClose: 204417.21500999999,
reinvestClose: 204417.21500999999,
netDeposits: 200000,
},
{
date: 1755302400000,
close: 203875.56671,
maxClose: 204419.98501,
reinvestClose: 204419.98501,
netDeposits: 200000,
},
{
date: 1755388800000,
close: 203875.56671,
maxClose: 204419.98501,
reinvestClose: 204419.98501,
netDeposits: 200000,
},
{
date: 1755475200000,
close: 203990.21558,
maxClose: 204534.66470999998,
reinvestClose: 204534.66470999998,
netDeposits: 200000,
},
{
date: 1755561600000,
close: 202693.85337,
maxClose: 203235.18933,
reinvestClose: 203235.18933,
netDeposits: 200000,
},
{
date: 1755648000000,
close: 202185.45487,
maxClose: 202725.1726,
reinvestClose: 202725.1726,
netDeposits: 200000,
},
{
date: 1755734400000,
close: 201559.8993,
maxClose: 202097.73681,
reinvestClose: 202097.73681,
netDeposits: 200000,
},
{
date: 1755820800000,
close: 204676.42605,
maxClose: 205222.81705,
reinvestClose: 205222.81705,
netDeposits: 200000,
},
{
date: 1755907200000,
close: 204680.26605,
maxClose: 205226.65705,
reinvestClose: 205226.65705,
netDeposits: 200000,
},
{
date: 1755993600000,
close: 204680.26605,
maxClose: 205226.65705,
reinvestClose: 205226.65705,
netDeposits: 200000,
},
{
date: 1756080000000,
close: 203829.67195,
maxClose: 204373.79742999998,
reinvestClose: 204373.79742999998,
netDeposits: 200000,
},
{
date: 1756166400000,
close: 204599.65998,
maxClose: 205145.98933,
reinvestClose: 205145.98933,
netDeposits: 200000,
},
{
date: 1756252800000,
close: 205156.94904,
maxClose: 205704.15302,
reinvestClose: 205704.15302,
netDeposits: 200000,
},
{
date: 1756339200000,
close: 205982.92992,
maxClose: 206531.6413,
reinvestClose: 206531.6413,
netDeposits: 200000,
},
{
date: 1756425600000,
close: 204750.72779,
maxClose: 205296.33207,
reinvestClose: 205296.33207,
netDeposits: 200000,
},
{
date: 1756512000000,
close: 204771.30779,
maxClose: 205316.91207,
reinvestClose: 205316.91207,
netDeposits: 200000,
},
{
date: 1756598400000,
close: 204771.30779,
maxClose: 205316.91207,
reinvestClose: 205316.91207,
netDeposits: 200000,
},
{
date: 1756684800000,
close: 204771.30779,
maxClose: 205316.91207,
reinvestClose: 205316.91207,
netDeposits: 200000,
},
{
date: 1756771200000,
close: 203336.77508,
maxClose: 203878.38012,
reinvestClose: 203878.38012,
netDeposits: 200000,
},
{
date: 1756857600000,
close: 204313.32762,
maxClose: 204857.62446,
reinvestClose: 204857.62446,
netDeposits: 200000,
},
{
date: 1756944000000,
close: 206164.82454,
maxClose: 206713.68974,
reinvestClose: 206713.68974,
netDeposits: 200000,
},
{
date: 1757030400000,
close: 205688.30079,
maxClose: 206235.95084,
reinvestClose: 206235.95084,
netDeposits: 200000,
},
{
date: 1757116800000,
close: 205694.24079,
maxClose: 206241.89084,
reinvestClose: 206241.89084,
netDeposits: 200000,
},
{
date: 1757203200000,
close: 205694.24079,
maxClose: 206241.89084,
reinvestClose: 206241.89084,
netDeposits: 200000,
},
{
date: 1757289600000,
close: 206248.11961,
maxClose: 206797.50779,
reinvestClose: 206797.50779,
netDeposits: 200000,
},
{
date: 1757376000000,
close: 206361.60467,
maxClose: 206911.90037,
reinvestClose: 206911.90037,
netDeposits: 200000,
},
{
date: 1757462400000,
close: 206931.57487,
maxClose: 207483.45488,
reinvestClose: 207483.45488,
netDeposits: 200000,
},
{
date: 1757548800000,
close: 208777.49902,
maxClose: 209334.08583,
reinvestClose: 209334.08583,
netDeposits: 200000,
},
{
date: 1757635200000,
close: 208367.30969,
maxClose: 209021.15418,
reinvestClose: 208923.34276,
netDeposits: 200000,
},
{
date: 1757721600000,
close: 208370.03969,
maxClose: 209021.15418,
reinvestClose: 208926.07276,
netDeposits: 200000,
},
{
date: 1757808000000,
close: 208370.03969,
maxClose: 209021.15418,
reinvestClose: 208926.07276,
netDeposits: 200000,
},
{
date: 1757894400000,
close: 209371.73869,
maxClose: 210020.92301,
reinvestClose: 209930.4328,
netDeposits: 200000,
},
{
date: 1757980800000,
close: 208934.04827,
maxClose: 209490.66585,
reinvestClose: 209490.66585,
netDeposits: 200000,
},
{
date: 1758067200000,
close: 208731.12737,
maxClose: 209287.02201000002,
reinvestClose: 209287.02201000002,
netDeposits: 200000,
},
{
date: 1758153600000,
close: 210068.19722,
maxClose: 210627.04514,
reinvestClose: 210627.04514,
netDeposits: 200000,
},
{
date: 1758240000000,
close: 210862.44012,
maxClose: 211513.10408,
reinvestClose: 211423.62606,
netDeposits: 200000,
},
{
date: 1758326400000,
close: 210859.46012,
maxClose: 211513.10408,
reinvestClose: 211420.64606,
netDeposits: 200000,
},
{
date: 1758412800000,
close: 210859.46012,
maxClose: 211513.10408,
reinvestClose: 211420.64606,
netDeposits: 200000,
},
{
date: 1758499200000,
close: 291534.89436,
maxClose: 292736.93416,
reinvestClose: 292736.93416,
netDeposits: 280000,
},
{
date: 1758585600000,
close: 289886.60569,
maxClose: 291084.14519,
reinvestClose: 291084.14519,
netDeposits: 280000,
},
{
date: 1758672000000,
close: 288803.00042,
maxClose: 289996.03962,
reinvestClose: 289996.03962,
netDeposits: 280000,
},
{
date: 1758758400000,
close: 287237.53875,
maxClose: 288424.69800000003,
reinvestClose: 288424.69800000003,
netDeposits: 280000,
},
{
date: 1758844800000,
close: 289098.40234,
maxClose: 290292.75549999997,
reinvestClose: 290292.75549999997,
netDeposits: 280000,
},
{
date: 1758931200000,
close: 289100.27234,
maxClose: 290294.6255,
reinvestClose: 290294.6255,
netDeposits: 280000,
},
{
date: 1759017600000,
close: 289100.27234,
maxClose: 290294.6255,
reinvestClose: 290294.6255,
netDeposits: 280000,
},
{
date: 1759104000000,
close: 289917.68812,
maxClose: 291114.99768000003,
reinvestClose: 291114.99768000003,
netDeposits: 280000,
},
{
date: 1759190400000,
close: 290927.67041,
maxClose: 292128.23201000004,
reinvestClose: 292128.23201000004,
netDeposits: 280000,
},
{
date: 1759276800000,
close: 291725.80907,
maxClose: 292930.93666,
reinvestClose: 292930.93666,
netDeposits: 280000,
},
{
date: 1759363200000,
close: 291954.38666,
maxClose: 293161.35379,
reinvestClose: 293161.35379,
netDeposits: 280000,
},
{
date: 1759449600000,
close: 291896.55473,
maxClose: 293103.65326,
reinvestClose: 293103.65326,
netDeposits: 280000,
},
{
date: 1759536000000,
close: 291896.97473,
maxClose: 293104.07326000003,
reinvestClose: 293104.07326000003,
netDeposits: 280000,
},
{
date: 1759622400000,
close: 291896.97473,
maxClose: 293104.07326000003,
reinvestClose: 293104.07326000003,
netDeposits: 280000,
},
{
date: 1759708800000,
close: 293116.53883,
maxClose: 294328.13765999995,
reinvestClose: 294328.13765999995,
netDeposits: 280000,
},
{
date: 1759795200000,
close: 292039.96981,
maxClose: 293246.27995999996,
reinvestClose: 293246.27995999996,
netDeposits: 280000,
},
{
date: 1759881600000,
close: 293856.81708,
maxClose: 295070.45254,
reinvestClose: 295070.45254,
netDeposits: 280000,
},
{
date: 1759968000000,
close: 293065.79468,
maxClose: 294275.58681999997,
reinvestClose: 294275.58681999997,
netDeposits: 280000,
},
{
date: 1760054400000,
close: 284898.1604,
maxClose: 286075.43213,
reinvestClose: 286075.43213,
netDeposits: 280000,
},
{
date: 1760140800000,
close: 284915.9704,
maxClose: 286093.24212999997,
reinvestClose: 286093.24212999997,
netDeposits: 280000,
},
{
date: 1760227200000,
close: 284915.9704,
maxClose: 286093.24212999997,
reinvestClose: 286093.24212999997,
netDeposits: 280000,
},
{
date: 1760313600000,
close: 289631.91477,
maxClose: 290827.74613,
reinvestClose: 290827.74613,
netDeposits: 280000,
},
{
date: 1760400000000,
close: 289375.17,
maxClose: 290569.7531,
reinvestClose: 290569.7531,
netDeposits: 280000,
},
{
date: 1760486400000,
close: 290518.57728,
maxClose: 291718.21911,
reinvestClose: 291718.21911,
netDeposits: 280000,
},
{
date: 1760572800000,
close: 288250.37569,
maxClose: 289441.6739,
reinvestClose: 289441.6739,
netDeposits: 280000,
},
{
date: 1760659200000,
close: 289546.09897,
maxClose: 290945.19578,
reinvestClose: 290945.19578,
netDeposits: 280000,
},
{
date: 1760745600000,
close: 289565.87897,
maxClose: 290968.16831000004,
reinvestClose: 290968.16831000004,
netDeposits: 280000,
},
{
date: 1760832000000,
close: 289565.87897,
maxClose: 290968.16831000004,
reinvestClose: 290968.16831000004,
netDeposits: 280000,
},
{
date: 1760918400000,
close: 292661.84624,
maxClose: 294226.94687,
reinvestClose: 294226.94687,
netDeposits: 280000,
},
{
date: 1761004800000,
close: 292596.13311,
maxClose: 294278.6664,
reinvestClose: 294278.6664,
netDeposits: 280000,
},
{
date: 1761091200000,
close: 291286.23879,
maxClose: 293053.57587999996,
reinvestClose: 293053.57587999996,
netDeposits: 280000,
},
{
date: 1761177600000,
close: 292980.34027,
maxClose: 294761.25367999997,
reinvestClose: 294761.25367999997,
netDeposits: 280000,
},
{
date: 1761264000000,
close: 295334.07645,
maxClose: 297264.83434,
reinvestClose: 297264.83434,
netDeposits: 280000,
},
{
date: 1761350400000,
close: 295339.16645,
maxClose: 297287.49003000004,
reinvestClose: 297287.49003000004,
netDeposits: 280000,
},
{
date: 1761436800000,
close: 295339.16645,
maxClose: 297287.49003000004,
reinvestClose: 297287.49003000004,
netDeposits: 280000,
},
{
date: 1761523200000,
close: 298898.51849,
maxClose: 301027.20428,
reinvestClose: 301027.20428,
netDeposits: 280000,
},
{
date: 1761609600000,
close: 299458.39529,
maxClose: 301698.80308000004,
reinvestClose: 301698.80308000004,
netDeposits: 280000,
},
{
date: 1761696000000,
close: 300155.03367,
maxClose: 302519.21663,
reinvestClose: 302519.21663,
netDeposits: 280000,
},
{
date: 1761782400000,
close: 296838.93323,
maxClose: 299172.16269,
reinvestClose: 299172.16269,
netDeposits: 280000,
},
{
date: 1761868800000,
close: 297645.82883,
maxClose: 299987.45458,
reinvestClose: 299987.45458,
netDeposits: 280000,
},
{
date: 1761955200000,
close: 297436.88883,
maxClose: 299778.51458,
reinvestClose: 299778.51458,
netDeposits: 280000,
},
{
date: 1762041600000,
close: 297436.88883,
maxClose: 299778.51458,
reinvestClose: 299778.51458,
netDeposits: 280000,
},
{
date: 1762128000000,
close: 297896.80552,
maxClose: 300467.03155,
reinvestClose: 300467.03155,
netDeposits: 280000,
},
{
date: 1762214400000,
close: 294259.78159,
maxClose: 296810.06841,
reinvestClose: 296810.06841,
netDeposits: 280000,
},
{
date: 1762300800000,
close: 295344.54123,
maxClose: 298175.78807999997,
reinvestClose: 298175.78807999997,
netDeposits: 280000,
},
{
date: 1762387200000,
close: 292176.91517,
maxClose: 294964.41625999997,
reinvestClose: 294964.41625999997,
netDeposits: 280000,
},
{
date: 1762473600000,
close: 293150.13876,
maxClose: 295951.40437,
reinvestClose: 295951.40437,
netDeposits: 280000,
},
{
date: 1762560000000,
close: 293164.76876,
maxClose: 296001.37622000003,
reinvestClose: 296001.37622000003,
netDeposits: 280000,
},
{
date: 1762646400000,
close: 293164.76876,
maxClose: 296001.37622000003,
reinvestClose: 296001.37622000003,
netDeposits: 280000,
},
{
date: 1762992000000,
close: 367372.436370742,
maxClose: 367372.436370742,
netDeposits: 355000,
},
];
// #endregion
const color = 'var(--color-chart-3)';
const compareColor = 'var(--color-chart-8)';
const { yAxisMin, yAxisMax } = data.reduce(
(acc, val) => {
const mc = val.maxClose ?? 0;
const nd = val.netDeposits ?? 0;
return {
yAxisMin: Math.min(acc.yAxisMin, mc, nd),
yAxisMax: Math.max(acc.yAxisMax, mc, nd),
};
},
{ yAxisMin: Infinity, yAxisMax: 0 },
);
const yAxisDomain = [Math.max(0, 0.75 * yAxisMin), 1.25 * yAxisMax];
const compareHoverDenominator = data.filter(d => !!d.reinvestClose).length;
const App: FC = () => {
const [hoverIndex, setHoverIndex] = useState(-1);
const isHovering = hoverIndex !== -1;
const invalidCompareValueLength = data.slice(0, hoverIndex).filter(d => !d.reinvestClose).length;
const compareHoverNumerator = hoverIndex - invalidCompareValueLength;
const compareHoverPercentage =
// eslint-disable-next-line no-nested-ternary
compareHoverDenominator > 0
? hoverIndex >= 0
? (compareHoverNumerator / compareHoverDenominator) * 100
: 100
: // handle the case of no valid data points. This only happens when the
// chart has only 2 data points.
compareHoverNumerator === 0
? 0
: 100;
const invalidNetDepositsLength = data.slice(0, hoverIndex).filter(d => !d.netDeposits).length;
const netDepositHoverPercentage =
hoverIndex >= 0
? ((hoverIndex - invalidNetDepositsLength) / (data.filter(d => !!d.netDeposits).length - 1)) * 100
: 100;
const hoverPercentage = hoverIndex >= 0 ? (hoverIndex / (data.filter(d => !!d.date).length - 1)) * 100 : 100;
const compareValue = data[hoverIndex]?.reinvestClose;
const handleMouseLeave = useMemo(() => () => setHoverIndex(-1), []);
const handleMouseMove = useMemo(
() =>
({ activeIndex }: MouseHandlerDataParam) =>
setHoverIndex(Number(activeIndex ?? -1)),
[],
);
const referenceLineSegment = useMemo(
() =>
[
{
x: data[hoverIndex]?.date ?? '',
y: compareValue ?? data[hoverIndex]?.close ?? 0,
},
{
x: data[hoverIndex]?.date ?? '',
y: data[hoverIndex]?.close ?? compareValue ?? 0,
},
] as const,
[hoverIndex, compareValue],
);
return (
<LineChart
style={{ width: '100%', height: 300 }}
responsive
data={data}
// Leave enough space to render the active dot
margin={{ left: 6, right: 6 }}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
>
<defs>
<linearGradient id="portfolioColor" x1="0%" y1="0" x2="100%" y2="0">
<stop offset="0%" stopColor={color} stopOpacity={1} />
<stop offset={`${hoverPercentage}%`} stopColor={color} stopOpacity={1} />
<stop offset={`${hoverPercentage}%`} stopColor={color} stopOpacity={0.3} />
<stop offset="100%" stopColor={color} stopOpacity={0.3} />
</linearGradient>
<linearGradient id="netDepositsColor" x1="0%" y1="0" x2="100%" y2="0">
<stop offset="0%" stopColor="#8c9699" stopOpacity={1} />
<stop offset={`${netDepositHoverPercentage}%`} stopColor="#8c9699" stopOpacity={1} />
<stop offset={`${netDepositHoverPercentage}%`} stopColor="#8c9699" stopOpacity={0.3} />
<stop offset="100%" stopColor="#8c9699" stopOpacity={0.3} />
</linearGradient>
<linearGradient id="compareValueColor" x1="0%" y1="0" x2="100%" y2="0">
<stop offset="0%" stopColor={compareColor} stopOpacity={1} />
<stop offset={`${compareHoverPercentage}%`} stopColor={compareColor} stopOpacity={1} />
<stop offset={`${compareHoverPercentage}%`} stopColor={compareColor} stopOpacity={0.3} />
<stop offset="100%" stopColor={compareColor} stopOpacity={0.3} />
</linearGradient>
</defs>
<YAxis
style={{
userSelect: 'none',
}}
dataKey={getDataKeyClose}
tickCount={3} // 3 ticks only: 0, half of max, max = portfolio value rounded to nearest 50k
orientation="right"
mirror
type="number"
name="Portfolio value"
tickFormatter={formatUsdCompressed}
domain={yAxisDomain}
/>
{/** XAxis is required to show the lowest tick on the y-axis */}
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={false} tickSize={0} tickMargin={0} />
<Tooltip
offset={10}
cursor={isHovering ? { stroke: 'var(--color-border-2)', strokeWidth: 1 } : false}
allowEscapeViewBox={{ x: true, y: true }}
wrapperStyle={{
top: '-24px',
outline: 'none',
fontSize: '12px',
zIndex: 1,
}}
content={CustomTooltip}
position={{
y: 30,
}}
animationDuration={0}
/>
{/* tracking error line */}
<ReferenceLine
stroke="#000AFF"
strokeOpacity={0.4}
strokeWidth={12}
strokeLinecap="round"
opacity={isHovering ? 1 : 0}
segment={referenceLineSegment}
/>
<Line
type="linear"
dataKey={getDataKeyNetDeposits}
name="Net deposits"
dot={false}
activeDot={false}
stroke="url(#netDepositsColor)"
strokeWidth={2}
opacity={isHovering ? 1 : 0}
animationDuration={700}
/>
<Line
type="linear"
dataKey={getDataKeyReinvestClose}
name="Compare value"
dot={false}
activeDot={{ fill: compareColor, stroke: 'var(--color-surface-base)' }}
stroke="url(#compareValueColor)"
strokeDasharray="0.1 4"
strokeLinecap="round"
strokeWidth={2}
strokeOpacity={isHovering ? 1 : 0}
animationDuration={900}
/>
<Line
type="linear"
dataKey={getDataKeyClose}
name="Portfolio value"
stroke="var(--color-chart-1)"
activeDot={{ fill: color, stroke: 'var(--color-surface-base)' }}
dot={false}
animationDuration={500}
/>
<RechartsDevtools />
</LineChart>
);
};
export default App;