Skip to content

Commit a714981

Browse files
committed
more docs
1 parent fe1521e commit a714981

File tree

161 files changed

+193872
-158
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+193872
-158
lines changed

docs/ex/animated-background.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!doctype html>
2+
<notebook theme="air">
3+
<title>Animated background</title>
4+
<script id="1" type="text/markdown">
5+
# Animated background
6+
7+
This notebook demonstrates how to place a full-screen canvas for an animated background. Based on work by [Tiffany Rayside](https://codepen.io/tmrDevelops/pen/vOPZBv).
8+
</script>
9+
<script id="2" type="module" pinned="">
10+
const dx = 24;
11+
const dy = 24;
12+
const canvas = display(html`<canvas width=${dx} height=${dy} style="position: fixed; width: 100%; height: 100%; inset: 0; z-index: -1;">`);
13+
const context = canvas.getContext("2d");
14+
15+
let frame;
16+
17+
function animate(now) {
18+
const {cos, sin, min, max, PI: pi} = Math;
19+
const t = now / 2000;
20+
for (let x = 0; x < dx; ++x) {
21+
for (let y = 0; y < dy; ++y) {
22+
const l1 = 0.5 + 0.3 * sin((x * 0.1 + y * 0.1 + t * 0.5));
23+
const c1 = 0.15 + 0.1 * sin((x * 0.12 - t * 0.3 + pi / 3));
24+
const h1 = 360 * sin((y * 0.09 - t * 0.7 + 2 * pi / 3));
25+
const l2 = 0.4 + 0.2 * sin((x * 0.15 - y * 0.08 + t * 0.8 + pi));
26+
const c2 = 0.12 + 0.08 * sin((x * 0.12 * cos(t * 0.2) + y * 0.18 * sin(t * 0.2) + t * 0.4 + pi / 2));
27+
const h2 = 180 + 180 * sin((-x * 0.09 + y * 0.14 + t * 0.9 + 3 * pi / 2));
28+
const l3 = 0.3 + 0.15 * sin((x * 0.25 - y * 0.2 + t * 1.2));
29+
const c3 = 0.08 + 0.06 * sin((y * 0.28 + t * 0.6 + pi / 4));
30+
const h3 = 90 + 90 * sin((-x * 0.18 - y * 0.24 + t * 1.5 + pi / 6));
31+
const l = (l1 + l2 * 0.7 + l3 * 0.4) / 2.1;
32+
const c = (c1 + c2 * 0.7 + c3 * 0.4) / 2.1;
33+
const h = (h1 + h2 * 0.7 + h3 * 0.4) / 2.1;
34+
const intensity = 1.3;
35+
const vl = max(0, min(1, 0.4 + l * intensity));
36+
const vc = max(0, 0.5 * c * intensity);
37+
const vh = ((h % 360) + 360) % 360;
38+
context.fillStyle = `oklch(${vl} ${vc} ${vh})`;
39+
context.fillRect(x, y, 1, 1);
40+
}
41+
}
42+
frame = requestAnimationFrame(animate);
43+
}
44+
45+
animate(performance.now());
46+
invalidation.then(() => cancelAnimationFrame(frame));
47+
</script>
48+
</notebook>

docs/ex/d3/animated-treemap.html

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<!doctype html>
2+
<notebook theme="air">
3+
<title>Animated treemap</title>
4+
<script id="5" type="text/markdown">
5+
# Animated treemap
6+
7+
This [treemap](./treemap) of U.S. population uses [d3.treemapResquarify](https://d3js.org/d3-hierarchy/treemap#treemapResquarify) for a stable layout with changing node values. Data: [U.S. Census Bureau](https://www.census.gov/population/www/censusdata/pop1790-1990.html)
8+
</script>
9+
<script id="6" type="module">
10+
const input = Scrubber(d3.range(data.keys.length), {
11+
delay: 2500,
12+
loop: false,
13+
format: (i) => data.keys[i]
14+
});
15+
const index = view(input);
16+
</script>
17+
<script id="1" type="module" pinned="">
18+
const width = 928;
19+
const height = width;
20+
21+
// This is normally zero, but could be non-zero if this cell is
22+
// re-evaluated after the animation plays.
23+
const initialIndex = input.value;
24+
25+
// To allow the transition to be interrupted and resumed, we parse
26+
// the displayed text (the state population) to get the current
27+
// value at the start of each transition; parseNumber and
28+
// formatNumber must be symmetric.
29+
const parseNumber = (string) => +string.replace(/,/g, "");
30+
const formatNumber = d3.format(",d");
31+
32+
// Get the maximum total population across the dataset. (We know
33+
// for this dataset that it’s always the last value, but that isn’t
34+
// true in general.) This allows us to scale the rectangles for
35+
// each state to be proportional to the max total.
36+
const max = d3.max(data.keys, (d, i) => d3.hierarchy(data.group).sum((d) => d.values[i]).value);
37+
38+
// The category10 color scheme per state, but faded so that the
39+
// text labels are more easily read.
40+
const color = d3.scaleOrdinal()
41+
.domain(data.group.keys())
42+
.range(d3.schemeCategory10.map((d) => d3.interpolateRgb(d, "white")(0.5)));
43+
44+
// Construct the treemap layout.
45+
const treemap = d3.treemap()
46+
.size([width, height])
47+
.tile(d3.treemapResquarify) // to preserve orientation when animating
48+
.padding((d) => d.height === 1 ? 1 : 0) // only pad parents of leaves
49+
.round(true);
50+
51+
// Compute the structure using the average value (since this
52+
// orientation will be preserved using resquarify across the
53+
// entire animation).
54+
const root = treemap(d3.hierarchy(data.group)
55+
.sum((d) => Array.isArray(d.values) ? d3.sum(d.values) : 0)
56+
.sort((a, b) => b.value - a.value));
57+
58+
const svg = d3.create("svg")
59+
.attr("width", width)
60+
.attr("height", height + 20)
61+
.attr("viewBox", [0, -20, width, height + 20])
62+
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif; overflow: visible;");
63+
64+
// Draw a box representing the total population for each time. Only
65+
// show the boxes after the current time (to avoid distracting gray
66+
// lines in between the padded treemap cells).
67+
const box = svg.append("g")
68+
.selectAll("g")
69+
.data(data.keys.map((key, i) => {
70+
const value = root.sum((d) => d.values[i]).value;
71+
return {key, value, i, k: Math.sqrt(value / max)};
72+
}).reverse())
73+
.join("g")
74+
.attr("transform", ({k}) => `translate(${(1 - k) / 2 * width},${(1 - k) / 2 * height})`)
75+
.attr("opacity", ({i}) => i >= initialIndex ? 1 : 0)
76+
.call((g) => g.append("text")
77+
.attr("y", -6)
78+
.attr("fill", "#777")
79+
.selectAll("tspan")
80+
.data(({key, value}) => [key, ` ${formatNumber(value)}`])
81+
.join("tspan")
82+
.attr("font-weight", (d, i) => i === 0 ? "bold" : null)
83+
.text((d) => d))
84+
.call((g) => g.append("rect")
85+
.attr("fill", "none")
86+
.attr("stroke", "#ccc")
87+
.attr("width", ({k}) => k * width)
88+
.attr("height", ({k}) => k * height));
89+
90+
// Render the leaf nodes of the treemap.
91+
const leaf = svg.append("g")
92+
.selectAll("g")
93+
.data(layout(initialIndex))
94+
.join("g")
95+
.attr("transform", (d) => `translate(${d.x0},${d.y0})`);
96+
97+
leaf.append("rect")
98+
.attr("id", (d, i) => (d.leafUid = `leaf-${i}`))
99+
.attr("fill", (d) => { while (d.depth > 1) d = d.parent; return color(d.data[0]); })
100+
.attr("width", (d) => d.x1 - d.x0)
101+
.attr("height", (d) => d.y1 - d.y0);
102+
103+
// Clip the text to the containing node.
104+
leaf.append("clipPath")
105+
.attr("id", (d, i) => (d.clipUid = `clip-${i}`))
106+
.append("use")
107+
.attr("xlink:href", (d) => `#${d.leafUid}`);
108+
109+
// Generate two tspans for two lines of text (name and value).
110+
const text = leaf.append("text")
111+
.attr("clip-path", (d) => `url(#${d.clipUid})`);
112+
113+
// Safari clipping bug: https://observablehq.com/d/ff34268ca0f9e2b5
114+
text.append("tspan")
115+
.text("\xa0");
116+
117+
text.append("tspan")
118+
.attr("x", 3)
119+
.attr("y", "1.1em")
120+
.text((d) => d.data.name);
121+
122+
text.append("tspan")
123+
.attr("x", 3)
124+
.attr("y", `${1.1 + 1.2}em`)
125+
.attr("fill-opacity", 0.7)
126+
.text((d) => formatNumber(d.value));
127+
128+
leaf.append("title")
129+
.text((d) => d.data.name);
130+
131+
display(svg.node());
132+
133+
// Scale the treemap layout to fit within a centered box whose area
134+
// is proportional to the total current value. This makes the areas
135+
// of each state proportional for the entire animation.
136+
function layout(index) {
137+
const k = Math.sqrt(root.sum((d) => d.values[index]).value / max);
138+
const tx = (1 - k) / 2 * width;
139+
const ty = (1 - k) / 2 * height;
140+
return treemap.size([width * k, height * k])(root)
141+
.each((d) => (d.x0 += tx, d.x1 += tx, d.y0 += ty, d.y1 += ty))
142+
.leaves();
143+
}
144+
145+
// Expose an update method on the chart that allows the caller to
146+
// initiate a transition. The given index represents the frame
147+
// number (0 for the first frame, 1 for the second, etc.).
148+
function update(index, duration) {
149+
box.transition()
150+
.duration(duration)
151+
.attr("opacity", ({i}) => i >= index ? 1 : 0);
152+
153+
leaf.data(layout(index)).transition()
154+
.duration(duration)
155+
.ease(d3.easeLinear)
156+
.attr("transform", (d) => `translate(${d.x0},${d.y0})`)
157+
.call((leaf) => leaf.select("rect")
158+
.attr("width", (d) => d.x1 - d.x0)
159+
.attr("height", (d) => d.y1 - d.y0))
160+
.call((leaf) => leaf.select("text tspan:last-child")
161+
.tween("text", function(d) {
162+
const i = d3.interpolate(parseNumber(this.textContent), d.value);
163+
return function(t) { this.textContent = formatNumber(i(t)); };
164+
}));
165+
}
166+
</script>
167+
<script id="4" type="module" pinned="">
168+
update(index, 2500); // trigger animation from the scrubber
169+
</script>
170+
<script id="3" type="module" pinned="">
171+
const keys = d3.range(1790, 2000, 10);
172+
const [regions, states] = await Promise.all([
173+
FileAttachment("data/census-regions.csv").csv(), // for grouping states hierarchically
174+
FileAttachment("data/population.tsv").tsv() // a wide dataset of state populations over time
175+
]).then(([regions, states]) => [
176+
regions,
177+
states.slice(1).map((d) => ({
178+
name: d[""], // the state name
179+
values: keys.map((key) => +d[key].replace(/,/g, "")) // parse comma-separated numbers
180+
}))
181+
]);
182+
const regionByState = new Map(regions.map((d) => [d.State, d.Region]));
183+
const divisionByState = new Map(regions.map((d) => [d.State, d.Division]));
184+
const data = display({keys, group: d3.group(states, (d) => regionByState.get(d.name), (d) => divisionByState.get(d.name))});
185+
</script>
186+
<script id="2" type="module" pinned="">
187+
import {Scrubber} from "./scrubber.js";
188+
</script>
189+
</notebook>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<!doctype html>
2+
<notebook theme="air">
3+
<title>Antimeridian cutting</title>
4+
<script id="6" type="text/markdown">
5+
# Antimeridian cutting
6+
</script>
7+
<script id="8" type="module">
8+
map(land, {width, invalidation})
9+
</script>
10+
<script id="40" type="text/markdown">
11+
A central challenge of projecting geography is that the globe is spherical while the display is planar. Projecting the globe onto the screen thus requires cutting the globe at least once. Most commonly, world maps are horizontally centered at the [prime meridian](https://en.wikipedia.org/wiki/Prime_Meridian) and cut the globe along ±180° longitude, which is called the *antimeridian*.
12+
</script>
13+
<script id="41" type="text/markdown">
14+
But what happens to shapes that cross the antimeridian, such as the Eastern tip of Russia? When projecting Russia using a normal [cylindrical projection](https://en.wikipedia.org/wiki/Equirectangular_projection), for example, the Western part of Russia appears on the right edge, while the Eastern part appears on the left edge. A naïve projection of lines that cross the antimeridian would also cross the map, leading to distracting visual artifacts!
15+
</script>
16+
<script id="61" type="text/markdown">
17+
To avoid this problem, most freely-available shapefiles are already cut along the antimeridian. This enables geographic software to ignore the topological complexity of a spherical globe. Unfortunately, by relying on pre-cut input, much geographic software cannot handle different aspects and rotations.
18+
</script>
19+
<script id="42" type="text/markdown">
20+
D3 takes a different approach: geometry is represented in spherical coordinates and then cut along the antimeridian during projection. This allows D3 to support arbitrary spherical rotations during projection without visual artifacts. Use your mouse to rotate the world above and see a new aspect.
21+
</script>
22+
<script id="32" type="module" pinned="">
23+
function map(land, {
24+
width = 960,
25+
height = 500,
26+
devicePixelRatio: dpr = devicePixelRatio,
27+
invalidation // optional promise to stop animation
28+
} = {}) {
29+
const canvas = document.createElement("canvas");
30+
canvas.width = width * dpr;
31+
canvas.height = height * dpr;
32+
canvas.style.width = `${width}px`;
33+
canvas.style.height = `${height}px`;
34+
35+
const context = canvas.getContext("2d");
36+
37+
const projection = d3.geoConicEqualArea()
38+
.scale(150 * dpr)
39+
.center([0, 33])
40+
.translate([width * dpr / 2, height * dpr / 2])
41+
.precision(0.3);
42+
43+
const path = d3.geoPath()
44+
.projection(projection)
45+
.context(context);
46+
47+
let frame;
48+
let x0, y0;
49+
let lambda0, phi0;
50+
51+
canvas.onpointermove = (event) => {
52+
if (!event.isPrimary) return;
53+
const [x, y] = d3.pointer(event);
54+
render([
55+
lambda0 + (x - x0) / (width * dpr) * 360,
56+
phi0 - (y - y0) / (height * dpr) * 360
57+
]);
58+
};
59+
60+
canvas.ontouchstart = (event) => {
61+
event.preventDefault();
62+
};
63+
64+
canvas.onpointerenter = (event) => {
65+
if (!event.isPrimary) return;
66+
cancelAnimationFrame(frame);
67+
([x0, y0] = d3.pointer(event));
68+
([lambda0, phi0] = projection.rotate());
69+
};
70+
71+
canvas.onpointerleave = (event) => {
72+
if (!event.isPrimary) return;
73+
frame = requestAnimationFrame(tick);
74+
};
75+
76+
function tick() {
77+
const [lambda, phi] = projection.rotate();
78+
render([lambda + 0.1, phi + 0.1]);
79+
return frame = requestAnimationFrame(tick);
80+
}
81+
82+
function render(rotate) {
83+
projection.rotate(rotate);
84+
context.clearRect(0, 0, width * dpr, height * dpr);
85+
context.beginPath();
86+
path(land);
87+
context.fill();
88+
context.beginPath();
89+
path({type: "Sphere"});
90+
context.lineWidth = dpr;
91+
context.stroke();
92+
}
93+
94+
tick();
95+
96+
if (invalidation) invalidation.then(() => cancelAnimationFrame(frame));
97+
98+
return canvas;
99+
}
100+
</script>
101+
<script id="12" type="module" pinned="">
102+
const world = await FileAttachment("data/land-110m.json").json().then(display);
103+
const land = topojson.feature(world, world.objects.land);
104+
</script>
105+
</notebook>

0 commit comments

Comments
 (0)