Migrating d3 v2 to d3 v4 for Chord Diagram - javascript

I am using this example to guide my foray into chord diagrams with d3. I have it working with my data in v2 (the version the example is in), and now I am attempting to upgrade to v4.
Here is the code that works with v2:
<script src="http://d3js.org/d3.v2.min.js?2.8.1"></script>
<script>
var width = 900,
height = 900,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(",.0f");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.02)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("teams.csv", function(cities) {
d3.json("matrix.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add a mouseover title.
group.append("title").text(function(d, i) {
return cities[i].name + ": " + formatPercent(d.value) + " as business unit";
});
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
// Add an elaborate mouseover title for each chord.
chord.append("title").text(function(d) {
return cities[d.source.index].name
+ " → " + cities[d.target.index].name
+ ": " + formatPercent(d.source.value)
+ "\n" + cities[d.target.index].name
+ " → " + cities[d.source.index].name
+ ": " + formatPercent(d.target.value);
});
function mouseover(d, i) {
chord.classed("fade", function(p) {
return p.source.index != i
&& p.target.index != i;
});
}
});
});
</script>
Here is the same code midway through my attempt to migrate to v4:
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>
var width = 900,
height = 900,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(",.0f");
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.chord()
.padAngle(.02)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.ribbon()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("teams.csv", function(cities) {
d3.json("matrix.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add a mouseover title.
group.append("title").text(function(d, i) {
return cities[i].name + ": " + formatPercent(d.value) + " as business unit";
});
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
// Add an elaborate mouseover title for each chord.
chord.append("title").text(function(d) {
return cities[d.source.index].name
+ " → " + cities[d.target.index].name
+ ": " + formatPercent(d.source.value)
+ "\n" + cities[d.target.index].name
+ " → " + cities[d.source.index].name
+ ": " + formatPercent(d.target.value);
});
function mouseover(d, i) {
chord.classed("fade", function(p) {
return p.source.index != i
&& p.target.index != i;
});
}
});
});
</script>
So far, I've flattened the namespaces (up to d3.csv), changed padding to padAngle, and changed var path = d3.chord() to var path = d3.ribbon(). As I make each change, I am checking the error messages in Chrome Developer. After making those changes, the current error is layout.matrix is not a function. This makes sense, based on v4 standards. To combat this, I tried adding .data(layout.matrix) to the var svg creation. I tried a few other routes gleaned from other chord diagram examples, to no avail.
How should I access and bind the data in v4?
Edit: I added .data(layout(matrix)) instead, and now the g elements with class=group are being created. However, this error is occurring: attribute d: Expected number, "MNaN,NaNLNaN,NaNZ". So I'm thinking this means the location is not being populated correctly.
Edit #2: Now I've gotten everything to show up except for the bars around the outside. Here is my current code. I believe the d attribute of the path elements within the g groups are wrong. They are set to be the same as the path elements outside the g groups. When I attempt to set them to arc instead (.attr("d", arc), the error attribute d: Expected number, "MNaN,NaNLNaN,NaNZ". occurs.
<script>
var width = 900,
height = 900,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(",.0f");
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.chord()
.padAngle(.02)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var ribbon = d3.ribbon()
.radius(innerRadius);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
svg.append("circle")
.attr("r", outerRadius);
d3.csv("teams.csv", function(cities) {
d3.json("matrix.json", function(matrix) {
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout(matrix))
.enter()
.append("g")
.attr("class", "group");
// Add the group arc.
var groupRibbon = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", ribbon)
//.style("fill", function(d, i) { return cities[i].color; })
;
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout(matrix))
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", ribbon);
});
});
</script>

Related

Iterative/chained transitions along line graph with discrete points and delay

I created a jsfiddle here.
I do have a graph - in this case a sine wave - and want to move a circle along this line (triggered by a click event), stop at certain x and y value pairs that are on this graph and then move on to the last point of the graph from where it jumps to the first again (ideally this should go on until I press a stop button).
My current problem is that the circle only moves horizontally but not in the ordinate direction and also the delay is visible only once (in the very beginning).
The relevant code is this one (the entire running example can be found in the link above):
Creation of the circle:
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("cx", x_scale(xval[0]))
.attr("cy", y_scale(yval[0]))
.attr("transform", "translate(" + (0) + "," + (-1 * padding + 15) + ")")
.attr("r", 6)
.style("fill", 'red');
The moving process:
var coordinates = d3.zip(xval, yval);
svg.select("#concindi").on("click", function() {
coordinates.forEach(function(ci, indi){
//console.log(ci[1] + ": " + indi);
//console.log(coordinates[indi+1][1] + ": " + indi);
if (indi < (coordinates.length - 1)){
//console.log(coordinates[indi+1][1] + ": " + indi);
console.log(coordinates[indi + 1][0]);
console.log(coordinates[indi + 1][1]);
d3.select("#concindi")
.transition()
.delay(2000)
.duration(5000)
.ease("linear")
.attr("cx", x_scale(coordinates[indi + 1][0]))
.attr("cy", y_scale(coordinates[indi + 1][1]));
}
});
I am pretty sure that I use the loop in a wrong manner. The idea is to start at the first x/y pair, then move to the next one (which takes 5s), wait there for 2s and move on to the next and so on. Currently, the delay is only visible initially and then it just moves horizontally.
How would this be done correctly?
Why don't you use Bostock's translateAlong function?
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
d3.select(this).transition()
.duration(5000)
.attrTween("transform", translateAlong(sin_graph.node()));
});
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You have to understand that forEach will loop to the end of the array almost instantaneously. Thus, you cannot make the circle jumping to one coordinate to the other with your approach right now (thus, unfortunately, you are correct here:"I am pretty sure that I use the loop in a wrong manner").
If you want to add the 2s waiting period between one point and another, the best idea is chaining the transitions. Something like this (I'm reducing the delay and the duration times in the demo, so we can better see the effect):
var counter = 0;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0]))
+ "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
var counter = 0;
var that = this;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0])) + "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

