How redraw labels in D3 maps? - javascript

I am following two tutorials to make a map in TOPOJson :
Display countries, borders and cities (dot & labels). Tutorial here.
Move and zoom the map. Tutorial here.
I am able to display the pan, to pan, to zoom, but the names of the cities are not redrawn.
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
/* What's hapenning here ? */
var svg = d3.select("#vis").append("svg:svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().on("zoom", redraw));
/* Format projected 2D geometry appropriately for SVG or Canvas. */
d3.json("uk.json", function(error, uk) {
svg.selectAll(".subunit")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary IRL");
svg.append("path")
.datum(topojson.feature(uk, uk.objects.places))
.attr("d", path)
.attr("class", "place");
svg.selectAll(".place-label")
.data(topojson.feature(uk, uk.objects.places).features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start" : "end"; })
.text(function(d) { return d.properties.name; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
});
function redraw() {
// d3.event.translate (an array) stores the current translation from the parent SVG element
// t (an array) stores the projection's default translation
// we add the x and y vales in each array to determine the projection's new translation
var tx = t[0] * d3.event.scale + d3.event.translate[0];
var ty = t[1] * d3.event.scale + d3.event.translate[1];
projection.translate([tx, ty]);
// now we determine the projection's new scale, but there's a problem:
// the map doesn't 'zoom onto the mouse point'
projection.scale(s * d3.event.scale);
// redraw the map
svg.selectAll("path").attr("d", path);
// redraw the labels
svg.selectAll(".place-label");
// redraw the x axis
xAxis.attr("x1", tx).attr("x2", tx);
// redraw the y axis
yAxis.attr("y1", ty).attr("y2", ty);
}
I have tried to add this line :
svg.selectAll(".place-label").attr("d", path);
in the redraw function but it did not worked.
Could you tell me which line should I add to refresh their positions ?
Here is my live code : Plunker live example & code

To make the labels move along with the map you need to do this:
On redraw function
svg.selectAll(".place-label")[0].forEach( function(d){
var data = d3.select(d).data()[0];//this will give you the text location data
d3.select(d).attr("transform", "translate("+projection(data.geometry.coordinates)+")" )//pass the location data here to get the new translated value.
});
For subunits do:
svg.selectAll(".subunit-label")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
Working example here
Hope this works!

Related

Using d3.nest to create a pie chart

I have an assortment of data, and I have used it to create a donut chart. I want to make a pie chart using a breakdown of the data, which I've acquired using d3.nest to subdivide the data I had (it was currently in 3 categories: nest breaks it down into 129). Basically, I have Olympic data based on medals awarded, and I want to subdivide the data on interaction into which sports they were earned in.
I'm just not sure how to use nested data to create a pie chart, particularly if the keys are variable. I'll include my implementation for the donut chart.
var pie = d3.pie();
// color based on medal awarded
// order: gold, silver, bronze
var color = d3.scaleOrdinal()
.range(['#e5ce0c', '#e5e4e0', '#a4610a']);
var arcs = d3.select(svg).selectAll("g.arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + (w/2) + "," + ((h-25)/2) + ")");
arcs.append("path")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc)
.attr("stroke", "white")
.style("stroke-width", "0.5px")
.on('mouseover', function(d) {
d3.select(this).attr('opacity', .7);
})
.on('mouseleave', function(d) {
d3.select(this).attr('opacity', 1);
});
// title
d3.select(svg).append('text')
.attr('x', function(d) {
return ((w/2) - 85);
})
.attr('y', '20')
.text(function(d) {
return ('Medal breakdown for ' + country);
})
.attr('font-size', '16px');
arcs.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d) {
return d.value;
});
Can you please confirm is this the format of data you have ?
Please find the code below and let me know if you have any doubt.
var w = 400;
var h = 400;
var r = h/2;
var color = d3.scale.category20c();
var data = [
{name:"football", medal:1},
{name:"hockey", medal:2},
{name:"cricket", medal:3},
{name:"tennis", medal:4},
{name:"table tennis", medal:5},
];
var vis = d3.select('#chart').append("svg:svg").data([data]).attr("width", w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function(d){return d.medal;});
// declare an arc generator function
var arc = d3.svg.arc().outerRadius(r);
// select paths, use arc generator to draw
var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
arcs.append("svg:path")
.attr("fill", function(d, i){
return color(i);
})
.attr("d", function (d) {
// log the result of the arc generator to show how cool it is :)
return arc(d);
});
// add the text
arcs.append("svg:text").attr("transform", function(d){
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")";}).attr("text-anchor", "middle").text( function(d, i) {
return data[i].name;}
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.15/d3.min.js"></script>
<div id="chart"></div>

D3 position x axis label within rectangle and rotate 90 degrees

I am using D3 to create a basic bar graph
For my x-axis, I want to position each label above their respective bar. The text should also be rotated 90 degrees
To see the code that does this, start at line 51. https://codepen.io/Fallenstedt/pen/xdYooE
//This is where I attempt to create an x Axis label
//create a container to hold the text element
var textContainer = svg.append('g')
.selectAll('g')
.data(data).enter()
.append('g')
.attr('class', 'x-axis')
.attr('x', function(d, i) {
return i * (width/data.length)
})
.attr('y', function(d, i) {
return height - (d.value) + 15;
})
.attr("transform", function(d, i) {return "translate(" + (i * (width/data.length)) + ",330)";});
//now that a container is made, I can append a text element to it so I can rotate the text 90 degrees.
textContainer.append('text')
.text(function(d) {
return d.type
})
.attr('font-size', '34px')
.attr('fill', 'white')
.attr("text-anchor","end")
.attr("transform", function(d, i) {return "translate(40,0) rotate(-90,0,0)";});
The labels appear and they are rotated 90 degrees, however I cannot position them to be above their respective rectangle. How can I position each x-axis label to be directly above their rectangle? I feel that my approach to this is overly complicated.
You can create the rect and text elements inside the same container, e.g.
var rContainer = svg
.selectAll(".bar")
.data(data)
.enter()
.append("g");
//append rectangles for the bar chart
rContainer
.append("rect")
.attr("class", "bar")
.style("fill", function(d, i) { return color(i); })
.attr("x", function(d) { return x(d.type); })
.attr("y", 0)
.attr("width", x.bandwidth())
.attr("height", 0)
.transition()
.duration(500) //length of animation
.delay(function(d, i) { return i * 100; }) //delay must be less than duration
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); });
//append a text element to it so I can rotate the text 270 degrees.
rContainer
.append("text")
.text(function(d) { return d.type; })
.attr("width", x.bandwidth())
.attr("font-size", "34px")
.attr("fill", "white")
.attr("text-anchor", "start")
.attr("transform", function(d, i) {
// http://stackoverflow.com/questions/11252753/rotate-x-axis-text-in-d3
var yVal = y(d.value) - 6;
var xVal = x(d.type) + x.bandwidth() / 1.6;
return "translate(" + xVal + "," + yVal + ") rotate(270)";
});
You can check this working demo // starts in line 40

