Reputation: 659
I am new to D3. What I need to do:
What I have been able to achieve so far: I have the first 3 requirements down. The problem is when I click on a county it zooms into that county instead of the region.
The code I wrote is based from the following examples:
Zoom to Bounding Box
https://bl.ocks.org/mbostock/4699541
NY State with Counties Clipped
https://bl.ocks.org/gregdevs/a73f8a16f129757c037e72ecdebdd8f2
The only part of the code I created myself (and the part that I think needs to changed) is the coloring of the regions. This was done using if then else statemenents to set the following class
.attr('class', function (d) {
if (d.id == "51105" || d.id == "51169" || d.id == "51191" || d.id == "51520" || d.id == "51077" || d.id == "51035" ||
d.id == "51141" || d.id == "51089" || d.id == "51143" || d.id == "51590" || d.id == "51195" || d.id == "51051" ||
d.id == "51027" || d.id == "51167" || d.id == "51185" || d.id == "51173" || d.id == "51021" || d.id == "51197" ||
d.id == "51071" || d.id == "51590" || d.id == "51155" || d.id == "51063" || d.id == "51067" || d.id == "51121" ||
d.id == "51161" || d.id == "51770") {
return "WesternRegion";
}
else if (d.id == "51083" || d.id == "51117" || d.id == "51025" || d.id == "51081" || d.id == "51037" || d.id == "51011" ||
d.id == "51590" || d.id == "51029" || d.id == "51049" || d.id == "51145" || d.id == "51041" || d.id == "51111" ||
d.id == "51147" || d.id == "51183" || d.id == "51181" || d.id == "51007" || d.id == "51135" || d.id == "51053" ||
d.id == "51149" || d.id == "51087" || d.id == "51760") {
return "SouthernRegion";
}
else if (d.id == "51175" || d.id == "51800" || d.id == "51550" || d.id == "51810" || d.id == "51710" || d.id == "51093" ||
d.id == "51001" || d.id == "51131") {
return "EasternRegion";
}
else if (d.id == "51165" || d.id == "51171" || d.id == "51069" || d.id == "51043" || d.id == "51107" || d.id == "51059" ||
d.id == "51013" || d.id == "51510" || d.id == "51139" || d.id == "51187" || d.id == "51157" || d.id == "51061" || d.id == "51153") {
return "NorthernRegion";
}
else return "CentralRegion";
})
;
Below is the full code. In order to get it working requires downloading us.json from https://bl.ocks.org/mbostock/raw/4090846/us.json and copying it to a folder called scripts.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.outline {
stroke: #000;
stroke-width: 1.5px;
}
path {
fill: #ccc;
stroke: #fff;
stroke-width: .5px;
}
.background {
fill: none;
pointer-events: all;
}
.feature {
fill: #ccc;
cursor: pointer;
}
.county.active {
fill: orange !important;
}
.WesternRegion
{
fill:Green;
}
.EasternRegion
{
fill:Blue;
}
.SouthernRegion
{
fill:#efce43;
}
.NorthernRegion
{
fill:Purple;
}
.mesh {
fill: none;
stroke: #fff;
stroke-linecap: round;
stroke-linejoin: round;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 500;
active = d3.select(null);
var projection = d3.geo.albers()
.scale(1000)
.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);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
var g = svg.append("g")
.style("stroke-width", "1.5px");
d3.json("/Scripts/us.json", function (error, us) {
if (error) throw error;
var states = topojson.feature(us, us.objects.states),
state = states.features.filter(function (d) { return d.id === 51; })[0];
projection.scale(1)
.translate([0, 0]);
var b = path.bounds(state),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s)
.translate(t);
g.selectAll("path")
.datum(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.on("click", clicked);
g.append("path")
.datum(state)
.attr("class", "outline")
.attr("d", path)
.attr('id', 'land');
g.append("clipPath")
.attr("id", "clip-land")
.append("use")
.attr("xlink:href", "#land");
g.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.attr('countyId', function (d) {
return d.id
})
.attr("clip-path", "url(#clip-land)")
.on("click", clicked)
.attr('class', function (d) {
if (d.id == "51105" || d.id == "51169" || d.id == "51191" || d.id == "51520" || d.id == "51077" || d.id == "51035" ||
d.id == "51141" || d.id == "51089" || d.id == "51143" || d.id == "51590" || d.id == "51195" || d.id == "51051" ||
d.id == "51027" || d.id == "51167" || d.id == "51185" || d.id == "51173" || d.id == "51021" || d.id == "51197" ||
d.id == "51071" || d.id == "51590" || d.id == "51155" || d.id == "51063" || d.id == "51067" || d.id == "51121" ||
d.id == "51161" || d.id == "51770") {
return "WesternRegion";
}
else if (d.id == "51083" || d.id == "51117" || d.id == "51025" || d.id == "51081" || d.id == "51037" || d.id == "51011" ||
d.id == "51590" || d.id == "51029" || d.id == "51049" || d.id == "51145" || d.id == "51041" || d.id == "51111" ||
d.id == "51147" || d.id == "51183" || d.id == "51181" || d.id == "51007" || d.id == "51135" || d.id == "51053" ||
d.id == "51149" || d.id == "51087" || d.id == "51760") {
return "SouthernRegion";
}
else if (d.id == "51175" || d.id == "51800" || d.id == "51550" || d.id == "51810" || d.id == "51710" || d.id == "51093" ||
d.id == "51001" || d.id == "51131") {
return "EasternRegion";
}
else if (d.id == "51165" || d.id == "51171" || d.id == "51069" || d.id == "51043" || d.id == "51107" || d.id == "51059" ||
d.id == "51013" || d.id == "51510" || d.id == "51139" || d.id == "51187" || d.id == "51157" || d.id == "51061" || d.id == "51153") {
return "NorthernRegion";
}
else return "CentralRegion";
})
;
});
function clicked(d) {
// debugger;
if (d3.select(this).classed("NorthernRegion")) {
alert("You selected Northern Region");
}
else if (d3.select(this).classed("SouthernRegion")) {
alert("You selected Southern Region");
}
else if (d3.select(this).classed("EasternRegion")) {
alert("You selected Eastern Region");
}
else if (d3.select(this).classed("WesternRegion")) {
alert("You selected Western Region");
}
else if (d3.select(this).classed("CentralRegion")) {
alert("You selected Central Region");
}
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d ),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1.5px")
.attr("transform", "");
}
</script>
The problem is that it zooms to the county not the region:
Upvotes: 3
Views: 1464
Reputation: 16576
Here's my solution, which seems to work pretty well. The important changes I made were:
Something weird that I noted is that, when filtering out non-applicable counties, one applicable county kept filtering out, so I manually added it back in (gross).
var width = 960,
height = 500,
active = "";
var projection = d3.geo.albers().scale(1000).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);
svg.append("rect").attr("class", "background").attr("width", width).attr("height", height).on("click", reset);
var g = svg.append("g").style("stroke-width", "1.5px");
d3.json("scripts/us.json", function(error, us) {
if (error) throw error;
var states = topojson.feature(us, us.objects.states),
state = states.features.filter(function(d) {
return d.id === 51;
})[0];
projection.scale(1).translate([0, 0]);
var b = path.bounds(state),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s).translate(t);
g.selectAll("path").datum(topojson.mesh(us, us.objects.states, function(a, b) {
return a !== b;
})).attr("class", "mesh").attr("d", path).on("click", clicked);
g.append("path").datum(state).attr("class", "outline").attr("d", path).attr('id', 'land');
g.append("clipPath").attr("id", "clip-land").append("use").attr("xlink:href", "#land");
us.objects.counties.geometries = us.objects.counties.geometries.filter(function(county) {
return county.id >= 51000 && county.id < 52000
});
// Not sure why this one needs to be re-added
us.objects.counties.geometries.push(us.objects.counties.geometries.find(function(d) {
return d.id == 51069
}));
console.log(us.objects);
g.selectAll("path").data(topojson.feature(us, us.objects.counties).features).enter().append("path").attr("d", path).attr('countyId', function(d) {
return d.id
}).attr("clip-path", "url(#clip-land)").on("click", clicked).attr('class', function(d) {
if (d.id == "51105" || d.id == "51169" || d.id == "51191" || d.id == "51520" || d.id == "51077" || d.id == "51035" || d.id == "51141" || d.id == "51089" || d.id == "51143" || d.id == "51590" || d.id == "51195" || d.id == "51051" || d.id == "51027" || d.id == "51167" || d.id == "51185" || d.id == "51173" || d.id == "51021" || d.id == "51197" || d.id == "51071" || d.id == "51590" || d.id == "51155" || d.id == "51063" || d.id == "51067" || d.id == "51121" || d.id == "51161" || d.id == "51770") {
return "WesternRegion";
} else if (d.id == "51083" || d.id == "51117" || d.id == "51025" || d.id == "51081" || d.id == "51037" || d.id == "51011" || d.id == "51590" || d.id == "51029" || d.id == "51049" || d.id == "51145" || d.id == "51041" || d.id == "51111" || d.id == "51147" || d.id == "51183" || d.id == "51181" || d.id == "51007" || d.id == "51135" || d.id == "51053" || d.id == "51149" || d.id == "51087" || d.id == "51760") {
return "SouthernRegion";
} else if (d.id == "51175" || d.id == "51800" || d.id == "51550" || d.id == "51810" || d.id == "51710" || d.id == "51093" || d.id == "51001" || d.id == "51131") {
return "EasternRegion";
} else if (d.id == "51165" || d.id == "51171" || d.id == "51069" || d.id == "51043" || d.id == "51107" || d.id == "51059" || d.id == "51013" || d.id == "51510" || d.id == "51139" || d.id == "51187" || d.id == "51157" || d.id == "51061" || d.id == "51153") {
return "NorthernRegion";
} else {
return "CentralRegion";
}
});
});
function clicked(d) {
var selected = d3.select(this).attr('class');
var dxAll = [];
var dyAll = [];
var xAll = [];
var yAll = [];
// Iterate through all in class and find max values
d3.selectAll('.' + selected).each(function(data) {
var bounds = path.bounds(data);
dxAll.push(bounds[1][0], bounds[0][0]);
dyAll.push(bounds[1][1], bounds[0][1]);
xAll.push(bounds[0][0], bounds[1][0]);
yAll.push(bounds[0][1], bounds[1][1]);
});
dx = Math.max.apply(null, dxAll) - Math.min.apply(null, dxAll);
dy = Math.max.apply(null, dyAll) - Math.min.apply(null, dyAll);
x = (Math.max.apply(null, xAll) + Math.min.apply(null, xAll)) / 2;
y = (Math.max.apply(null, yAll) + Math.min.apply(null, yAll)) / 2;
if (active === selected) return reset();
active = selected;
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition().duration(750).style("stroke-width", 1.5 / scale + "px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active = "";
g.transition().duration(750).style("stroke-width", "1.5px").attr("transform", "");
}
Upvotes: 2
Reputation: 102194
First of all, you don't need those big if...else
statements. They could be hugely simplified. For instance, to get the class inside the clicked
function:
var thisClass = d3.select(this).attr("class");
Back to your question.
An solution is getting all the paths with the class of the clicked element and pushing their bounds inside an array:
var allBounds = [];
var allPaths = d3.selectAll("path." + thisClass).each(function(d) {
allBounds.push(path.bounds(d))
});
Then calculate the corners of all those bounds:
var bound0 = d3.min(allBounds, function(d) {
return d[0][0]
});
var bound1 = d3.min(allBounds, function(d) {
return d[0][1]
});
var bound2 = d3.max(allBounds, function(d) {
return d[1][0]
});
var bound3 = d3.max(allBounds, function(d) {
return d[1][1]
});
var bounds = path.bounds(d),
dx = bound2 - bound0,
dy = bound3 - bound1,
x = (bound0 + bound2) / 2,
y = (bound1 + bound3) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
Here is the updated bl.ocks: https://bl.ocks.org/anonymous/3e473b01de29cb7a3c0a6d8807b8b247/f6675e001dc7dcdb7ffd4c437944bb3233b417ca
PS: If you click the central region (the gray area), it will not work. The reason is simple: in your code, you are setting the class centralRegion
to all paths without the given IDs in your if...else
statement. You have to change that.
PPS: You'll have to refactor the code to set the active
class to all the counties.
Upvotes: 1