adding class names to arcs from data in d3.layout.pie() - d3.js

I'm creating a pie chart from a JSON file. I wonder if there is a way I can take some names from the JSON file and assign them as class names of the arcs created by d3.layout.pie().
Here is an example I created: http://blockbuilder.org/jinlong25/532d889e01d02cef2d24
Essentially, I want to do something like the last line of code below:
var data = [
{
'name': 'apple',
'value': 250
},
{
'name': 'banana',
'value': 100
},
{
'name': 'orange',
'value': 150
}
];
var arcs = svg.selectAll('g.arc')
.data(pie(data.map(function(d) { return d.value; })))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d) { return d.name; };
but since the data has been transformed by pie(), I wonder if there is anyway to add class names to the data generated by pie().
thanks!

d3's layouts helpfully provide a .value() accessor which allows you to specify how get the value of the datum, instead of doing the data.map() operation. So, you could do:
var pie = d3.layout.pie().value(function(d) { return d.value; })
That way, your original datum is preserved in d.data.
So using that definition of pie, your code would change to this:
var arcs = svg.selectAll('g.arc')
.data(pie(data))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d) { return d.data.name; };
edit: added link the relevant documentation.

Some D3 layouts mutate the original dataset but others create a new dataset (like voronoi). In those cases, you can use the array position from the original dataset when working with the new dataset. So from your example:
var arcs = svg.selectAll('g.arc')
.data(pie(data.map(function(d) { return d.value; })))
.enter().append('g')
.attr('transform', 'translate(70, 70)')
.attr('class', function(d,i) { return data[i].name; };

Related

Would like to use length (from d3.nest) as radius

I'm having a mental block about using the result of:
.rollup(function(leaves) { return leaves.length;})
as the radius of a circle in a scatter plot. My complete code (and sample data) is in a plunk here https://plnkr.co/edit/Cwuce6inLV5jouCWTFfN
The scatter works with a static value of 5 but I'd like to use value based on the .rollup from the d3.nest as explained in this other SO question I had: Capturing leaves.length value from d3.nest
I think I'm missing a key concept about in this section of code:
d3.tsv("etds_small.tsv", function(error, dataset) {
dataset.forEach(function(d) {
if(deptlist.indexOf(d.dept) == -1) deptlist.push(d.dept);
if(years.indexOf(d.year) == -1) years.push(d.year);
})
var deptYearCount = d3.nest()
//.key(function(d) { return d.college;} )
.key(function(d) { return d.dept})
.key(function(d) { return d.year })
.rollup(function(leaves) { return leaves.length;})
.map(dataset);
console.log(dataset); // retains the college, dept, and year labels
console.log(deptYearCount); // replaces labels with "key"
x.domain(years.sort(d3.ascending));
y.domain(deptlist.sort(d3.ascending));
//console.log(y.domain());
//console.log(x.domain());
svg.selectAll(".dot")
.data(dataset) //should this be deptYearCount from the d3.nest above?
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5) // Would like to use length (from d3.nest) as radius
//.attr("r", function(d) {return d.values.length*1.5;}) works with .data(debtYearCount)
.style("opacity", 0.3)
.style("fill", "#e31a1c" )
.attr("cx", function(d) {
return x(d.year);
})
.attr("cy", function(d) {
return y(d.dept);
});
Give this a try for your radius:
.attr("r", function(d) {return Object.keys(deptYearCount[d.dept]).length*1.5;})
Because you are using .map(dataset) instead of .entries(dataset), d3.next() is returning one-big-object instead of an array of objects. That one-big-object does not contain a property called values.
Updated explanation:
First, look at the structure of the object deptYearCount. It has property names like Earth Sciences., Education., etc.
Our d3 data is iterating over an array of objects. Each object has property dept that looks like Earth Sciences., Education., etc.
So, deptYearCount[d.dept] is getting us to the correct property within deptYearCount.
For example, at one round of our iteration we are looking at deptYearCount["Education."]. That turns out to be another object with properties like 2007,2008, etc. Therefore, the number of properties in deptYearCount["Education."] is the value we want for the radius.
How do we find the number of properties of deptYearCount["Education."]? One way is the Object.keys(someObject) function. It returns an array of strings corresonding to the property names, and we just need its .length.

d3 force map renders the links incorrectly after re-render

I have made an org hierarchy chart using react and Force layout. Org object has a user and defines user's relationship with others at work - like boss, coworkers, subordinates. A new person can be dynamically added in the org map which re-renders the map with new information.
However, after re-render, the map displays the links and relation text incorrectly. Even the names on the nodes get incorrectly assigned even though the data associated with node is correct. With debugging, I found that links, nodes and linklabels objects - all are correct. But the enter and exit seems a little funky and could be the source of the problem.
I have a jsfiddle to simulate the bug.
jsfiddle initially renders an org map with four nodes. Joe is the user and he has a boss John, coworker Shelley, and subordinate Maria.
I have created a button to simulate dynamic adding of a new person. Clicking the button will add (data is hard coded for bug simulation) Kelly as co-worker to Maria and re-render the map. You will notice that after the render, all the links and labels are incorrect. However, when I look at the data associated with nodes in debug mode, it's correct.
I have spent a lot of time trying to figure this out but can't seem to catch the bug.
The jsfiddle is written in react. If you are not familiar with react, please ignore the react code and just focus on d3 code.
The jsfiddle code is pasted here:
Javascript:
const ForceMap = React.createClass({
propTypes: {
data: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number
},
componentDidMount(){
let {width,height} = this.props;
this.forceLayout = d3.layout.force()
.linkDistance(100)
.charge(-400)
.gravity(.02)
.size([width, height])
this.svg = d3.select("#graph")
.append("svg")
.attr({id:'#org-map',width:width,height:height,backgroundColor:'white'})
let container = this.svg.append("g").attr('class','container');
let rect = container.append("rect")
.attr({width:width,height:height})
.style({fill:"white","pointer-events":"all"})
this.org = this.props.data;
this.org.x = width / 2;
this.org.y = height / 2;
this.org.fixed = true;
console.log('Initial Org:',this.org);
this.d3render(this.org);
}, //componentDidMount
d3render(org) {
let container = d3.selectAll('g.container')
let nodes = this.flatten(org);
let links = d3.layout.tree().links(nodes);
let force = this.forceLayout.on("tick", tick);
force.nodes(nodes) // Restart the force layout.
.links(links)
.start();
debugger;
// Links line that connects two org members together
let link = container.selectAll(".link").data(links);
link.exit().remove()
link.enter().append("line")
.attr('class',"link")
.attr('id', (d)=> d.source.name + '-' +d.target.name)
console.log('link:',link);
//Relationship label for every link
let linkLabel = container.selectAll(".linklabelholder").data(links);
linkLabel.exit().remove();
linkLabel.enter()
.append("g")
.attr("class", "linklabelholder")
.attr('id', (d) => `linklabel-${d.source.name}-${d.target.name}`)
.append("text")
.attr({dx:1, dy: ".35em", "text-anchor": "middle"})
.text((d) => d.target.relation)
.style("font-size",12);
console.log('link Labels: ',linkLabel);
// Update nodes. Each node represents one person
let node = container.selectAll(".node").data(nodes);
node.exit().remove();
let nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr('id', (d) => `node-${d.name}`)
nodeEnter.append('circle')
.attr('r',25)
.attr('id',(d) => d.name)
.style('fill', 'steelblue')
nodeEnter.append("text")
.attr("dy", ".35em")
.text((d) => d.name)
.attr('id', (d) => d.name)
.style("font-size",12);
console.log('Nodes: ',node);
function tick() {
node.attr("cx", function(d) { return d.x = Math.max(25, Math.min(475, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(25, Math.min(275, d.y)); });
link.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y)
linkLabel.attr("transform", (d) => `translate(${(d.source.x+d.target.x)/2},${(d.source.y+d.target.y)/2})`);
node.attr("transform", (d) => `translate(${d.x},${d.y})`)
} //tick
}, //d3 render
addNewPerson() {
let newPerson = {_id: "5",name: 'Kelly' ,relation:'coworker'};
let addTo = {_id:"4", name: "Maria"};
add(this.org);
console.log('RE-RENDER AFTER ADDING NEW PERSON');
console.log('Org after addding new person: ', this.org);
this.d3render(this.org);
function add(node) {
if (node.children) node.children.forEach(add);
if (node._id === addTo._id) {
if (!node.children) node.children = [];
node.children.push(newPerson);
}
}
},
flatten(org) {
var nodes = [], i = 0;
recurse(org);
return nodes;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
}, //flatten
render() {
return (
<div>
<div id="graph"></div>
<button className='btnClass' onClick={this.addNewPerson} type="submit">Add new person
</button>
</div>
);
},
});
var user = {
name: 'Joe',
_id: "1",
children:[
{_id:"2", name: "John", relation:"boss"},
{ _id:"3", name: "Shelley", relation:"coworker"},
{_id:"4", name: "Maria", relation:"subordinate"}
]
}
ReactDOM.render(
<ForceMap
data={user}
width={500}
height={300}
/>,
document.getElementById('container')
);
You need key functions for your selections to make anything other than simple datum-element binding work properly, otherwise they just replace/overwrite each other based on index:
https://github.com/mbostock/d3/wiki/Selections#data
In this case, these will work:
let link = container.selectAll(".link").data(links, function(d) { return d.source_id+"-"+d.target._id+"-"+d.target.relation; });
...
let linkLabel = container.selectAll(".linklabelholder").data(links, function(d) {
return d.source._id+"-"+d.target._id+"-"+d.target.relation;
});
...
let node = container.selectAll(".node").data(nodes, function(d) { return d._id; });
https://jsfiddle.net/aqu1h7zr/3/
As for why new links overwrite nodes it's because elements are drawn in the order they are encountered in the dom. Your new links are added in the dom after your old nodes so they're drawn on top. To get round this add nodes and links to separate 'g' elements (I haven't done this in the updated fiddle), so that all links are drawn first
e.g. not
<old links>
<old nodes>
<new links>
<new nodes>
but
<g>
<old links>
<new links>
</g>
<g>
<old nodes>
<new nodes>
</g>

Replace data with new set

Using D3.js, I have something like this:
var sets = [
{ data:[{date:1980,value:10},{date:1981,value:20},{date:1982,value:30}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:30}] }
];
And I bind it to make a chart like this:
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { return d.data; })
.attr("class","line")
.attr("d", line);
Where g is a g element inside an svg element. This works. For each item in set I get a path using the values in data. Now what I want to do is click an element and replace the data with a different set:
var altData = [
{ data:[{date:1980,value:30},{date:1981,value:20},{date:1982,value:10}] },
{ data:[{date:1981,value:10},{date:1982,value:20},{date:1983,value:30}] },
{ data:[{date:1982,value:10},{date:1983,value:20},{date:1984,value:0}] }
];
d3.select("#transition").on("click", function() {
paths.data(altData);
console.log("click");
});
But the paths.data(altData) doesn't appear to do anything. There are no console errors, but the chart doesn't change. What do I need to do to tell it that the data has changed and the lines should be redrawn? As a bonus, I'd really like this transition to be animated.
Full fiddle
Basically you need to tell d3 to redraw it. In your case, it is by calling attr("d", line).
For transition, put transition() between two attr("d", fnc). Your onclick function will look like the following
d3.select("#transition").on("click", function() {
paths.attr("d", line)
.transition()
.attr("d", function(d, i){
return line(altData[i].data)
})
});
Jsfiddle http://jsfiddle.net/8fLufc65/
Look at this plnkr that will change the data when transition is clicked.
I made the part that draws the lines into a function and pass the data for which it should be drawing the lines.
drawPaths(sets) ;
function drawPaths(sets) {
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var paths = g.selectAll("path")
.data(sets);
paths.enter()
.append("path")
.datum(function(d) { console.log(d); return d.data; })
.attr("class","line")
.attr("d", line);
}

Adding point and value to line chart... how to color the point like the line?

I've started from the multiline example http://bl.ocks.org/mbostock/3884955
I extended it to show the dots along the line, but I'm not able to give to the circle the same color of the line...
I'm really new in d3.js and I really need an advice.
here the example page: http://www.danielepennati.com/d3/linea.html
I've change some variable name to make the script more general so there is some difference with the original example code. the main one is the name of the variable that contain the mapped data: it is "column" and not "cities"
d3.tsv(surce_data, function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "id"; }));
var column = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {id: d.id, value: +replace(d[name])};
})
};
});
the second main difference is the x axsis: in my code it is ordinal and not linear.
so to draw the line the code is:
var tracciato = svg.selectAll(".line-group")
.data(column)
.enter().append("g")
.attr("class", "line-group");
tracciato.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
to make the points along the line I've wrote this code:
var point = tracciato.append("g")
.attr("class", "line-point");
point.selectAll('circle')
.data(function(d,i){ return d.values})
.enter().append('circle')
.attr("cx", function(d, i) {
return x(i) + x.rangeBand() / 2;
})
.attr("cy", function(d, i) { return y(d.value) })
.attr("r", 5);
I'd link the points color be the same of the line, but the problem is the colors are assigned to the "column" object. I don't know how to give to each new circle within the same column the same column color...
I don't know if my problem is clear, please ask me if you need any more specification.
Thanks

