Published
Edited
Jun 28, 2021
1 fork
Importers
21 stars
Insert cell
Insert cell
Insert cell
Insert cell
mercator_segment = projectJSON(mercator)({
type: "LineString",
coordinates: [
[-70.67, -33.45], // Santiago
[-21.88 - 360*4, 64.13]] }) // Reykjavík - 4 full turns
Insert cell
great_circle_arc = projectJSON(mercator.invert)(mercator_segment) // red line above
Insert cell
rhumb_line = inverseResampleJSON(mercator, .05)(mercator_segment) // blue line above
Insert cell
Insert cell
Insert cell
precise_graticule = inverseResampleJSON(d3.geoEquirectangularRaw, .01)({
type: 'MultiLineString',
coordinates: [].concat(
d3.range(-180, 180, 10).map(λ => [[λ, -80], [λ, 80]]),
d3.range(-80, 81, 10).map(φ => [[-180, φ], [180, φ]])) })
Insert cell
diagonals = inverseResampleJSON(d3.geoEquirectangularRaw, .01)({
type: 'MultiLineString',
coordinates: d3.range(-180, 180, 20).map(λ => [[λ, -90], [λ+180, 90], [λ+360, -90]]) })
Insert cell
Insert cell
Insert cell
bounds = countries110.features.map(feature => d3.geoBounds(feature))
Insert cell
boxes = inverseResampleJSON(d3.geoEquirectangularRaw, .02)({
type: 'GeometryCollection',
geometries: bounds.map(([[λ0, φ0], [λ1, φ1]]) => ({
type: 'Polygon',
coordinates: (φ0 === -90)
? [[[λ0, φ1], [λ1, φ1]]] // Antarctica
: [[[λ0, φ0], [λ0, φ1], [λ1 += (λ1 < λ0) * 360, φ1], [λ1, φ0], [λ0, φ0]]] })) })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
hexgrid = {
var map2 = (arr, cb) => arr.map(pts => pts.map(cb));
// draw hexagon edges within one prototypical square:
var square = [[[0, 0], [1/3, 1/3]], [[1, 0], [1/3, 1/3]], [[0, 1], [1/3, 1/3]]];
return (n) => map2([].concat(
...d3.range(n).map(i => [].concat(
...d3.range(n).map(j => [].concat(
map2(square, ([x, y]) => [i+x, j+y]), map2(square, ([x, y]) => [-i-x, -j-y]),
map2(square, ([x, y]) => [i+1-x, -j-1+y]), map2(square, ([x, y]) => [-i-1+x, j+1-y]) )) ))
), ([x, y]) => [x/n, y/n]) }
Insert cell
lonlat_hexgrid = inverseResampleJSON(sac_quincuncial_degrees, .01)({
type: 'MultiLineString',
coordinates: hexgrid(grid_resolution) })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
spherical = {
const degrees = 180/Math.PI;
return ([x, y, z]) => [
degrees*Math.atan2(y, x),
degrees*Math.asin(z)]}
Insert cell
cartesian = {
const radians = Math.PI/180;
return ([λ, φ]) => [
Math.cos(radians*φ) * Math.cos(radians*λ),
Math.cos(radians*φ) * Math.sin(radians*λ),
Math.sin(radians*φ)]}
Insert cell
Insert cell
stereo_length2 = ([x0, y0, z0], [x1, y1, z1]) => {
var
pxy = x0 * (y1 - y0) - (x1 - x0) * y0,
pyz = y0 * (z1 - z0) - (y1 - y0) * z0,
pzx = z0 * (x1 - x0) - (z1 - z0) * x0,
q = x0*(x1 + x0) + y0*(y1 + y0) + z0*(z1 + z0);
return (pxy*pxy + pyz*pyz + pzx*pzx + !(q*q)) / (q*q); // adding !(q*q) means q==0 => return Infinity
}
Insert cell
Insert cell
stereo_area2 = ([x0, y0, z0], [x1, y1, z1], [x2, y2, z2]) => {
var
p = x0*((y1 - y0)*(z2 - z0) - (y2 - y0)*(z1 - z0)) +
y0*((z1 - z0)*(x2 - x0) - (z2 - z0)*(x1 - x0)) +
z0*((x1 - x0)*(y2 - y0) - (x2 - x0)*(y1 - y0)),
q = (x0 + x2)*(x0 + x1) + (y0 + y2)*(y0 + y1) + (z0 + z2)*(z0 + z1);
return (p*p + !(q*q)) / (q*q); // adding !(q*q) means q==0 => return Infinity
}
Insert cell
planar_midpoint = ([x0, y0], [x1, y1]) => [
0.5 * (x0 + x1),
0.5 * (y0 + y1)]
Insert cell
Insert cell
inverseResampleJSON = function (projection, delta) {
// delta = angle-measure tolerance, in degrees
const
maxDepth = 16,
radians = Math.PI/180,
dd = Math.tan(radians*delta/2) ** 2;
var resampleLineTo = function(
w0, u0, // w = input coordinates
w1, u1, // u = cartesian coordinates on the sphere
ll01, // ll = stereographic distance squared
depth, array) {

if (depth--) {
var
w2 = planar_midpoint(w0, w1),
λφ2 = projection.invert(...w2),
u2 = cartesian(λφ2),
ll02 = stereo_length2(u2, u0),
ll12 = stereo_length2(u2, u1),
AA = stereo_area2(u2, u0, u1),
hh = AA * (1 + 0.25*ll01)*(1 + 0.25*ll01) / (dd * ll01), // perpendicular projected distance
ww = 2 * ((ll02 - ll12) / ll01) * ((ll02 - ll12) / ll01); // midpoint roughly in the middle
// (ll02 + ll12 > 0.25) forces bisection for over-long segments, and handles ll01 == Infinity edge case
// (ll02 + ll12 > dd)) stops bisection when the segment gets tiny
if (((hh + ww > 1) & (ll02 + ll12 > dd)) | (ll02 + ll12 > 0.25)) {
resampleLineTo(w0, u0, w2, u2, ll02, depth, array);
array.push(λφ2);
resampleLineTo(w2, u2, w1, u1, ll12, depth, array);
}
}
}
var resampleChain = function(pointarray) {
let outarray = [];
let w0 = pointarray[0], λφ0 = projection.invert(...w0), u0 = cartesian(λφ0);
outarray.push(λφ0);
for (var i = 1, n = pointarray.length; i < n; i++) {
let w1 = pointarray[i], λφ1 = projection.invert(...w1), u1 = cartesian(λφ1);
resampleLineTo(w0, u0, w1, u1, stereo_length2(u0, u1), maxDepth, outarray)
outarray.push(λφ1);
w0 = w1, u0 = u1;
}
return outarray;
}
var project = w => projection.invert(...w);
var mapInPlace = fn => array => array.forEach((e, i) => array[i] = fn(e));
var convert, convertType = ({
Point: o => o.coordinates = project(o.coordinates),
MultiPoint: o => mapInPlace(project)(o.coordinates),
LineString: o => o.coordinates = resampleChain(o.coordinates),
Polygon: o => mapInPlace(resampleChain)(o.coordinates),
MultiLineString: o => mapInPlace(resampleChain)(o.coordinates),
MultiPolygon: o => o.coordinates.forEach(mapInPlace(resampleChain)),
Feature: o => convert(o.geometry),
GeometryCollection: o => o.geometries.forEach(convert),
FeatureCollection: o => o.features.forEach(convert),
});
convert = o => (convertType?.[o?.type]?.(o), o);
return function(json) {
json = JSON.parse(JSON.stringify(json)); // make deep copy
return convert(json);
}
}
Insert cell
// apply the provided projection to every coordinate pair of a GeoJSON object
projectJSON = function (projection) {
var project = w => projection(...w);
var projectChain = arr => arr.map(project);
var mapInPlace = fn => array => array.forEach((e, i) => array[i] = fn(e));
var convert, convertType = ({
Point: o => o.coordinates = project(o.coordinates),
MultiPoint: o => mapInPlace(project)(o.coordinates),
LineString: o => o.coordinates = projectChain(o.coordinates),
Polygon: o => mapInPlace(projectChain)(o.coordinates),
MultiLineString: o => mapInPlace(projectChain)(o.coordinates),
MultiPolygon: o => o.coordinates.forEach(mapInPlace(projectChain)),
Feature: o => convert(o.geometry),
GeometryCollection: o => o.geometries.forEach(convert),
FeatureCollection: o => o.features.forEach(convert),
});
convert = o => (convertType?.[o?.type]?.(o), o);
return function(json) {
json = JSON.parse(JSON.stringify(json)); // make deep copy
return convert(json);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more