D3 JS make polygon on click

Consider the following code
var width = 960,
height = 500;
var vertices = d3.range(100).map(function(d) {
return [Math.random() * width, Math.random() * height];
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); });
var path = svg.append("g").selectAll("path");
svg.selectAll("circle")
.data(vertices.slice(1))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 1.5);
redraw();
function redraw() {
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) { return "q" + (i % 9) + "-9"; })
.attr("d", polygon);
path.order();
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
How can I add a new Polygon with a CLICK & at the same time draw a center dot as well ?
You have a good start. In addition to the mousemove listener on the svg you also need a click listener. With this you can just add a new vertex each time the user clicks. I've done this by adding a variable to the redraw function to distinguish between redraws triggered by a click. You might be able to find a cleaner way to do this, but hopefully this helps!
var width = 960,
height = 500;
var vertices = d3.range(100).map(function(d) {
return [Math.random() * width, Math.random() * height];
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); })
.on('click', function() {
vertices.push(d3.mouse(this));
redraw(true);
});
var path = svg.append("g").selectAll("path");
var circle = svg.selectAll("circle");
redraw();
function redraw(fromClick) {
var data = voronoi(vertices);
path = path
.data(data, polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) { return "q" + (i % 9) + "-9"; })
.attr("d", polygon);
path.order();
circle = circle.data(vertices)
circle.attr("transform", function(d) { return "translate(" + d + ")"; })
circle.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 1.5)
.attr('fill', fromClick ? 'white' : '')
circle.exit().remove();
}
function polygon(d) {
return d ? "M" + d.join("L") + "Z" : null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

d3 donut chart multy ring with text

I create a multi ring donut chart following some examples on the web and everything was ok till i try to display text into the ring and got stuck with different errors.
I think at this point i can access data but i when comes to create rings, i got NaN values for my path and text.
Here is my code:
var dataset = {
ringOne: [{"label":"70%", "value":70},
{"label":"10%", "value":10},
{"label":"20%", "value":20}],
ringTwo: [{"label":"70%", "value":70},
{"label":"10%", "value":10},
{"label":"20%", "value":20}],
};
var width = 460,
height = 300,
cwidth = 45,
outerR = 100,
color = d3.scale.ordinal().range(["#07e", "#00aced", "#e32"]);
var svgDonut = d3.select("#donut")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")" );
var arc = d3.svg.arc();
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.value; });
var rings = svgDonut.selectAll("g.slice")
.data(pie([dataset]))
.enter()
.append("g")
.attr("class", "slice");
rings.append("path")
.attr("fill", function(d, i){ return color(i); })
.attr("d", function(d, i, j){ return arc.innerRadius( 80 + cwidth * j )
.outerRadius( outerR * (j) )(d); });
rings.append("text")
.attr("transform", function(d) {
d.innerRadius = 0;
d.outerRadius = outerR;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d, i) { return dataset.ringOne[i].label; });
The error i get is:
Error: Invalid value for <path> attribute d="M4.898587196589413e-15,-80A80,80 0 1,1 NaN,NaNL0,0Z"
Error: Invalid value for <text> attribute transform="translate(NaN,NaN)"
fiddle here: https://jsfiddle.net/anaketa/8u7gejjc/
Any ideas?

Adding clock points to d3 pie chart

