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.
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.
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.
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>
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With