How to create legend for D3 Sequence Sunburst? - javascript

I'm modifying the original D3 Sequence Sunburst file to better suit my needs. The original colors variable is a hard-coded object. This clearly cannot be the best method. I'm using the flare.json example, which is larger, harder to read, and still much smaller than the json file I will be user after testing.
I'd like to randomly generate colors, apply them to each datum in the createvisualization function, but I'm new to D3, and do not know how to 1) fetch names (everything but the leaves) from the json file, and 2) pair them with their random color.
Edit:
Adding random colors and applying them turned out to be trivial,
var colors = d3.scale.category10();
...
.style("fill", function(d,i) { return colors(i); })
But I'm still note sure how to fetch the names of all non-leaves in the json, then create an array from both the random colors and the non-leaves.
Help here greatly appreciated.

To get the names of all non-leaf elements, you can do something like this.
var names = [];
function getNames(node) {
if(node.children) {
names.push(node.name);
node.children.forEach(function(c) { getNames(c); });
}
}
getNames(root);
After running this code, names will contain all the names you want. To then generate a legend from that, you can use the names array as the data:
var gs = svg.selectAll("g.name").data(names).enter().append("g").attr("class", "name");
gs.append("rect")
// set position, size etc
.attr("fill", d);
gs.append("text")
// set position etc
.text(String);
This will append a g element for each name and within each g element, append a rect that is filled with the colour corresponding to the name and a text element that shows the name.

Related

How should I bind data to appended elements to a transformed D3.js group?

