Right way to add labels to d3-geomap - d3.js

I try to add labels to map done with d3-geomap, but can't make it work.
The choropleth map itself gets painted correctly, but adding the labels doesn't work out right. The labels show up on the wrong position.
After painting the map I loaded again the topojson file again and then add text blocks like that:
d3.json("https://d3-geomap.github.io/d3-geomap/topojson/countries/ESP.json").then(function(es) {
let path = d3.geoPath().projection(d3.geoMercator());
svg.select("svg").append("g")
.selectAll("labels")
.data(topojson.feature(es, es.objects.units).features)
.enter().append("text")
.attr("class", "place-label")
.attr("x", function(d) { return path.centroid(d)[0]; })
.attr("y", function(d) { return path.centroid(d)[1]; })
.attr("text-anchor","middle")
.text(function(d) { return d.properties.name; });
});
The problem here is that I can't figure out the correct position of the labels. I also tried to apply the same transform as to the polygons, but then have all the same y position.
Here is the example on bl.ocks.

I made some changes to your code and published it in this gist. When testing it locally, the map displayed like the image below. At this size, labels don't work well, but if you resize the map and/or show fewer labels it should be okay.
Some info on the changes. Whenever you want to draw something on top of a map with d3-geomap, it should go in the postUpdate function. This way the map is already rendered and its SVG elements, the geo data and the path object are accessible via the map object you created. No need to load the Topojson file a second time. The function passed to postUpdate looks like follows:
function drawLabels() {
map.svg.append("g").attr('class', 'zoom')
.selectAll("text")
.data(topojson.feature(map.geo, map.geo.objects.units).features)
.enter().append("text")
.attr("class", "place-label")
.attr("x", function(d) { return map.path.centroid(d)[0]; })
.attr("y", function(d) { return map.path.centroid(d)[1]; })
.attr("text-anchor","middle")
.text(function(d) { return d.properties.name; })
.on('click', map.clicked.bind(map));
}
This page of the documentation shows the available map attributes and accessor functions. Hope this helps.

Related

How to add label or custom value on Mapchart's path using geoChoroplethChart and dc.js?

var IndChart = dc.geoChoroplethChart("#india-chart");
var states = data.dimension(function (d) {
return d["state_name"];
});
var stateRaisedSum = states.group().reduceSum(function (d) {
return d["popolation"];
});
IndChart
.width(700)
.height(500)
.dimension(states)
.group(stateRaisedSum)
.colors(d3.scale.ordinal().domain().range(["#27AE60", "#F1C40F", "#F39C12","#CB4335"]))
.overlayGeoJson(statesJson.features, "state", function (d) { //console.log(d.properties.name);
return d.id;
})
.projection(d3.geo.mercator().center([95, 22]).scale(940))
.renderLabel(true)
.title(function (d) { console.log(d); return d.key + " : " + d.value ;
})
.label(function (d) { console.log(d);}) ;
wanted to add Label or custom value(25%, added in Map chart screen-shots) in map chart for each path using dc.js.
In the comments above, you found or created a working example that answers your original question. Then you asked how to make it work for two charts on the same page.
This is just a matter of getting the selectors right, and also understanding how dc.js renders and redraws work.
First off, that example does
var labelG = d3.select("svg")
which will always select the first svg element on the page. You could fix this by making the selector more specific, i.e. #us-chart svg and #us-chart2 svg, but I prefer to use the chart.select() function, which selects within the DOM tree of the specific chart.
Next, it's important to remember that when you render a chart, it will remove everything and start from scratch. This example calls dc.renderAll() twice, so any modifications made to the first chart will be lost on the second render.
In contrast, a redraw happens when any filter is changed, and it incrementally changes the chart, keeping the previous content.
I prefer to listen to dc.js chart events and make my modifications then. That way, every time the chart is rendered or redrawn, modifications can be made.
In particular, I try to use the pretransition event whenever possible for modifying charts. This happens right after drawing, so you have a chance to change things without any glitches or pauses.
Always add event listeners before rendering the chart.
Adding (the same) handler for both charts and then rendering, looks like this:
usChart.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
usChart2.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
dc.renderAll();
I used one more trick there: since pretransition happens for both renders and redraws, but we only want to add these labels once, I use this pattern:
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("class", "Title");
This is the simplest data binding there is: it says we want one g.Title and its data is just the value 0. Since we give the g element the Title class, this ensures that we'll add this element just once.
Finally, the result of this expression is an enter selection, so we will only add text elements when the Title layer is new.
Fork of your fiddle.

In d3.js map, points are hidden behind other features

