d3 US Map zoom on load to marked state - javascript

US map with d3.v3 using Mike Bostock's example:
I want the map to zoom into the marked locations initially when the page loads but the entire map should be rendered so that a user can zoom out if he wants to.
var w = 300;
var h = 280;
//Define map projection
var projection = d3.geo.albersUsa()
.translate([w/2, h/2])
.scale([300]);
//Define path generator
var path = d3.geo.path()
.projection(projection);
//Create SVG element
var svg = d3.select("#map1").append("svg")
.attr("width", w)
.attr("height", h)
var g = svg.append("g");
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "1000")
.style('opacity', 0)
.style("font-family", "sans-serif")
.style("background-color", "white")
.style("border-radius", "5px")
.style("padding", "10px")
.style('color', '#000')
.style("font-size", "12px");
//Load in GeoJSON data
d3.json("us-states.json", function(json) {
d3.csv("cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.longi, d.lati])[0];
})
.attr("cy", function(d) {
return projection([d.longi, d.lati])[1];
})
.attr("r", 4)
.style("fill", "#4F6D88")
.on("mouseover", function(d){
tooltip.transition().style("opacity", 0.9)
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY) + 'px')
.text(d.city)
})
.on("mousemove", function(event){
tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");
})
.on("mouseout", function(){
tooltip.transition().delay(500).style("opacity", 0);
});
});
//Bind data and create one path per GeoJSON feature
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path);
});
var zoom = d3.behavior.zoom()
.scaleExtent([1, 50])
.on("zoom", function() {
var e = d3.event,
tx = Math.min(0, Math.max(e.translate[0], w - w * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], h - h * e.scale));
zoom.translate([tx, ty]);
g.attr("transform", [
"translate(" + [tx, ty] + ")",
"scale(" + e.scale + ")"
].join(" "));
});
svg.call(zoom)
I have the code to zoom in with scroll which i have pasted above but i want it to zoom on load to those specific locations. How i want it to be:

There are two primary ways to zoom a map in d3:
modify the projection which will re-draw the paths, or
modify the drawn paths with scale and transform.
Modifying the projection is easiest in d3v4 with fitSize or fitExtent - though you would need to turn your points into geojson. You can also manually calculate the translate and scale values to update a projection (see this answer by Mike Bostock which explains this common d3v3 approach).
Alternatively, you can modify the drawn paths by calling the zoom function - this question asked yesterday has an excellent example of doing so (in d3v4). Or you can calculate and apply the zoom manually and then update the zoom to indicate the current scale and translate. I'll use the common method of modifying a d3v3 projection mentioned above (with Mike's answer) and apply it to the transform on the paths - rather than modifying the projection. Though it should not be difficult to see how my answer could be changed to modify the projection instead.
First you need to determine the maximum difference between the x and y coordinates of your points. If dealing with two points, this will be fairly easy:
var data = [[-100,45],[-110,45]];
var p1 = projection(data[0]);
var p2 = projection(data[1]);
var dx = Math.abs(p1[0] - p2[0]);
var dy = Math.abs(p1[1] - p2[1]);
I'm assuming a simple data format for the sake of a shorter answer. Also, if dealing with many points, this would be a bit more complex. One potential option would be to place your points in geojson and get the bounding box of the points.
Now we need to find out the centroid of the points - in the case of two points this is just the average of the x and y values:
var x = (p1[0] + p2[0])/2;
var y = (p1[1] + p2[1])/2;
Next we need to calculate a new scale, while also determining if the scale is restricted by the difference in x values of the coordinates or the difference in y values of the coordinates:
var scale = 0.9 / Math.max( dx/w , dy/h );
The 0.9 reduces the scale slightly, it is the same as 0.9 * scale and allows a variable amount of margin. The value returned by dx/w is one over the scale value we need to stretch the difference across the width of the svg container.
(it would probably make more sense written like: var scale = 0.9 * Math.min(w/dx,h/dy); - we want to limit the zoom by the lowest scale value and multiply it by some percentage to give margins. But the other representation is ubiquitous in online examples)
Now we have a scale, we only need to determine a translate. To do so we find out how far we need to re-position the values held in the x and y variables so that those values would be centered:
var translate = [w/2 - scale * x, h/2-scale*y];
Now you can set the initial scale and translate of the map:
g.attr("transform", "translate("+translate+")scale("+scale+")");
But, you probably want to update the zoom parameters on page load to reflect the initial zoom and translate:
zoom.translate(translate);
zoom.scale(scale);
This way when you zoom in or out from the initial view, the change is relative to your initial zoom.
Now all you have to do is include the above code when you add the points. Note that this technique might require some modification if you want to return to the initial position.