I am approaching the problem of appending a complex (two or more glyphs) symbol to some data. With D3.js, it seems that the right way to do so is appending the glyphs (in this example, circles) to groups (g) joined to data:
datum <=> g <=> (circle, circle)
Both the groups and the appended glyphs have properties depending on data, so that for example g is translated by .start and the position of the second circle is given by .end for each datum.
In order to achieve this, I wrote the following code (see the notebook), which however does not work as expected
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
console.log(a[0].end - a[0].start);
t += 0.1;
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
The expected result would be as follows: two circles oscillate around their initial position, with a phase of period/4 between them. Instead, the second circle (to which I assigned an attribute cx, in order to give the position relative to the first one) is not refreshed, but instead all of its positions are drawn one after the other, oscillating with the translation in the attribute "transform".
I think that the problem is appending circles every time I update data; but how should I then append them? I tried something like this, following https://bost.ocks.org/mike/nest/:
var groups = svg.selectAll('g').data(a).enter().append('g').attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
var circle_start = groups.selectAll('circle').data((d)=>{return d.start;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
var circle_end = groups.selectAll('circle').data((d)=>{return d.end;}).enter().append('circle').attr('cx', d=>d).attr('cy', d=>100).attr('r', 10);
but it gives no output. Doing a bit of debug, for example assigning another dataset to one of the two circles, apparently the problem lies in .data(d)=>{return d.end;}).
Problem
On the pragmatic side, your update function doesn't work as expected because each update you append two new circles to each g entered or updated with selectAll().join():
function updatea (){
// join g's
var miao = svg.selectAll('g.aaa').data(a).join('g')
.classed('aaa',true)
.attr('transform',(d, i)=>('translate('+d.start+','+(i+1)*50+')'));
// append two circles to each g entered or updated:
miao.append('circle').attr('r', 10).attr('fill','red');
miao.append('circle').attr('r', 10).attr('cx', d=>d.end).attr('fill','red');
}
If you inspect the page you'll see two new circles appended each update. You're never updating the circles that are already on the page, just the translate on the g.
On the more theoretical side, you are unclear if your approach is most appropriate for binding data to complex symbols.
Solution
In proposing a solution let's consider the theoretical side first.
D3 was designed with data binding in mind. Normally in D3 you want to bind one datum per element (or one datum per element with a single child in each level of nesting). Where data is grouped and each datum is represented with multiple children, we would often see a second, nested, selectAll().data().join() statement.
However, if your visualization uses a symbol that is always comprised of the same set of child elements, then we don't need to do a nested join. In fact we do not need to in order to stay true to the data binding philosophy in D3: we'll bind one datum to one symbol (symbol in the data visualization sense).
This is the approach I'll propose here.
Rationale
This approach has advantages depending on situation, for example, there may be cases where the symbol's elements share parts of the datum (as in your case where d.start and d.end are both used to set the position of one of the sub-components) - splitting the datum into a new data array would be unnecessarily cumbersome. Changes in the symbol's representation/behavior/etc may require different parts of the datum as well, in which case it doesn't make sense to split the parent datum up.
Also, another reason why the proposed approach is attractive is that if you break the datum into smaller sub-components by using a nested selection:
svg.selectAll("g").data(data).enter().append("g")
.selectAll("circle").data(function(d) { return [d.start,d.end]; })
...
Or by flattening your array:
svg.selectAll("g").data([data[0].start,data[0].end,data[1].start,...])
...
It isn't as clear what child datum corresponds to what property when entering/updating your elements or what even what child datum corresponds to what parent datum. But also, say you dislike the symbol and now want a circle and rect, or two circles and a rect, then you need to substantially adjust the above approaches (perhaps by creating a fancy enter function that returns different types of shapes depending on index or on some identifier that tells you what symbol sub-component the datum corresponds to).
I believe attempting to create one unique datum per element is not ideal in this case, which is why I'm advocating for one datum per symbol.
Implementation
So, let's do one datum per symbol (where the symbols have child elements). This should be fairly easy to implement, I'll go over a simple method to do this here:
We can create the symbol in the join's enter function, and update it in the update function:
function updatea (){
a[0].start += 10*Math.sin(t);
a[0].end += 10*Math.cos(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
First, it's important to note, even though you've likely noticed, the parent datum is passed to child elements when using selection.append().
In the enter function passed to selection.join() We enter the g, style it as appropriate. Then we add the symbol sub-components and set their initial properties.
In the update function we update the overall position of the symbol and then the sub components.
Nothing occurs outside the join method in this case.
I cannot fork your observable without creating another account somewhere, so I'll just make a snippet of your example:
const svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 150);
var a = [{'start': 100, 'end': 200},{'start':100, 'end':200}];
var t = 0;
function updatea (){
a[0].start += 5*Math.sin(t);
a[0].end += 5*Math.cos(t);
a[1].start += 5*Math.cos(t);
a[1].end += 5*Math.sin(t);
t += 0.1;
var miao = svg.selectAll('g').data(a).join(
enter => {
// create the parent element to hold the symbol
let entered = enter.append('g')
.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
.attr('class','symbol');
// append the sub-components of the symbol
entered.append('circle').attr('r', 10).attr('fill','red');
entered.append('circle').attr('class','end').attr('r', 15).attr('fill','yellow').attr('cx',d=>d.end);
},
update => {
// update overall positioning
update.attr('transform', (d,i) =>'translate('+d.start+','+(i+1)*50+')')
// update the sub-components
update.select('.end').attr('cx',d=>d.end);
return update
},
exit => exit.remove()
)
}
updatea();
setInterval(updatea, 100)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
The idea is that since we have two circles, have two data with them. (Ignore what's in them for now:)
var a = [{...},
{...}];
Let's create a group:
var group = svg.append("g")
Then, d3 will "join" the data to DOM nodes. For each data, d3 creates a DOM node. In this case, since we're join()ing to circles, d3 will create a circle for each data. See this page for more details.
group
.selectAll('circle')
.data(a)
.join('circle')
.attr('r', 10)
.attr('fill','red')
.attr('transform', (d, i) => ('translate('+d.start+','+(i+1)*50+')'));
As for the actual logic, there's a couple things I changed.
Each circle now stores its own t and nothing else:
var a = [{t: 0},
{t: Math.PI/2}];
Then, the start and end attributes are set in order to have a representation independent of the current object's state. This allows us to have circles which have different t phases:
a.forEach((d, i) => {
d.start = 200 + 100*Math.sin(d.t);
d.end = 200 + 100*Math.cos(d.t);
d.t += 0.1;
})
Breaking it down:
(initial (range)
position)
200 + 100*Math.cos(d.t);
So it starts at position 200 and can either go to +100 or -100: the effective range is [100, 300].
You notice we do a lot of number crunching here. Basically, we're converting one domain of numbers (-1, 1) to a range (100, 300). This is a common use case for a scale, which can convert any domain to any range.
Observable notebook

Can't reference data objects in d3js

I can't wrap my head around manipulating the data object in d3js. I'm planning to create a chart composed of horizontal bars to hold data elements. Each data element is a circle. I figured out how to insert circles into the different bars, but I'm stuck on how to equally space the circles in each bar. For example, if the width is 800 and there are 8 circles, the x attribute should be 100*i.
Here's a link to my project: https://plnkr.co/edit/fHrdJsItEqA5qc35iUxG?p=preview
I think the problem is how to reference the data object in this block of code. Anyways, I would like to equally space the circles using scaleBand which I defined as variable x earlier in my code:
var x = d3.scaleBand()
.range([0,width]);
I think the solution would look something like this: .attr("x",x.domain(data.map(function(d,i){return d[i]})); x.bandwidth(), but obviously data is not the right object.
Selecting each bar and inserting circles:
bar.selectAll("rect")
.data(function(d,i){console.log(data_group[i].values.length);return data_group[i].values})
.enter().append("circle")
.attr("class","circle")
.attr("width", width)
//.attr("x",) //how to equally space these circle elements???
.attr("height",20)
.attr("y", y.bandwidth())
console.log(y.bandwidth());
As always, I would really appreciate your help.
There are a number of issues with your code that are preventing it from working, including:
You aren't setting a domain for your x scale.
You are attempting to place <circle>s inside of <rect>s but you cannot nest shapes in SVGs. You should place both inside of a <g>.
A <circle>'s position is set using the cx and cy attributes (and you also need to provide it an r radius attribute).
To address your question, you will need to determine how you want your items laid out. Because you are referencing the index in your question, I will use that.
You are breaking your data into nested groups where each one has a values array. You are rendering a <circle> for each datum in that array, so you will want to determine the length of the longest values array.
var longest = data_group.reduce(function(acc, curr) {
return curr.values.length > acc ? curr.values.length : acc;
}, -Infinity);
Once you have the length of the longest values array, you can set the domain for your x scale.
You are using d3.scaleBand (d3.scalePoint would probably work better here), which is an ordinal scale. Ordinal scales work on discrete domains, which means that you will need to have a domain value for each possible input (the indices). For this, you will need to generate an array of the possible indices from 0 to longest-1.
var domainValues = d3.range(longest);
Now that you have the input domain values, you can set them for the x scale.
x.domain(domainValues);
Then, for each <circle>, you will set its cx value using the index of the circle in its group and the x scale.
.attr('cx', function(d,i) { return x(i); })
As I mentioned in the beginning, there are other errors in your code, so just fixing this won't get it running correctly, but it should push you in the right direction.

Coloring a map using D3.js

I'm new to StackOverflow and I just started using D3.
I need to show the values on a map. I saw this question that is very similar to what I should do.
What I'd like is to color the countries based on the values in the column Date to a CSV and based on selected year by user (radio button).
How can I do that?
I created a gray color scale and have included them in an array, then I created a method chooseColor(value) that returns the correct color based on the value of the country in that year.
I think it is not the most efficient method to do this thing...
Also in my CSV there are not all the countries present in the European Union. For example, I have no data on Russia so I "turned off" some countries putting an if inside the event on mouseover.
But I would also cut part of Russia in the map in order to enlarge the known countries. How can I do also that thing?
I looked at these examples: Choropleth and Threshold Choropleth by Mike Bostock on bl.ocks.org but I have not understand how to color the countries...
(I wanted to put links but I can't post more than 2 links because of my low reputation)
This is my code.
I apologize for my bad English. Thank you all,
Pier
EDIT
I admit I did not understand some things in your code.
Why I need events on mouseover and mouseout? And what are hover and rhover? I thought they were events related to this question. But in my case I don't need it, no?
Use array_values or d is the same, right? Does not change if I use d or array_values, right? It is a stupid question but it confused me.
I modified the makemap method in this way. I understand correctly how to use your code?
function makemap(error, europe, dessease) {
dess = dessease.slice();
counties = topojson.feature(europe, europe.objects.collection);
vector = svg.selectAll("path")
.data(counties.features)
.enter()
.append("path")
.attr("class", "county")
.attr("id", function(d) {
return "coun" + d.properties.indx;
})
.attr("d", path)
.style("fill", function(array_values) {
return color(array_values[d.country]);
});
In this case there is an error concerning d, of course. Sorry, I do not know where I'm wrong...
The country's color will depend on a value. So the color IS a function of "value". To do that you must to define a range of color based on your values:
var color = d3.scale.linear()
.domain([mn,mx]) // <--- min and MAX of your value
.range(["#ffffff","000000"]);
then define the color of your country:
svg.selectAll(".county")
.style("fill", function(array_values) {
return color(array_values[d.country]);
});
Must-Read: Jerome Cukier - d3: scales, and color

Improving D3 Sequence Sunburst Example

This D3 example served as my starting point:
http://bl.ocks.org/kerryrodden/7090426
I wanted to change data that feeds the diagram, and I made following new example:
http://jsfiddle.net/ZGVK3/
One can notice at least two problems:
Legend is wrong. This is because it still contains 'hardcoded' names from original example.
All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.
How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?
You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.
Step 1. Create an ordinal scale for the colors
Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:
var colors = d3.scale.ordinal()
.range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);
This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.
As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.
Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].
Step 2. Use your data to construct the domain of the scale
The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) {
output.push(d.name);
}
});
return output;
})(nodes);
This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.
Then you can simply set the new array to be the domain of the color scale:
colors.domain(uniqueNames);
Step 3. Use the scale's domain to build the legend
Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.
You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", colors.domain().length * (li.h + li.s));
var g = legend.selectAll("g")
.data(colors.domain())
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors(d); });
And that's about it. Hope that helps.
Here's the updated JSFiddle.

