Problem selecting element by id using d3 js - javascript

I'm trying to make a choropleth map in d3 js.
In my code I'm using gejson to draw french departments (counties) and then I want to color them using data from a csv file.
First I populate every counties with their official ID which is a number of 5 digits (ex : 75001).
Then I want to color them using a colorScale. To do so, I do a for each loop where I select counties using their ID (in the csv file this time) and I use the color scale and the data form the csv to get the color of the countie on the map.
I think that the problem problem is d3.select("#d" + e.insee) doesn't work.
I have taken the problem in every way possible and I really can"t figure out what is wrong in the code.
Here is my entire code. The data are loaded from a github so every one can execute it.
I apologized for the long code.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
<head>
<meta charset="utf-8">
<title>All in One</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style type="text/css">
#info {
margin-top: 50px;
}
#deptinfo {
margin-top: 30px;
}
.department {
cursor: pointer;
stroke: black;
stroke-width: .5px;
}
.department:hover {
stroke-width: 2px;
}
div.tooltip {
position: absolute;
opacity:0.8;
z-index:1000;
text-align:left;
border-radius:4px;
-moz-border-radius:4px;
-webkit-border-radius:4px;
padding:8px;
color:#fff;
background-color:#000;
font: 12px sans-serif;
max-width: 300px;
height: 40px;
}
#svg {
display: block;
margin: auto;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>
<script type="text/javascript">
const width = 850,
const height = 800,
colors = ['#d4eac7', '#c6e3b5', '#b7dda2', '#a9d68f', '#9bcf7d', '#8cc86a', '#7ec157', '#77be4e', '#70ba45', '#65a83e', '#599537', '#4e8230', '#437029', '#385d22', '#2d4a1c', '#223815'];
const path = d3.geoPath();
const projection = d3.geoMercator()
.center([2.332978, 48.860117])
.scale(40000)
.translate([width / 2, height / 2]);
path.projection(projection);
const svg = d3.select('#map').append("svg")
.attr("id", "svg")
.attr("width", width)
.attr("height", height)
.attr("class", "Blues");
// Append the group that will contain our paths
const deps = svg.append("g");
var promises = [];
promises.push(d3.json('https://raw.githubusercontent.com/cerezamo/dataviz/master/Graphique_bokeh/pop_comgeo.geojson'))
promises.push(d3.csv("https://raw.githubusercontent.com/cerezamo/dataviz/master/variables.csv"))
Promise.all(promises).then(function(values){
const geojson = values[0];
const csv = values[1];
var features = deps
.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr('id', function(d) {return "d" + d.properties.insee;})// Creation of the id as (ex :"d75005")
// I add a d so the id is not a pure number as it could create error when selecting it
.attr("d", path);
var quantile = d3.scaleQuantile()
.domain([0, d3.max(csv, function(e) { return + e.densitehabkm2; })])
.range(colors);
var legend = svg.append('g')
.attr('transform', 'translate(725, 150)')
.attr('id', 'legend');
legend.selectAll()
.data(d3.range(colors.length))
.enter().append('svg:rect')
.attr('height', '20px')
.attr('width', '20px')
.attr('x', 5)
.attr('y', function(d) { return d * 20; })
.style("fill", function(d) { return colors[d]; });
var legendScale = d3.scaleLinear()
.domain([0, d3.max(csv, function(e) { return +e.densitehabkm2; })])
.range([0, colors.length * 20]);
var legendAxis = svg.append("g")
.attr('transform', 'translate(750, 150)')
.call(d3.axisRight(legendScale).ticks(3));
csv.forEach(function(e,i) {
d3.select(("#d" + e.insee)) // Line where I think the problem is
// Here I'm trying to select Id's using the same code but reading it in the csv file. I have check and id's in geojson and csv do correspond
.style("fill", function(d) { return quantile(+e.densitehabkm2); })
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html("IZI")
.style("left", (d3.event.pageX + 30) + "px")
.style("top", (d3.event.pageY - 30) + "px");
})
.on("mouseout", function(d) {
div.style("opacity", 0);
div.html("")
.style("left", "-500px")
.style("top", "-500px");
});
});
//console.log(csv.insee);
});
// Append a DIV for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
</script>
Thank you very much for your time.

