How to center & scale map in d3js (use projection.center/translate/rotate?) - d3.js

Given that I have topoJSON data of a given geographical feature and a specific projection.
How should I center and scale the map to fit its parent object?
It seems I can use either projection.rotate(), projection.translate() or projection.center() to center a map:
https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Projections.md
What are the differences and how does scale affect the different functions?

Use projection.fitExtent() in v4. Documentation. Example.
fitExtent takes two parameters:
extent is the top left and bottom right corner of the projection, represented by an array of two arrays – e.g. [[0, 0], [width, height]].
object is a GeoJSON object.
If the top left corner of the projection is [0, 0], you can use the convenience method projection.fitSize(), where you only pass the bottom right corner of the extent, represented by a single array of two items – e.g. [width, height].

Actually, it's a mix of both. According to the API, projection.center:
sets the projection’s center to the specified location, a two-element array of longitude and latitude in degrees and returns the projection.
So, it's used to set the center of the map. Regarding projection.translate:
If point is specified, sets the projection’s translation offset to the specified two-element array [x, y] and returns the projection. If point is not specified, returns the current translation offset which defaults to [480, 250]. The translation offset determines the pixel coordinates of the projection’s center. The default translation offset places ⟨0°,0°⟩ at the center of a 960×500 area.
As you can see, projection.translate depends on projection.center ("the translation offset determines the pixel coordinates of the projection’s center"). So, both values will determine how the map sits in its container
This is a demo showing the map of Japan (this code is not mine) in a smaller SVG, 500x500. In this one, we'll set the translate to the middle of the SVG:
.translate([width/2, height/2]);
Check the demo:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
stroke-width: 1.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
And, in this one, to the left:
.translate([width/4, height/2]);
Check the demo:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 4, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
stroke-width: 1.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
In both cases, however, changing projection.center will move the map in its container.

Related

d3 donut/pie chart - drawing a line between arcs

can't figure to find the endpoint of the arc to draw a line from (0,0) to the arc's endpoint..image attached
I could find the centroid of the arc and draw a line but here I want to pull a line to end of arc so that I can extend that line to the left /right side (and then append the circle at line's endpoint)...could't find any such solution over whole google. Any help will be appreciated. Just a hint will do.
When you pass a data array to the pie generator, it returns an array of objects with the following properties:
data - the input datum; the corresponding element in the input data array.
value - the numeric value of the arc.
index - the zero-based sorted index of the arc.
startAngle - the start angle of the arc.
endAngle - the end angle of the arc.
padAngle - the pad angle of the arc.
From these, you can use startAngle or endAngle to draw your lines, since they hold the arcs' starting points (and endpoints).
But there is a catch: unlike the regular trigonometric representation, D3 pie generator puts the 0 angle at 12 o'clock:
The angular units are arbitrary, but if you plan to use the pie generator in conjunction with an arc generator, you should specify angles in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise.
Therefore, we have to subtract Math.PI/2 to get the correct angles.
In the following demo, the coordinates are calculates using sine and cosine:
.attr("y2", function(d) {
return Math.sin(d.startAngle - Math.PI / 2) * (outerRadius)
})
.attr("x2", function(d) {
return Math.cos(d.startAngle - Math.PI / 2) * (outerRadius)
})
Check the demo:
var data = [10, ,12, 50, 15, 20, 40, 6, 32, 17];
var width = 500,
height = 400,
radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory10)
var pie = d3.pie()
.sort(null);
var arc = d3.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll(null)
.data(pie(data))
.enter().append("path")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
var lines = svg.selectAll(null)
.data(pie(data))
.enter()
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("y2", function(d) {
return Math.sin(d.startAngle - Math.PI / 2) * (radius - 50)
})
.attr("x2", function(d) {
return Math.cos(d.startAngle - Math.PI / 2) * (radius - 50)
})
.attr("stroke", "black")
.attr("stroke-width", 1)
<script src="https://d3js.org/d3.v4.min.js"></script>
Once you apply pie layout to your dataset by doing
var pieData = myPieLayout(myDataset)
inside pieData you will find, for each element of your dataset, two properties called startAngle and endAngle. Using that, you can find the position of the point you want, from the center of the pie by iterating through pieData elements and doing
var x = Math.cos(d.endAngle)*radius
var y = Math.sin(d.endAngle)*radius

