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.
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.
Share Improve this question asked Nov 29, 2016 at 19:11 elliotelliot 4981 gold badge4 silver badges13 bronze badges 3- What is unclever about filtering? – Falk Schuetzenmeister Commented Nov 29, 2016 at 19:30
- 1 Run through the dataset multiple times unnecessarily if there is a way to do it at draw time. – elliot Commented Nov 29, 2016 at 20:26
- The way I've done it in the past was to group the data into buckets of continuous data, then plot each of the lines individually. – reptilicus Commented Nov 29, 2016 at 23:02
2 Answers
Reset to default 3Assuming 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 ments.
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/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/d3.v4.min.js"></script>