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);
});
Related
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>
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/
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.
I have a map created from a geojson using d3.js lib and I colored randomly different states of map. Now I want to get color of a state when I hover it in mouseover function :
var lastColor;
function mouseover(d) {
lastColor = d.color; //This code is not works for me
d3.select(this)
.style('fill', 'orange')
.style('cursor', 'pointer');
}
function mouseout(d) {
d3.select(this)
.style('fill', lastColor);
}
Is it possible to get the color from d so that I return to this color when I mouseout from this state ?
In the on function, this refers to the DOM element. So, if you set the colour using style, you can get the same colour using style as a getter:
.on('mouseover', function(d){
console.log(d3.select(this).style("fill"))//'style' as a getter
});
Check this demo, hovering over the states (I set the colours using Math.random()):
var width = 720,
height = 375;
var colorScale = d3.scale.category20();
var projection = d3.geo.albersUsa()
.scale(800)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("https://dl.dropboxusercontent.com/u/232969/cnn/us.json", function(error, us) {
svg.selectAll(".state")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.style('fill', function(d) {
return colorScale(Math.random() * 20)
})
.attr('class', 'state')
.on('mouseover', function(d) {
console.log(d3.select(this).style("fill"))
});
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) {
return a !== b;
}))
.attr("d", path)
.attr("class", "state-boundary");
});
.land {
fill: #222;
}
.county-boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
.state-boundary {
fill: none;
stroke: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
EDIT: I have to confess that I read only the title of your question when you first posted it ("How to find the color of a state in mouseover"). Now, after properly reading the text of your post, I reckon the solution is even easier (btw, "Preserve the color of a state" is indeed a better title to the question).
If you set the colour using any property in the data (let's say, id):
.style('fill', function(d){
return colorScale(d.id)
})
You can simply set it again in the "mouseout":
.on('mouseover', function(d) {
d3.select(this).style("fill", "orange")
}).on("mouseout", function(d) {
d3.select(this).style('fill', function(d) {
return colorScale(d.id)
})
});
Check this other demo:
var width = 720,
height = 375;
var colorScale = d3.scale.category20();
var projection = d3.geo.albersUsa()
.scale(800)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("https://dl.dropboxusercontent.com/u/232969/cnn/us.json", function(error, us) {
svg.selectAll(".state")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.style('fill', function(d){
return colorScale(d.id)
})
.attr('class', 'state')
.on('mouseover', function(d){
d3.select(this).style("fill", "orange")
}).on("mouseout", function(d){
d3.select(this).style('fill', function(d){
return colorScale(d.id)
})});
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) {
return a !== b;
}))
.attr("d", path)
.attr("class", "state-boundary");
});
.land {
fill: #222;
}
.county-boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
.state-boundary {
fill: none;
stroke: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
I am using this example in D3.js as my start point and I wanted to do the same thing as in this example. With the help of stackoverflow I could already learn alot about D3 but now I'm stuck with a problem I don't understand. My code is on this repo because I don't know yet (?) how to share with you my files without wasting to much space. I get an error when I move the slider back and forth. The movement to the right will spawn points but they won't disappear when the slider is moved to the left what they are doing in the example. Also I am not sure if they always spawn the same amount points if I arrive at the end of the timeline. This image shows, when I click to end at once
and this screenshot shows when i click/slide to end of the timeline. In my unerstanding it should always spawn the same amount
<!DOCTYPE html>
<head>
<title>D3 Mapping Timeline</title>
<meta charset="utf-8">
<link rel="stylesheet" href="d3.slider.css" />
<style>
path {
fill: none;
stroke: #333;
stroke-width: .5px;
}
.land-boundary {
stroke-width: 1px;
}
.county-boundary {
stroke: #ddd;
}
.site {
opacity: 0.2;
fill: #9cf;
}
#slider3 {
margin: 20px 0 10px 20px;
width: 900px;
}
svg {
background: #eee;
}
.sphere {
fill: rgb(92, 136, 255)
}
.land {
fill: rgb(255, 239, 204)
}
.incident{
fill:#07f5e7;
opacity: 0.3;
}
.boundary {
fill: none;
stroke: rgb(224, 91, 49);
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.state {
fill: #000;
}
.city{
fill: #de1ae8;
}
.overlay {
fill: none;
pointer-events: all;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="d3.slider.js"></script>
</head>
<body>
<div id="slider3"></div>
<script>
var width = 1240,
height = 720;
var projection = d3.geo.mercator()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.PI);
var zoom = d3.behavior.zoom()
.scaleExtent([3, 77])
.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 sites = svg.append("g");
svg.call(zoom)
.call(zoom.event);
d3.json("countries.topo.json", function(error, world) {
if (error) throw error;
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);
//_______________________________________________________________________________________________________________________________________
//________________________________________________________________________________________________________________________________________
d3.json("germany.topo.json", function(error, ger){
if (error) throw error;
var states = topojson.feature(ger, ger.objects.states),
cities = topojson.feature(ger, ger.objects.cities );
g.selectAll(".states")
.data(states.features)
.enter()
.append("path")
.attr("class", "state")
.attr("class", function(d) { return "state " + d.id; })
.attr("d", path);
g.append("path")
.datum(cities)
.attr("d", path.pointRadius('0.05'))
.attr("class", "city");
});
});
function zoomed() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
sites.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
d3.select(self.frameElement).style("height", height + "px");
d3.json("https://raw.githubusercontent.com/RitterLean/Slider-geojson-testing/master/vorfaelle.json", function(error, data){
console.log(data.features[1].geometry.coordinates, "sad");
window.site_data = data;
});
var displaySites = function(data) {
//console.log(data)
sites.selectAll(".site")
.data(data)
.enter()
.append("circle")
.attr("class", "site")
.attr("cx", function(d) {
var p = projection(d.geometry.coordinates);
return p[0];
})
.attr("cy", function(d) {
var p = projection(d.geometry.coordinates);
return p[1]
})
.attr("r", 0)
.transition().duration(400)
.attr("r", 0.23);
// "".attr""
sites.exit()
.transition().duration(200)
.attr("r",0)
.remove();
};
var dateParser = d3.time.format("%d.%m.%Y").parse;
var minDate = dateParser("01.01.2015");
var maxDate = dateParser("31.12.2015");
console.log(minDate);
var secondsInDay = 60 * 60 * 24;
d3.select('#slider3').call(d3.slider()
.axis(true).min(minDate).max(maxDate)
.on("slide", function(evt, value) {
newData = site_data.features.filter(function(d){
return dateParser(d.properties.date) < new Date(value);
});
console.log("New set size ", newData.length);
displaySites(newData);
})
);
</script>
</body>
Why is the slider not working properly?
The problem with the slider was:
That you are calling the exit function on selection like this:
sites.exit()
.transition().duration(200)
.attr("r",0)
.remove();
But it should have been like this:
sites.selectAll(".site")
.data(data).exit()//remove the selection which are to be removed from dataset
.transition().duration(200)
.attr("r",0)
.remove();
Working code here
Hope this helps!