diff --git a/README.md b/README.md index 89a2f80..bc913cd 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Given two spherical arcs [point0, point1] and [point2, point3], returns their in ## Projections -d3-geo-polygon adds polygon clipping to the polyhedral projections from [d3-geo-projection](https://github.com/d3/d3-geo-projection). Thus, it supersedes the following symbols: +d3-geo-polygon adds polygon clipping to the polyhedral and interrupted projections from [d3-geo-projection](https://github.com/d3/d3-geo-projection). Thus, it supersedes the following symbols: # d3.geoPolyhedral(tree, face) · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/polyhedral/index.js), [Examples](https://observablehq.com/@fil/polyhedral-projections-with-d3-geo-polygon) @@ -78,6 +78,61 @@ The Collignon butterfly projection. A butterfly projection inspired by Steve Waterman’s design. +# d3.geoBerghaus · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +The Berghaus projection. + +# d3.geoGingery · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +The Gingery projection. + +# d3.geoHealpix · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +The HEALPix projection. + +# d3.geoInterruptedBoggs · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +Bogg’s interrupted eumorphic projection. + +# d3.geoInterruptedHomolosine · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +Goode’s interrupted homolosine projection. + +# d3.geoInterruptedMollweide · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +Goode’s interrupted Mollweide projection. + +# d3.geoInterruptedMollweideHemispheres · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +The Mollweide projection interrupted into two (equal-area) hemispheres. + +# d3.geoInterruptedSinuMollweide · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +Alan K. Philbrick’s interrupted sinu-Mollweide projection. + +# d3.geoInterruptedSinusoidal · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/reclip.js) + +[](https://observablehq.com/@d3/interrupted-clipped) + +An interrupted sinusoidal projection with asymmetrical lobe boundaries. + + New projections are introduced: # d3.geoPolyhedralVoronoi([parents], [polygons], [faceProjection], [faceFind]) · [Source](https://github.com/d3/d3-geo-polygon/blob/main/src/polyhedral/voronoi.js) diff --git a/package.json b/package.json index 630568a..68c7dda 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "sideEffects": false, "dependencies": { "d3-array": "2.5.0 - 3", - "d3-geo": "2 - 3" + "d3-geo": "2 - 3", + "d3-geo-projection": "4" }, "devDependencies": { "@rollup/plugin-terser": "0.4", diff --git a/src/index.js b/src/index.js index d30a047..92d605c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,3 +17,15 @@ export {default as geoRhombic} from "./rhombic.js"; export {default as geoDeltoidal} from "./deltoidal.js"; export {default as geoCahillKeyes, cahillKeyesRaw as geoCahillKeyesRaw} from "./cahillKeyes.js"; export {default as geoComplexLog, complexLogRaw as geoComplexLogRaw} from "./complexLog.js"; +export { + geoBerghaus, + geoGingery, + geoHealpix, + geoInterrupt, + geoInterruptedBoggs, + geoInterruptedHomolosine, + geoInterruptedMollweide, + geoInterruptedMollweideHemispheres, + geoInterruptedSinuMollweide, + geoInterruptedSinusoidal +} from "./reclip.js"; diff --git a/src/reclip.js b/src/reclip.js new file mode 100644 index 0000000..4e6c95c --- /dev/null +++ b/src/reclip.js @@ -0,0 +1,99 @@ +import {merge} from "d3-array"; +import {geoInterpolate} from "d3-geo"; +import { + geoBerghaus as berghaus, + geoGingery as gingery, + geoHealpix as healpix, + geoInterrupt as interrupt, + geoInterruptedBoggs as interruptedBoggs, + geoInterruptedHomolosine as interruptedHomolosine, + geoInterruptedMollweide as interruptedMollweide, + geoInterruptedMollweideHemispheres as interruptedMollweideHemispheres, + geoInterruptedSinuMollweide as interruptedSinuMollweide, + geoInterruptedSinusoidal as interruptedSinusoidal, +} from "d3-geo-projection"; +import geoClipPolygon from "./clip/polygon.js"; + +/** + * Reclip projections from d3-geo-projection + */ +export function geoBerghaus() { return reclip(berghaus.apply(this, arguments)); } +export function geoGingery() { return reclip(gingery.apply(this, arguments)); } +export function geoHealpix() { return reclip(healpix.apply(this, arguments), true); } +export function geoInterrupt() { return clipInterrupted(interrupt.apply(this, arguments)); } +export function geoInterruptedBoggs() { return clipInterrupted(interruptedBoggs.apply(this, arguments)); } +export function geoInterruptedHomolosine() { return clipInterrupted(interruptedHomolosine.apply(this, arguments)); } +export function geoInterruptedMollweide() { return clipInterrupted(interruptedMollweide.apply(this, arguments)); } +export function geoInterruptedMollweideHemispheres() { return clipInterrupted(interruptedMollweideHemispheres.apply(this, arguments)); } +export function geoInterruptedSinuMollweide() { return clipInterrupted(interruptedSinuMollweide.apply(this, arguments)); } +export function geoInterruptedSinusoidal() { return clipInterrupted(interruptedSinusoidal.apply(this, arguments)); } + +function reclip(projection, vertical = false) { + const {lobes} = projection; + function reset(projection) { + const rotate = projection.rotate(); + const scale = projection.scale(); + const translate = projection.translate(); + projection.rotate([0, 0]).translate([0, 0]); + projection.lobes = function (_) { + return !arguments.length ? lobes() : reset(lobes(_)); + }; + + projection.preclip((stream) => stream); // clipNone + const R = 1 - 1e-7; + const Rx = vertical ? 1 : R; + let points = []; + projection + .stream({ + point(x, y) { + points.push([x * Rx, y * R]); + }, + lineStart() {}, + lineEnd() {}, + polygonStart() {}, + polygonEnd() {}, + sphere() {}, + }) + .sphere(); + + projection.scale(scale); + points = points.map(projection.invert); + points.push(points[0]); + + return projection + .rotate(rotate) + .translate(translate) + .preclip(geoClipPolygon({ type: "Polygon", coordinates: [points] })); + } + return reset(projection); +} + +function clipInterrupted(projection) { + const { lobes } = projection; + function reset(projection) { + const l = lobes?.(); + const polygon = merge( + Array.from(l, (d, i) => { + const hemisphere = d.flatMap( + (q) => Array.from(q, (p) => geoInterpolate(p, [q[1][0], 0])(1e-9)) // pull inside each lobe + ); + return i === 0 + ? hemisphere // north + : [...hemisphere].reverse(); + }) + ); + + projection.lobes = function (_) { + return !arguments.length ? lobes() : reset(lobes(_)); + }; + + return projection.preclip( + geoClipPolygon({ + type: "Polygon", + coordinates: [[...polygon, polygon[0]]], + }) + ); + } + + return reset(projection); +} diff --git a/test/snapshots.js b/test/snapshots.js index 248d07c..7fed37a 100644 --- a/test/snapshots.js +++ b/test/snapshots.js @@ -2,13 +2,24 @@ import { Canvas } from "canvas"; import { readFile } from "fs/promises"; import { feature } from "topojson-client"; import { geoGraticule, geoPath } from "d3-geo"; +import { geoHomolosineRaw } from "d3-geo-projection"; import { geoAirocean, + geoBerghaus, geoCox, geoCahillKeyes, geoComplexLog, geoCubic, geoDeltoidal, + geoGingery, + geoHealpix, + geoInterrupt, + geoInterruptedBoggs, + geoInterruptedHomolosine, + geoInterruptedMollweide, + geoInterruptedMollweideHemispheres, + geoInterruptedSinuMollweide, + geoInterruptedSinusoidal, geoRhombic, geoDodecahedral, geoIcosahedral, @@ -28,7 +39,9 @@ async function renderWorld(projection, { extent, clip = false } = {}) { extent === undefined ? { type: "Sphere" } : graticule.extent(extent).outline(); - const world = JSON.parse(await readFile("./node_modules/world-atlas/world/50m.json")); + const world = JSON.parse( + await readFile("./node_modules/world-atlas/world/50m.json") + ); const canvas = new Canvas(width, height); const context = canvas.getContext("2d"); const path = geoPath(projection, context); @@ -122,6 +135,106 @@ export async function tetrahedralLeeSouth() { ); } +// reclip +export async function berghaus() { + return renderWorld(geoBerghaus()); +} + +export async function gingery() { + return renderWorld(geoGingery()); +} + +export async function berghaus7() { + return renderWorld(geoBerghaus().lobes(7).fitSize([960, 500], { type: "Sphere" })); +} + +export async function berghaus13() { + return renderWorld(geoBerghaus().lobes(13).fitSize([960, 500], { type: "Sphere" })); +} + +export async function gingery7() { + return renderWorld(geoGingery().lobes(7).fitSize([960, 500], { type: "Sphere" })); +} + +export async function gingery3() { + return renderWorld(geoGingery().lobes(3).fitSize([960, 500], { type: "Sphere" })); +} + +export async function goodeOcean() { + return renderWorld( + geoInterrupt(geoHomolosineRaw, [ + [ + [ + [-180, 0], + [-130, 90], + [-95, 0], + ], + [ + [-95, 0], + [-30, 90], + [55, 0], + ], + [ + [55, 0], + [120, 90], + [180, 0], + ], + ], + [ + [ + [-180, 0], + [-120, -90], + [-60, 0], + ], + [ + [-60, 0], + [20, -90], + [85, 0], + ], + [ + [85, 0], + [140, -90], + [180, 0], + ], + ], + ]) + .rotate([-204, 0]) + .precision(0.1) + ); +} + +export async function interruptedBoggs() { + return renderWorld(geoInterruptedBoggs()); +} + +export async function healpix() { + return renderWorld(geoHealpix()); +} + +export async function healpix5() { + return renderWorld(geoHealpix().lobes(5)); +} + +export async function interruptedHomolosine() { + return renderWorld(geoInterruptedHomolosine()); +} + +export async function interruptedMollweide() { + return renderWorld(geoInterruptedMollweide()); +} + +export async function interruptedMollweideHemispheres() { + return renderWorld(geoInterruptedMollweideHemispheres()); +} + +export async function interruptedSinuMollweide() { + return renderWorld(geoInterruptedSinuMollweide()); +} + +export async function interruptedSinusoidal() { + return renderWorld(geoInterruptedSinusoidal()); +} + // more tests // https://github.com/d3/d3-geo-polygon/issues/7 @@ -150,17 +263,19 @@ export async function rhombic00() { // https://github.com/d3/d3-geo-polygon/issues/62 export async function rhombicHalf1() { - return renderWorld(geoRhombic() - .parents([-1, 0, 6, 2, 1, 9, 11, 3, 4, 8, 6, 10]) - .precision(0.1) - .fitSize([960, 500], { type: "Sphere" }) + return renderWorld( + geoRhombic() + .parents([-1, 0, 6, 2, 1, 9, 11, 3, 4, 8, 6, 10]) + .precision(0.1) + .fitSize([960, 500], { type: "Sphere" }) ); } export async function rhombicHalf2() { - return renderWorld(geoRhombic() - .parents([4, 0, 6, 2, 1, 9, 11, 3, 4, 8, -1, 10]) - .angle(-19.5) - .precision(0.1) - .fitSize([960, 500], { type: "Sphere" }) + return renderWorld( + geoRhombic() + .parents([4, 0, 6, 2, 1, 9, 11, 3, 4, 8, -1, 10]) + .angle(-19.5) + .precision(0.1) + .fitSize([960, 500], { type: "Sphere" }) ); } diff --git a/test/snapshots/berghaus.png b/test/snapshots/berghaus.png new file mode 100644 index 0000000..78bca4e Binary files /dev/null and b/test/snapshots/berghaus.png differ diff --git a/test/snapshots/berghaus13.png b/test/snapshots/berghaus13.png new file mode 100644 index 0000000..51f47d0 Binary files /dev/null and b/test/snapshots/berghaus13.png differ diff --git a/test/snapshots/berghaus7.png b/test/snapshots/berghaus7.png new file mode 100644 index 0000000..6950b17 Binary files /dev/null and b/test/snapshots/berghaus7.png differ diff --git a/test/snapshots/gingery.png b/test/snapshots/gingery.png new file mode 100644 index 0000000..40379be Binary files /dev/null and b/test/snapshots/gingery.png differ diff --git a/test/snapshots/gingery3.png b/test/snapshots/gingery3.png new file mode 100644 index 0000000..b26f8a0 Binary files /dev/null and b/test/snapshots/gingery3.png differ diff --git a/test/snapshots/gingery7.png b/test/snapshots/gingery7.png new file mode 100644 index 0000000..be16e20 Binary files /dev/null and b/test/snapshots/gingery7.png differ diff --git a/test/snapshots/goodeOcean.png b/test/snapshots/goodeOcean.png new file mode 100644 index 0000000..9dde739 Binary files /dev/null and b/test/snapshots/goodeOcean.png differ diff --git a/test/snapshots/healpix.png b/test/snapshots/healpix.png new file mode 100644 index 0000000..a4227d4 Binary files /dev/null and b/test/snapshots/healpix.png differ diff --git a/test/snapshots/healpix5.png b/test/snapshots/healpix5.png new file mode 100644 index 0000000..8100b1d Binary files /dev/null and b/test/snapshots/healpix5.png differ diff --git a/test/snapshots/interruptedBoggs.png b/test/snapshots/interruptedBoggs.png new file mode 100644 index 0000000..0cd6419 Binary files /dev/null and b/test/snapshots/interruptedBoggs.png differ diff --git a/test/snapshots/interruptedHomolosine.png b/test/snapshots/interruptedHomolosine.png new file mode 100644 index 0000000..e03ed18 Binary files /dev/null and b/test/snapshots/interruptedHomolosine.png differ diff --git a/test/snapshots/interruptedMollweide.png b/test/snapshots/interruptedMollweide.png new file mode 100644 index 0000000..2d363c9 Binary files /dev/null and b/test/snapshots/interruptedMollweide.png differ diff --git a/test/snapshots/interruptedMollweideHemispheres.png b/test/snapshots/interruptedMollweideHemispheres.png new file mode 100644 index 0000000..1cea268 Binary files /dev/null and b/test/snapshots/interruptedMollweideHemispheres.png differ diff --git a/test/snapshots/interruptedSinuMollweide.png b/test/snapshots/interruptedSinuMollweide.png new file mode 100644 index 0000000..9350e4d Binary files /dev/null and b/test/snapshots/interruptedSinuMollweide.png differ diff --git a/test/snapshots/interruptedSinusoidal.png b/test/snapshots/interruptedSinusoidal.png new file mode 100644 index 0000000..bff18cb Binary files /dev/null and b/test/snapshots/interruptedSinusoidal.png differ diff --git a/yarn.lock b/yarn.lock index d083fe2..17fee9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -338,6 +338,11 @@ commander@2, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -357,14 +362,23 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -"d3-array@2.5.0 - 3": +"d3-array@1 - 3", "d3-array@2.5.0 - 3": version "3.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== dependencies: internmap "1 - 2" -"d3-geo@2 - 3": +d3-geo-projection@4: + version "4.0.0" + resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz#dc229e5ead78d31869a4e87cf1f45bd2716c48ca" + integrity sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg== + dependencies: + commander "7" + d3-array "1 - 3" + d3-geo "1.12.0 - 3" + +"d3-geo@1.12.0 - 3", "d3-geo@2 - 3": version "3.1.1" resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==