1

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

0 Answers0