How do I change the position of a circle svg element using the transform attribute?

I am currently building a sunburst chart in D3JS and am trying to append circles to each node. You can view current project here: https://jsfiddle.net/mhxuo260/.
I am trying to position each of the circles in the top right hand corner of their respective node. Currently they will only position in the center which covers the node labels. I have been scouring the net in search for a clue but just haven't come up with anything yet. Any suggestion would be appreciated.
d3.json("flare.json", function(error, root) {
if (error) throw error;
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
path = g.append("path")
.attr("d", arc)
.attr('stroke', 'white')
.attr("fill", function(d) { return color((d.children ? d : d.parent).name); })
.on("click", magnify)
.each(stash);
var text = g.append("text")
// .attr("x", function(d) { return d.x; })
// .attr("dx", "6") // margin
// .attr("dy", ".35em") // vertical-align
.text(function(d) {
return d.name;
})
.attr('font-size', function(d) {
return '10px';
})
.attr("text-anchor", "middle")
.attr("transform", function(d) {
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getStartAngle(d) + ")";
} else {
return null;
}
})
.on("click", magnify);
var circle = g.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.dy; })
.attr('r', '10')
.attr('fill', 'white')
.attr('stroke', 'lightblue')
.attr("transform", function(d) {
console.log(arc.centroid(d))
if (d.depth > 0) {
return "translate(" + arc.centroid(d) + ")" +
"rotate(" + getStartAngle(d) + ")";
} else {
return null;
}
});
You are using the ´´´arc.centroid´´´ function which always returns the x,y midpoint of the arc. All that function is doing is:
The midpoint is defined as (startAngle + endAngle) / 2 and (innerRadius + outerRadius) / 2
You just need to calculate a different position using these values depending on where you want it. Use the transform like this (sudo code):
.attr( "transform", function(d) {
var x = (startAngle + endAngle) / 2;
var y = (innerRadius + outerRadius) / 2;
return "translate(" + x +"," + y + ")";
});
You don't need to rotate your circle.
(FYI: javascript will convert arrays into strings by joining each number with commas, this is why arc.centroid returning an array works here)

