Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force a d3 line chart to ignore null values

I have a timeseries line chart that contains null values, and thereby leaves a gap in my lines. What I want to do is optionally have the d3 line generator ignore the null values and span the gap.

As you can see in the image, the blue series has gaps.

As you can see in the image, the blue series has gaps.

Part of my problem is I have standardized on this data format:

[
  {"x":1397102460000,"y0":11.4403,"y1":96.5},
  {"x":1397139660000,"y0":13.1913,"y1":96.5},
  {"x":1397522940000,"y1":96.5},
  ...
]

So when one series has a reading for a particular timestamp, the other series has a null value.

Ultimately, I could try solving this by filtering my data prior drawing, but I am hoping for a more clever solution, maybe around the line generator.

My line generator is pretty simple:

line = d3.line()
         .x(function(d) {
           return ~~_this.x(d[xKey]);
         })
         .y(function(d) {
           return ~~_this.y(d[yKey]);
         })
         .defined(function(d) {
           return d[yKey] || d[yKey] === 0;
         });

If I remove my defined method, the lines connect, but the null y value is interpreted as 0px rather than just not existing.

enter image description here

Is there a way to tell the line generator not to include a point at null data?

I should also note that I am using d3 v4.x.

like image 955
elliot Avatar asked Nov 29 '16 19:11

elliot


2 Answers

Assuming that you want the line to go from the last valid point to the next valid point, as a straight line, the best idea is filtering your data, as advised in the comments.

line.defined creates gaps in the line, that's the expected behaviour and that's what this function is for.

So, if you don't want to filter the data and you don't want to create gaps (using defined), you can change the line generator function. It's not a good idea, it's a ugly code, but it's doable.

set a counter:

var counter;

If the value is valid (!= null), increase the counter. If it's not, tell the line generator to keep the last valid value:

var line = d3.line()
    .x((d, i) => {
        if (data[i].y) {
            return xScale(d.x)
        } else {
            return xScale(data[counter].x)
        }
    })
    .y((d, i) => {
        if (d.y) {
            counter = i;
            return yScale(d.y)
        } else {
            return yScale(data[counter].y)
        }
    });

In this demo, the line jumps from the fifth data point (the last valid value) to the ninth data point (the next valid value):

var w = 500,
    h = 300;
var svg = d3.select("body")
    .append("svg")
    .attr("width", w)
    .attr("height", h);

var data = [{
    x: 10,
    y: 50
}, {
    x: 20,
    y: 70
}, {
    x: 30,
    y: 20
}, {
    x: 40,
    y: 60
}, {
    x: 50,
    y: 40
}, {
    x: 60,
    y: null
}, {
    x: 70,
    y: null
}, {
    x: 80,
    y: null
}, {
    x: 90,
    y: 90
}, {
    x: 100,
    y: 20
}];

var xScale = d3.scaleLinear()
    .domain([0, 100])
    .range([30, w - 20]);

var yScale = d3.scaleLinear()
    .domain([0, 100])
    .range([h - 20, 20]);

var counter;

var line = d3.line()
    .x((d, i) => {
        if (data[i].y) {
            return xScale(d.x)
        } else {
            return xScale(data[counter].x)
        }
    })
    .y((d, i) => {
        if (d.y) {
            counter = i;
            return yScale(d.y)
        } else {
            return yScale(data[counter].y)
        }
    });

svg.append("path")
    .attr("d", line(data))
    .attr("stroke-width", 2)
    .attr("stroke", "teal")
    .attr("fill", "none");

var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);

svg.append("g")
    .attr("transform", "translate(0," + (h - 20) + ")")
    .call(xAxis);

svg.append("g")
    .attr("transform", "translate(30,0)")
    .call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 61
Gerardo Furtado Avatar answered Nov 17 '22 13:11

Gerardo Furtado


There is another way could do this.

var line = d3.line()
.x(function(d){ return xScale(d[0]);})
.y(function(d){ return yScale(d[1]);})
.defined(function(d) {
        return d[1] || d[1] === '0';
     });

var filteredData = data.filter(line.defined());

svg.append("path")
.attr("d", line(filteredData)

var w = 500,
  h = 300;
var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var data = [
  [10, 50],
  [20, 70],
  [30, 20],
  [40, 60],
  [50, 40],
  [60, null],
  [70, null],
  [80, null],
  [90, 90],
  [100, 20]
];

var xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([30, w - 20]);

var yScale = d3.scaleLinear()
  .domain([0, 100])
  .range([h - 20, 20]);

var counter;

var line = d3.line()
  .x(function(d) {
    return xScale(d[0]);
  })
  .y(function(d) {
    return yScale(d[1]);
  })
  .defined(function(d) {
    return d[1] || d[1] === '0';
  });

var filteredData = data.filter(line.defined());

svg.append("path")
  .attr("d", line(filteredData))
  .attr("stroke-width", 2)
  .attr("stroke", "teal")
  .attr("fill", "none");

var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);

svg.append("g")
  .attr("transform", "translate(0," + (h - 20) + ")")
  .call(xAxis);

svg.append("g")
  .attr("transform", "translate(30,0)")
  .call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 22
Anjaly Avatar answered Nov 17 '22 14:11

Anjaly