Related

how do you interpret the d3 event.x and event.y coordinates?

I'm trying to implement drag events in d3 (v6.2), and I'm having some trouble interpreting the d3 x and y coordinates. Consider the following code. When I inspect the console output, it seems to me like, in the drag handler:
event.x and event.y are the SUM of the user (graph) coordinate object locations and the total movement/distance in SVG coordinates?
event.dx and event.dy are indicator that tell you whether since the last update, you've moved left/up (-1), not moved (0), or right/down (1)?
event.subject.x and event.subject.y give the location of the object being dragged?
if I want the current coordinates of the drag (either in user/graph coordinates or SVG coordinates), I need to calculate them myself (see example in code, which seems to work)?
I can't find where the specifics of this are documented. My questions are:
Are the above impressions correct?
If not correct, what's the best way to get the current drag coordinates?
If correct, why would one SUM the values from two different coordinate systems? That doesn't make sense to me.
<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
<style>
circle.circ {
fill: red;
stroke: black;
stroke-width: 1px;
}
</style>
<script>
function dragHandler(e, d){
const objx = scaleX.invert(e.x - d.x + scaleX(d.x))
const objy = scaleY.invert(e.y - d.y + scaleY(d.y))
console.log(`x: ${e.x}, dx: ${e.dx} sx: ${e.subject.x} objx: ${objx}\ny: ${e.y} dy: ${e.dy} sy: ${e.subject.y} objy: ${objy}`)
}
var drag = d3
.drag()
.on('drag', dragHandler)
var x = [{x: 150, y: 150}]
var svg = d3.select("svg")
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
}
var width = svg.attr("width") - margin.left - margin.right
var height = svg.attr("height") - margin.top - margin.bottom
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var scaleX = d3.scaleLinear().range([0, width]).domain([140, 160])
var scaleY = d3.scaleLinear().range([height, 0]).domain([140, 160])
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(scaleX));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(scaleY))
var circ = g.selectAll(".circ")
.data(x)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) { return scaleX(d.x) })
.attr("cy", function(d) { return scaleY(d.y) })
.attr("class", "circ")
.call(drag);
</script>
</body>
<html>
Your impression is right. You can either
compute the distance in local coordinates as the difference between event.x/y and event.subject.x/y
retrieve the screen distance with event.clickDistance() as a getter
These two work in different coordinate systems.
The client coordinate system is that of the browser viewport. They are just read from the native DOM mousemove event.
The user coordinate system is the one of the thing being dragged, using the same coordinates as those used when writing SVG markup. They are computed from the client coordinates with a transform matrix the SVG API provides. The source code is here.
What you did was the in-between way that normally is not needed: by applying the scaleX/Y transform, you computed the coordinates in the system of the SVG viewBox, i. e. the coordinate system that is found when the viewBox is applied to the outermost <svg> element. That can be the client coordinate system, but only if the attribute is not set, or the width and height values of the element match the contents of the viewBox.
In almost every practical use case, the two will differ. The "Scalable" in SVG is just about that distinction. Leaving out the viewBox like you did deprives you of the possibility to easily scale your grafic as a whole.
Adding to #ccprog's answer above ( that worked for me ). It is also useful to add .container(selection.node) to your drag object. This makes the coordinates work well. Pay attention not to pass the selection but the selection.node() to .container(...).

hide circles on Orthographic drag