How to use quantile color scale in bar graph with drill-down?

I'm using the following script to generate a bar chart with drill down capability. (source: http://mbostock.github.io/d3/talk/20111116/bar-hierarchy.html).
What I am trying to do is - I want the bars to be different shades of a color depending on the data (pretty much what this question asks - D3.js: Changing the color of the bar depending on the value). Except, in my case... the graph is horizontal and not static so the answer may be different.
So Ideally, at the parent node and all sub nodes except the child node, it will display lets say different shades of blue based on the data, and once it reaches the end after drilling down, the remaining bars will be grey.
I've recently started using d3 and am kinda lost as to where to start. I tried
adding different colors to the color range z but that did not work.
Any help will be appreciated! Thanks.
NOTE: in my case, I am assuming.. that after a transition, either all nodes will lead to subnodes OR no node will lead to subnodes. Basically, at no point in the graph will there be bars, where some will drill down further while some won't. This assumption is based on the type of data I want to show with my graph.
<script>
var m = [80, 160, 0, 160], // top right bottom left
w = 1280 - m[1] - m[3], // width
h = 800 - m[0] - m[2], // height
x = d3.scale.linear().range([0, w]),
y = 25, // bar height
z = d3.scale.ordinal().range(["steelblue", "#aaa"]); // bar color
var hierarchy = d3.layout.partition()
.value(function(d) { return d.size; });
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var svg = d3.select("body").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
svg.append("svg:rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h)
.on("click", up);
svg.append("svg:g")
.attr("class", "x axis");
svg.append("svg:g")
.attr("class", "y axis")
.append("svg:line")
.attr("y1", "100%");
d3.json("flare.json", function(root) {
hierarchy.nodes(root);
x.domain([0, root.value]).nice();
down(root, 0);
});
function down(d, i) {
if (!d.children || this.__transition__) return;
var duration = d3.event && d3.event.altKey ? 7500 : 750,
delay = duration / d.children.length;
// Mark any currently-displayed bars as exiting.
var exit = svg.selectAll(".enter").attr("class", "exit");
// Entering nodes immediately obscure the clicked-on bar, so hide it.
exit.selectAll("rect").filter(function(p) { return p === d; })
.style("fill-opacity", 1e-6);
// Enter the new bars for the clicked-on data.
// Per above, entering bars are immediately visible.
var enter = bar(d)
.attr("transform", stack(i))
.style("opacity", 1);
// Have the text fade-in, even though the bars are visible.
// Color the bars as parents; they will fade to children if appropriate.
enter.select("text").style("fill-opacity", 1e-6);
enter.select("rect").style("fill", z(true));
// Update the x-scale domain.
x.domain([0, d3.max(d.children, function(d) { return d.value; })]).nice();
// Update the x-axis.
svg.selectAll(".x.axis").transition()
.duration(duration)
.call(xAxis);
// Transition entering bars to their new position.
var enterTransition = enter.transition()
.duration(duration)
.delay(function(d, i) { return i * delay; })
.attr("transform", function(d, i) { return "translate(0," + y * i * 1.2 + ")"; });
// Transition entering text.
enterTransition.select("text").style("fill-opacity", 1);
// Transition entering rects to the new x-scale.
enterTransition.select("rect")
.attr("width", function(d) { return x(d.value); })
.style("fill", function(d) { return z(!!d.children); });
// Transition exiting bars to fade out.
var exitTransition = exit.transition()
.duration(duration)
.style("opacity", 1e-6)
.remove();
// Transition exiting bars to the new x-scale.
exitTransition.selectAll("rect").attr("width", function(d) { return x(d.value); });
// Rebind the current node to the background.
svg.select(".background").data([d]).transition().duration(duration * 2); d.index = i;
}
function up(d) {
if (!d.parent || this.__transition__) return;
var duration = d3.event && d3.event.altKey ? 7500 : 750,
delay = duration / d.children.length;
// Mark any currently-displayed bars as exiting.
var exit = svg.selectAll(".enter").attr("class", "exit");
// Enter the new bars for the clicked-on data's parent.
var enter = bar(d.parent)
.attr("transform", function(d, i) { return "translate(0," + y * i * 1.2 + ")"; })
.style("opacity", 1e-6);
// Color the bars as appropriate.
// Exiting nodes will obscure the parent bar, so hide it.
enter.select("rect")
.style("fill", function(d) { return z(!!d.children); })
.filter(function(p) { return p === d; })
.style("fill-opacity", 1e-6);
// Update the x-scale domain.
x.domain([0, d3.max(d.parent.children, function(d) { return d.value; })]).nice();
// Update the x-axis.
svg.selectAll(".x.axis").transition()
.duration(duration * 2)
.call(xAxis);
// Transition entering bars to fade in over the full duration.
var enterTransition = enter.transition()
.duration(duration * 2)
.style("opacity", 1);
// Transition entering rects to the new x-scale.
// When the entering parent rect is done, make it visible!
enterTransition.select("rect")
.attr("width", function(d) { return x(d.value); })
.each("end", function(p) { if (p === d) d3.select(this).style("fill-opacity", null); });
// Transition exiting bars to the parent's position.
var exitTransition = exit.selectAll("g").transition()
.duration(duration)
.delay(function(d, i) { return i * delay; })
.attr("transform", stack(d.index));
// Transition exiting text to fade out.
exitTransition.select("text")
.style("fill-opacity", 1e-6);
// Transition exiting rects to the new scale and fade to parent color.
exitTransition.select("rect")
.attr("width", function(d) { return x(d.value); })
.style("fill", z(true));
// Remove exiting nodes when the last child has finished transitioning.
exit.transition().duration(duration * 2).remove();
// Rebind the current parent to the background.
svg.select(".background").data([d.parent]).transition().duration(duration * 2);
}
// Creates a set of bars for the given data node, at the specified index.
function bar(d) {
var bar = svg.insert("svg:g", ".y.axis")
.attr("class", "enter")
.attr("transform", "translate(0,5)")
.selectAll("g")
.data(d.children)
.enter().append("svg:g")
.style("cursor", function(d) { return !d.children ? null : "pointer"; })
.on("click", down);
bar.append("svg:text")
.attr("x", -6)
.attr("y", y / 2)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; });
bar.append("svg:rect")
.attr("width", function(d) { return x(d.value); })
.attr("height", y);
return bar;
}
// A stateful closure for stacking bars horizontally.
function stack(i) {
var x0 = 0;
return function(d) {
var tx = "translate(" + x0 + "," + y * i * 1.2 + ")";
x0 += x(d.value);
return tx;
};
}
</script>
you can create a new scale to handle the "shades" of your colors,
var shades = d3.scale.sqrt()
.domain([your domain])
.clamp(true)
.range([your range]);
and create a variable to control the "depth" of your drill-down, so when you are going to color your bars, you simply set the level of the color "shade" with d3.lab (Doc), like this:
function fill(d) {
var c = d3.lab(colorScale(d.barAttr));
c.l = shades(d.depth);
return c;
}
Using the same code as the second in the link you posted you could add (the ellipsis indicates nothing is changed compared to the code in the fiddle):
//initialize the scale
var colors = ["#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#253494", "#081d58"];
var colorScale = d3.scale.quantile()
Then when d3 reads the data, you need to add a domain and a range. This assumes that all the biggest value any bar would have is in the children of the root node (ie in the bars that are initially displayed).
d3.json(..., function(root) {
...
colorScale.domain([0, colors.length - 1,d3.max(root.children, function(d) {
return d.value;
})]).range(colors);
...
});
You can then use the colorScale to color the bars according the value during the transitions. Here are the lines I modified:
enter.select("rect").style("fill", colorScale(d.value));
...
enterTransition.select("rect")
.attr("width", function(d) {
return x(d.value);
})
.style("fill", function(d) {
if(!d.children) return "#aaa";
return colorScale(d.value);
});
...
enter.select("rect")
.style("fill", function(d) {
return colorScale(d.value);
})
.filter(function(p) {
return p === d;
})
.style("fill-opacity", 1e-6);
...
exitTransition.select("rect")
.attr("width", function(d) {
return x(d.value);
})
.style("fill", colorScale(d.value));
Here's a working fiddle: https://jsfiddle.net/f640v0yj/2/

