I'm creating a stacked area chart using d3.js
. Right now I'm not able to figure out on (mousemove)
how to focus separate tooltip on the respective chart curve.
I'm trying to achieve this but for stacked area chart.
I have created a sandbox of my progress here: https://codesandbox.io/s/recursing-carlos-3t0lg
Relevant code:
function mouseMove() {
d3.event.preventDefault();
const mouse = d3.mouse(d3.event.target);
const [xCoord, yCoord] = mouse;
const mouseDate = x.invert(xCoord);
const mouseDateSnap = d3.timeDay.floor(mouseDate);
const bisectDate = d3.bisector((d) => d.date).left;
const xIndex = bisectDate(data, mouseDateSnap, 0);
const mouseHours = data[xIndex].hours;
let demandHours =
data[xIndex].resourceType === "DMND" ? data[xIndex].hours : "";
let supplyHours =
data[xIndex].resourceType === "SPLY" ? data[xIndex].hours : "";
if (x(mouseDateSnap) <= 0) return;
svg
.selectAll(".hoverLine")
.attr("x1", x(mouseDateSnap))
.attr("y1", margin.top)
.attr("x2", x(mouseDateSnap))
.attr("y2", height - margin.bottom)
.attr("stroke", "#69b3a2")
.attr("fill", "#cce5df");
svg
.select(".hoverPoint1")
.attr("cx", x(mouseDateSnap))
.attr("cy", y(supplyHours))
.attr("r", "7")
.attr("fill", "green");
svg
.select(".hoverPoint2")
.attr("cx", x(mouseDateSnap))
.attr("cy", y(demandHours))
.attr("r", "7")
.attr("fill", "yellow");
const isLessThanHalf = xIndex > data.length / 2;
const hoverTextX = isLessThanHalf ? "-0.75em" : "0.75em";
const hoverTextAnchor = isLessThanHalf ? "end" : "start";
svg
.selectAll(".hoverText")
.attr("x", x(mouseDateSnap))
.attr("y", y(mouseHours))
.attr("dx", hoverTextX)
.attr("dy", "-1.25em")
.style("text-anchor", hoverTextAnchor)
.text(
data[xIndex].resourceType === "DMND"
? demandHours "sec"
: supplyHours "sec"
);
}
svg.append("line").classed("hoverLine", true);
svg.append("circle").classed("hoverPoint1", true);
svg.append("circle").classed("hoverPoint2", true);
svg.append("text").classed("hoverText", true);
svg
.append("rect")
.attr("fill", "transparent")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
svg.on("mousemove", mouseMove);
In above code I'm creating 2 separate tooltips using selections hoverPoint1
(to show hours SPLY hours) and hoverPoint2
(to show DMND hours)
Expected:
on (mousemove)
, the green circle should move along the curve of blue plotted area AND at same time yellow circle should move along curve of gray plotted area. (please see this as example which shows single area tooltip https://observablehq.com/@elishaterada/simple-area-chart-with-tooltip)
thanks!
function stackedAreaPlot(data) {
// set the dimensions and margins of the graph
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 60
},
width = 700 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#stackedAreaPlot")
.append("svg")
.attr("width", width margin.left margin.right)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Add X axis --> it is a date format
const x = d3
.scaleTime()
.domain(d3.extent(data, (d: any) => new Date(d.date)))
.range([0, width]);
svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
// Add Y axis
const y = d3.scaleLinear().domain([0, 2000]).range([height, 0]);
svg.append("g").call(d3.axisLeft(y));
let sumstat: any = d3
.nest()
.key((d: any) => {
return d.date;
})
.entries(data);
// stacked groups
const myGroups = ["DMND", "SPLY"];
const stackedData: any = d3
.stack()
// .keys(myGroup)
.keys(myGroups)
.value((data: any, key: any) => {
return data &&
data.values &&
data.values.find((d) => d.resourceType === key) ?
data.values.find((d) => d.resourceType === key).hours :
null;
})(sumstat);
const color = d3
.scaleOrdinal()
.domain(myGroups)
.range(["#a9b4bc", "#4b6f93"]);
// Show the areas
svg
.selectAll("mylayers")
.data(stackedData)
.enter()
.append("path")
.style("fill", (d: any) => color(d.key))
.attr(
"d",
d3
.area()
.x(function(d: any) {
return x(new Date(d.data.key));
})
.y0(function(d: any) {
return y( d[0]);
})
.y1(function(d: any) {
return y( d[1]);
})
);
// tooltip with hover point and line
function mouseMove() {
d3.event.preventDefault();
const mouse = d3.mouse(d3.event.target);
const [xCoord, yCoord] = mouse;
const mouseDate = x.invert(xCoord);
const mouseDateSnap = d3.timeDay.floor(mouseDate);
const bisectDate = d3.bisector((d) => d.date).left;
const xIndex = bisectDate(data, mouseDateSnap, 0);
const mouseHours = data[xIndex].hours;
let demandHours =
data[xIndex].resourceType === "DMND" ? data[xIndex].hours : "";
let supplyHours =
data[xIndex].resourceType === "SPLY" ? data[xIndex].hours : "";
if (x(mouseDateSnap) <= 0) return;
svg
.selectAll(".hoverLine")
.attr("x1", x(mouseDateSnap))
.attr("y1", margin.top)
.attr("x2", x(mouseDateSnap))
.attr("y2", height - margin.bottom)
.attr("stroke", "#69b3a2")
.attr("fill", "#cce5df");
svg
.select(".hoverPoint1")
.attr("cx", x(mouseDateSnap))
.attr("cy", y(supplyHours))
.attr("r", "7")
.attr("fill", "green");
svg
.select(".hoverPoint2")
.attr("cx", x(mouseDateSnap))
.attr("cy", y(demandHours))
.attr("r", "7")
.attr("fill", "yellow");
const isLessThanHalf = xIndex > data.length / 2;
const hoverTextX = isLessThanHalf ? "-0.75em" : "0.75em";
const hoverTextAnchor = isLessThanHalf ? "end" : "start";
svg
.selectAll(".hoverText")
.attr("x", x(mouseDateSnap))
.attr("y", y(mouseHours))
.attr("dx", hoverTextX)
.attr("dy", "-1.25em")
.style("text-anchor", hoverTextAnchor)
.text(
data[xIndex].resourceType === "DMND" ?
demandHours " hours" :
supplyHours " hours"
);
}
svg.append("line").classed("hoverLine", true);
svg.append("circle").classed("hoverPoint1", true);
svg.append("circle").classed("hoverPoint2", true);
svg.append("text").classed("hoverText", true);
svg
.append("rect")
.attr("fill", "transparent")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
svg.on("mousemove", mouseMove);
}
var data = [{
date: "2021-12-10T05:00:00.000Z",
hours: 388.5421,
resourceType: "SPLY"
},
{
date: "2021-12-11T05:00:00.000Z",
hours: 357.17214,
resourceType: "SPLY"
},
{
date: "2021-12-12T05:00:00.000Z",
hours: 194.80227,
resourceType: "SPLY"
},
{
date: "2021-12-12T05:00:00.000Z",
hours: 24.5,
resourceType: "DMND"
},
{
date: "2021-12-13T05:00:00.000Z",
hours: 466.68397,
resourceType: "SPLY"
},
{
date: "2021-12-13T05:00:00.000Z",
hours: 48,
resourceType: "DMND"
},
{
date: "2021-12-14T05:00:00.000Z",
hours: 591.11745,
resourceType: "SPLY"
},
{
date: "2021-12-14T05:00:00.000Z",
hours: 62.75,
resourceType: "DMND"
},
{
date: "2021-12-15T05:00:00.000Z",
hours: 631.64018,
resourceType: "SPLY"
},
{
date: "2021-12-15T05:00:00.000Z",
hours: 84.73,
resourceType: "DMND"
},
{
date: "2021-12-16T05:00:00.000Z",
hours: 175.7,
resourceType: "DMND"
},
{
date: "2021-12-16T05:00:00.000Z",
hours: 628.53835,
resourceType: "SPLY"
},
{
date: "2021-12-17T05:00:00.000Z",
hours: 240.24,
resourceType: "DMND"
},
{
date: "2021-12-17T05:00:00.000Z",
hours: 673.66929,
resourceType: "SPLY"
},
{
date: "2021-12-18T05:00:00.000Z",
hours: 256.68,
resourceType: "DMND"
},
{
date: "2021-12-18T05:00:00.000Z",
hours: 635.43202,
resourceType: "SPLY"
},
{
date: "2021-12-19T05:00:00.000Z",
hours: 634.73701,
resourceType: "SPLY"
},
{
date: "2021-12-19T05:00:00.000Z",
hours: 212.44,
resourceType: "DMND"
},
{
date: "2021-12-20T05:00:00.000Z",
hours: 604.94103,
resourceType: "SPLY"
},
{
date: "2021-12-20T05:00:00.000Z",
hours: 209.76,
resourceType: "DMND"
},
{
date: "2021-12-21T05:00:00.000Z",
hours: 618.83085,
resourceType: "SPLY"
},
{
date: "2021-12-21T05:00:00.000Z",
hours: 200.78,
resourceType: "DMND"
},
{
date: "2021-12-22T05:00:00.000Z",
hours: 580.31758,
resourceType: "SPLY"
},
{
date: "2021-12-22T05:00:00.000Z",
hours: 204.78,
resourceType: "DMND"
},
{
date: "2021-12-23T05:00:00.000Z",
hours: 231.15,
resourceType: "DMND"
},
{
date: "2021-12-23T05:00:00.000Z",
hours: 679.8791100000001,
resourceType: "SPLY"
},
{
date: "2021-12-24T05:00:00.000Z",
hours: 654.02485,
resourceType: "SPLY"
},
{
date: "2021-12-24T05:00:00.000Z",
hours: 281.28,
resourceType: "DMND"
}
];
stackedAreaPlot(data);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="stackedAreaPlot"></div>
CodePudding user response:
I think you shot yourself in the foot with a couple of design decisions. The first problem was that you were comparing mouseDateSnap
(a Date) with data[0].date
, which is still a string. You needed to make it a date.
Furthermore, by looking at the stacked data, you would ensure you always have all the data belonging to a point, for all traces. Then, you can easily draw the circles, but also draw tooltips for both traces, not just for one.
By using d3.select().enter()
, you can make this work for any number of traces, and you remove the need of having the .hoverPoint1
and .hoverPoint2
logic.
Finally, if you use mouseDateSnap
in combination with bisect
, you will draw the points right at the axis ticks (nice), but the y value would be off. It would match the y value of the closest drawn point. So the circles would actually not look nice at all.
function stackedAreaPlot(data) {
// set the dimensions and margins of the graph
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 60
},
width = 700 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#stackedAreaPlot")
.append("svg")
.attr("width", width margin.left margin.right)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Add X axis --> it is a date format
const x = d3
.scaleTime()
.domain(d3.extent(data, (d: any) => new Date(d.date)))
.range([0, width]);
svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
// Add Y axis
const y = d3.scaleLinear().domain([0, 2000]).range([height, 0]);
svg.append("g").call(d3.axisLeft(y));
let sumstat: any = d3
.nest()
.key((d: any) => {
return d.date;
})
.entries(data);
// stacked groups
const myGroups = ["DMND", "SPLY"];
const stackedData: any = d3
.stack()
// .keys(myGroup)
.keys(myGroups)
.value((data: any, key: any) => {
return data &&
data.values &&
data.values.find((d) => d.resourceType === key) ?
data.values.find((d) => d.resourceType === key).hours :
null;
})(sumstat);
const color = d3
.scaleOrdinal()
.domain(myGroups)
.range(["#a9b4bc", "#4b6f93"]);
// Show the areas
svg
.selectAll("mylayers")
.data(stackedData)
.enter()
.append("path")
.style("fill", (d: any) => color(d.key))
.attr(
"d",
d3
.area()
.x(function(d: any) {
return x(new Date(d.data.key));
})
.y0(function(d: any) {
return y( d[0]);
})
.y1(function(d: any) {
return y( d[1]);
})
);
// tooltip with hover point and line
function mouseMove() {
d3.event.preventDefault();
const mouse = d3.mouse(d3.event.target);
const [xCoord, yCoord] = mouse;
const mouseDate = x.invert(xCoord);
// Use `sumstat`, not `data`, to get the correct data object for all traces
const bisectDate = d3.bisector(d => new Date(d.key)).left;
const xIndex = bisectDate(sumstat, mouseDate);
// We get the key directly from xVal
const xVal = new Date(sumstat[xIndex].key);
if (x(xVal) <= 0) return;
svg
.selectAll(".hoverLine")
.attr("x1", x(xVal))
.attr("y1", y.range()[0])
.attr("x2", x(xVal))
.attr("y2", y.range()[1])
.attr("stroke", "#69b3a2")
.attr("fill", "#cce5df");
const isLessThanHalf = xIndex > sumstat.length / 2;
const hoverTextX = isLessThanHalf ? "-0.75em" : "0.75em";
const hoverTextAnchor = isLessThanHalf ? "end" : "start";
// Create a mapping of type (DMND/SPLY) to single and stacked Y values
const yVals = {
DMND: { color: "green" },
SPLY: { color: "yellow" }
};
sumstat[xIndex].values.forEach((el) => {
// Get the single values from `sumstat`
yVals[el.resourceType].hours = el.hours;
});
stackedData.forEach((group) => {
// Get the cumulative values from `stackedData`
yVals[group.key].cumulative = group[xIndex][1];
});
let hoverPoints = svg
.selectAll(".hoverPoint")
.data(Object.values(yVals));
const newHoverPoints = hoverPoints
.enter()
.append("g")
.classed("hoverPoint", true);
newHoverPoints
.append("circle")
.attr("r", 7)
.attr("fill", d => d.color);
newHoverPoints
.append("text")
.attr("dy", "-1.25em");
newHoverPoints
.merge(hoverPoints)
.attr("transform", d => `translate(${x(xVal)}, ${y(d.cumulative)})`)
.select("text")
.attr("dx", hoverTextX)
.style("text-anchor", hoverTextAnchor)
.text(d => `${d.hours || 0}sec`);
}
svg.append("line").classed("hoverLine", true);
svg
.append("rect")
.attr("fill", "transparent")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
svg.on("mousemove", mouseMove);
}
var data = [{
date: "2021-12-10T05:00:00.000Z",
hours: 388.5421,
resourceType: "SPLY"
},
{
date: "2021-12-11T05:00:00.000Z",
hours: 357.17214,
resourceType: "SPLY"
},
{
date: "2021-12-12T05:00:00.000Z",
hours: 194.80227,
resourceType: "SPLY"
},
{
date: "2021-12-12T05:00:00.000Z",
hours: 24.5,
resourceType: "DMND"
},
{
date: "2021-12-13T05:00:00.000Z",
hours: 466.68397,
resourceType: "SPLY"
},
{
date: "2021-12-13T05:00:00.000Z",
hours: 48,
resourceType: "DMND"
},
{
date: "2021-12-14T05:00:00.000Z",
hours: 591.11745,
resourceType: "SPLY"
},
{
date: "2021-12-14T05:00:00.000Z",
hours: 62.75,
resourceType: "DMND"
},
{
date: "2021-12-15T05:00:00.000Z",
hours: 631.64018,
resourceType: "SPLY"
},
{
date: "2021-12-15T05:00:00.000Z",
hours: 84.73,
resourceType: "DMND"
},
{
date: "2021-12-16T05:00:00.000Z",
hours: 175.7,
resourceType: "DMND"
},
{
date: "2021-12-16T05:00:00.000Z",
hours: 628.53835,
resourceType: "SPLY"
},
{
date: "2021-12-17T05:00:00.000Z",
hours: 240.24,
resourceType: "DMND"
},
{
date: "2021-12-17T05:00:00.000Z",
hours: 673.66929,
resourceType: "SPLY"
},
{
date: "2021-12-18T05:00:00.000Z",
hours: 256.68,
resourceType: "DMND"
},
{
date: "2021-12-18T05:00:00.000Z",
hours: 635.43202,
resourceType: "SPLY"
},
{
date: "2021-12-19T05:00:00.000Z",
hours: 634.73701,
resourceType: "SPLY"
},
{
date: "2021-12-19T05:00:00.000Z",
hours: 212.44,
resourceType: "DMND"
},
{
date: "2021-12-20T05:00:00.000Z",
hours: 604.94103,
resourceType: "SPLY"
},
{
date: "2021-12-20T05:00:00.000Z",
hours: 209.76,
resourceType: "DMND"
},
{
date: "2021-12-21T05:00:00.000Z",
hours: 618.83085,
resourceType: "SPLY"
},
{
date: "2021-12-21T05:00:00.000Z",
hours: 200.78,
resourceType: "DMND"
},
{
date: "2021-12-22T05:00:00.000Z",
hours: 580.31758,
resourceType: "SPLY"
},
{
date: "2021-12-22T05:00:00.000Z",
hours: 204.78,
resourceType: "DMND"
},
{
date: "2021-12-23T05:00:00.000Z",
hours: 231.15,
resourceType: "DMND"
},
{
date: "2021-12-23T05:00:00.000Z",
hours: 679.8791100000001,
resourceType: "SPLY"
},
{
date: "2021-12-24T05:00:00.000Z",
hours: 654.02485,
resourceType: "SPLY"
},
{
date: "2021-12-24T05:00:00.000Z",
hours: 281.28,
resourceType: "DMND"
}
];
stackedAreaPlot(data);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
<div id="stackedAreaPlot"></div>