A stand-alone version of https://observablehq.com/@fil/a-conformal-airocean
Built with blockbuilder.org
A stand-alone version of https://observablehq.com/@fil/a-conformal-airocean
Built with blockbuilder.org
| <!DOCTYPE html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <script src="https://d3js.org/d3.v6.min.js"></script> | |
| <script src="https://unpkg.com/[email protected]"></script> | |
| <style> | |
| body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| function geoConformalAirocean() { | |
| const {acos} = Math; | |
| const degrees = 180 / Math.PI; | |
| const sqrt3_4 = Math.sqrt(3) / 4; | |
| const rotate = [115, acos(1 / 3) * .5 * degrees - 90, 180] | |
| const rawLee = d3.geoTetrahedralLee() | |
| .rotate(rotate) | |
| .fitExtent([[-4, -1.5 / sqrt3_4], [4, 1.5 / sqrt3_4]], { | |
| type: "Sphere" | |
| }); | |
| const forward = (l, p) => { | |
| let [x, y] = rawLee([l * degrees, p * degrees]); | |
| y = -y; | |
| if (x < -2.1 || y < -2.73) { | |
| x = -x - 4; | |
| y = -y; | |
| } | |
| if (x < -4) { | |
| x = -x - 8; | |
| y = 6.93 - y; // 6.93 is a visual approximation—haven't figured out the exact value yet (please help!) | |
| } | |
| return [x, y]; | |
| }; | |
| forward.invert = (x, y) => { | |
| y = -y; | |
| const a = | |
| rawLee.invert([x, y]) || | |
| rawLee.invert([-x - 4, -y]) || | |
| rawLee.invert([-x - 8, 6.93 - y]) || | |
| rawLee.invert([-(-x - 8) - 4, 6.93 + y]); | |
| if (a) return [a[0] * radians, a[1] * radians]; | |
| }; | |
| const clipPolygon = { | |
| type: "Polygon", coordinates: [[ | |
| [-20, 0], | |
| [-27, 14], | |
| [-27, 18], | |
| [-20, 37], | |
| [-30, 37], | |
| [-35, -40], | |
| [-30, -70], | |
| [40, -50], | |
| [70, -40], | |
| [120, -60], | |
| [150, -60], | |
| [180, -60], | |
| [-150, -50], | |
| [-110, -40], | |
| [-110, -30], | |
| [-170, 10], | |
| [160, 40], | |
| [150, 40], | |
| [150, 30], | |
| [-170, 0], | |
| [-150, -10], | |
| [-140, -30], | |
| [-170, -50], | |
| [120, -50], | |
| [90, -30], | |
| [60, -25], | |
| [30, -38], | |
| [10, -38], | |
| [-20, 0] | |
| ].map(d => [d[0] - .01, d[1] - .01])] | |
| }; | |
| return d3.geoProjection(forward) | |
| .preclip(d3.geoClipPolygon(clipPolygon)) | |
| .angle(57 - 180) | |
| .fitExtent([[0,0], [960, 500]], {type: "Sphere"}); | |
| } | |
| const projection = geoConformalAirocean(); | |
| const path = d3.geoPath(projection); | |
| const svg = d3.select("body").append("svg") | |
| .attr("width", 960) | |
| .attr("height", 500); | |
| const land = svg.append("path"); | |
| d3.json("https://unpkg.com/[email protected]/world/110m_land.geojson") | |
| .then(geo => land.datum(geo).attr("d", path)); | |
| </script> | |
| </body> | |