Updating the coordinates in d3js for a tree layout

I am currently using the d3.layout.tree() to compute the positions of my data.
var tree = d3.layout.tree()
.sort(null)
.size([size.height, size.width - maxLabelLength * options.fontSize])
.children(function(d)
{
return (!d.contents || d.contents.length === 0) ? null : d.contents;
});
Initially I compute and add my nodes like this:
var nodes = tree.nodes(treeData);
var nodeGroup = layoutRoot.selectAll("g.node")
.data(nodes, function (d) { return d.name })
.enter()
.append("svg:g")
.attr("class", "node")
.attr("transform", function(d)
{
return "translate(" + d.y + "," + d.x + ")";
});
nodeGroup.append("svg:circle")
.attr("class", "node-dot")
.attr("r", options.nodeRadius);
Now I add a new node to the treeData and also to the layoutRoot:
var grp = layoutRoot.selectAll("g.node")
.data(nodes, function (d) { return d.name })
.enter()
.append('svg:g')
.attr("transform", function (d)
{
return "translate(" + d.y + "," + d.x + ")";
})
grp.append("svg:circle")
.attr("class", "node-dot")
.attr("r", options.nodeRadius)
The problem is now, that the newly computed nodes that are already present in the rootLayout have different x,y coordinates after having added a new node. But they are not within the enter() or exit() selection and are thus not redrawn at their correct position. How is this supposed to be handled, ie. how should the position of the nodes that have not changed anything but their coordinates be updated/refreshed?
I a noob to d3js. So don't be too harsh :D
I would separate the enter() selection from the update of nodes like this :
var nodeGroup = layoutRoot.selectAll("g.node")
.data(nodes, function (d) { return d.name });
// Enter selection
nodeGroup.enter()
.append("svg:g")
.attr("class", "node")
// Update
nodeGroup.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
var nodeDots = layoutRoot.selectAll("g.node-dot")
.data(nodes, function (d) { return d.name });
// Enter
nodeDots.enter()
.append("circle")
.attr("class", "node-dot")
// Update
nodeDots.attr("r", options.nodeRadius);
Hope this helps, but in a general way of speaking, it is perhaps easier to code this way, with separation of enter and updates (see here for more info)

Categories