I'm very new to D3 - in fact I only started yesterday - have a donut pie chart here:
var dataset = new Array();
dataset[0] = {"value":"50","color":"red"};
dataset[1] = {"value":"20","color":"blue"};
var pie = d3.layout.pie().sort(null).value(function(d){return d.value;});
var h = w = 500;
var center = w / 2;
var outerRadius = ((h/2)-5);
var innerRadius = outerRadius-10;
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var arcOutter = d3.svg.arc()
.innerRadius(outerRadius)
.outerRadius(outerRadius + 1);
var arcInner = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(innerRadius - 1);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Set up groups
var arcs = svg.selectAll("g.arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + center + ", " + center + ")");
//Set up outter arc groups
var outterArcs = svg.selectAll("g.outter-arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "outter-arc")
.attr("transform", "translate(" + center + ", " + center + ")");
//Set up outter arc groups
var innerArcs = svg.selectAll("g.inner-arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "inner-arc")
.attr("transform", "translate(" + center + ", " + center + ")");
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i)
{
return d.data.color;
}).attr("d", arc);
//Draw outter arc paths
outterArcs.append("path")
.attr("fill", 'green')
.attr("d", arcOutter).style('stroke', 'white')
.style('stroke-width', 0);
//Draw inner arc paths
innerArcs.append("path")
.attr("fill", 'green')
.attr("d", arcInner).style('stroke', 'white')
.style('stroke-width', 0);
jsFiddle chart
But I'm struggling to add 4 clock points and their time tables to it, (12am, 3pm, 6pm, 9pm), I've tried searching clock examples but they're all working clocks, not just the points.
I want it to look pretty much like this:
Any help would be greatly appreciated.
I don't know how aestheticaly correct it is, but here it goes. What you could do, is add 4 line segments in your chart at these locations:
[w/2, 0],[w/2,h],[0,h/2],[w,h/2]
You can achieve that if you add the following lines:
var x=d3.scale.linear().domain([0,outerRadius]).range([0,w])
var y=d3.scale.linear().domain([0,outerRadius]).range([h,0])
svg.append('line').attr("x1",x(outerRadius/2)).attr("y1",0).attr("x2",x(outerRadius/2)).attr("y2",20)
svg.append('line').attr("x1",x(outerRadius/2)).attr("y1",y(outerRadius)).attr("x2",x(outerRadius/2)).attr("y2",y(outerRadius)-20)
svg.append('line').attr("x1",0).attr("y1",y(outerRadius/2)).attr("x2",20).attr("y2",y(outerRadius/2))
svg.append('line').attr("x1",x(outerRadius)).attr("y1",y(outerRadius/2)).attr("x2",x(outerRadius)-20).attr("y2",y(outerRadius/2))
Please note that you have to create a css entry, so that the line is shown:
line{
display:block;
stroke:black;
}
JSFiddle here
Hope this helps
Following the lovely example here.
var radians = 0.0174532925;
var hourScale = d3.scale.linear()
.range([0,330])
.domain([0,11]);
var labelGroup = svg.append('g')
.attr('transform','translate(' + (center + margin) + ',' + (center + margin) + ')');
labelGroup.selectAll('.hour-label')
.data([12,3,6,9])
.enter()
.append('text')
.attr('class', 'hour-label')
.attr('text-anchor','middle')
.style('font-size','16pt')
.attr('x',function(d){
return outerRadius * Math.sin(hourScale(d)*radians);
})
.attr('y',function(d){
return -outerRadius * Math.cos(hourScale(d)*radians);
})
.text(function(d){
return d;
});
Updated fiddle.

Select d3 node by its datum

I’d like to select a node in a callback without using d3.select(this).
I have some code that draws a pie…
function drawPie(options) {
options || (options = {});
var data = options.data || [],
element = options.element,
radius = options.radius || 100,
xOffset = Math.floor(parseInt(d3.select(element).style('width'), 10) / 2),
yOffset = radius + 20;
var canvas = d3.select(element)
.append("svg:svg")
.data([data])
.attr("width", options.width)
.attr("height", options.height)
.append("svg:g")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")");
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(data) {
return data.percentageOfSavingsGoalValuation;
});
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.on("mouseover", divergeSlice);
You’ll notice at the end I have a call to divergeSlice(). That looks like this:
function divergeSlice(datum, index) {
var angle = (datum.endAngle + datum.startAngle) / 2,
x = Math.sin(angle) * 10,
y = -Math.cos(angle) * 10;
d3.select(this)
.transition()
.attr("transform", "translate(" + x + ", " + y + ")");
}
This works, but I’d like to accomplish this without using this as I mentioned earlier. When I log the datum object, I get something like the following:
{
data: {
uniqueID: "XX00X0XXXX00"
name: "Name of value"
percentageOfValuation: 0.4
totalNetAssetValue: 0
}
endAngle: 5.026548245743669
innerRadius: 80
outerRadius: 120
startAngle: 2.5132741228718345
value: 0.4
}
How could I use d3.select() to find a path that holds datum.data.uniqueID that is equal to "XX00X0XXXX00"?
You can't do this directly with .select() as that uses DOM selectors. What you can do is select all the candidates and then filter:
d3.selectAll("g")
.filter(function(d) { return d.data.uniqueID === myDatum.data.uniqueID; });
However, it would be much easier to simply assign this ID as an ID to the DOM element and then select based on that:
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("id", function(d) { return d.data.uniqueID; })
.attr("class", "slice");
d3.select("#" + myDatum.data.uniqueID);

Categories