Redrawing a key on top of a D3 projection after transition - javascript

I'm working with a D3 map projection similar to Mike Bostock's Choropleth seen here.
The issue I'm having is that I've added a transition; and when I transition the projection, the map key (seen in the top right corner) is being covered by the background color of the map.
I know I probably just need to redraw the g layer after the transition, but I'm not able to get that working as expected.
I'm originally drawing the key on the map with the following code:
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0,40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d, i) { return 350 + (i * 30)})
.attr("width", 30)
.attr("fill", function(d) { console.log(d[1]); return color(d[1]); });
g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Number of Licensed Establishments");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickValues(color.domain()))
.select(".domain")
.remove();
Then I'm transitioning the projection with this code (which also works fine).
path = d3.geoPath(projection);
svg.selectAll("path").transition().duration(2000).attr("d", path);
But the key gets covered. I've tried redrawing it like this:
g.selectAll("g").attr("transform", "translate(0,40)");
It doesn't do anything though. What step am I missing to correctly redraw that g layer on top?

Transitioning a path shouldn't change where it appears in the DOM. Transitioning element attributes with d3 modifies that element in place in the DOM. The following example should demonstrate this (path is appended first and should be behind the text, the path then transitions its d attribute through two d3 symbol paths remaining behind the text):
var svg = d3.select('body').append('svg').attr('width',400).attr('height',200);
var cross = "M-21.213203435596427,-7.0710678118654755L-7.0710678118654755,-7.0710678118654755L-7.0710678118654755,-21.213203435596427L7.0710678118654755,-21.213203435596427L7.0710678118654755,-7.0710678118654755L21.213203435596427,-7.0710678118654755L21.213203435596427,7.0710678118654755L7.0710678118654755,7.0710678118654755L7.0710678118654755,21.213203435596427L-7.0710678118654755,21.213203435596427L-7.0710678118654755,7.0710678118654755L-21.213203435596427,7.0710678118654755Z";
var star = "M0,-29.846492114305246L6.700954981042517,-9.223073285798176L28.38570081386192,-9.223073285798177L10.8423729164097,3.5229005144437298L17.543327897452222,24.146319342950797L1.7763568394002505e-15,11.400345542708891L-17.543327897452215,24.1463193429508L-10.842372916409698,3.522900514443731L-28.38570081386192,-9.22307328579817L-6.7009549810425195,-9.223073285798176Z";
var wye = "M8.533600336205877,4.926876451265144L8.533600336205877,21.9940771236769L-8.533600336205877,21.9940771236769L-8.533600336205877,4.9268764512651435L-23.31422969000131,-3.6067238849407337L-14.78062935379543,-18.387353238736164L0,-9.853752902530289L14.78062935379543,-18.387353238736164L23.31422969000131,-3.6067238849407337Z"
var symbol = svg.append('path')
.attr('transform','translate(100,100)')
.attr('d', cross )
.attr("fill","orange");
var text = svg.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor','middle')
.text('THIS IS SOME TEXT')
symbol.transition()
.delay(2000)
.attr('d', star )
.duration(2000)
.transition()
.attr('d', wye )
.duration(2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Given your example, it is likely that the key is initially rendered behind the features of the map - only there is no overlap between the two. Each appears as intended. When transitioning, with say a zoom, the features overlap and the key is hidden. As noted in the comments, try g.raise() or d3.select(".key").raise() to move the key to the bottom of the parent container, effectively lifting it above other svg elements (as elements are rendered in the order they appear in the DOM, as close as we get to a z-index in svg). You should only need to apply .raise() once - as the transition won't change the ordering, or alternatively, ensure that the key is appended to the svg last.

Related

Making more space for a graph legend in d3js

The names of the legend don't fully show.They are cut off, if I increase the width, the bars just grow bigger. How can I accommodate more space for my legend?
I tried appending the legend to the 'svg' tag instead of the 'g' tag but still not the desired results. I even plotted the axis, bars and legend on the 'svg' tag but its still not working.
javascript
const g= svg.append('g')
.attr("transform", `translate(${margin.left},${margin.top})`)
const xAxis= g.append('g')
.call(d3.axisBottom(x).tickSizeOuter(0))
.attr("transform", "translate(0," + innerHeight + ")")
const yAxis= g.append('g')
.call(d3.axisLeft(y).tickSizeOuter(0))
//stack the data? --> stack per subgroup
var stackedData = d3.stack()
.keys(subgroups)
(data)
var legend = g.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${210},${20})`);
legend.selectAll('rect')
.data(subgroups)
.enter()
.append('rect')
.attr('x', 0)
.attr('y', function(d, i){
return i * 20;})
.attr('width', 14)
.attr('height', 14)
.attr('fill', function(d, i){
return color(i);
});
legend.selectAll('text')
.data(subgroups)
.enter()
.append('text')
.text(function(d){
return d;
})
.attr('x',18)
.attr('y', function(d, i){
return i * 18;
})
.attr('text-anchor', 'start')
.attr('alignment-baseline', 'hanging');
//below is my plotted data
telescope,allocated,unallocated
IRSF,61,28
1.9-m,89,0
1.0-m,64,23
// width=300 and heigh=300 for svg
I want to show the full names of the legends just next to the right of the bars for the graph.
Link to the graph is here.
How do I solve the problem?
Your issue is that the SVG's width is too narrow to accomodate showing all of the legend texts.
If your graph follows D3 conventions, space for elements that are auxilliary to graph itself (axis names, ticks, legends, etc.) is made using an margin object. Although I can't see how your graph is made, it looks like yours is setup to use a margin object as well. Following D3 conventions the margin values are fixed, which would explain why changing the width value just makes the bars wider yet still doesn't make space for the legend texts.
Therefore, locate the margin object and change its right value to something higher. Inspecting your example, it looks like doubling it should do it.
Hope this helps!

Overlays not showing up on top of data-generated d3 shapes

I am a noob with d3.js. I am using topoJSON data to render maps and so far it's been working great. Now I want to overlay some data such as text or circles on top of each country/region and I am hitting a wall. I have code similar to this:
var countries = g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("path")
.attr("d", path)
.style("fill", colorize)
.attr("class", "country")
.on("click", clicked)
which properly renders my map. In order to overlay some circles on it, I do the following:
countries
.append("circle")
.attr("r", function(d, i, j) {
return 10; // for now
})
// getCentroid below is a function that returns the
// center of the poligon/path bounding box
.attr("cy", function(d, i, j) { return getCentroid(countries[0][j])[0]})
.attr("cx", function(d, i, j) { return getCentroid(countries[0][j])[1]})
.style("fill", "red")
Which is pretty cumbersome (specially the way it accesses the countries array), but it succeeds at appending a circle for each path representing a country. The problem is that the circle exists in the SVG markup, but doesn't show up at all in the document. I am obviously doing something wrong, but I am at a loss of what is it.
The problem is that you're appending the circle elements to path elements, which you can't do in SVG. You need to append them to the parent g elements. The code would look something like this.
var countries = g.selectAll("g.countries")
.data(topojson.feature(collection, collection.objects.countries).features)
.enter().append("g")
.attr("id", "countries");
countries.append("path")
.datum(function(d) { return d; })
.attr("d", path)
// etc
countries.append("circles")
// etc

d3 - Rotate text elements in an array

I am trying to use selectAll("text") in d3 to to add an array of string values (called 'data') to my graph. I want each individual data point to be rotated at the point it is placed at, defined here as (i * (width/ data.length) + 8, 170). However, it is currently rotating the entire array set as one long string, with the first element at the (x, y) point I set. How can I appropriately apply the translate rotation to rotate each element individually?
new_svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d, i) {
return i * (width / data.length) + 8;
})
.attr("y", function(d) {
return 170;
})
.attr("dx", -barWidth/2)
.attr("text-anchor", "middle")
.attr("style", "font-size: 12; font-family: Garamond, sans-serif")
.text(function(d) { return d;})
.attr("transform", function(d) {
return "rotate(45)"
});
There was more than one text element in the array, which I was able to verify. It turns out that the problem comes from setting the x and y elements separate from the translate transform. The solution offered here worked for me: d3 x axis labels outputted as long string

D3 adding element to zoomed map

I have a map where circles (origin of people) appear when clicking on a legend.
Additionally, it is possible to zoom in, and then, circles (and country path) are transformed (using d3.behavior.zoom).
Though, if I first zoom in, and then click on the legend, circles do not appear at the right places. How can I solve this problem and append them at the right coordinates (within the zoomed map).
Any ideas? I'm sure the solution is not that difficult, but I'm stucked.
See (http://wahrendorf.de/circlemapping/world_question.html) for an example.
Thanks,
Morten
You need to take into account d3.event.translate and d3.event.scale when you draw the circles. The easiest way to do this is to factor out your zoom function so that it may be called by the circle drawing function.
var translate = [0,0];
var scale = 1;
var zoom_function = function() {
canvas.selectAll("path")
.attr("transform","translate("+translate.join(",")+")scale("+scale+")");
canvas.selectAll("circle.origin")
.attr("transform","translate("+translate.join(",")+")scale("+scale+")")
.attr("r", function(d) { return radius/scale; });
};
var zoom = d3.behavior.zoom().scaleExtent([1,6])
.on("zoom",function() {
translate = d3.event.translate;
scale = d3.event.scale;
zoom_function();
});
// ... The rest of the code ...
canvas.append("text")
.text("show circles")
.attr("x", 30 ) .attr("y", 480 )
.attr("dy", ".35em")
.on("click", function(d) {
/// load data with long/lat of circles
d3.csv("./World_files/places_q.csv", function(error, origin) {
canvas.selectAll("circle.origin").remove();
canvas.selectAll("circle.origin")
.data(origin)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.originlong, d.originlat])[0];})
.attr("cy", function(d) {return projection([d.originlong, d.originlat])[1];})
.attr("r", 2)
.style("fill", "red")
.style("opacity", 0.5)
.attr("class", "origin");
// Call the zoom function here to fix the placement of the circles.
zoom_function();
});
});
You will need to track the last known d3.event.translate and d3.event.scale values since they will be undefined when you are drawing the circles.

Placing labels at the center of nodes in d3.js

I am starting with d3.js, and am trying to create a row of nodes each of which contains a centered number label.
I am able to produce the desired result visually, but the way I did it is hardly optimal as it involves hard-coding the x-y coordinates for each text element. Below is the code:
var svg_w = 800;
var svg_h = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", svg_w)
.attr("weight", svg_h);
var dataset = [];
for (var i = 0; i < 6; i++) {
var datum = 10 + Math.round(Math.random() * 20);
dataset.push(datum);
}
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d, i) {
return (i * 70) + 50;
})
.attr("cy", svg_h / 2)
.attr("r", 20);
var labels = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("dx", function(d, i) {
return (i * 70) + 42
})
.attr("dy", svg_h / 2 + 5)
.text(function(d) {
return d;
});
The node class is custom CSS class I've defined separately for the circle elements, whereas classes nodes and labels are not explicitly defined and they are borrowed from this answer.
As seen, the positioning of each text label is hard-coded so that it appears at the center of the each node. Obviously, this is not the right solution.
My question is that how should I correctly associate each text label with each node circle dynamically so that if the positioning of a label changes along with that of a circle automatically. Conceptual explanation is extremely welcome with code example.
The text-anchor attribute works as expected on an svg element created by D3. However, you need to append the text and the circle into a common g element to ensure that the text and the circle are centered with one another.
To do this, you can change your nodes variable to:
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
// Add one g element for each data node here.
.append("g")
// Position the g element like the circle element used to be.
.attr("transform", function(d, i) {
// Set d.x and d.y here so that other elements can use it. d is
// expected to be an object here.
d.x = i * 70 + 50,
d.y = svg_h / 2;
return "translate(" + d.x + "," + d.y + ")";
});
Note that the dataset is now a list of objects so that d.y and d.x can be used instead of just a list of strings.
Then, replace your circle and text append code with the following:
// Add a circle element to the previously added g element.
nodes.append("circle")
.attr("class", "node")
.attr("r", 20);
// Add a text element to the previously added g element.
nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Now, instead of changing the position of the circle you change the position of the g element which moves both the circle and the text.
Here is a JSFiddle showing centered text on circles.
If you want to have your text be in a separate g element so that it always appears on top, then use the d.x and d.y values set in the first g element's creation to transform the text.
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
The best answer came from the asker himself:
just a further observation: with only .attr("text-anchor", "middle")
for each text element, the label is at the middle horizontally but
slightly off vertically. I fixed this by adding attr("y", ".3em")
(borrowed from examples at d3.js website), which seems to work well
even for arbitrary size of node circle. However, what exactly this
additional attribute does eludes my understanding. Sure, it does
something to the y-coordinate of each text element, but why .3em in
particular? It seems almost magical to me...
Just add .attr("text-anchor", "middle") to each text element.
Example:
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
This page describes what's going on under the svg hood when it comes to text elements. Understanding the underlying machinery and data structures helped me get a better handle on how I had to modify my code to get it working.

Categories

Resources