Synchronize an image with the rest of the svg with D3js

I am trying to plot some points on a map. My map is a svg generated separately (in the PATH_PARAM file). It is supposed to act as a background, with details that must be at specific coordinates in the final plot. Even if it's not exactly that you can imagine the map is a picture of a face I traced myself (so I know its size and where are each elements in the map's referential) and I want to add points precisely in the middle of the eyes after importing it on my svg with D3.js.
For now I simply append it to the svg, like so: (this javascript is called during the loading of the page)
var SIZE_PARAM = 200;
var DATA_PARAM = [{position: [0,1]}, {position: [4,5]}] //actually it's a far more bigger set of points.
var graph = d3.select("#graph");
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(d3.behavior.zoom().on("zoom",
function () {svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")")}
))
.append("g");
background = svg.selectAll("image")
.data([0])
.enter()
.append("svg:image");
background.attr("xlink:href", PATH_PARAM)
.attr("width",SIZE_PARAM)
.attr("height",SIZE_PARAM);
scale = d3.scale.linear()
.domain([dom_x_min, dom_x_max])
.rangeRound([ 0, SIZE_PARAM ])
.clamp(true);
circles = svg.selectAll("circle")
.data(DATA_PARAM)
.enter()
.append("circle");
circles.attr("class", "point")
.attr("cx", function(d,i) {return scale(d.position[0]);})
.attr("cy", function(d,i) {return scale(d.position[1]);})
.attr("r", function(d,i) {return 1;})
The problem I got is that the map and the points are not synchronized with the map. The relative position between points is good, and I managed to have the same scale for the map and the points by setting a unique SIZE_PARAM parameter, but there is a translation between the points and the map. This translation is not visible for a small number of points (like the two of the example) but becomes more and more noticeable as this number increases.
Following some advice I added this to the CSS:
#graph * {
position: absolute;
top: 0px;
left: 0px;
}
But it doesn't solve my problem.
What can I do to synchronize the map with the points ?

How does d3's path.bounds work?