SVG <path> elements have, indeed, a black fill by default. However, that's not a problem here: your style("fill", ...) should work, regardless.
The problem here is that you have several paths with the same ID. If you have a look at the original GeoJson, you'll see that you have several insee properties, for different years. So, your code is painting several black paths, one on top of the other. When you select by ID you select only one of them (by the way, it goes without saying that IDs should be unique in the document), and the other black paths avoid you seeing the painted path. Also, your solution simply makes all paths transparent, so when you paint any one of them it will be visible, but all other transparent paths are still there, over the path you selected.
All that being said, the simplest solution is filtering the original data, for instance:
geojson.features = geojson.features.filter(function(d) {
return d.properties.year === 1962;
});
Basically, by selecting just one year, you avoid all those paths stacked one on top of the other (and also the browser will render the page way faster).
With that change alone, your style method will work. Here is the running demo:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
<head>
<meta charset="utf-8">
<title>All in One</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<style type="text/css">
#info {
margin-top: 50px;
}
#deptinfo {
margin-top: 30px;
}
.department {
cursor: pointer;
stroke: black;
stroke-width: .5px;
}
.department:hover {
stroke-width: 2px;
}
div.tooltip {
position: absolute;
opacity: 0.8;
z-index: 1000;
text-align: left;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
padding: 8px;
color: #fff;
background-color: #000;
font: 12px sans-serif;
max-width: 300px;
height: 40px;
}
#svg {
display: block;
margin: auto;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>
<script type="text/javascript">
const width = 850,
height = 800,
colors = ['#d4eac7', '#c6e3b5', '#b7dda2', '#a9d68f', '#9bcf7d', '#8cc86a', '#7ec157', '#77be4e', '#70ba45', '#65a83e', '#599537', '#4e8230', '#437029', '#385d22', '#2d4a1c', '#223815'];
const path = d3.geoPath();
const projection = d3.geoMercator()
.center([2.332978, 48.860117])
.scale(40000)
.translate([width / 2, height / 2]);
path.projection(projection);
const svg = d3.select('#map').append("svg")
.attr("id", "svg")
.attr("width", width)
.attr("height", height)
.attr("class", "Blues");
// Append the group that will contain our paths
const deps = svg.append("g");
var promises = [];
promises.push(d3.json('https://raw.githubusercontent.com/cerezamo/dataviz/master/Graphique_bokeh/pop_comgeo.geojson'))
promises.push(d3.csv("https://raw.githubusercontent.com/cerezamo/dataviz/master/variables.csv"))
Promise.all(promises).then(function(values) {
const geojson = values[0];
const csv = values[1];
geojson.features = geojson.features.filter(function(d) {
return d.properties.year === 1962;
})
var features = deps
.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr('id', function(d) {
return "d" + d.properties.insee;
}) // Creation of the id as (ex :"d75005")
// I add a d so the id is not a pure number as it could create error when selecting it
.attr("d", path);
var quantile = d3.scaleQuantile()
.domain([0, d3.max(csv, function(e) {
return +e.densitehabkm2;
})])
.range(colors);
var legend = svg.append('g')
.attr('transform', 'translate(725, 150)')
.attr('id', 'legend');
legend.selectAll()
.data(d3.range(colors.length))
.enter().append('svg:rect')
.attr('height', '20px')
.attr('width', '20px')
.attr('x', 5)
.attr('y', function(d) {
return d * 20;
})
.style("fill", function(d) {
return colors[d];
});
var legendScale = d3.scaleLinear()
.domain([0, d3.max(csv, function(e) {
return +e.densitehabkm2;
})])
.range([0, colors.length * 20]);
var legendAxis = svg.append("g")
.attr('transform', 'translate(750, 150)')
.call(d3.axisRight(legendScale).ticks(3));
csv.forEach(function(e, i) {
d3.select(("path#d" + e.insee)) // Line where I think the problem is
// Here I'm trying to select Id's using the same code but reading it in the csv file. I have check and id's in geojson and csv do correspond
.style("fill", function() {
return quantile(+e.densitehabkm2);
})
.on("mouseover", function(d) {
console.log(d);
div.transition()
.duration(200)
.style("opacity", .9);
div.html("IZI")
.style("left", (d3.event.pageX + 30) + "px")
.style("top", (d3.event.pageY - 30) + "px");
})
.on("mouseout", function(d) {
div.style("opacity", 0);
div.html("")
.style("left", "-500px")
.style("top", "-500px");
});
});
//console.log(csv.insee);
});
// Append a DIV for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
</script>

Well the problem was in fact in the css.
Thanks to Ryan Morton for the answer.
Add .attr('fill', 'none') to when you first create the map objects.
It's autofilling with black and somehow preventing your colors later:
https://jsfiddle.net/bz3o5yah/

