Published
Edited
Dec 15, 2021
1 fork
Importers
47 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
w = dem0.naturalWidth
Insert cell
h = dem0.naturalHeight
Insert cell
Insert cell
projection = d3.geoMercator()
.scale(297582.21102247556)
.translate([-61857.49875166787, 206233.6124812287])
Insert cell
Insert cell
pixelAltitude = (r, g, b) => 256 * (r - 128) + g + b / 256
Insert cell
Insert cell
Insert cell
Insert cell
grid0 = {
const w = dem0.naturalWidth;
const h = dem0.naturalHeight;
const context = DOM.context2d(w, h, 1);
context.drawImage(dem0, 0, 0);
const { data } = context.getImageData(0, 0, w, h);
return Float32Array.from({ length: w * h }, (_, i) => {
const pixel = data.subarray(4 * i, 4 * i + 3);
const a = pixelAltitude(...pixel);
return a < 0 ? -0.5 : a;
});
}
Insert cell
Insert cell
peak = d3.max(grid0)
Insert cell
Insert cell
color = (alt) => {
const r = Math.floor(128 + alt / 256);
alt -= 256 * (r - 128);
const g = Math.floor(alt);
const b = Math.round(256 * (alt - g));
return [r, g, b];
}
Insert cell
Insert cell
Insert cell
heatmap(grid, {
width: w,
color: (t) => (t === 0 ? "lightblue" : d3.interpolatePiYG(1 - t)),
dither: false
})
Insert cell
import {heatmap} from "@fil/heatmap"
Insert cell
Insert cell
Insert cell
peakJ = d3.greatestIndex(grid) % w
Insert cell
line = grid.subarray(w * peakJ, w * (peakJ + 1))
Insert cell
Plot.lineY(line).plot({
height: 180,
x: { axis: null },
y: { label: "elevation (m)", ticks: 5, grid: true }
})
Insert cell
Insert cell
joy = Plot.plot({
marks: d3.range(peakJ - 400, peakJ + 450, 25).map((j) => {
const line = grid.subarray(w * j, w * (j + 1));
const dx = j / 8;
const dy = -110 + j / 4;
return [
Plot.areaY(line, { dx, dy, fill: j }),
Plot.lineY(line, { dx, dy, strokeWidth: 0.75 })
];
}),
height: 240,
x: { axis: null },
y: { axis: null },
color: { scheme: "magma" },
marginBottom: 150,
marginRight: 135,
width
})
Insert cell
Insert cell
Insert cell
Insert cell
contourMap = grid => heatmap(grid, {
width: w,
color: d3.interpolateViridis,
contours: true
})
Insert cell
Insert cell
DZX = Float32Array.from(grid, (d, i) => Math.max(grid[i + 1] || 0, 0) - d)
Insert cell
DZY = Float32Array.from(grid, (d, i) => Math.max(grid[i + w] || 0, 0) - d)
Insert cell
html`
${small(
heatmap(DZX, {
width: w,
color: d3.interpolateRdBu,
extent: [-18, 18],
clamp: true
})
)}${small(
heatmap(DZY, {
width: w,
color: d3.interpolateRdBu,
extent: [-18, 18],
clamp: true
})
)}`
Insert cell
hillShadingMap = (grid, { elevation = 45, azimuth = 315, color }) => {
const strength = 0.2;

const r = elevation * (Math.PI / 180);
const a = -azimuth * (Math.PI / 180);
const cr = Math.cos(r);
const sr = Math.sin(r);
const ca = Math.cos(a);
const sa = Math.sin(a);

const lz = sr;
const lx = cr * ca;
const ly = cr * sa;

const DZX = Float32Array.from(
grid,
(d, i) => Math.max(grid[i + 1] || 0, 0) - d
);
const DZY = Float32Array.from(
grid,
(d, i) => Math.max(grid[i + w] || 0, 0) - d
);

return heatmap(
Float32Array.from(grid, (_, i) => {
const dzx = DZX[i];
const dzy = DZY[i];
const norm = Math.hypot(1, dzx, dzy);
const dot = (lz + dzx * lx + dzy * ly) / norm;
return 1 + strength * (dot - cr);
}),
{ width: w, extent: [0.7, 1.15], color }
);
}
Insert cell
Insert cell
(await visibility(), small(rain({ DZX, DZY, invalidation })))
Insert cell
npoints = w * h
Insert cell
rain = ({ DZX, DZY, invalidation }) => {
const random = d3.randomNormal(0, 200);
const nodes = Array.from({ length: Math.min(w * 10, npoints * 2) }, () => ({
x: w / 2 + random(),
y: h / 2 + random()
}));

const context = DOM.context2d(w, h, 1);
const path = d3.geoPath(d3.geoIdentity()).context(context);

function gradient(x, y) {
x = Math.floor(x * w);
y = Math.floor(y * h);
const k = x + w * y;
return [DZX[k] || 0, DZY[k] || 0];
}

const f = -0.06;

const simulation = d3
.forceSimulation(nodes)
.alphaDecay(0)
.force("wind", () => {
nodes.forEach((d) => {
if (
!d.age ||
Math.random() > d.age ||
d.x < 0 ||
d.x >= w - 1 ||
d.y < 0 ||
d.y >= h - 1
) {
d.x = w * Math.random();
d.y = h * Math.random();
d.vx = 0;
d.vy = 0;
d.age = 1;
} else {
const [vx, vy] = gradient(d.x / w, d.y / h);
d.vx += f * vx;
d.vy += f * vy;
d.age *= 0.9999;
}
});
})
.on("tick", () => {
fade(context, 0.1);

context.beginPath();
context.strokeStyle = "steelblue";
context.lineWidth = 3;
for (const { x, y, vx, vy } of nodes) {
context.moveTo(x + vx / 2, y + vy / 2);
context.lineTo(x - vx, y - vy);
}
context.stroke();
});

invalidation.then(() => simulation.stop());

context.canvas.style = `max-width:${w / 2}px`;
return context.canvas;
}
Insert cell
function fade(context, alpha) {
const { globalAlpha, globalCompositeOperation } = context;
context.globalCompositeOperation = "destination-out";
context.fillStyle = "white";
context.globalAlpha = alpha || 1;
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
context.globalAlpha = globalAlpha;
context.globalCompositeOperation = globalCompositeOperation;
}
Insert cell
Insert cell
(await visibility(),
html`
${small(streak({ strokeWidth: 0.5, avoidance: 0.5 }))}${small(
rain({ DZX, DZY, invalidation }),
"position: absolute; top: 0;"
)}${small(streak({ strokeWidth: 0.25, stroke: "#777", avoidance: 0.01 }))}
`)
Insert cell
function streak({ stroke = "black", strokeWidth = 1, ...options } = {}) {
const context = DOM.context2d(w, h, 1);
const path = d3.geoPath(d3.geoIdentity()).context(context);

for (const line of streaklines(grid, DZX, DZY, options)) {
context.moveTo(...line[0]);
for (const p of line) context.lineTo(...p);
}
context.lineWidth = strokeWidth;
context.strokeStyle = stroke;
context.stroke();

return context.canvas;
}
Insert cell
streaklines = (grid, DZX, DZY, { avoidance = 0.01, seed = 42 } = {}) => {
function altitude(x, y) {
const k = Math.floor(x) + w * Math.floor(y);
return grid[k];
}
function gradient(x, y) {
const k = Math.floor(x) + w * Math.floor(y);
return [DZX[k], DZY[k]];
}

const rng = d3.randomLcg(seed);
const cW = w >> 3;
const cH = h >> 3;
const visited = new Float32Array(cW * cH).fill(-1);

let maxstep = 200000;

const streaklines = [];

const order = d3.shuffle(d3.range(cW * cH));
for (const i of order) {
// already visited? drop
if (visited[i] > -1) continue;
const streakline = [];
visited[i] = i;

let x;
let y;
let j = i;
let vx, vy, dx, dy;

for (const direction of [-1, 1]) {
x = (i % cW) * (w / cW);
y = Math.floor(i / cW) * (h / cH);
vx = 0;
vy = 0;
do {
dx = vx;
dy = vy;
if (altitude(x, y) <= 0) break;
[vx, vy] = gradient(x, y).map((d) => d * direction);
const delta = Math.hypot(vx, vy);
if (delta === 0) break;
x += vx / delta;
y += vy / delta;
j = Math.floor((x / w) * cW) + cW * Math.floor((y / w) * cW);
streakline.push([x, y, i, j]);
if (visited[j] < 0) visited[j] = i;
if (visited[j] !== i && rng() > 1 - avoidance) break;
} while (
vx * dx + vy * dy >= 0 &&
x > 0 &&
y > 0 &&
x < w &&
y < h &&
maxstep-- > 0
);
streakline.reverse();
}
if (streakline.length > 5) streaklines.push(streakline);
}

return streaklines;
}
Insert cell
Insert cell
B = require("array-blur@1")
Insert cell
laplacian = B.blur()
.width(w)(grid)
.map((d, i) => grid[i] - d)
.map((d) => Math.atan(d / 10)) // dampen "infinite" values near the shoreline
Insert cell
Insert cell
(await visibility(),
html`
${small(
heatmap(laplacian, {
width: w,
color: (t) => d3.interpolateRdBu(1 - t)
})
)}${small(
rain({ DZX, DZY, invalidation }),
"position: absolute; top: 0;"
)}${small(
heatmap(
laplacian.map((d) => Math.min(0, d)),
{
width: w,
extent: [0, -2]
}
)
)}
`)
Insert cell
Insert cell
grid = B.blur().radius(0.5).width(w)(grid0) // I blur the grid a little to make the drawings nicer, try setting radius = 5 to make it smoooooth
Insert cell
Insert cell
function small(canvas, style = "") {
return Object.assign(canvas, {
style: `vertical-align: bottom;max-width: ${
((width / 2) | 0) - 1
}px;${style}`
});
}
Insert cell
function smaller(canvas, style = "") {
return Object.assign(canvas, {
style: `vertical-align: bottom;max-width: ${((width / 3) | 0) - 1}px;${style}`
});
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more