Dynamically updating in d3 works for circles but not external SVGs

Suppose I want to dynamically update the position and number of circles on a page using d3. I can do this, using the .data(), .enter(), .exit() pattern. Here is a working example.
http://jsfiddle.net/csaid/MFBye/6/
function updatePositions(data) {
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle");
circles.exit().remove();
circles.attr("r", 6)
.attr("cx", 50)
.attr("cy", function (d) {
return 20 * d
});
}
However, when I try to do the same thing with external SVGs instead of circles, many of the new data points after the first update do not appear on the page. Example:
http://jsfiddle.net/csaid/bmdQz/8/
function updatePositions(data) {
var gs = svg.selectAll("g")
.data(data);
gs.enter().append("g");
gs.exit().remove();
gs.attr("transform", function (d, i) {
return "translate(50," + d * 20 + ")";
})
.each(function (d, i) {
var car = this.appendChild(importedNode.cloneNode(true));
d3.select(car).select("path")
});
}
I suspect this has something to do with the .each() used to append the external SVG objects, but I am at a loss for how to get around this. Also, the "cx" and "cy" attributes are specific for circles, and so I can't think how they could be used for external SVGs.
Thanks in advance!
There are two problems with your code. The first problem, and reason why you're not seeing all the data points, is that your external SVGs contain g elements, which you are selecting. What this means is that after you first appended the elements, any subsequent .selectAll("g") selections will contain elements from those external SVGs. This in turn means that the data you pass to .data() gets matched to those and hence your selections do not contain what you expect. This is easily fixed by adding a class to the g elements you add explicitly and selecting accordingly.
The second problem is that you're executing the code that appends the external SVGs as part of the update selection. This means that those elements get added multiple times -- not something you would notice (as they overlap), but not desirable either. This is easily fixed by moving the call to clone the nodes to the .enter() selection.
Complete jsfiddle here. As for your question about cx and cy, you don't really need them. You can set the position of any elements you append using the transform attribute, as you are doing already in your code.

Categories