I've created a globe which has circles and a drag. The problem is that the circles appear on the far side of the globe. I would like those circles to be hidden.
My bl.ock can be found here:
http://bl.ocks.org/anonymous/dc2d4fc810550586d40d4b1ce9088422/40c6e199a5be4e152c0bd94a13ea94eba41f004b
For example, I would like my globe to function like this one: https://bl.ocks.org/larsvers/f8efeabf480244d59001310f70815b4e
I've seen solutions such as this one: How to move points in an orthogonal map? but it doesn't quite work for me. The points simply disappear, as d[0] and d[1] seem to be undefined.
I've also tried using methods such as this: http://blockbuilder.org/tlfrd/df1f1f705c7940a6a7c0dca47041fec8 but that also doesn't seem to work. The problem here seems to be that he is using the json as his data, while my circles data are independent of the json.
Only similar example I've found is the one: https://bl.ocks.org/curran/115407b42ef85b0758595d05c825b346 from Curran but I don't really understand his code. His method is quite different than mine.
Here is my JavaScript code:
(function(){
var h = 600;
var w = 900;
var i = 0;
var map = void 0;
var world = void 0;
var US = void 0;
var margin = {
top: 10,
bottom: 40,
left: 0,
right: 30
};
var circleScale = d3.scaleSqrt()
.domain([0, 4445])
.range([0.5, 10])
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var dragging = function(d){
var c = projection.rotate();
projection.rotate([c[0] + d3.event.dx/6, c[1] - d3.event.dy/6])
map.selectAll('path').attr('d', path);
map.selectAll(".circles").attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
}
var drag = d3.drag()
.on("drag", dragging)
var projection = d3.geoOrthographic().clipAngle(90);
var path = d3.geoPath().projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", w)
.attr("height", h)
d3.json("world.json", function(json){
d3.csv("arms_transfer_2012_2016_top - arms_transfer_2012_2016_top.csv", function(error, data){
var countries = topojson.feature(json, json.objects.countries).features
var US = countries[168]
map = svg.append('g').attr('class', 'boundary');
world = map.selectAll('path').data(countries);
US = map.selectAll('.US').data([US]);
Circles = map.selectAll(".circles").data(data)
console.log(countries[168])
world.enter()
.append("path")
.attr("class", "boundary")
.attr("d", path)
US.enter()
.append("path")
.attr("class", "US")
.attr("d", path)
.style("fill", "lightyellow")
.style("stroke", "orange")
Circles.enter()
.append("circle")
.attr("class", "circles")
.attr("r", function(d){
return circleScale(d.Millions)
})
.attr("cx", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[0];
})
.attr("cy", function(d){
var coords = projection([d.Longitude_imp, d.Latitude_imp])
return coords[1];
})
.style("fill", "#cd0d0e")
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h)
.call(drag)
})
})
})();
There are a few different methods to achieve this, but one of the easier methods would be to calculate the angular distance between the projection centroid (as determined by the rotation) and the circle center on the drag event:
map.selectAll("circle")
.style("display", function(d) {
var circle = [d.Longitude_imp, d.Latitude_imp];
var rotate = projection.rotate(); // antipode of actual rotational center.
var center = [-rotate[0], -rotate[1]]
var distance = d3.geoDistance(circle,center);
return (distance > Math.PI/2 ) ? 'none' : 'inline';
})
Take the center of each point and get the rotational center with projection.rotate() - note that the rotation values are inverse of the centering point. A rotation of [10,-20] centers the map at [-10,20], you move the map under you. With these two points we can use d3.geoDistance() which calculates the distance between two points in radians, hence the use of Math.PI/2 - which gives us points outside of 90 degrees, for these we hide, for the rest we show.
This can be incorporated a little nicer into your code, but I keep it separate here to show what is happening clearer.
Here's an example block - drag to trigger, I haven't applied the logic to the initial load.
An alternative approach, as noted by Gerardo Furtado, would be to use a path to display the circles - using path.pointRadius to set the size of the circle for each point. Instead of appending a circle, you could append path with the following format:
Circles.enter()
.append("path")
.attr("class", "circles")
.attr("d",createGeojsonPoint)
The, on update/drag:
map.selectAll('.circles').attr('d',createGeojsonPoint);
This method uses the clip angle of the orthographic to hide features when they are more than 90 degrees from the center of the projection (as determined by rotation). Your createGeojsonPoint function needs to set the radius and return a valid geojson object:
var createGeojsonPoint = function(d) {
console.log(d);
path.pointRadius(circleScale(d.Millions)); // set point radius
return path({"type":"Point","coordinates":[d.Longitude_imp,d.Latitude_imp]}) // create geojson point, return path data
}
All together, with the necessary modifications, your code might look like this.

How to position links at specific radius in a D3 Radial Reingold–Tilford Tree?

I'm using the code from Mike Bostock's Radial Reingold–Tilford Tree pretty much as the example, but would like to control the position (just the radius) of the nodes according to a variable contained in each element of the JSON data.
I can do this for the nodes like this (using a linear scale on the variable I mentioned):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + scale(d.variable) + ")"; })
But I can't figure out how the start and end positions of the links (which need to correspond with the nodes) are being controlled by the diagonal's default projection:
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
Can anyone explain how this projection works, and what I should do to modify it to make the links start and end at a specific radius?

How to apply d3.js svg clipping after zooming

