- Goal: I'm using D3.js with ReactJS I want to hover over one chart and display a tooltip on all the other charts with their own data.
- The actual result: The tooltip displays the data of the same hovered chart in all other charts.
- Code: This is what I have tried multiline-chart-tooltip-acoss-all-charts.
- More details: In recharts there is a syncId props if two charts have the same syncId, these two charts can sync the position tooltip.I want to sync the position tooltip with D3JS.
- The problem is in this file: ChartComponents/Tooltip.js.
const Tooltip = ({
xScale,
yScale,
width,
height,
data,
margin,
anchorEl,
children,
currentIndex,
tooltipColor,
...props
}) => {
const ref = useRef(null);
const drawLine = useCallback(
(x) => {
d3.select(ref.current)
.select(".tooltipLine")
.attr("x1", x)
.attr("x2", x)
.attr("y1", -margin.top)
.attr("y2", height);
},
[ref, height, margin]
);
const drawLineForOthers = useCallback(
(x) => {
d3.selectAll(".tooltip")
.select(".tooltipLine")
.attr("x1", x)
.attr("x2", x)
.attr("y1", -margin.top)
.attr("y2", height);
},
[ref, height, margin]
);
const drawContent = useCallback(
(x) => {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// console.log('xScale.invert(x)', convertTZ(xScale.invert(x), tz));
const tooltipContent = d3.select(ref.current).select(".tooltipContent");
tooltipContent.attr("transform", (cur, i, nodes) => {
const nodeWidth = nodes[i].getBoundingClientRect().width || 0;
const translateX = nodeWidth + x > width ? x - nodeWidth - 12 : x + 8;
return `translate(${translateX}, ${-margin.top})`;
});
tooltipContent
.select(".contentTitle")
.text("Le " + d3.timeFormat("%d %b %Y, %H:%M")(xScale.invert(x)));
// .text(d3.timeFormat('%b %d, %Y %Z')(convertTZ(xScale.invert(x), tz)));
},
[xScale, margin, width]
);
const drawContentForOthers = useCallback(
(x) => {
// console.log('xScale.invert(x)', xScale.invert(x));
const tooltipContentOthers = d3
.selectAll(".tooltip")
.select(".tooltipContent");
tooltipContentOthers.attr("transform", (cur, i, nodes) => {
const nodeWidth = nodes[i].getBoundingClientRect().width || 0;
const translateX = nodeWidth + x > width ? x - nodeWidth - 12 : x + 8;
return `translate(${translateX}, ${-margin.top})`;
});
tooltipContentOthers
.select(".contentTitle")
.text("Le " + d3.timeFormat("%d %b %Y, %H:%M")(xScale.invert(x)));
// .text(d3.timeFormat('%b %d, %Y %H:%M')(xScale.invert(x)));
// .text(
// d3.timeFormat('%b %d, %Y %H:%M')(convertTZ(xScale.invert(x) + '', tz))
// );
},
[xScale, margin, width]
);
const drawBackground = useCallback(() => {
// reset background size to defaults
const contentBackground = d3
.select(ref.current)
.select(".contentBackground");
contentBackground.attr("width", 125).attr("height", 20);
// calculate new background size
const tooltipContentElement = d3
.select(ref.current)
.select(".tooltipContent")
.node();
if (!tooltipContentElement) return;
const contentSize = tooltipContentElement.getBoundingClientRect();
contentBackground
.attr("width", contentSize.width + 8)
.attr(
"height",
contentSize.height > 80
? contentSize.height + 10
: contentSize.height + 4
);
}, []);
const drawBackgroundForOthers = useCallback(() => {
// reset background size to defaults for others
const contentBackgroundOthers = d3
.selectAll(".tooltip")
.select(".contentBackground")
.filter((item, i) => currentIndex !== i);
contentBackgroundOthers.attr("width", 125).attr("height", 40);
// calculate new background size for others
const tooltipContentElementOthers = d3
.selectAll(".tooltip")
.select(".tooltipContent")
.node();
if (!tooltipContentElementOthers) return;
const contentSizeOthers = tooltipContentElementOthers.getBoundingClientRect();
contentBackgroundOthers
.attr("width", contentSizeOthers.width + 8)
.attr(
"height",
contentSizeOthers.height > 80
? contentSizeOthers.height + 26
: contentSizeOthers.height + 4
);
}, []);
const onChangePosition = useCallback((d, i, isVisible) => {
d3.select(ref.current)
.selectAll(".performanceItemMarketValue")
.filter((td, tIndex) => tIndex === i)
.text(d && d.value && !isVisible ? "No data" : d && financial(d.value));
const maxNameWidth = d3.max(
d3.select(ref.current).selectAll(".performanceItemName").nodes(),
(node) => node.getBoundingClientRect().width
);
d3.select(ref.current)
.selectAll(".performanceItemValue")
.attr(
"transform",
(datum, index, nodes) =>
`translate(${
nodes[index].previousSibling.getBoundingClientRect().width + 14
},4)`
);
d3.select(ref.current)
.selectAll(".performanceItemMarketValue")
.attr("transform", `translate(${maxNameWidth + 60},4)`);
}, []);
const followPoints = useCallback(
(e) => {
const [x] = [e.layerX - margin.left];
const xDate = xScale.invert(x);
const bisectDate = d3.bisector((d) => d.date).left;
let baseXPos = 0;
drawCirclesOnLine();
drawCirclesOnLineForOthers();
drawLine(baseXPos);
drawContent(baseXPos);
drawBackground();
drawLineForOthers(baseXPos);
drawContentForOthers(baseXPos);
drawBackgroundForOthers();
function drawCirclesOnLine() {
d3.select(ref.current)
.selectAll(".tooltipLinePoint")
.attr("transform", (cur, i) => {
const index = bisectDate(data[i].items, xDate, 1);
const d0 = data[i].items[index - 1];
const d1 = data[i].items[index];
let d;
if (d1) d = xDate - d0.date > d1.date - xDate ? d1 : d0;
if (d && d.date === undefined && d.value === undefined) {
// move point out of container
return "translate(-100,-100)";
}
const xPos = d && xScale(d.date) ? xScale(d.date) : 0;
if (i === 0) {
baseXPos = xPos;
}
let isVisible = true;
if (xPos !== baseXPos) {
isVisible = false;
}
const yPos = d && yScale(d.value) ? yScale(d.value) : 0;
onChangePosition(d, i, isVisible);
return isVisible
? `translate(${xPos}, ${yPos})`
: "translate(-100,-100)";
});
}
function drawCirclesOnLineForOthers() {
d3.selectAll(".tooltip")
.selectAll(".tooltipLinePoint")
.attr("transform", (cur, i) => {
const index = data[i] && bisectDate(data[i].items, xDate, 1);
const d0 = data[i] && data[i].items[index - 1];
const d1 = data[i] && data[i].items[index];
let d = {};
if (d1) d = xDate - d0.date > d1.date - xDate ? d1 : d0;
if (d && d.date === undefined && d.value === undefined) {
// move point out of container
return "translate(-100,-100)";
}
const xPos = d && xScale(d.date) ? xScale(d.date) : 0;
if (i === 0) {
baseXPos = xPos;
}
let isVisible = true;
if (xPos !== baseXPos) {
isVisible = false;
}
const yPos = d && yScale(d.value) ? yScale(d.value) : 0;
// onChangePositionForOthers(d, i, isVisible);
return isVisible
? `translate(${xPos}, ${yPos})`
: "translate(-100,-100)";
});
}
},
[
// anchorEl,
xScale,
yScale,
data,
margin.left,
drawLine,
drawContent,
drawBackground,
onChangePosition,
drawLineForOthers,
drawContentForOthers,
drawBackgroundForOthers
// onChangePositionForOthers
]
);
useEffect(() => {
d3.select(anchorEl)
.on("mouseout.tooltip", () => {
d3.select(ref.current).attr("opacity", 0);
d3.selectAll(".tooltip").attr("opacity", 0);
})
.on("mouseover.tooltip", () => {
d3.select(ref.current).attr("opacity", 1);
d3.selectAll(".tooltip").attr("opacity", 1);
})
.on("mousemove.tooltip", (e) => {
d3.select(ref.current)
.selectAll(".tooltipLinePoint")
.attr("opacity", 1);
d3.selectAll(".tooltip")
.selectAll(".tooltipLinePoint")
.attr("opacity", 1);
followPoints(e);
});
}, [anchorEl, followPoints]);
if (!data.length) return null;
return (
<g ref={ref} opacity={0} {...props}>
<line className="tooltipLine" />
<g className="tooltipContent">
<rect
style={{ fill: tooltipColor }}
className="contentBackground"
rx={4}
ry={4}
opacity={0.4}
/>
<text className="contentTitle" transform="translate(4,14)" />
<g className="content" transform="translate(4,32)">
{data.map(({ name, color }, i) => (
<g key={name + "_" + i} transform={`translate(6,${22 * i})`}>
<circle r={6} fill={color} />
<text className="performanceItemName" transform="translate(10,4)">
{trimText(name, 14)}
</text>
<text className="performanceItemMarketValue" />
</g>
))}
</g>
</g>
{data.map(({ name, color }, index) => (
<circle
fill={color}
className="tooltipLinePoint"
r={6}
key={name + "_" + index}
opacity={0}
/>
))}
</g>
);
};