D3.js does not draw all lines (only some of them)

时间:2018-02-01 18:07:54

标签: d3.js

Consider the following:

page.html:

<!DOCTYPE html>
<!-- https://bl.ocks.org/mbostock/3886208 -->
<style>

    .grid line {
        stroke: lightgrey;
        stroke-opacity: 0.7;
        shape-rendering: crispEdges;
    }

    .grid path {
        stroke-width: 0;
        pointer-events: none;
    }

    .monthNumbersLabel {
        position:relative;top:15px;
    }

    /*Line Graph*/
    .line {
        fill: none;
        stroke-width: 2px;
    }

    /*Points on line graph*/
    .point {
        r: 3.5;
    }

    svg:first-of-type {
        margin-top: 30px;
    }

</style>

<script src="https://d3js.org/d3.v4.js"></script>
<body>
<script>
var margin = {top: 50, right: 20, bottom: 50, left: 80},
    width = 1400 - margin.left - margin.right,
    height = 700 - margin.top - margin.bottom;

var parseDate = d3.timeParse("%m/%d/%Y");

var svg = d3.select("body").append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom);

var g = svg.append("g")
           .attr("transform", "translate(" + margin.left + "," + margin.top + ")");


var x  = d3.scaleTime()
           .range([0, width - margin.left - margin.right]);

var y  = d3.scaleLinear().range([height, 0]);

var z  = d3.scaleOrdinal()
                      .range(['#4472C4','#ED7D31','#FFC000','#C00000',
                              '#E495A5']);

var xMonthAxis  = d3.axisBottom(x)
                    .ticks(d3.timeMonth.every(1))
                    .tickFormat(d3.timeFormat("%b")); // label every month

var xYearAxis  = d3.axisBottom(x)
                   .ticks(d3.timeYear.every(1))
                   .tickFormat(d3.timeFormat("%Y")); // label every year

var yAxis  = d3.axisLeft(y).tickFormat(d3.format(".0%")).tickSize(-width+margin.left+margin.right);

// load .csv file
d3.csv("test_data.csv" + '?' + Math.floor(Math.random() * 1000), // for avoiding caching: see https://stackoverflow.com/questions/13053096/avoid-data-caching-when-using-d3-text
function(error, data){
    if (error) throw error; 

    data.forEach(function(d) {
        d.Week_Ending = parseDate(d.Week_Ending);
        d.Percent = +d.Percent;
    });

    data.sort(function(a, b) { return b.Week_Ending - a.Week_Ending; });

    x.domain(d3.extent( data, function(d){ return d.Week_Ending; }) );

    var max = x.domain()[1];
    var min = x.domain()[0];

    x.domain([min,max]);
    y.domain([0, d3.max(data, function(d) { return d.Percent; })]).nice();

    // x-axis
    var monthAxis = g.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xMonthAxis);

    const firstDataYear = x.domain()[0];
    if (firstDataYear.getMonth() == 11){ // When .getmonth() == 11, this shows two years overlapping with each other, making the label look ugly
        const firstDataYearOffset = d3.timeDay.offset(firstDataYear, 1);
        var tickValues = x.ticks().filter(function(d) { return !d.getMonth()});
        if(tickValues.length && firstDataYearOffset.getFullYear() !== tickValues[1].getFullYear()) {
            tickValues = [firstDataYearOffset].concat(tickValues); 
        } else {
            tickValues = [firstDataYearOffset];
        }
    } else { 
        var tickValues = x.ticks().filter(function(d) { return !d.getMonth()});
        if(tickValues.length && firstDataYear.getFullYear() !== tickValues[0].getFullYear()) {
            tickValues = [firstDataYear].concat(tickValues); 
        } else {
            tickValues = [firstDataYear];
        }
    }

    xYearAxis.tickValues(tickValues);

    var yearAxis = g.append("g")
                     .attr("class", "yearaxis axis")
                     .attr("transform", "translate(0," + (height + 25) + ")")
                     .call(xYearAxis);

    var valueAxis = g.append("g")
                 .attr("class", "y axis grid")
                 .call(yAxis);

    monthAxis.selectAll("g").select("text");

    var dataByCategory = d3.nest()
                           .key(function(d) { return d.Site; })
                           .entries(data);

    // https://stackoverflow.com/a/15030117/3625022
    function flatten(arr) {
        return arr.reduce(function (flat, toFlatten) {
        return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
        }, []);
    }

    var dataFrame = flatten(dataByCategory.map(function(a) {return a.values;}));

    x.domain(d3.extent(data, function(d) { return d.Week_Ending; }));
    y.domain([0, d3.max(data, function(d) { return d.Percent; })]);

    var valueline = d3.line()
                      .x(function(d) { return x(d.Week_Ending); })
                      .y(function(d) { return y(d.Percent); });

    g.selectAll('path')
                    .data(dataByCategory)
                    .enter()
                    .append("path")
                    .attr("class", function(d){
                        return "line " + d.key;
                    })
                    .attr("stroke", function(d){
                        return z(d.key);
                    })
                    .attr("d", function(d) {
                        return valueline(d.values);
                    });

    g.selectAll('dot')  
                    .data(dataFrame) // have to use a "flat" version to get the points
                    .enter()
                    .append("circle")
                    .attr("class", function(d){
                        return "point " + d.Site;
                    })
                    .attr("fill", function(d){
                        return z(d.Site);
                    })
                    .attr("cx", function (d, i){
                        return x(d.Week_Ending);
                    })
                    .attr("cy", function (d, i){
                        return y(d.Percent);
                    });

    }
);
</script>
</body>

