So I am drawing a tree (somewhat similar to a decision tree) with D3.js, using a premade template I found online. My goal at this point is to color the edges (or links) according to a property of each link object.
My first step was to color all the edges regardless of their property, so I added the following line:
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
//.style("stroke", "red") ---- this new line changes the color of all links
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
Now, I want to color them according to their property. It is accessible through the first link initialization:
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
// const booleanProperty = d.target.colorProperty; --- change color according to this property
return d.target.id;
});
My thought was to create 2 different variables - one storing the links with the property set to true and another with the property set to false, so I could change the color of each link group. Somewhat like this:
// Update the links…
var trueLink = svgGroup.selectAll("path.link")
.data(links, function(d) {
const booleanProperty = d.target.colorProperty; --- change color according to this property
if (booleanProperty) return d.target.id;
});
var falseLink = svgGroup.selectAll("path.link")
.data(links, function(d) {
const booleanProperty = d.target.colorProperty; --- change color according to this property
if (!booleanProperty) return d.target.id;
});
This attempt didn't work, since selectAll("path.link") returns all the links regardless if there is a condition to return only in specific cases. I also tried using select("path.link"), but I got exactly the same result.
Is there a way I can pull this off?
I assume that you want to set that style in the "enter" selection for new items, so you could try to do the condition inside the .style call, something like this:
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.style("stroke", function(d) { return condition ? "red" : "blue"; } )
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
Where "condition" may be anything you need. If the condition or the color is stored in the datum, you can return d.target.colorProperty or use it for the condition.
Related
I have a multi-line graph and I'm trying to create an interactive legend that when you click on each name, it will disable the corresponding line. It works only for the first one. Here is my code from this example:
svg.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend") // style the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key);
})
.on("click", function() {
// Determine if current line is visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements based on the ID
d3.select("#tag" + d.key.replace(/\s+/g, ''))
.transition().duration(100)
.style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(d.key);
Also, I have this plunker with my visualisation (the data is just a small part of the dataset).
Thank you in advance.
Generally appending things with a foreach loop isn't the preferable option in d3. This is why you are having difficulty.
The first country in the legend is Albania, and only Albania works in the legend. Why? Because every path in the chart has the same id (the id for Albania):
So, when clicking on Albania in the legend, the on click function searches for the line with id tagAlbania (and looks only for the first entry as IDs should be unique) and toggles it.
The key issue you need to solve is why do all your paths share the same id. Let's look at your code:
dataNest.forEach(function(d, i) {
var country = plot.selectAll(".country")
.data(countries)
.enter().append("g")
.attr("class", "country");
country.append("path")
.attr("class", "line")
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
});
On the first pass through this for each loop we add a plot for each country using selectAll().data().enter().append(). As we start with an empty selection, .enter().append() will create an element for each item in the data array: all countries' paths are drawn on the first pass.
On the second pass of the loop, the enter selection is empty - there is an element in the initial selection, selectAll('.country'), for each item in the data array. Therefore no elements are entered and appended on each iteration of the loop beyond the first.
Looking back at the first loop, we can see that d is an item in the array dataNest, and on the first loop, d is the zeroth item - Albania. So, as all paths are appended on the first loop, all paths share the same id:
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
To fix that, we could simply change that line to access the datum of the line being appended:
.attr("id", function(datum) { return 'tag' + datum.name.replace(/\s+/g, '') }) // assign ID
As the datum is the item in the array countries, not dataNest here, I've replaced the property key with the property name
Here's an updated plunkr.
Better yet, we can make this more idiomatic with d3 by dropping the loop altogether, see this plunkr.
My code looks like the following (omitting the scales that I'm using):
// Nesting data attaching "name" as data key.
var dataNest = d3.nest()
.key(function(d) { return d.name; })
.entries(data);
// Defining what the transition for the path.
var baseTransition = d3.transition()
.duration(1000)
.ease(d3.easeLinear);
// Bind data to SVG (use name as key).
var state = svg2
.selectAll(".line2")
.data(dataNest, function(d, i) { return d.key; });
// Enter data join.
state.enter()
.append("path")
.attr("class", "line2");
// Set transition and define new path being transition to.
state.transition(baseTransition)
.style("stroke", "#000")
.style("fill", "none")
.attr("id", function(d) { return 'tag'+d.key.replace(/\s+/g, ''); })
.attr("d", function(d) { return line(d.values); });
state.exit().remove();
I'm mostly following this example in which the transitions are working on top of a drop down list. However, while the code I have above displays paths, it does not transition between the paths. Are there any obvious flaws in my approach?
EDIT: I have a more concrete example of what I want to do with this JSFiddle. I want to transition from data to data2 but the code immediately renders data2.
I have this table and chart with scattergraph:
https://jsfiddle.net/horacebury/bygscx8b/6/
And I'm trying to update the positions of the scatter dots when the values in the second table column change.
Based on this SO I thought I could just use a single line (as I'm not changing the number of points, just their positions):
https://stackoverflow.com/a/16071155/71376
However, this code:
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
Is giving me this error:
Uncaught TypeError: svg.selectAll(...).data is not a function
The primary issue is that:
svg.selectAll("circle") is not a typical selection as you have redefined svg to be a transition rather than a generic selection:
var svg = d3.select("#chart").transition();
Any selection using this svg variable will return a transition (from the API documentation), for example with transition.selectAll():
For each selected element, selects all descendant elements that match
the specified selector string, if any, and returns a transition on the
resulting selection.
For transitions, the .data method is not available.
If you use d3.selectAll('circle') you will have more success. Alternatively, you could drop the .transition() when you define svg and apply it only to individual elements:
var svg = d3.select('#chart');
svg.select(".line").transition()
.duration(1000).attr("d", valueline(data));
...
Here is an updated fiddle taking the latter approach.
Also, for your update transition you might want to change scale and values you are using to get your new x,y values (to match your variable names):
//Update all circles
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return x(d.date);
})
.attr("cy", function(d) {
return y(d.close);
});
}
I'm currently trying to implement a line-graph with D3.js
The D3-documentation contains the following examples:
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis");
g.append("path")
.attr("d", line);
... and says "Whatever data is bound to g (in this example) will be passed to the line instance."
What is meant with "bounding data"?
I've got the following code which works:
var lineGen = d3.svg.line()
.x(function(d, i) {
return xScale(i);
})
.y(function(d) {
return HEIGHT - yScale(d);
})
.interpolate('basis');
var lineVal = lineGen(data);
svg.append('path').attr('d', lineVal)
.attr({
'fill': 'none',
'stroke': 'black',
'stroke-width': '3'
});
With lineGen(data) I generate a string. Then I assign this string to the attr-setter.
But the way which is explained in the official documentation doesn't work.
What do I have to do to bound my data to the SVG element directly?
svg.data(data) doesn't work. I've tried that already.
UPDATE
I've found the solution in a tutorial.
// Data-structure. In this case an
// one-dimensional array.
var data = [ 22, 31, 29, 32, 21, 38, 30 ];
svg.append('path')
// Assign the data-structure as ONE
// element an array-literal.
.data([data])
.attr('d', lineGen)
.attr({
'fill': 'none',
'stroke': 'black',
'stroke-width': '3'
});
By assigning the data-structure as an element of an array-literal the
whole data are bound to one element.
An alternative to this technique is the usage of selection.datum().
https://github.com/mbostock/d3/wiki/Selections#datum
In d3 data is bond to the selections. You can use enter and exit selections to create new nodes for incoming data and remove outgoing nodes that are no longer needed.
In your example, you should try as shown below.
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis");
g.selectAll("path") //Selects all the elements that match the specific selector, here elements having path tag
.data(data) //The data operator joins the specified array of data with the current selection.
.enter() //The enter selection contains placeholders for any missing elements. Here it has placeholders for n path elements where n is the length of data array.
.append("path") //For each placeholder element created in the previous step, a path element is inserted.
.attr("d", line); //The first param of line function would be the data (corresponding array element) bond to the current path element. And the second param is the data index.
To know more about data binding in d3 refer : Binding-data-to-dom-elements.
Edit :
To create a single path element with data bonded, try as shown below.
g.append("path")
.datum(dataObj) //Where dataObj is data[i]
.attr("d", line);
I am trying to build a graph using the force layout in D3. I would like to build different looking nodes depending on the data. Currently all nodes have a category and a name. So I draw an svg:g consisting of two rect and two text elements.
My code currently looks something like this:
// nodes are in the form: { group: 'string', name: 'string2' }
this.node = this.node.data(this.node, function(d) { return d.id; });
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
If the node doesn't have a name, however, I'd like to supress the creation of the second rect and text. Logically, if it wasn't for the implicit iterator in d3 I'd be doing something like:
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
// unfortunately 'd' isn't defined out here.
// EDIT: based on comment from the answer below; the conditional should
// be for the text and the related rectangle.
if(d.name) {
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
}
You could use an each call on your g selection to decide whether or not to add the label.
g.each(function(d) {
if (d.name){
var thisGroup = d3.select(this);
thisGroup.append("text")
.text(d.group);
thisGroup.append("text")
.attr("y", 20)
.text(d.name);
});
However, be aware that this structure could get confusing if you're going to be updating the data.
If you want to be able to update neatly, I would recommend doing a nested selection:
var labels = g.selectAll("text")
.data(function(d){ d.name? [d.group, d.name]:[]; });
labels.enter().append("text");
labels.exit().remove();
labels.text(function(d){return d;})
.attr("y", function(d,i){return i*20;});
The data-join function tests the parent's data object, and based on it either passes an array containing the two values you want to use for the label text, or an empty array. If it passes the empty array, then no labels are created; otherwise, each label has it's text set by the value in the array and it's vertical position set by the index.