d3.jssvgchartsburndowncharts

Changing the line style in d3js for every segment?


I'm trying to visualise the "additional work" resulting from change request in a burn chart. I managed to draw the chart, but I like to have the vertical lines that show the increase to have their own colour. Something like this:

Burnchart

My data is like this:

   var actualRaw = [{
        date: new Date(2017, 1, 1),
        added: 0,
        done: 50
    }, {
        date: new Date(2017, 1, 15),
        added: 10,
        done: 40
    }]

which I transform like this:

    var actual = [];

    actualRaw.map(line => {
        actual.push({
            date: line.date,
            points: line.done,
            class: 'la'
        });
        actual.push({
            date: line.date,
            points: line.done + line.added,
            class: 'ln'
        });

    })

and then try to apply formatting like this:

    chart.append("path")
        .datum(actual)
        .attr("class", function(d, i) {
            return 'line ' + d[i].class;
        })
        .attr("d", actualLine);

However that function gets called only once. What do I miss?

My attempt so far complete.


Solution

  • Simply put: you can't style different segments of a SVG <path> element like that.

    There are alternatives, though. Here, for instance, I'm simply adding a key/value pair in actual...

    actualRaw.map(line => {
        actual.push({
            date: line.date,
            points: line.done,
            class: 'la'
        });
        actual.push({
            date: line.date,
            points: line.done + line.added,
            class: 'ln',
            added: line.added//this one here...
        });
    })
    

    ... and using it to draw simple lines (that is, SVG <line> elements):

    var redLines = chart.selectAll(null)
        .data(actual.filter(function(d) {
            return d.added
        }))
        .enter()
        .append("line")
        .attr("x1", function(d) {
            return x(d.date)
        })
        .attr("x2", function(d) {
            return x(d.date)
        })
        .attr("y1", function(d) {
            return y(d.points)
        })
        .attr("y2", function(d) {
            return y(d.points - d.added)
        })
        .style("stroke", "red");
    

    Here is the code with those changes:

    <head>
      <title>Burn baby burn</title>
      <script src="https://d3js.org/d3.v4.min.js"></script>
      <style type="text/css">
        .chart {
          border: 1px solid black;
        }
        
        .chart div {
          font: 10px sans-serif;
          background-color: steelblue;
          text-align: right;
          padding: 3px;
          margin: 1px;
          color: white;
        }
        
        .chart rect {
          stroke: white;
          fill: steelblue;
        }
        
        .axis path,
        .axis line {
          fill: none;
          stroke: #000;
          shape-rendering: crispEdges;
        }
        
        .line {
          fill: none;
          stroke-width: 2px;
        }
        
        .line.ideal {
          stroke: steelblue;
        }
        
        .la {
          fill: none;
          stroke-width: 2px;
          stroke: green;
        }
        
        .ln {
          fill: none;
          stroke: red;
          stroke-width: 4px;
        }
      </style>
    </head>
    
    <body>
      <h1>Burn Chart</h1>
      <script type="text/javascript">
        var margin = {
            top: 20,
            right: 20,
            bottom: 30,
            left: 50
          },
          width = 960 - margin.left - margin.right,
          height = 500 - margin.top - margin.bottom;
        var x = d3.scaleTime()
          .range([0, width]);
        var y = d3.scaleLinear()
          .range([height, 0]);
        var ideal = [{
          date: new Date(2017, 1, 1),
          points: 50
        }, {
          date: new Date(2017, 12, 31),
          points: 0
        }];
        var actualRaw = [{
          date: new Date(2017, 1, 1),
          added: 0,
          done: 50
        }, {
          date: new Date(2017, 1, 15),
          added: 10,
          done: 40
        }, {
          date: new Date(2017, 2, 1),
          added: 0,
          done: 40
        }, {
          date: new Date(2017, 3, 1),
          added: 20,
          done: 30
        }, {
          date: new Date(2017, 4, 1),
          added: 10,
          done: 20
        }, {
          date: new Date(2017, 5, 1),
          added: 5,
          done: 10
        }, {
          date: new Date(2017, 6, 1),
          added: 0,
          done: 10
        }, {
          date: new Date(2017, 7, 1),
          added: 5,
          done: 10
        }, {
          date: new Date(2017, 8, 1),
          added: 0,
          done: 10
        }, {
          date: new Date(2017, 9, 1),
          added: 20,
          done: 20
        }, {
          date: new Date(2017, 10, 1),
          added: 0,
          done: 10
        }, {
          date: new Date(2017, 11, 1),
          added: 5,
          done: 10
        }, {
          date: new Date(2017, 12, 1),
          added: 0,
          done: 0
        }];
        var actual = [];
        actualRaw.map(line => {
          actual.push({
            date: line.date,
            points: line.done,
            class: 'la'
          });
          actual.push({
            date: line.date,
            points: line.done + line.added,
            class: 'ln',
            added: line.added
          });
        })
        var idealLine = d3.line()
          .x(function(d) {
            return x(d.date);
          })
          .y(function(d) {
            return y(d.points);
          });
        var actualLine = d3.line()
          .x(function(d) {
            return x(d.date);
          })
          .y(function(d) {
            return y(d.points);
          });
        x.domain(d3.extent(ideal, function(d) {
          return d.date;
        }));
        y.domain(d3.extent(actual, function(d) {
          return d.points;
        }));
        var xAxis = d3.axisBottom()
          .scale(x)
          .tickFormat(d3.timeFormat("%b %d"));
        var yAxis = d3.axisLeft()
          .scale(y);
        var chart = d3.select("body").append("svg")
          .attr("class", "chart")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
          .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
        // Create the x-axis
        chart.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")")
          .call(xAxis);
        // Create the y-axis
        chart.append("g")
          .attr("class", "y axis")
          .call(yAxis)
          .append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", 6)
          .attr("dy", ".71em")
          .style("text-anchor", "end")
          .text("Points");
        // Paint the ideal line
        chart.append("path")
          .datum(ideal)
          .attr("class", "line ideal")
          .attr("d", idealLine);
        var counter = 0;
        // Paint the actual line
        chart.append("path")
          .datum(actual)
          .attr("class", function(d, i) {
            return 'line ' + d[i].class;
          })
          .attr("d", actualLine);
    
        var redLines = chart.selectAll(null)
          .data(actual.filter(function(d) {
            return d.added
          }))
          .enter()
          .append("line")
          .attr("x1", function(d) {
            return x(d.date)
          })
          .attr("x2", function(d) {
            return x(d.date)
          })
          .attr("y1", function(d) {
            return y(d.points)
          })
          .attr("y2", function(d) {
            return y(d.points - d.added)
          })
          .style("stroke", "red")
          .style("stroke-width", 4);
      </script>
    </body>