I have a file world.topo.json in TopoJson format which I took from https://datamaps.github.io/ and use it in a d3 geo chart (using merchant projection).
It works well, but I find quite odd why, when I call path.bounds(<TopoJson File Content>.objects.world.feature) and get these values:
[
[-25.272818452358365, -114.9648719971861],
[917.2049776245796, 507.5180814546301]
]
So, why is the botom/left corner pointing to -25 and -114? Shouldn't them be either 0,0 or -917, -507 instead?
Update: I have a zoom behavior object bound to my d3 chart, which works for me exactly as expected. So, I've written a console.log for every zoom/drag even like below:
const topojson = <response of an ajax request>;
const bounds = path.bounds(topojson.objects.world.feature);
console.log(translate, JSON.stringify(path.bounds(feature))); // XXX
So, every single time zoom/drag even is called, this is the type of output I get:
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
The first array being the current translate and the second being the bounds.
But, when I drag/pan or zoom, here is the output:
[0.021599999999999998, 0.10368] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[24.88185889212827, 119.4329226822157] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-15, 119] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27, 117] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27.32184332502962, 117.03468139278337] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-125.83796642848066, 127.65064293410353] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-165.15379127139124, 131.88726199045166] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.4557969093005, 132.7818746669505] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
As you can see, although the first argument changes constantly according to zoom and pan events, the bounds remain untouched.
The documentation about path.bounds(object)has it covered:
Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON object. The bounding box is represented by a two-dimensional array: [[x₀, y₀], [x₁, y₁]], where x₀ is the minimum x-coordinate, y₀ is the minimum y-coordinate, x₁ is maximum x-coordinate, and y₁ is the maximum y-coordinate.
So, -25 and -114 are the minimum x and y values, and refer to the top left corner (in the SVG coordinates system), not the bottom left.
Have in mind that path.bounds is different from geoBounds, which:
Returns the spherical bounding box for the specified GeoJSON feature. The bounding box is represented by a two-dimensional array: [[left, bottom], [right, top]], where left is the minimum longitude, bottom is the minimum latitude, right is maximum longitude, and top is the maximum latitude.
How does it work?
path.bounds(object) will use your projection to drawn a "rectangle" around your object and will return an array with the four corners of that rectangle, as described above. Let's see how it works in these demos (this code is not mine):
In this first demo, the map of Japan has an scale of 1000. Check the console to see path.bounds.
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(1000)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
console.log(JSON.stringify(path.bounds(mapJapan)))
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
It logs:
[[-12.878670523380151,73.71036362631844],[529.0014631418044,535.5463567314675]]
Which are [[x0, y0],[x1, y1]] values.
Now the same code, but with a scale of 500:
var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
var width = 500,
height = 500,
scale = 1;
d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g").attr("id", "all-g");
var projection = d3.geo.mercator()
.center([138, 38])
.scale(500)
.translate([width / 2, height / 2]);
d3.json(topoJsonUrl, onLoadMap);
function onLoadMap (error, jpn) {
var path = d3.geo.path()
.projection(projection);
var features = topojson.object(jpn, jpn.objects.japan);
var mapJapan = features;
console.log(JSON.stringify(path.bounds(mapJapan)))
d3.select("#all-g")
.append("g").attr("id", "path-g").selectAll("path")
.data(features.geometries)
.enter()
.append("path")
.attr("fill", "#f0f0f0")
.attr("id", function(d,i){ return "path" + i})
.attr("stroke", "#999")
.attr("stroke-width", 0.5/scale)
.attr("d", path);
}
path {
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>
It logs different values:
[[118.56066473830992,161.85518181315928],[389.5007315709022,392.77317836573377]]

d3.js. How to make map full width?

Code available here. Map looks like full width, but not at all. There is some indents at the top and left and right sides. All examples of d3.js maps use some strange magic numbers in scale method. I try use such numbers from this answer, but looks like it is not true for my case. So what is true way to scale map? I want to make it full width without any indents on any screen.
var body = d3.select("body").node().getBoundingClientRect();
var coef = 640 / 360;
var width = body.width;
var height = width / coef;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
var projection = d3.geo.equirectangular()
.rotate([-180, 0])
.scale(width / 640 * 100)
.translate([width / 2, height / 2])
var path = d3.geo.path()
.projection(projection);
d3.json("https://dl.dropboxusercontent.com/s/20g0rtglh9xawb8/world-50m.json?dl=1", function(error, world) {
svg.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("d", path);
});

How can I color ocean with topojson in d3 when I have coordinate info for land?

I am learning topojson with d3.
I have coordinate information for land, which is rendered correctly.
Then, how can I add color to ocean (basically outside land)? I tried coloring graticule, but doesn't fill up the entire map and leaves empty spots.
The visualization is hosted on http://jbk1109.github.io/
var projection = d3.geo.stereographic()
.scale(245)
.translate([width / 2, height / 2])
.rotate([-20, 0])
.clipAngle(180 - 1e-4)
.clipExtent([[0, 0], [width, height]])
.precision(.1);
var path = d3.geo.path()
.projection(projection)
var graticule = d3.geo.graticule();
var g = svg.append("g")
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill","none")
.style("stroke","#777")
.style("stroke-width",0.2)
var land = svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path)
.style("fill",'#cbcbcb')
.style("opacity",0.8)
There's no need (and it would be pretty difficult and somewhat expensive computationally) to figure out the inverse of the landmass. But you can just color the background.
I.e you can use CSS:
svg {
background: lightBlue;
}
or you can prepend a <rect> element with a blue fill behind the map:
svg.append('rect')
.attr('width', mapWidth)
.attr('height', mapHeight)
.attr('fill', 'lightBlue')
Just want to add to this: in order to only color the globe itself you have to make your svg a circle using border-radius. The result looks great, though: http://codeasart.com/globe/

Resources