I am trying to use an svg-clippath with d3.js and the zoom behaviour.
The following code creates a rectangle, which will then be clipped by a rectangualar clipping region.
<svg class="chart"></svg>
<script>
var width = 800;
var height = 600;
var svg = d3.select(".chart")
.attr("width", width)
.attr("height", height)
.append("g");
var clip = svg.append("defs")
.append("clipPath")
.attr("id","clip")
.append("rect")
.attr("width",200)
.attr("height",200)
.attr("x",100)
.attr("y",100);
var zoom = d3.behavior.zoom().
on("zoom",zoomed);
function zoomed(){
container.attr("transform", "translate(" + d3.event.translate
+")scale(" + d3.event.scale + ")");
container.attr("clip-path","url(#clip)");
}
svg.call(zoom);
var container = svg.append("g")
.attr("clip-path","url(#clip)");
var rect = container.append("rect")
//.attr("clip-path","url(#clip)")
.attr("class","bar")
.attr("x",150)
.attr("y",150)
.attr("width",350)
.attr("height",350);
</script>
What I want is for the clipping to be applied again after zooming / moving (so that I cannot
move the rectangle outh of the clipping region, which right now i can do without any problems.) How do I do that?
I am assuming that the current behaviour is caused by the fact that the clipping is applied before the transformation.
I had the same problem and spent the last couple of hours trying to figure out a solution. Apparently, the clip-path operates on the object prior to transformation. So I tried to reverse-transform the clip object when performing the zoom transformation, and this worked !
It is something in the spirit of:
var clip_orig_x = 100, clip_orig_y = 100;
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;
// standard zoom transformation:
container.attr("transform", "translate(" + t +")scale(" + s + ")");
// the trick: reverse transform the clip object!
clip.attr("transform", "scale(" + 1/s + ")")
.attr("x", clip_orig_x - t[0])
.attr("y", clip_orig_y - t[1]);
}
where clip is the rectangle in the clipPath. Because of interactions between zooming and translation, you need to set "x" and "y" explicitly instead of using transform.
I am sure experienced d3 programmers out there will come up with a better solution, but this works !

How do make my plot points smooth in d3.js using projection?

I'm able to plot some weather data onto a map using the following code. However the points are rectangles and i'd like to get them smoother.
,
I'd like to plot them smoother like something similar to
I believe I need to look into interpolating, spatial analysis, and/or Choropleth maps. I think they are different algorithms in doing this. I feel like i need to fill in more points in between the existing ones? And with that is it possible to make gradient like points? Is this doable in D3? Or should i consider using three.js or WebGL stuff?
var width = 960,
height = 960;
var map = {};
var projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.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("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-50m.json", function(error, world) {
svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
svg.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
map.plot_points = [];
map.max = 30;
map.min = -1;
var opacity = d3.scale.linear()
.domain([map.min, map.max])
.range([0,1]);
var rainbow = ["#CE0C82", "#800CCE", "#1F0CCE", "#0C5BCE", "#0C99CE", "#2ECE0C", "#BAE806", "#FEFF00", "#FFCD00", "#FF9A00", "#FF6000", "#FF0000"];
zs.forEach(function(zv,zi){
zv.forEach(function(zzv, zzi){
if(zzv != 999)
{
map.plot_points.push({lat: ys[zi], long:xs[zzi],value:zzv});
}
})
});
console.log(map);
var points = svg.selectAll("rects.points")
.data(map.plot_points)
.enter()
.append("rect")
.attr("class", "points")
.style("fill", function(d) {
var scale = d3.scale.linear().domain([map.min, map.max]).range([1, rainbow.length]);
return rainbow[Math.round(scale(d.value))];
}).attr("width", 8)
.attr("height", 8)
.style("fill-opacity", 1)
.attr("transform", function(d) {
return "translate(" + projection([d.long, d.lat]) + ")";
})
It sounds like the problem in your case is the data. What you would need to do is take the original data and interpolate it to a smoother form. For this, you can use a GIS program such as QGIS. How exactly to do that depends on what format your original data is in.
Once you have the smoother data, you can plot it again in D3. My guess is that the end result would be somewhat similar to what I've done here, where contour lines are drawn to much the same effect as what you're aiming for.
Maybe you could take a look into heatmap js.
http://www.patrick-wied.at/static/heatmapjs/
Although is point based it may give you a hint.
It uses canvas instead of svg.
Jason Davies wrote an implementation of the conrec algorithm that does exactly what you need:
https://github.com/jasondavies/conrec.js
It's got a working example inside
I believe that the white stripes are happening because of the projection you are using;
In fact the height of each rectangle should adjust accordingly going north and south from the Equator, because the Mercator projection alters the distance going north and south.
To have a fixed height of the rectangles you could try with this projection instead:
http://bl.ocks.org/mbostock/3757119
which preserves the dimension going north and south

Categories

Resources