I am pretty new to D3 and as the title says I am trying to implement a D3 stacked area chart similar to Stacked area graph example.
However, I am by no means a D3 expert and am having a hard time trying to get a vertical line with tooltips to show up for each value, like how nvd3 has done it in NVD3 stacked area graph example. Unfortunately, a lot of the examples I have seen seem to be using older versions of D3, and I don't feel they are necessarily implemented properly or translate well to a v7 solution. Found Examples: Show the tooltip inside a div in a D3 stacked area chart d3: how to focus separate tooltips for stacked area chart?
I was able to find the D3 line chart with tooltip example and think it could be modified pretty easily to work with the stacked area graph example mentioned previously.
StackedAreaChart(data, eventData, {
x = ([x]) => x, // given d in data, returns the (ordinal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
z = () => 1, // given d in data, returns the (categorical) z-value
title, // given d in data, returns the title text
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 910, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
zDomain, // array of z-values
offset = d3.stackOffsetDiverging, // stack offset method
order = d3.stackOrderNone, // stack order method
xFormat, // a format specifier string for the x-axis
yFormat, // a format specifier for the y-axis
yLabel, // a label for the y-axis
colors = d3.schemeTableau10 // array of colors for z
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// console.log("X: ", X);
// console.log("Y: ", Y);
// console.log("Z; ", Z);
// console.log("colors: ", colors);
// Compute default x- and z-domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// console.log("y: ", y);
// console.log("zDomain: ", zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
// console.log("I: ", I );
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// console.log("series: ", series)
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
// console.log("xScale: ", xScale)
// console.log("yScale: ", yScale)
// console.log("color: ", color)
// console.log("xAxis: ", xAxis)
// console.log("yAxis: ", yAxis)
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
const svg = d3.select("#stacked_area_chart").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.on("pointerenter pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("touchstart", event => event.preventDefault());
// console.log("svg: ", svg)
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
// console.log("svg2: ", svg)
svg.append("g")
.attr('id', 'graphArea')
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title");
// console.log(xAxis);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);
//applies the vertical event lines
var eventLine = d3.select('#graphArea');
eventLine.selectAll("lines")
.data(eventData)
.enter()
.append('line')
.attr("y1", height - 30)
.attr("y2", 20)
.attr("x1", d => xAxis.scale()(d.date))
.attr("x2", d => xAxis.scale()(d.date))
.style("stroke-width", 2)
.style("stroke", "black")
.style("opacity", .5)
//appends labels to the vertical event lines
eventLine.append("g")
.selectAll("text")
.data(eventData)
.enter()
.append("text")
.attr("x", d => xAxis.scale()(d.date))
.attr("y", 15)
.style("font-size","10px")
.text(d => d.event);
// Compute titles.
if (title === undefined) {
const formatDate = xScale.tickFormat(null, "%Y");
const formatValue = yScale.tickFormat(100, yFormat);
title = i => `${(Z[i])}\n${Y[i]}`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
//adds tooltip to each area
const tooltip = svg.append("g")
.style("pointer-events", "none");
function pointermoved(event) {
const i = d3.bisectCenter(X, xScale.invert(d3.pointer(event)[0]));
tooltip.style("display", null);
tooltip.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
console.log(`${title(i)}`.split(/\n/));
const path = tooltip.selectAll("path")
.data(data)
.join("path")
.attr("fill", "white")
.attr("stroke", "black");
const text = tooltip.selectAll("text")
.data(data)
.join("text")
.call(text => text
.selectAll("tspan")
.data(`${title(i)}`.split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d.node));
const {x, y, width: w, height: h} = text.node().getBBox();
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
// svg.property("value", O[i]).dispatch("input", {bubbles: true});
}
function pointerleft() {
tooltip.style("display", "none");
svg.node().value = null;
svg.dispatch("input", {bubbles: true});
}
return Object.assign(svg.node(), {scales: {color}});
}
}
Extracted Tooltip code
//adds tooltip to each area
const tooltip = svg.append("g")
.style("pointer-events", "none");
function pointermoved(event) {
const i = d3.bisectCenter(X, xScale.invert(d3.pointer(event)[0]));
tooltip.style("display", null);
tooltip.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
console.log(`${title(i)}`.split(/\n/));
const path = tooltip.selectAll("path")
.data(data)
.join("path")
.attr("fill", "white")
.attr("stroke", "black");
const text = tooltip.selectAll("text")
.data(data)
.join("text")
.call(text => text
.selectAll("tspan")
.data(`${title(i)}`.split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d.node));
const {x, y, width: w, height: h} = text.node().getBBox();
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
// svg.property("value", O[i]).dispatch("input", {bubbles: true});
}
function pointerleft() {
tooltip.style("display", "none");
svg.node().value = null;
svg.dispatch("input", {bubbles: true});
}
Data sample
areaChartData: {
nodeData: [
{node: 'rti-dds-subscriber', percentConsumption: 15, date: new Date('2021-04-23')},
{node: 'rti-dds-publisher', percentConsumption: 25, date: new Date('2021-04-23')},
{node: 'openDDS-publisher', percentConsumption: 25, date: new Date('2021-04-23')},
{node:'osi-dds-publisher', percentConsumption: 15, date: new Date('2021-04-23')},
{node: 'osi-dds-subscriber', percentConsumption: 20, date: new Date('2021-04-23')},
{node: 'rti-dds-subscriber', percentConsumption: 20, date: new Date('2022-04-23')},
{node: 'rti-dds-publisher', percentConsumption: 20, date: new Date('2022-04-23')},
{node: 'openDDS-publisher', percentConsumption: 30, date: new Date('2022-04-23')},
{node:'osi-dds-publisher', percentConsumption: 20, date: new Date('2022-04-23')},
{node: 'osi-dds-subscriber', percentConsumption: 10, date: new Date('2022-04-23')},
{node: 'rti-dds-subscriber', percentConsumption: 30, date: new Date('2023-04-23')},
{node: 'rti-dds-publisher', percentConsumption: 20, date: new Date('2023-04-23')},
{node: 'openDDS-publisher', percentConsumption: 10, date: new Date('2023-04-23')},
{node:'osi-dds-publisher', percentConsumption: 25, date: new Date('2023-04-23')},
{node: 'osi-dds-subscriber', percentConsumption: 15, date: new Date('2023-04-23')},
{node: 'rti-dds-subscriber', percentConsumption: 10, date: new Date('2024-04-23')},
{node: 'rti-dds-publisher', percentConsumption: 10, date: new Date('2024-04-23')},
{node: 'openDDS-publisher', percentConsumption: 60, date: new Date('2024-04-23')},
{node:'osi-dds-publisher', percentConsumption: 10, date: new Date('2024-04-23')},
{node: 'osi-dds-subscriber', percentConsumption: 10, date: new Date('2024-04-23')},
{node: 'rti-dds-subscriber', percentConsumption: 30, date: new Date('2025-04-23')},
{node: 'rti-dds-publisher', percentConsumption: 10, date: new Date('2025-04-23')},
{node: 'openDDS-publisher', percentConsumption: 25, date: new Date('2025-04-23')},
{node:'osi-dds-publisher', percentConsumption: 20, date: new Date('2025-04-23')},
{node: 'osi-dds-subscriber', percentConsumption: 15, date: new Date('2025-04-23')}
],
eventData: [
{event: 'Ddos Test', date: new Date('2023')},
{event: 'Bad Actor 1 Test', date: new Date('2025')}
]
}
Function call
this.StackedAreaChart(areaChartData.nodeData, areaChartData.eventData, {
x: d => d.date,
y: d => d.percentConsumption,
z: d => d.node,
yLabel: "% Energy Consumption",
width: 800,
height: 350
})
Here is a link code pen, but it is broken and don't really have enough experience with it to get it working... codepen