Creating a force layout in D3.js visualisation library

I'm working on a project, which visualises the references between books. It's worth to mention that I'm a total beginner in Javascript. So, I couldn't get far by reading the D3.js API reference. I used this example code, which works great.
The structure of my CSV file is like this:
source,target
"book 1","book 2"
"book 1","book 3"
etc.
The source and target are connected by a link. These are the points for the layout:
Create two different circles respectively for source and target node.
Set a specific color for source and target node.
The circles should be labeled by the book information, e.g., source node
is labeled by "book 1" and target node is labeled by "book 2".
If there is a link between targets, then make this specific link wider
than the others links from source to target.
I hope you can help me by creating these points.
Thanks in advance.
Best regards
Aeneas
d3.js plays much nicer with json data files than with csv files, so I would recommend transferring your csv data into a json format somehow. I recently coded something similar to this, and I had my nodes and links stored in a json file as a dictionary formatted as such:
{
'links': [{'source': 1, 'target': 2, 'value': 0.3}, {...}, ...],
'nodes': [{'name': 'something', 'size': 2}, {...}, ...]
}
This allows you to initialize your nodes and links as follows (after starting the view):
d3.json("data/nodesandlinks.json", function(json) {
var force = self.force = d3.layout.force()
.nodes(json.nodes)
.links(json.links)
.linkDistance(function(d) { return d.value; })
.linkStrength(function(d) { return d.value; })
.size([width, height])
.start();
var link = vis.selectAll("line.link")
.data(json.links)
.enter().append("svg:line")
.attr("class", "link")
.attr("source", function(d) { return d.source; })
.attr("target", function(d) { return d.target; })
.style("stroke-width", function(d) { return d.value; });
var node = vis.selectAll("g.node")
.data(json.nodes)
.enter().append("svg:g")
.attr("class", "node")
.attr("name", function(d) { return d.name; })
.call(force.drag);
Hope this helped!

Resources