I have tried to create a map of India with some points in it. I followed the codebase from here.
Everything is fine except the points. They are hidden behind other features on the map, and because of this are not visible. How do I layer the features so that the points are visible?
In d3.js map layering can be handled in two ways. If this is your code (paraphrasing from your example)
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
Data will be added to the 'g' element based on the order in which the callback functions are completed, so it is possible that the csv data will be drawn first and the json data will be drawn after it.
The first method I'll present here is the cleanest way in most situations to specify data layer order (in my mind). SVG 'g' elements are appended in the order that they are specified. This gives you easy control over the layering of data:
var gBackground = svg.append("g"); // appended first
var gDataPoints = svg.append("g"); // appended second
// ... and so forth
Then, all you have to do is specify to which 'g' element/layer data gets appended/inserted into. So, your code would look more like:
d3.json("path.json",function (json) {
gBackground.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("path.csv",function (csv) {
gDataPoints.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
The second option appends data to the same 'g' element but ensures the order in which this is done is controlled, by drawing the second layer in the callback function that draws the first, after the first is drawn:
To control the ordering of the data with this method we would modify the code to something like:
d3.json("path.json",function (json) {
g.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
// once the json is drawn, draw the csv:
d3.csv("path.csv",function (csv) {
g.selectAll("circle")
.data(csv)
.enter().append("circle")
.attr("cx", function(d) { projection([d.x,d.y])[0] })
.attr("cy", function(d) { projection([d.x,d.y])[1] })
.attr("r",4);
});
});

Plotting two datasets

I am trying to show two data sets: one with a lower opacity and the other with normal dots using D3.js. I tried this:
svg.selectAll("*").remove();
if (olddset!=dset) {
svg.selectAll("circle") .data(datasets[olddset]) .enter() .append("circle")
.attr('cx',function(a){ return xscales[whichscale][xval](a[xval]); })
.attr('cy',function(a){ return yscales[whichscale][yval](a[yval]); })
.attr('r',1)
.style("opacity", 0.2)
;
}
svg.selectAll("circle") .data(datasets[dset]) .enter() .append("circle")
.attr('cx',function(a){ return xscales[whichscale][xval](a[xval]); })
.attr('cy',function(a){ return yscales[whichscale][yval](a[yval]); })
.attr('r',3)
.style("opacity", 1)
;
However, that does not do what I was looking for. I wanted olddset to be small dots and the new dataset (dset) with r=3 and opacity=1. What am I doing wrong?
You are using the same selection for the first AND second dataset. Thus for the second dataset you are selecting <circle>s that already exist. Instead, use a different selection, for example:
svg.selectAll("circle2")
You can handle this with styles. Like:
svg.selectAll("circle.oldset").data(datasets[olddset]).enter().append("circle")
.attr('cx',function(a){ return xscales[whichscale][xval](a[xval]); })
.attr('cy',function(a){ return yscales[whichscale][yval](a[yval]); })
.attr('r',1)
.classed("oldset", true); // where the oldset class in yr styles has the opacity defined
svg.selectAll("circle.dset") .data(datasets[dset]) .enter() .append("circle")
.attr('cx',function(a){ return xscales[whichscale][xval](a[xval]); })
.attr('cy',function(a){ return yscales[whichscale][yval](a[yval]); })
.attr('r',3)
.classed("dset", true);
You will run into trouble with the "circle2" trick if you try to update because it will never select anything.

Adding a label to a path with D3

I am building a graph with D3 and represent the links between nodes as paths. I want to add a label for each path by using the following code:
path_labels = path_labels.data(links);
path_labels.enter().append("text")
.attr("class", "linklabel")
.style("font-size", "12px")
.attr("text-anchor", "start")
.append("textPath")
.attr("xlink:href", function (d) {
return "#linkId_0";
})
.text(function (d) {
return "my text";
});
When I look at the result, text is appended, but not the textPath inside the text element.
Can someone help?
The full code can be found at http://jsfiddle.net/3u0oage7/
I found the answer to the problem. The code I posted works as supposed. The problem was that in my code I had this:
svg.selectAll('text').
text(function (d) {
return d.label;
});
This code was changing all the text elements. I changed the selector so it selects only the text elements corresponding to a circle.

D3.js: alternative to selecting elements by data attributes?

I'm using D3.js to build a circular heat chart, and I want to add events so that when I mouseover any section of the chart, all the elements at the same angle also highlight. (Like the mouseover events on this Guardian visualisation.)
At the moment, I'm doing this by explicitly adding data attributes to the HTML for every path element:
g.selectAll("path").data(data)
.enter().append("path")
.attr("d", d3.svg.arc().innerRadius(ir).outerRadius(or).startAngle(sa).endAngle(ea))
.attr("data-pathnumber", function(d) { return d.pathNumber });
And then my mouseover event selects by data attribute:
d3.selectAll("#chart4 path").on('mouseover', function() {
var d = d3.select(this).data()[0];
d3.selectAll('path[data-pathnumber="' + d.pathNumber + '"]').attr('fill', 'black');
});
However, is this actually the correct way to do things in D3? It feels to me like there "ought" to be a way to select the path based only on the data stored in the DOM, not on explicit data attributes.
If you store the reference to your paths, you can use selection.filter here:
var paths = g.selectAll("path").data(data)
.enter().append("path")
.attr("d", d3.svg.arc().innerRadius(ir).outerRadius(or).startAngle(sa).endAngle(ea))
;
Mouseover:
d3.selectAll("#chart4 path").on('mouseover', function(thisData) {
paths
.filter(function (d) { return d.pathNumber === thisData.pathNumber; })
.attr('fill', 'black');
});

Resources