Multiline chart, can't get line data - javascript

I have a demo here
I have a responsive line chart with multi lines from one data source.
The lines also have circles at each data point.
When the page is resized I want to redraw the graph to change the width.
The circles are working but I can't get the same data for the lines.
I'm creating the actual paths but cant seem to add the data to draw the line.
const dataLine = d3.line()
.x((d) => this.x(d.date) + 0.5 * this.x.bandwidth())
.y((d) => this.y(d.value));
let lines = this.chart.append('g')
.classed('lines', true)
lines.selectAll('.line-group')
.data(data).enter()
.append('g')
.classed('line-group', true)
let linesUpdate = d3.selectAll('.line-group').selectAll("path")
.data(d => d.values)
linesUpdate.enter()
.append('path')
.classed('line', true)
.merge(linesUpdate)
.attr("d", dataLine);
How can I add the data to draw the line

This part of D3's data binding process may be a bit counterintuitive: you have to return an array for the data() method, that's sure... but then each object in the data array will be treated as an individual data! That's why you see no path, because you're actually appending a dozen paths in that group.
The solution is just wrapping the array inside another array. So turn this...
data(d => d.values)
... into this:
data(d => [d.values])
That way, each array will be treated as an individual data point. Then, that data point, which is actually an array of objects, gets passed to the line generator.
Here is your updated code: https://stackblitz.com/edit/multiline-responsive-xbtqzc-fldp7c?file=src/app/bar-chart.ts

Related

D3: Separating data exit/remove/merge from drawing of elements

I am drawing some complex interactive SVGs with D3 v4 and running into some problems. My goals are:
Each data element corresponds to a group with multiple SVG shape elements (e.g. <g><circle></circle><circle></circle></g>)
The multiple SVG shape elements have to be drawn in a certain order (because they overlap)
Certain shape elements are updated without data elements being added or removed (e.g. when clicking on a shape, change the shape color)
I am running into trouble because the .data() -> .exit().remove() -> .enter() -> .merge() process requires a specific order and that order conflicts with the necessary draw order as well as the ability to update styles on the fly. This is what I started with, which does not work because of draw order:
function updateGraph() {
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes, function (d) {
return d._id;
})
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeGEnter
.append('circle')
.classed('interactivecircle', true)
.on('click', function (d) {...})
let eachNodeG = eachNodeGEnter
.merge(eachNodeG)
.style('fill', function (d) {...}) //this is here b/c it needs to change
// when data change (without them being added/removed)
// this must be separate because the background circle needs to change even
// when nodes are not added and removed; but this doesn't work here because
// the circle needs to be in the background
eachNodeG
.append('circle')
.classed('bgcircle', true)
}
I thought maybe I could separate the data update process from the data drawing process entirely, by doing enter() exit() merge() just on the groups containing the data and then drawing everything afterward. But here I run into a different problem: either I remove and re-add all of the shapes on every update (which makes double-clicking difficult and seems like a waste of processing power), or I have to figure out some way to update only the shapes that have changed. Does it using the remove and re-add method looks like this:
// add/remove individual groups based on updated data
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes)
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeG = eachNodeGEnter
.merge(eachNodeG)
// draw (or remove and re-draw) elements within individual groups
d3.selectAll('.bgcircle').remove()
eachNodeG.append('circle')
.classed('bgcircle', true)
d3.selectAll('.interactivecircle').remove()
eachNodeG.append('circle')
.classed('interactivecircle', true)
.style('fill', function (d) {...})
.on('click',function(d){...})
})
Is there a better way to draw the shapes in order while keeping them updateable?
You could use selection.raise or selection.lower to move circles after they have been created.

Drawing multiple sets of multiple lines on a d3.js chart

I have a d3 chart that displays two lines showing a country's imports and exports over time. It works fine, and uses the modular style described in 'Developing a D3.js Edge' so that I could quite easily draw multiple charts on the same page.
However, I now want to pass in data for two countries and draw imports and exports lines for both of them. After a day of experimentation, and getting closer to making it work, I can't figure out how to do this with what I have. I've successfully drawn multi-line charts with d3 before, but can't see how to get there from here.
You can view what I have here: http://bl.ocks.org/philgyford/af4933f298301df47854 (or the gist)
I realise there's a lot of code. I've marked with "Hello" the point in script.js where the lines are drawn. I can't work out how to draw those lines once for each country, as opposed to just for the first one, which is what it's doing now.
I'm guessing that where I'm applying data() isn't correct for this usage, but I'm stumped.
UPDATE: I've put a simpler version on jsfiddle: http://jsfiddle.net/philgyford/RCgaL/
The key to achieving what you want are nested selections. You first bind the entire data to the SVG element, then add a group for each group in the data (each country), and finally get the values for each line from the data bound to the group. In code, it looks like this (I've simplified the real code here):
var svg = d3.select(this)
.selectAll('svg')
.data([data]);
var g = svg.enter().append('svg').append('g');
var inner = g.selectAll("g.lines").data(function(d) { return d; });
inner.enter().append("g").attr("class", "lines");
inner.selectAll("path.line.imports").data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports')
.attr("d", function(d) { return imports_line(d); });
The structure generated by this looks like svg > g > g.lines > path.line.imports. I've omitted the code for the export line here -- that would be below g.lines as well. Your data consists of a list of key-value pairs with a list as value. This is mirrored by the SVG structure -- each g.lines corresponds to a key-value pair and each path to the value list.
Complete demo here.
The point is that you're thinking to imperative. That's why you have so much code. I really can't put it better than Mike Bostock, you have to start Thinking with Joins:
svg.append("circle")
.attr("cx", d.x)
.attr("cy", d.y)
.attr("r", 2.5);
But that’s just a single circle, and you want many circles: one for each data point. Before you bust out a for loop and brute-force it, consider this mystifying sequence from one of D3’s examples.
Here data is an array of JSON objects with x and y properties, such as: [{"x": 1.0, "y": 1.1}, {"x": 2.0, "y": 2.5}, …].
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 2.5);
I'll leave translating this example to the "from one line to many lines" as an excerxise.