Related

How do I properly sort the data for my d3 bubble map so that smaller bubbles show up on top of larger bubbles?

I'm making a bubble map similar to this one: https://observablehq.com/#d3/bubble-map
Everything is working except that my smaller bubbles are not always showing on top of the larger ones. I can't see why, as I've sorted the data before drawing the circles. Can anyone see what I'm doing wrong?
Here is a plunker:
https://plnkr.co/edit/JKWeQKkhN2TQwvNZ?open=lib%2Fscript.js
Code is below. The other files are too large for stack overflow but can be accessed via the Plunker.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.states {
fill: #d3d3d3;
stroke: #ffffff;
stroke-linejoin: round;
}
div.tooltip {
position: absolute;
left: 75px;
text-align: center;
height: 12px;
padding: 8px;
font-size: 13px;
font-family: 'Proxima-Nova', sans-serif;
background: #FFFFFF;
border: 1px solid #989898;
pointer-events: none;
}
.block {
width: 18%;
height: 15px;
display: inline-block;
}
</style>
<body>
<div class="g-chart"></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories#0.1.0/dist/geo-albers-usa-territories.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script>
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var margin = { top: 10, left: 10, bottom: 10, right: 10 },
width = window.outerWidth,
width = width - margin.left - margin.right,
mapRatio = .5,
height = width * mapRatio;
const epsilon = 1e-6;
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
var map = d3.select(".g-chart").append("svg")
.style('height', height + 'px')
.style('width', width + 'px')
.call(d3.zoom().on("zoom", function () {
map.attr("transform", d3.event.transform)
d3.selectAll()
}))
.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "test.csv")
.await(ready);
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function () {
return this.each(function () {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
function ready(error, us, data) {
if (error) throw error;
data.forEach(function (d) {
d.amount = +d.amount;
})
map.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
// sort by descending size so that the smaller circles are drawn on top - not working
map.append('g')
.attr('class', 'facility')
.selectAll("circle")
.data(data.sort((a, b) => +b.amount - +a.amount))
.enter()
.append("circle")
.attr("cx", function (d) {
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return projection([d.longitude, d.latitude])[1];
})
.attr('r', function (d) {
if (d.amount <= 25) { return 3 }
else if (d.amount > 25 && d.amount <= 50) { return 5 }
else if (d.amount > 50 && d.amount <= 75) { return 7 }
else { return 9 }
})
.style("fill", "#EF4136")
.style("stroke", "#BE2C2D")
.style("stroke-width", 1)
.style("opacity", 0.5)
.on("mouseover", function (d) {
var sel = d3.select(this);
sel.moveToFront();
d3.select(this).transition().duration(0)
.style("opacity", 0.8)
.style("stroke", "#FFFFFF")
div.transition().duration(0)
.style("opacity", .85)
div.text(d.amount)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 30) + "px");
})
.on("mouseout", function () {
var sel = d3.select(this);
d3.select(this)
.transition().duration(0)
.style("opacity", 0.5)
.style("stroke", "#BE2C2D")
div.transition().duration(0)
.style("opacity", 0);
});
//RESPONSIVENESS
d3.select(window).on('resize', resize);
function resize() {
var w = d3.select(".g-chart").node().clientWidth;
width = w - margin.left - margin.right;
height = width * mapRatio;
var newProjection = d3.geoAlbersUsa()
.scale(width)
.translate([width / 2, height / 2]);
path = d3.geoPath()
.projection(newProjection);
map.selectAll("circle")
.attr("cx", function (d) {
return newProjection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return newProjection([d.longitude, d.latitude])[1];
})
.attr("r", 5)
map
.style('width', width + 'px')
.style('height', height + 'px');
map.selectAll("path").attr('d', path);
}
}
</script>
I would suggest you to split your data to a couple separate datasets grouped by size and create distinct group (g element) for each one. This will also fix issues with circles highlighting.
I slightly updated your plunker to make it work as described (check the lines 91-167) https://plnkr.co/edit/rayo5IZQrBqfqBWR?open=lib%2Fscript.js&preview
Also check the raise and lower methods. They might be a good replacement for your moveToFront and moveToBack methods.
https://riptutorial.com/d3-js/example/18029/svg--the-drawing-order

Trying to Refactor Codebase with NY topojson file

I built an animated choropleth map using d3 which uses your typical usa topojson file (by county). The file can be found here:
https://d3js.org/us-10m.v1.json
My code works fine, however because my data is ny based, I would like to use just a ny map (by county), as opposed to the entire united states. Like the file here for example:
https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-
states/NY-36-new-york-counties.json
However, when I replace the old file with the new one, I get the following error:
Uncaught ReferenceError: counties is not defined
I am assuming the error can be ultimately traced back to this code block:
counties = svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
.call(style,currentYear)
Specifically, this line:
.data(topojson.feature(us, us.objects.counties).features)
My assumption is because the shapefiles are slightly different, this line needs to be refactored somehow to be specific to this ny shapefile (or perhaps I'm wrong).
Anyways, here is my code. Any help would be immensely appreciated:
HTML
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<svg width="960" height="600"></svg>
CSS
div.tooltip {
position: absolute;
text-align: center;
vertical-align: middle;
width: auto;
height: auto;
padding: 2px;
font: 12px sans-serif;
color: white;
background: gray;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
.counties :hover {
stroke: black;
stroke-width: 2px;
}
.county-borders {
fill: none;
stroke: #fff;
stroke-width: 0.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
.year.label {
font: 500 85px "Helvetica Neue";
fill: gray;
}
.overlay {
fill: none;
pointer-events: all;
cursor: ew-resize;
}
JS
choroplethMap();
function choroplethMap() {
var svg = d3.select("svg");
var path = d3.geoPath();
var format = d3.format("");
var height = 600;
var width = 960;
var colorScheme = d3.schemeReds[9];
colorScheme.unshift("#eee");
var color = d3.scaleQuantize()
.domain([0, 20])
.range(colorScheme);
var x = d3.scaleLinear()
.domain(d3.extent(color.domain()))
.rangeRound([600,860]);
var g = svg.append("g")
.attr("transform", "translate(0,40)");
g.selectAll("rect")
.data(color.range().map(function(d){ return color.invertExtent(d); }))
.enter()
.append("rect")
.attr("height", 8)
.attr("x", function(d){ return x(d[0]); })
.attr("width", function(d){ return x(d[1]) - x(d[0]); })
.attr("fill", function(d){ return color(d[0]); });
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("Unemployment Rate (%)");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(format)
.tickValues(color.range().slice(1).map(function(d){ return color.invertExtent(d)[0];
})))
.select(".domain")
.remove();
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Add the year label; the value is set on transition.
var label = svg.append("text")
.attr("class", "year label")
.attr("text-anchor", "end")
.attr("y", 575)
.attr("x", 625)
.text(2013);
queue()
// .defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.json,
"https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-
states/NY-36-new-york-counties.json")
.defer(d3.csv, "../choropleth-ny.csv")
.await(ready);
function ready(error, us, unemployment) {
if (error) throw error;
// Initialize data to 1990
var currentYear = 2013;
// Add an overlay for the year label.
var box = label.node().getBBox();
var overlay = svg.append("rect")
.attr("class", "overlay")
.attr("x", box.x)
.attr("y", box.y)
.attr("width", box.width)
.attr("height", box.height)
.on("mouseover", enableInteraction);
// Start a transition that interpolates the data based on year.
svg.transition()
.duration(25000)
.ease(d3.easeLinear)
.tween("year", tweenYear)
//.each();
counties = svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
.call(style,currentYear)
function style(counties, year){
newunemployment = interpolateData(year);
var rateById = {};
var nameById = {};
newunemployment.forEach(function(d) {
var newcode = '';
if (d.code.length < 5) {
newcode = '0' + d.code;
d.code = newcode;
}
rateById[d.code] = +d.rate;
nameById[d.code] = d.name;
});
counties.style("fill", function(d) { return color(rateById[d.id]); })
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(nameById[d.id] + ' in ' + Math.round(currentYear) +': <br><strong>'
+ rateById[d.id] + '%</strong>')
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");})
// fade out tooltip on mouse out
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);});
}
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b))
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("d", path);
// After the transition finishes, you can mouseover to change the year.
function enableInteraction() {
var yearScale = d3.scaleLinear()
.domain([2013, 2021])
.range([box.x + 10, box.x + box.width - 10])
.clamp(true);
// Cancel the current transition, if any.
svg.transition().duration(0);
overlay
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("mousemove", mousemove)
.on("touchmove", mousemove);
function mouseover() { label.classed("active", true); }
function mouseout() { label.classed("active", false); }
function mousemove() { displayYear(yearScale.invert(d3.mouse(this)[0])); }
}
// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
var year = d3.interpolateNumber(2013, 2021);
return function(t) { displayYear(year(t)); };
}
// Updates the display to show the specified year.
function displayYear(year) {
currentYear = year;
counties.call(style,year)
label.text(Math.round(year));
}
// Interpolates the dataset for the given (fractional) year.
function interpolateData(year) {
return unemployment.filter(function(row) {
return row['year'] == Math.round(year);
});
}
};
};
Here is a snapshot of my csv file:
name. |. year. |. rate|. code
Bronx. 2021. 1. 36005
Bronx. 2020. 2. 36005
Queens. 2021. 4. 36081
Queens. 2017. 8. 36081
Try to replace us.objects.counties for us.objects.cb_2015_new_york_county_20m
Please, compare both images.
cbertelegni is right in noting that you need to update the property you are accessing when using the new data. Once that is resolved you have a few new problems though:
The data you have is not projected, before it was pre-projected and you didn't need a projection.
The state outline is gone as we don't have a states property in the topojson.
The first is pretty easy, we need to use a projection, perhaps something like:
var geojson = topojson.feature(topo, topo.objects.cb_2015_new_york_county_20m);
var projection = d3.geoAlbers()
.fitSize([width,height],geojson);
var path = d3.geoPath(projection);
The second problem is also fairly straightforward. The states outlines were drawn where two polygons representing two different states shared an arc: topojson.mesh(us, us.objects.states, (a, b) => a !== b) (a and b represent states, where an arc separates two different states a !== b). If we use the counties data here, we'll just get a mesh that separates the counties.
Instead we can change the equation a bit when using the counties geometry: if an arc is shared only by one feature, a and b will both represent that feature, so we can use:
var outline = topojson.mesh(topo, topo.objects.cb_2015_new_york_county_20m, (a, b) => a === b);
to find which arcs are not shared between counties (ie: the outer edges or the boundary of the state).
I've created a simplistic chorlopleth below that demonstrates the two changes in this answer in combination with cbertelegni's change.
var svg = d3.select("svg");
var path = d3.geoPath();
var format = d3.format("");
var height = 360;
var width = 500;
var names = ["Steuben","Sullivan","Tioga","Fulton","Lewis","Rockland","Schuyler","Dutchess","Westchester","Clinton","Seneca","Jefferson","Wyoming","Monroe","Chemung","Erie","Richmond","Rensselaer","Tompkins","Montgomery","Schoharie","Bronx","Franklin","Otsego","Allegany","Yates","Cortland","Ontario","Wayne","Niagara","Albany","Onondaga","Herkimer","Cattaraugus","Ulster","Nassau","Livingston","Cayuga","Chenango","Columbia","Oswego","Putnam","Greene","New York","Orange","Madison","Warren","Suffolk","Oneida","Chautauqua","Orleans","Saratoga","Schenectady","St. Lawrence","Kings","Genesee","Essex","Queens","Broome",,"Washington","Hamilton","Delaware"]
var max = 20;
var lookup = new Map();
names.forEach(function(name,i) {
lookup.set(name, max - i * max / names.length);
})
var colorScheme = d3.schemeReds[9];
colorScheme.unshift("#eee");
var color = d3.scaleQuantize()
.domain([0, 20])
.range(colorScheme);
d3.json("https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-states/NY-36-new-york-counties.json", function(topo) {
var geojson = topojson.feature(topo, topo.objects.cb_2015_new_york_county_20m);
var outline = topojson.mesh(topo, topo.objects.cb_2015_new_york_county_20m, (a, b) => a === b);
var projection = d3.geoAlbers()
.fitSize([width,height],geojson);
var path = d3.geoPath(projection);
var counties = svg.selectAll(null)
.data(geojson.features)
.enter()
.append("path")
.attr("d",path)
.attr("fill", d=> color(lookup.get(d.properties.NAME)))
var state = svg.append("path")
.attr("d", path(outline))
.attr("class","state");
})
.county-borders {
fill: none;
stroke: #fff;
stroke-width: 0.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
.state {
fill: none;
stroke: black;
stroke-dashArray: 4 6;
stroke-width: 1px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<svg width="960" height="600"></svg>

d3.js onclick and touch works erratically on topojson map

I've been playing with d3.js for sometime now and I've been trying to create a d3 map wherein clicking on different districts/counties on a state map displays details of the districts/counties next to the map.
I was initially using mouseover and mouseout to display the same, but it wasn't mobile friendly. So now I'm trying to do the same with onclick but its not working in the same way.
The district should change colour on click (it worked with mouseover). However it changes colour only after several repeated random clicks inside the district.
This is what I have done.
var width = 345,
height = 450;
var projection = d3.geoMercator()
.center([88.36, 27.58])
.translate([width / 2, height / 2])
.scale(6000);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select('#Sk_Map').append('svg')
.attr('width', width)
.attr('height', height);
var g = svg.append('g');
d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
.then(state => {
g.append('path')
.datum(topojson.merge(state, state.objects.Sikkim.geometries))
.attr('class', 'land')
.attr('d', path);
g.append('path')
.datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
.attr('class', 'boundary')
.attr('d', path);
g.append("g")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
.attr("d", path)
.attr("class","boundary")
//.on("mouseover", function(d)){
.on("click", function(d) {
var prop = d.properties;
var string = "<p><strong>District Name</strong>: " + prop.Dist_Name;
d3.select("#Place_Details")
.html("")
.append("text")
.html(string);
d3.select(this).attr("class","boundary hover");
})
//.on("mouseout"), function(d){
.on("click", function(d) {
d3.select("h2").text("");
d3.select(this).attr("class","boundary")
.attr("fill", "#ff1a75");
});
});
.columns {
float: left;
width: 50%;
}
/* Clear floats after the columns */
.mapcontainer:after {
content: "";
display: table;
clear: both;
}
svg {
background: #ffffff;
}
.land {
fill: #ff1a75;
}
.boundary {
fill: none;
stroke: #00ffff;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 1px;
vector-effect: non-scaling-stroke;
}
h2 {
top: 50px;
font-size: 1.6em;
}
.hover {
fill: yellow;
}
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script src="https://unpkg.com/topojson#3" charset="utf-8"></script>
<div id="Sk_Map" style="width: 300px; float:left; height:450px; margin:5px"></div>
<div id="Place_Details" style="width: 400px; float:right; height:450px; overflow: auto; margin:5px"></div>
How do I optimize this code? I wish to add zoom functionality to the map but for now I want to display the name of the district/county.
There are a few issues in your code.
Currently you assign two event listeners to the same selection:
g.append("g")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
...
.on("click", function(d) {
/* on click code here */
})
.on("click", function(d) {
/* on click code here too */
});
When assigning event listeners like this, the second one overwrites the first. So the only event listener in your snippet that is used is the second one:
.on("click", function(d) {
d3.select("h2").text("");
d3.select(this).attr("class","boundary")
.attr("fill", "#ff1a75");
});
As you don't have an h2 element (in the snippet at least), nothing happens.
If we drop the second event listener and use only the first, we still don't get much of an on click event. In the below, I remove the other features (the ones without click events, as well I remove unrelated css, resize for snippet, and change the feature stroke color). It should be clear why a click event doesn't work very well, the features have no fill. Click only triggers an event on the boundary:
var width = 345,
height = 300;
var projection = d3.geoMercator()
.center([88.36, 27.58])
.translate([width / 2, height / 2])
.scale(7000);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select('#Sk_Map').append('svg')
.attr('width', width)
.attr('height', height);
var g = svg.append('g');
d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
.then(state => {
g.append("g")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
.attr("d", path)
.attr("class","boundary")
.on("click", function(d) {
alert("click!");
})
});
.boundary {
fill: none;
stroke: black;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 1px;
vector-effect: non-scaling-stroke;
}
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script src="https://unpkg.com/topojson#3" charset="utf-8"></script>
<div id="Place_Details"></div>
<div id="Sk_Map" style="width: 300px; float:left; height:200px; margin:5px"></div>
The solution would be to give the features a fill. This brings us to an optimization: we don't need the first feature to be drawn:
g.append('path')
.datum(topojson.merge(state, state.objects.Sikkim.geometries))
.attr('class', 'land')
.attr('d', path);
Because it will entirely be covered by clickable features.
Also, if we want the boundaries to be not clickable, we should draw this feature:
g.append('path')
.datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
.attr('class', 'boundary')
.attr('d', path);
After we draw the clickable features, so that it is drawn on top. If we don't care if the borders are clickable, we could skip drawing this feature as we could just apply a stroke to the clickable features. Though internal borders might be somewhat thicker/darker.
Here's the above modifications:
var width = 345,
height = 300;
var projection = d3.geoMercator()
.center([88.36, 27.58])
.translate([width / 2, height / 2])
.scale(7000);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select('#Sk_Map').append('svg')
.attr('width', width)
.attr('height', height);
var g = svg.append('g');
d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
.then(state => {
g.append("g")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
.attr("d", path)
.attr("class","feature")
.on("click", function(d) {
var prop = d.properties;
var string = "<p><strong>District Name</strong>: " + prop.Dist_Name;
d3.select("#Place_Details")
.html(string)
})
g.append('path')
.datum(topojson.mesh(state, state.objects.Sikkim, (a, b) => a !== b))
.attr('class', 'boundary')
.attr('d', path);
});
.boundary {
fill: none;
stroke: #00ffff;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 1px;
vector-effect: non-scaling-stroke;
}
.feature {
fill: steelblue;
}
.hover {
fill: yellow;
}
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script src="https://unpkg.com/topojson#3" charset="utf-8"></script>
<div id="Place_Details"></div>
<div id="Sk_Map" style="width: 300px; float:left; height:200px; margin:5px"></div>
If you have a question about how to manage two click events, or alternating click events on a feature, that should be a new question.

Adding Zoom behavior to custom map points

As an exercise to learn D3, I used a dataset from a previous project on the locations and names of airports all over the world. I'm loading this into my webpage using D3.csv and plotting the points on a map using topojson.
At this point in my exercise, I'm trying to add a feature to let users zoom in & out on the world map. As you can imagine, there are a lot of airports and the map gets crowded since I haven't added any filter logic yet.
Darndest thing is, I can get the Zoom behavior to work on countries, but I'm unsure how to get it to work on the circles I've drawn. If I zoom in on my map using the scroll-wheel, the map zooms in, but the circles stay in place.
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<style type="text/css">
.feature {
fill: none;
stroke: grey;
stroke-width: 1px;
stroke-linejoin: round;
}
.mesh {
fill: none;
stroke: lightgrey;
stroke-width: 2px;
stroke-linejoin: round;
}
h1 {
font-family: sans-serif;
}
svg {
background: #eee;
}
.sphere {
fill: #fff;
}
.land {
fill: #000;
}
.boundary {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.overlay {
fill: none;
pointer-events: all;
}
circle{
fill: steelblue;
stroke-width: 1.5px;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
</head>
<body>
<h1>Lots of airports across the world</h1>
<script type="text/javascript">
var width = 950,
height = 550;
scale0 = (width - 1) / 2 / Math.PI;
var projection = d3.geo.mercator();
var zoom = d3.behavior.zoom()
.translate([width / 2, height / 2])
.scale(scale0)
.scaleExtent([scale0, 8 * scale0])
.on("zoom", zoomed);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var g = svg.append("g");
var circle = svg.append("circle");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg
.call(zoom)
.call(zoom.event);
var tip = d3.tip()
.attr("class", "d3-tip")
.offset([-10, 0])
.html(function(d) {
return "Name" + ": " + d[2] + "<br>" + "Location" + ": " + d[3];
});
svg.call(tip);
d3.json("world-110m.v1.json", function(error, world) {
if (error) throw error;
g.append("g")
.attr("d", path)
.on("click", clicked)
.on("zoom", zoomed);
g.append("path")
.datum({type: "Sphere"})
.attr("class", "sphere")
.attr("d", path);
g.append("path")
.datum(topojson.merge(world, world.objects.countries.geometries))
.attr("class", "land")
.attr("d", path);
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path)
.on("click", clicked);
d3.csv("output.csv",
function(data) {return {name: data.Airport_name, location: data.Location_served,
long : +data.Longitude, lat : +data.Latitude}},
function(data) {
var new_array = data.map(function (d) {return [d.long, d.lat, d.name, d.location]});
console.log("new", new_array)
svg.selectAll("circle")
.data(new_array)
.enter()
.append("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
.attr("r", "2px")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
});
}) //closes the json, do not move.
// begin click-zoom listeners
function clicked(d) {
console.log("d:",d)
var centroid = path.centroid(d),
translate = projection.translate();
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path").transition()
.duration(700)
.attr("d", path);
}
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
</script>
</body>
So what starts looking like this
ends looking like this upon zooming in
I'd like the circles to move as well as the countries.
CSV sample:
Airport_name,DST,IATA,ICAO,Location_served,Time,Latitude,Longitude
Anaa Airport,,AAA,NTGA,"Anaa, Tuamotus, French Polynesia",UTC?10:00,-16.9419074,-144.8646172
Arrabury Airport,,AAB,YARY,"Arrabury, Queensland, Australia",UTC+10:00,-26.7606354,141.0269959
El Arish International Airport,,AAC,HEAR,"El Arish, Egypt",UTC+02:00,31.1272509,33.8045859
Adado Airport,,AAD,,"Adado (Cadaado), Galguduud, Somolia",UTC+03:00,9.56045635,31.65343724
Rabah Bitat Airport (Les Salines Airport),,AAE,DABB,"Annaba, Algeria",UTC+01:00,36.8970249,7.7460806
Apalachicola Regional Airport,Mar-Nov,AAF,KAAF,"Apalachicola, Florida, United States",UTC?05:00,29.7258675,-84.9832278
Arapoti Airport,Oct-Feb,AAG,SSYA,"Arapoti, Paraná, Brazil",UTC?03:00,-24.1458941,-49.8228117
Merzbrück Airport,Mar-Oct,AAH,EDKA,"Aachen, North Rhine-Westphalia, Germany",UTC+01:00,50.776351,6.083862
Arraias Airport,,AAI,SWRA,"Arraias, Tocantins, Brazil",UTC?03:00,-12.9287788,-46.9437231
Your zoom function does two things, it modifies the projection and updates the paths using the modified projection:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale); // modify the projection
g.selectAll("path").attr("d", path); // update the paths
}
Ok, so in addition to modifying the paths on each zoom using the bound datum, we need to modify the circles:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale); // modify the projection
g.selectAll("path").attr("d", path); // update the paths
// update the circles/points:
svg.selectAll("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
});
}
However this doesn't quite work, we need to see how you append the circles:
svg.selectAll("circle")
.data(new_array)
.enter()
.append("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
This is great if there is no circle already on the svg - but there is, you appended one here:
var circle = svg.append("circle");
Which means that the first airport in the array won't be added as there is already a circle in the svg for that item in the data array. A null selection (d3.selectAll(null)) will ensure that an item is entered for every item in the data array.
Most importantly here, is that the first circle doesn't have a bound datum until after the data has loaded. This will cause some issues when calling the zoom, there is no bound data to use to rescale the circle and you'll get an error. Instead, you could append the airports with a class and select these during zoom events.
In my example here I've used a null selection to enter the airports, and given them a class so I can easily select the circles that I want to re position based on an updated projection. (For demonstration, I also simplified the world map and increased the point radius).
This looks like:
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
svg.selectAll(".airport")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
}
With the enter being:
svg.selectAll() // selectAll() is equivilant to selectAll(null)
.data(new_array)
.enter()
.append("circle")
.attr("class","airport")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
.attr("r", "6px")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
});

