ZIndexLayer
A layer that renders its children into a portal corresponding to the given zIndex. We can't use regular CSS This component must be used inside a Chart component. Available since Recharts 3.4z-index because SVG does not support it. So instead, we create separate DOM nodes for each zIndex layer and render the children into the corresponding DOM node using React portals.
// #region Imports, data, and helper functions
import {
LabelList,
Line,
LineChart,
LineProps,
ReferenceDot,
XAxis,
YAxis,
DefaultZIndexes,
ZIndexLayer,
LabelListEntry,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';
const AirplaneIcon = () => (
// https://upload.wikimedia.org/wikipedia/commons/7/7d/Plane_icon.svg
<svg xmlns="http://www.w3.org/2000/svg" width="238.45445" height="228.24998" version="1.0">
<path
transform="scale(0.1)"
fillRule="evenodd"
d="M194.67321 0 70.641958 53.625c-10.38227-6.92107-34.20058-21.27539-38.90545-23.44898-39.4400301-18.22079-36.9454001 14.73107-20.34925 24.6052 4.53917 2.70065 27.72352 17.17823 43.47345 26.37502l17.90625 133.9375 22.21875 13.15625 11.531252-120.9375 71.53125 36.6875 3.84375 39.21875 14.53125 8.625 11.09375-42.40625.125.0625 30.8125-31.53125-14.875-8-35.625 16.90625-68.28125-42.4375L217.36071 12.25 194.67321 0z"
/>
</svg>
);
function between(a: number, b: number) {
return Math.min(a, b) + Math.abs(a - b) / 2;
}
const topRow = 4;
const midRow = 2;
const bottomRow = 0;
const startA = 0;
const gapA = 11;
const mustek = { name: 'Můstek', yPos: between(topRow, midRow), xPos: 94 };
const florenc = { name: 'Florenc', yPos: mustek.yPos, xPos: 127 };
const muzeum = { name: 'Muzeum', yPos: between(midRow, bottomRow), xPos: between(mustek.xPos, florenc.xPos) };
const hlavak = {
name: 'Hlavní nádraží',
yPos: muzeum.yPos + (florenc.yPos - muzeum.yPos) / 2,
xPos: muzeum.xPos + (florenc.xPos - muzeum.xPos) / 2,
};
const motol = { name: 'Nemocnice Motol', yPos: topRow, xPos: startA };
const petriny = { name: 'Petřiny', yPos: topRow, xPos: motol.xPos + gapA };
const veleslavin = { name: 'Nádraží Veleslavín', yPos: topRow, xPos: petriny.xPos + gapA };
const borislavka = { name: 'Bořislavka', yPos: topRow, xPos: veleslavin.xPos + gapA };
const dejvicka = { name: 'Dejvická', yPos: topRow, xPos: borislavka.xPos + gapA };
const hradcanska = { name: 'Hradčanská', yPos: topRow, xPos: dejvicka.xPos + gapA };
const malostranska = { name: 'Malostranská', yPos: topRow, xPos: hradcanska.xPos + gapA };
const staromestska = { name: 'Staroměstská', yPos: topRow, xPos: malostranska.xPos + gapA };
// mustek
// muzeum
const mirak = { name: 'Náměstí Míru', yPos: bottomRow, xPos: muzeum.xPos + 13 };
const jzp = { name: 'Jiřího z Poděbrad', yPos: bottomRow, xPos: mirak.xPos + gapA };
const flora = { name: 'Flora', yPos: bottomRow, xPos: jzp.xPos + gapA };
const zelivarna = { name: 'Želivského', yPos: bottomRow, xPos: flora.xPos + gapA };
const strasnicka = { name: 'Strašnická', yPos: bottomRow, xPos: zelivarna.xPos + gapA };
const skalka = { name: 'Skalka', yPos: bottomRow, xPos: strasnicka.xPos + gapA };
const depohostivar = { name: 'Depo Hostivař', yPos: bottomRow, xPos: skalka.xPos + gapA };
const lineA = {
name: 'A',
color: '#1aa300',
stations: [
motol,
petriny,
veleslavin,
borislavka,
dejvicka,
hradcanska,
malostranska,
staromestska,
mustek,
muzeum,
mirak,
jzp,
flora,
zelivarna,
strasnicka,
skalka,
depohostivar,
],
};
const gapB = 8;
const narodka = { name: 'Národní třída', yPos: midRow, xPos: mustek.xPos - 20 };
const karlak = { name: 'Karlovo náměstí', yPos: midRow, xPos: narodka.xPos - gapB };
const andel = { name: 'Anděl', yPos: midRow, xPos: karlak.xPos - gapB };
const smichac = { name: 'Smíchovské nádraží', yPos: midRow, xPos: andel.xPos - gapB };
const radlicka = { name: 'Radlická', yPos: midRow, xPos: smichac.xPos - gapB };
const jinonice = { name: 'Jinonice', yPos: midRow, xPos: radlicka.xPos - gapB };
const novebutovice = { name: 'Nové Butovice', yPos: midRow, xPos: jinonice.xPos - gapB };
const hurka = { name: 'Hůrka', yPos: midRow, xPos: novebutovice.xPos - gapB };
const luziny = { name: 'Lužiny', yPos: midRow, xPos: hurka.xPos - gapB };
const luka = { name: 'Luka', yPos: midRow, xPos: luziny.xPos - gapB };
const stodulky = { name: 'Stodůlky', yPos: midRow, xPos: luka.xPos - gapB };
const zlicin = { name: 'Zličín', yPos: midRow, xPos: stodulky.xPos - gapB };
// mustek
const namestirepubliky = {
name: 'Náměstí Republiky',
yPos: mustek.yPos,
xPos: muzeum.xPos,
};
// florenc
const krizikova = { name: 'Křižíkova', yPos: midRow, xPos: florenc.xPos + 20 };
const invalidovna = { name: 'Invalidovna', yPos: midRow, xPos: krizikova.xPos + gapB };
const ceskomoravska = { name: 'Českomoravská', yPos: midRow, xPos: invalidovna.xPos + gapB };
const palmovka = { name: 'Palmovka', yPos: midRow, xPos: ceskomoravska.xPos + gapB };
const vysocanska = { name: 'Vysočanská', yPos: midRow, xPos: palmovka.xPos + gapB };
const kolbenova = { name: 'Kolbenova', yPos: midRow, xPos: vysocanska.xPos + gapB };
const hloubetin = { name: 'Hloubětín', yPos: midRow, xPos: kolbenova.xPos + gapB };
const rajskazahrada = { name: 'Rajská zahrada', yPos: midRow, xPos: hloubetin.xPos + gapB };
const cernak = { name: 'Černý Most', yPos: midRow, xPos: rajskazahrada.xPos + gapB };
const lineB = {
name: 'B',
color: '#fed330',
stations: [
zlicin,
stodulky,
luka,
luziny,
hurka,
novebutovice,
jinonice,
radlicka,
smichac,
andel,
karlak,
narodka,
mustek,
namestirepubliky,
florenc,
krizikova,
invalidovna,
ceskomoravska,
palmovka,
vysocanska,
kolbenova,
hloubetin,
rajskazahrada,
cernak,
],
};
const gapC = 9;
const ipak = { name: 'I. P. Pavlova', yPos: bottomRow, xPos: muzeum.xPos - 18 };
const vysehrad = { name: 'Vyšehrad', yPos: bottomRow, xPos: ipak.xPos - gapC };
const povstani = { name: 'Pražského povstání', yPos: bottomRow, xPos: vysehrad.xPos - gapC };
const pankrac = { name: 'Pankrác', yPos: bottomRow, xPos: povstani.xPos - gapC };
const budejarna = { name: 'Budějovická', yPos: bottomRow, xPos: pankrac.xPos - gapC };
const kacerov = { name: 'Kačerov', yPos: bottomRow, xPos: budejarna.xPos - gapC };
const roztyly = { name: 'Roztyly', yPos: bottomRow, xPos: kacerov.xPos - gapC };
const chodov = { name: 'Chodov', yPos: bottomRow, xPos: roztyly.xPos - gapC };
const opatov = { name: 'Opatov', yPos: bottomRow, xPos: chodov.xPos - gapC };
const haje = { name: 'Háje', yPos: bottomRow, xPos: opatov.xPos - gapC };
const vltavska = { name: 'Vltavská', yPos: topRow, xPos: florenc.xPos + 13 };
const holesky = { name: 'Nádraží Holešovice', yPos: topRow, xPos: vltavska.xPos + gapC };
const kobylisy = { name: 'Kobylisy', yPos: topRow, xPos: holesky.xPos + gapC };
const ladvi = { name: 'Ládví', yPos: topRow, xPos: kobylisy.xPos + gapC };
const letnany = { name: 'Letňany', yPos: topRow, xPos: ladvi.xPos + gapC };
const strizkov = { name: 'Střížkov', yPos: topRow, xPos: letnany.xPos + gapC };
const prosek = { name: 'Prosek', yPos: topRow, xPos: strizkov.xPos + gapC };
const lineC = {
name: 'C',
color: '#ff242e',
stations: [
haje,
opatov,
chodov,
roztyly,
kacerov,
budejarna,
pankrac,
povstani,
vysehrad,
ipak,
muzeum,
hlavak,
florenc,
vltavska,
holesky,
kobylisy,
ladvi,
letnany,
strizkov,
prosek,
],
};
// Some labels are rendering overlapped so we need to show them in custom positions
const customLabels = [mustek, muzeum, florenc, hlavak, veleslavin].map(s => s.name);
function MetroLine(props: LineProps) {
return (
<Line
type="monotone"
dataKey="yPos"
strokeWidth={15}
dot={{ strokeWidth: 3, stroke: '#fff', r: 10, fill: props.stroke, fillOpacity: 1 }}
isAnimationActive={false}
activeDot={false}
{...props}
>
<LabelList
angle={-65}
fill={props.stroke}
fontWeight="bold"
textAnchor="start"
// dataKey="name"
position="top"
dy={-15}
dx={5}
valueAccessor={(entry: LabelListEntry) => {
const label: string | undefined = entry.payload?.name;
if (label != null && customLabels.includes(label)) {
return undefined;
}
return label;
}}
/>
</Line>
);
}
// #endregion
export default function PrahaMetro() {
return (
<LineChart
width={900}
height={480}
margin={{
left: 30,
bottom: 30,
right: 60,
top: 150,
}}
>
<MetroLine id="C" data={lineC.stations} stroke={lineC.color} />
<MetroLine id="A" data={lineA.stations} stroke={lineA.color} />
<MetroLine id="B" data={lineB.stations} stroke={lineB.color} />
<XAxis type="number" dataKey="xPos" domain={['dataMin', 'dataMax']} hide />
<YAxis domain={[0, 'dataMax']} hide />
<ReferenceDot
x={mustek.xPos}
y={mustek.yPos}
label={{
value: mustek.name,
fill: '#000',
angle: -65,
textAnchor: 'start',
fontWeight: 'bold',
dy: -18,
dx: 10,
}}
r={18}
fill={lineA.color}
fillOpacity={1}
// one lower zIndex so that these dots render as a background behind the line dots
zIndex={DefaultZIndexes.scatter - 1}
/>
<ReferenceDot
x={muzeum.xPos}
y={muzeum.yPos}
label={{
value: muzeum.name,
fill: '#000',
angle: -65,
textAnchor: 'end',
fontWeight: 'bold',
dy: 45,
}}
r={18}
fill={lineC.color}
fillOpacity={1}
// one lower zIndex so that these dots render as a background behind the line dots
zIndex={DefaultZIndexes.scatter - 1}
/>
<ReferenceDot
x={florenc.xPos}
y={florenc.yPos}
label={{
value: florenc.name,
fill: '#000',
angle: -65,
textAnchor: 'start',
fontWeight: 'bold',
dy: -18,
dx: -15,
}}
r={18}
fill={lineC.color}
fillOpacity={1}
// one lower zIndex so that these dots render as a background behind the line dots
zIndex={DefaultZIndexes.scatter - 1}
/>
<ReferenceDot
x={hlavak.xPos}
y={hlavak.yPos}
label={{
value: hlavak.name,
fill: '#000',
angle: -73,
textAnchor: 'start',
fontWeight: 'bold',
dy: 45,
dx: 15,
}}
r={20}
// one higher zIndex so that this dot renders over the line dot - we want to highlight this station
zIndex={DefaultZIndexes.scatter + 1}
fill={lineC.color}
fillOpacity={1}
stroke="#fff"
strokeWidth={3}
/>
<ReferenceDot
x={veleslavin.xPos}
y={veleslavin.yPos}
label={{
value: veleslavin.name,
fill: lineA.color,
angle: -65,
textAnchor: 'start',
fontWeight: 'bold',
dy: -25,
dx: 0,
}}
r={20}
// one higher zIndex so that this dot renders over the line dot - we want to highlight this station
zIndex={DefaultZIndexes.scatter + 1}
fill={lineA.color}
fillOpacity={1}
stroke="#fff"
strokeWidth={3}
/>
{/* Position the Airplane icon on top of the dot */}
<ZIndexLayer zIndex={DefaultZIndexes.scatter + 2}>
<svg xmlns="http://www.w3.org/2000/svg" x={495} y={290}>
<AirplaneIcon />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" x={147} y={140}>
<AirplaneIcon />
</svg>
</ZIndexLayer>
<RechartsDevtools />
</LineChart>
);
}import React, { useRef, useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { Legend, Pie, PieChart, Tooltip } from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';
// #region shadow DOM wrapper component, and data
interface ShadowDomWrapperProps {
children: React.ReactNode;
}
const ShadowDomWrapper: React.FC<ShadowDomWrapperProps> = ({ children }) => {
const hostRef = useRef<HTMLDivElement>(null);
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
useEffect(() => {
if (hostRef.current && !hostRef.current.shadowRoot) {
const shadow = hostRef.current.attachShadow({ mode: 'closed' });
setShadowRoot(shadow);
}
}, []);
useEffect(() => {
if (shadowRoot) {
// Clear existing content
shadowRoot.innerHTML = '';
// Create a container for React to render into
const container = document.createElement('div');
container.id = 'shadow-container';
shadowRoot.appendChild(container);
// Render children into the shadow DOM
const root = ReactDOM.createRoot(container);
root.render(children);
return () => {
root.unmount();
};
}
return () => {};
}, [shadowRoot, children]);
return <div ref={hostRef} className="shadow-host" />;
};
const pieData = [
{ name: 'Desktop', value: 400, fill: '#0088FE' },
{ name: 'Mobile', value: 300, fill: '#00C49F' },
{ name: 'Tablet', value: 200, fill: '#FFBB28' },
{ name: 'Other', value: 100, fill: '#FF8042' },
];
// #endregion
export default function ShadowDomExample({ isAnimationActive = true }: { isAnimationActive?: boolean }) {
return (
<ShadowDomWrapper>
<PieChart width={380} height={300}>
<Pie
data={pieData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
outerRadius={100}
dataKey="value"
isAnimationActive={isAnimationActive}
/>
<Tooltip />
<Legend />
<RechartsDevtools />
</PieChart>
</ShadowDomWrapper>
);
}zIndexnumber
Z-Index of this component and its children.
The higher the value, the more on top it will be rendered. Components with higher zIndex will appear in front of components with lower zIndex.
If
undefinedor0, the content is rendered in the default layer without portals.childrenReactNodeoptional
The content to render inside this zIndex layer. Undefined children are allowed and will render nothing and will still report the zIndex to the portal system.