Updating a pie chart with a new data set with more values

I'm relatively new to D3 and have been following a few pie chart tutorials.
Namely the Mike Bostock Tutorials. http://bl.ocks.org/mbostock/1346410
But I have question about a donut chart updating from one data set to another with the 2nd data set having much more values than the first.
I have attempted this numerous times through an update function but no luck, I'll keep it simple and give a hypothetical example , lets say my first data set had 5 values
[1,2,3,4,5]
and my second data set had 10 values
[1,2,3,4,5,6,7,8,9,10]
only 5 values of the new data set would be depicted on the arcs after the dynamic update. It's like the pie is fixed with only 5 arc sections being able to display 5 values of the new dataset.
Any help would be appreciated as its been stumbling around with the idea for awhile!
The key to making it work with data of different size is to handle the .enter() and .exit() selections. This tutorial goes into more detail, but briefly the enter selection represents data for which no DOM elements exist (e.g. in the case where you pass in more data), the update selection (which you're already handling) represents data for which DOM elements exist and the exit selection DOM elements for which no data exists anymore (e.g. when you have more elements to start with).
So in your change function, you would need to do something like this.
function change() {
clearTimeout(timeout);
var path = svg.datum(data).selectAll("path")
.data(pie);
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // add the new arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
path.exit().remove(); // remove old arcs
}
This assumes that you're updating your data variable as you suggest above instead of getting a different value from the existing data structure as in the example.
Here I made a simple update that triggers when you click the text above the pie chart: JsFiddle
The main thing happening is all the data is updated when the .on("click") event triggers, so the chart gets updated like so:
d3.select("#update")
.on("click", function (d) {
data = [1,2,3,4,5,6,7,8,9,10];
vis.data([data]);
arc = d3.svg.arc().outerRadius(r);
pie = d3.layout.pie().value(function(d){return d; });
arcs.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.append("svg:path")
.attr("fill", function(d, i){return color(i);}).attr("d", arc);
});

Circles aren't updating in a multiline graph

I have a D3.js multiline graph with circles added on every path peak. When I update my graph, the paths update just fine with the new data but the circles don't seem to get updated at all. Here's my code: http://jsbin.com/eMuQOHoV/3/edit
Does anyone know what I'm doing wrong?
You need to update the data point circles in the same way that you've created them. In particular, you're using a nested selection when creating them, but not when updating. This means that the data won't get matched correctly on update and nothing happens.
The code for the update should look like this.
var sel = svg.selectAll('.series')
.data(sources);
sel.select('path')
.transition()
// etc
// update circles
sel.selectAll('.datapoint')
.data(function (d) {
return d.values;
})
// etc
Complete jsbin here.

Multiple multi-series d3 line charts on one page

Using d3 I want to draw several time series line charts on a single page, each one featuring two lines.
Using the example on this page for multiple charts, I've got code working with single lines on each chart. But I'm not sure how best to modify that example to work with multi-line charts.
The example does this:
d3.csv("sp500.csv", function(data) {
var formatDate = d3.time.format("%b %Y");
d3.select("#example")
.datum(data)
.call(timeSeriesChart()
.x(function(d) { return formatDate.parse(d.date); })
.y(function(d) { return +d.price; }));
});
with TimeSeriesChart() defined in this file.
How would I best adapt this to cope with two (or more) lines (with the same x-axis values, and the same y scales)?
FWIW, my data is in JS arrays/hashes, rather than being read from CSV, but I guess the principle will be the same.
You can pass in your data structure that contains data for several lines in the same way. The only difference would in how you would handle the data in the referenced file. You need to change the function in
selection.each(function(data) {
First, you need to adapt the preprocessing being done and similarly the code that determines the limits of the axes. Further down, you would change
// Update the line path.
g.select(".line")
.attr("d", line);
to something like
g.selectAll(".line").data(<data for your two lines here>)
.enter()
.append("path")
.attr("class", "line")
.attr("d", line);
and remove the line
gEnter.append("path").attr("class", "line");
before that.
The exact changes will depend on the format that you're passing in.
An alternative (and if you're just starting probably easier) approach would be to simply add the additional data in a new attribute, add a new line generator that accesses that data and repeat the code that generates the line and calls the line generator with a different class name and the different generator. This is a hacky approach that I wouldn't recommend in general, but it might be easier to get started that way.

Categories

Resources