test_data.csv:

Site,Percent,Week_Ending
1,0.01999,8/27/2016
1,0.02564,9/24/2016
1,0.0287,10/22/2016
1,0.02751,11/19/2016
1,0.02624,12/17/2016
1,0.02826,1/14/2017
1,0.02698,2/11/2017
1,0.026,3/11/2017
1,0.031479367,4/8/2017
1,0.030840815,5/6/2017
1,0.03475,6/3/2017
1,0.03592,7/1/2017
1,0.03729,7/29/2017
1,0.041,8/26/2017
1,0.03856,9/23/2017
1,0.03886,10/21/2017
1,0.03723,11/18/2017
1,0.03513,12/16/2017
1,0.03172,1/13/2018
2,0.03819,8/27/2016
2,0.021,9/24/2016
2,0.02641,10/22/2016
2,0.01769,11/19/2016
2,0.01597,12/17/2016
2,0.01894,1/14/2017
2,0.01881,2/11/2017
2,0.01572,3/11/2017
2,0.014007761,4/8/2017
2,0.012521765,5/6/2017
2,0.01319,6/3/2017
2,0.0188,7/1/2017
2,0.01517,7/29/2017
2,0.02421,8/26/2017
2,0.01511,9/23/2017
2,0.01333,10/21/2017
2,0.0127,11/18/2017
2,0.01023,12/16/2017
2,0.01205,1/13/2018
3,0.01157,8/27/2016
3,0.01265,9/24/2016
3,0.00864,10/22/2016
3,0.01176,11/19/2016
3,0.01069,12/17/2016
3,0.01086,1/14/2017
3,0.01228,2/11/2017
3,0.01108,3/11/2017
3,0.00915436,4/8/2017
3,0.005970321,5/6/2017
3,0.01039,6/3/2017
3,0.01026,7/1/2017
3,0.01368,7/29/2017
3,0.01323,8/26/2017
3,0.00651,9/23/2017
3,0.00753,10/21/2017
3,0.00776,11/18/2017
3,0.00827,12/16/2017
3,0.0097,1/13/2018
4,0.0322,8/27/2016
4,0.02678,9/24/2016
4,0.03054,10/22/2016
4,0.04087,11/19/2016
4,0.02543,12/17/2016
4,0.04023,1/14/2017
4,0.02782,2/11/2017
4,0.03351,3/11/2017
4,0.036742785,4/8/2017
4,0.032985858,5/6/2017
4,0.03487,6/3/2017
4,0.03926,7/1/2017
4,0.03093,7/29/2017
4,0.03565,8/26/2017
4,0.03464,9/23/2017
4,0.03243,10/21/2017
4,0.0351,11/18/2017
4,0.03647,12/16/2017
4,0.03273,1/13/2018
5,0.02887,8/27/2016
5,0.04095,9/24/2016
5,0.04198,10/22/2016
5,0.05206,11/19/2016
5,0.0473,12/17/2016
5,0.04028,1/14/2017
5,0.05827,2/11/2017
5,0.05404,3/11/2017
5,0.055141464,4/8/2017
5,0.044402436,5/6/2017
5,0.04225,6/3/2017
5,0.03228,7/1/2017
5,0.03833,7/29/2017
5,0.03042,8/26/2017
5,0.03674,9/23/2017
5,0.03439,10/21/2017
5,0.0348,11/18/2017
5,0.03562,12/16/2017
5,0.03496,1/13/2018

enter image description here

Why is it that some paths are not being drawn, while others are?

I don't think it's because of missing data, at a quick glance. If this does happen to be because of missing data, I would at least prefer that points in adjacent months be connected. I've looked at http://bl.ocks.org/d3noob/38744a17f9c0141bcd04 as well as https://bocoup.com/blog/showing-missing-data-in-line-charts#solution2.

1 个答案:

答案 0 :(得分:2)

The issue is here:

g.selectAll('path')
    .data(dataByCategory)
    .enter()
    .append("path")

An enter selection is used to add one element to the DOM for each item in the data array that does not have a corresponding element (in the DOM). If you have no corresponding elements (empty selection), every item in the data array will result in an element in the enter selection.

But, you don't have an empty selection. You are selecting three paths, which means the first three elements in your data array won't be entered - they already exist and don't need entering.

What are you selecting though? You are selecting paths in your three axes:

enter image description here

Instead, try something like:

g.selectAll(".line")     // select the series of the chart
  .data(data)
  .enter()
  .append("path")
  .attr("class","line"); // identify each series as series

Or a null selection:

g.selectAll(null)  // empty
  .data(data)
  .enter()
  .append("path") // enter each item in the data array.