d3.js onclick and touch works erratically on topojson map - javascript

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.

Related

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>

Problem selecting element by id using d3 js

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/

d3.js hover on map not displaying text box, css problem

I've just started learning javascript and css and recently I've been playing around with d3.js. I'm trying to display a map of a state. The map is getting displayed. I've increased the line width of the district boundaries, but I couldn't do so for the outer boundary of the state.
Also, on move hover on each district of the state, I've been trying to a text box on the side, which is not happening. Mouse hover also changes the color of the district as well as the line width of the boundary. Why is the text box not appearing? Or is it appearing somewhere out of screen? Where am I going wrong and how do I fix these?
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="//unpkg.com/topojson#3"></script>
<script>
var width = 500,
height = 500;
const projection = d3.geoMercator()
.center([88.4, 27.5])
.translate([width / 2, height / 2])
.scale(10000);
const path = d3.geoPath()
.projection(projection);
const svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
.then(state => {
g.append('path')
.datum({
type: 'Sphere'
})
.attr('class', 'sphere')
.attr('d', path);
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")
.attr("id", "dts")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
.attr("d", path)
//.on("click", clicked)
.on("mouseover", function(d) {
d3.select("h2").text(d.properties.Dist_Name);
d3.select(this)
.attr("fill", "yellow")
.attr("opacity", 1);
var prop = d.properties;
var string = "<p><strong>District Code</strong>: " + prop.State_Code + "</p>";
string += "<p><strong>Disctrict Name</strong>: " + prop.Dist_Name + "</p>";
d3.select("#textbox")
.html("")
.append("text")
.html(string)
})
.on("mouseout", function(d) {
d3.select(this)
.attr("fill", "deeppink")
})
.attr("opacity", 0)
.attr("fill", "deeppink")
});
</script>
Here is the css part:
svg {
background: #eeeeee;
}
.land {
fill: #ff1a75;
}
.boundary {
fill: none;
stroke: #00ffff;
stroke-width: 2px;
}
h2 {
top: 50px;
font-size: 1.6em;
}
.hover {
fill: yellow;
}
#textbox {
position: absolute;
top: 600px;
left: 50px;
width: 275px;
height: 100px;
}
#textbox text p {
font-family: Arial, sans-serif;
font-size: 0.8em;
margin: 0;
}
Try to move your script below #textbox element.
svg {
background: #eeeeee;
}
.land {
fill: #ff1a75;
}
.boundary {
fill: none;
stroke: #00ffff;
stroke-width: 2px;
}
h2 {
top: 50px;
font-size: 1.6em;
}
.hover {
fill: yellow;
}
#textbox {
position: absolute;
top: 600px;
left: 50px;
width: 275px;
height: 100px;
}
#textbox text p {
font-family: Arial, sans-serif;
font-size: 0.8em;
margin: 0;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="textbox"></div>
<!-- move your JS code here -->
<script>
var width = 500,
height = 500;
const projection = d3.geoMercator()
.center([88.4, 27.5])
.translate([width / 2, height / 2])
.scale(10000);
const path = d3.geoPath()
.projection(projection);
const svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
d3.json('https://raw.githubusercontent.com/shklnrj/IndiaStateTopojsonFiles/master/Sikkim.topojson')
.then(state => {
g.append('path')
.datum({
type: 'Sphere'
})
.attr('class', 'sphere')
.attr('d', path);
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")
.attr("id", "dts")
.selectAll("path")
.data(topojson.feature(state, state.objects.Sikkim).features)
.enter()
.append("path")
.attr("d", path)
//.on("click", clicked)
.on("mouseover", function(d) {
d3.select("h2").text(d.properties.Dist_Name);
d3.select(this)
.attr("fill", "yellow")
.attr("opacity", 1);
var prop = d.properties;
var string = "<p><strong>District Code</strong>: " + prop.State_Code + "</p>";
string += "<p><strong>Disctrict Name</strong>: " + prop.Dist_Name + "</p>";
d3.select("#textbox")
.html("")
.append("text")
.html(string)
})
.on("mouseout", function(d) {
d3.select(this)
.attr("fill", "deeppink")
})
.attr("opacity", 0)
.attr("fill", "deeppink")
});
</script>

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);
});

d3.js Map not Showing

I have been trying to create a basic map using d3.js with a world shapefile I downloaded (ne_50m_admin_0_countries). I have been trying for hours with all sorts of different code to try and get it to display, but I always end up with a blank HTML page. I am a new to d3.js and am pretty amateur. Here is the code I have been trying to get to work, it is from http://bl.ocks.org/mbostock/4180634
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background: #fcfcfa;
}
.stroke {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.fill {
fill: #fff;
}
.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .5;
}
.land {
fill: #222;
}
.boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 580;
var color = d3.scale.category10();
var projection = d3.geo.kavrayskiy7()
.scale(170)
.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("defs").append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
svg.append("use")
.attr("class", "stroke")
.attr("xlink:href", "#sphere");
svg.append("use")
.attr("class", "fill")
.attr("xlink:href", "#sphere");
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-110.json", function(error, name) {
if (error) throw error;
var countries = topojson.feature(name, world.objects.countries).features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
svg.selectAll(".country")
.data(name)
.enter().insert("path", ".graticule")
.attr("class", "country")
.attr("d", path)
.style("fill", function(d, i) { return color(d.color = d3.max(neighbors[i], function(n) { return countries[n].color; }) + 1 | 0); });
svg.insert("path", ".graticule")
.datum(topojson.mesh(name, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
d3.select(self.frameElement).style("height", height + "px");
</script>
</body>
</html>
It seems like my JSON isn't working for any of them, but I saved it properly through QGIS. Here is an excerpt of it:
{"type":"Topology","transform":{"scale":[0.03600360036003601,0.017366249624962495],"translate":[-180,-90]},"objects":{"land":{"type":"MultiPolygon","arcs":[[[0]],[[1]],[[2]],[[3]],[[4]],[[5]],[[6]],[[
Any guidance as to what I may be doing wrong would be greatly appreciated.
Try to copy the code again from the original website.
The code in your post differs in several lines from the one on http://bl.ocks.org/mbostock/4180634
E.g. you are using parameter name instead of world but are still trying to access a world variable which must be undefined:
// YOUR CODE:
d3.json("world-110.json", function(error, name) {
if (error) throw error;
var countries = topojson.feature(name, world.objects.countries).features, // world is UNDEFINED here
neighbors = topojson.neighbors(world.objects.countries.geometries);
-----------------------------------------------------
// ORIGINAL:
d3.json("./world-50m.json", function(error, world) {
if (error) throw error;
var countries = topojson.feature(world, world.objects.countries).features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
I just copied the code from the website and the according json from http://bl.ocks.org/mbostock/raw/4090846/world-50m.json and it is working fine and showing the countries.

Categories