Reputation: 1845
I'm pretty new to D3 so I'm sorry if this is redundant with other posts. I'm attempting to make a map with points that the user selects by clicking on a barplot. Currently I have a map of the world, and the points enter and exit based on the clicked bar. I was wondering how to also include a zoom effect that would crop the globe to only include the point area of the selected bar. For instance: the first bar should result in a map of the United States of America.
var data = [{
"name": "Apples",
"value": 20,
"latlong": [
{"latitude": 32.043478, "longitude": -110.7851017},
{"latitude": 40.49, "longitude": -106.83},
{"latitude": 39.1960652, "longitude": -120.2384172},
{"latitude": 36.137076, "longitude": -81.183722},
{"latitude": 35.1380976, "longitude": -90.0611644},
{"latitude": 33.76875, "longitude": -84.376217},
{"latitude": 32.867153, "longitude": -79.9678813},
{"latitude": 39.61078, "longitude": -78.79830099999},
{"latitude": 40.8925, "longitude": -89.5074},
{"latitude": 44.1862, "longitude": -85.8031},
{"latitude": 35.48759154, "longitude": -86.10236359},
{"latitude": 37.9342807, "longitude": -107.8073787999},
{"latitude": 41.3530864, "longitude": -75.6848074},
{"latitude": 38.423137, "longitude": -80.021118},
{"latitude": 43.5121, "longitude": -72.4021},
{"latitude": 48.4070083, "longitude": -114.2827366}
]
},
{
"name": "Oranges",
"value": 26,
"latlong": [
{"latitude": -36.8506158, "longitude": 174.7678785},
{"latitude": -27.4510454, "longitude": 153.0319808},
{"latitude": -33.867111, "longitude": 151.217941},
{"latitude": -34.8450381, "longitude": 138.4985548},
{"latitude": -37.7928386, "longitude": 144.9051327},
{"latitude": -32.0582947, "longitude": 115.7460244},
{"latitude": 29.934926599999, "longitude": -90.0615085},
{"latitude": -34.4829472, "longitude": -58.518548},
{"latitude": -33.460464, "longitude": -70.656868},
{"latitude": 4.8007362, "longitude": -74.0373992},
{"latitude": 4.9375556, "longitude": -73.9649426},
{"latitude": -23.701185, "longitude": -46.7001431},
{"latitude": 33.678023, "longitude": -116.23754},
{"latitude": 51.8451208, "longitude": 5.6872547},
{"latitude": 40.3688321, "longitude": -3.6866294},
{"latitude": 40.4817271, "longitude": -3.6330802},
{"latitude": 40.4642, "longitude": -3.6131},
{"latitude": 52.327353537, "longitude": 1.67658117421},
]
}
];
//set up svg using margin conventions - we'll need plenty of room on the left for labels
var margin = {
top: 15,
right: 25,
bottom: 15,
left: 60
};
var width = 400 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("#graphic").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scale.linear()
.range([0, width])
.domain([0, d3.max(data, function (d) {
return d.value;
})]);
var y = d3.scale.ordinal()
.rangeRoundBands([height, 0], .1)
.domain(data.map(function (d) {
return d.name;
}));
//make y axis to show bar names
var yAxis = d3.svg.axis()
.scale(y)
//no tick marks
.tickSize(0)
.orient("left");
var gy = svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var bars = svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
//append rects
bars.append("rect")
.attr("class", "bar")
.attr("y", function (d) {
return y(d.name);
})
.attr("height", y.rangeBand())
.attr("x", 0)
.attr("width", function (d) {
return x(d.value);
})
.on("click", function (d) {
// d3.select("#chart circle")
// .attr("fill", function () { return "rgb(0, 0, " + Math.round(d.key * 10) + ")"; });
d3.selectAll('rect').style('fill', '#5f89ad');
d3.select(this).style("fill", "#012B4E");
var circle = svg_map.select("g").selectAll("circle")
.data(d.latlong);
circle.exit().remove();//remove unneeded circles
circle.enter().append("circle")
.attr("r",0);//create any new circles needed
//update all circles to new positions
circle.transition()
.duration(500)
.attr("cx", function(d){ return projection([d.longitude, d.latitude])[0] })
.attr("cy", function(d){ return projection([d.longitude, d.latitude])[1] })
.attr("r", 7)
.attr("class", "circle")
.style("fill", "#012B4E")
.attr("stroke", "#012B4E")
.style("opacity", 0.7)
.attr("r", 4)
})
/////////////////////// WORLD MAP ////////////////////////////
var width = window.innerWidth,
height = window.innerHeight,
centered,
clicked_point;
var projection = d3.geoMercator()
// .translate([width / 2.2, height / 1.5]);
var plane_path = d3.geoPath()
.projection(projection);
var svg_map = d3.select("#graphic").append("svg")
.attr("width", 900)
.attr("height", 550)
.attr("class", "map");
var g = svg_map.append("g");
var path = d3.geoPath()
.projection(projection);
// load and display the World
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, topology) {
g.selectAll("path")
.data(topojson.feature(topology, topology.objects.countries)
.features)
.enter()
.append("path")
.attr("d", path)
;
});
path {
stroke: #2296F3;
stroke-width: 0.25px;
fill: #f8f8ff;
}
body {
font-family: "Arial", sans-serif;
}
.bar {
fill: #5f89ad;
}
.axis {
font-size: 13px;
}
.axis path,
.axis line {
fill: none;
display: none;
}
.label {
font-size: 13px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Simple Bar chart</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.13/d3.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-geo.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
</head>
<body>
<div id="graphic"></div>
</body>
</html>
Upvotes: 3
Views: 1119
Reputation: 1157
This function does basically what you want. The use case is slightly different, as it is called after a map is redrawn with new data, but the result is the same. A selection of data appears in the map, and the map is zoomed to the extents of the dots rendered on it.
recenter() {
console.log('recentering')
try {
// get a handle to the transform object, and reset it to the
// zoom identity (resets to zoom out all the way)
let ztrans = this.zoom.transform
let t = this.d3.zoomIdentity
this.svg.transition().duration(DURATION*2).call(ztrans,t)
// get the object and measurements to determine the virtual
// "bounding" rectangle around the directional extremes of sites
// i.e., north (topmost), east (rightmost), etc
let circles = this.g.selectAll('a > circle')
let data = circles.data()
let long = data.map(o => parseFloat(o.Longitude))
let lat = data.map(o => parseFloat(o.Latitude))
let rightmost = this.d3.max(long)
let leftmost = this.d3.min(long)
let topmost = this.d3.max(lat)
let bottommost = this.d3.min(lat)
// convert the lat/long to points in the projection
let lt = this.projection([leftmost,topmost])
let rb = this.projection([rightmost,bottommost])
// calc the gaps (east - west, south - north) in pixels
let g = rb[0]-lt[0]
let gh = rb[1]-lt[1]
// get the dimensions of the panel in which the map sits
let w = this.svg.node().parentElement.getBoundingClientRect().width
let h = this.svg.node().parentElement.getBoundingClientRect().height
// the goal here is to move the halfway point between leftmost and rightmost
// on the projection sites to the halfway point of the panel in which the
// svg element resides, and same for 'y'
// the scale is the value 90% of the scale factor of either east-west to width
// or south-north to height, whichever is greater
let neoScale = 0.9 / Math.max(g/w, gh/h)
// now recalculate what will be the difference between the current
// center and the center of the scaled virtual rectangle
// this finds the difference between the centers
// the new center of the scaled rectangle is the average of the left and right
// or top and bottom points
let neoX = w/2 - (neoScale * ((lt[0]+rb[0])/2))
let neoY = h/2 - (neoScale * ((lt[1]+rb[1])/2))
// TRANSLATE FIRST! then scale.
t = this.d3.zoomIdentity.translate(neoX,neoY).scale(neoScale)
this.svg.transition().duration(DURATION*2).call(ztrans, t)
}
catch(e)
{
console.log(e)
}
UPDATE
I forked the op's fiddle, and created a working fiddle with the following changes:
recenter
function as depicted above was originally written in d3 v4. The op's fiddle was in v3. The new fiddle uses v4.recenter
function above was copied and pasted "as is" from another project. It is modified in the fiddle to account for local variable names and scopes.Also, the dots don't render on first click of a bar, but they do on subsequent clicks.
Upvotes: 4