Interacting with a tooltip in d3js v4

I'm creating a proof of concept in v4 of D3JS. One of the things I'm trying to do is have a tooltip display when hovering over a data point. I found a good example of this here.
What I now need to do is add a link (or any clickable element) to the tooltip. I created a plunkr based on the example above and added a link to the tooltip. I can't click on the link and the tooltip appears to be below the line-chart as far as z-index goes.
I've tried setting the z-index on the chart and the tooltip to no avail. Can anyone point me in the right direction to sort this?
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
div.tooltip {
position: absolute;
text-align: center;
width: 150px;
height: 100px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");
var formatTime = d3.timeFormat("%e %B");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Get the data
d3.csv("data.csv", function(error, data) {
if (error) throw error;
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
});
// scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// add the dots with tooltips
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.close); })
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTime(d.date) + "<br/>" + d.close + "<br/><a href='www.google.com'>Test it</a>")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
});
// add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
});
</script>
</body>
When creating your tooltip based on the example in the link, you copied its CSS:
div.tooltip {
pointer-events: none;
...
}
The reason we generally set pointer-events to none in a <div> tooltip, as the linked example did, is that we want to get the mouseout event on the element that fired the mouseover (normally to set the tooltip's opacity to zero), and if the tooltip is positioned to close from the element (sometimes even directly over it) the pointer can hover over the div and ruin the mouseout. Besides that, another important reason to set pointer-events to none is that it allows other elements behind the tooltip to get mouseover events, just like if the tooltip was not there.
However, because in your code there is no mouseout, the easier solution here is simply eliminating the pointer-events: none in the CSS. That way the <div> get the click event.
This is the updated plunker: https://plnkr.co/edit/xfa8cjQd3tHYNu0dUla4?p=preview

Categories