|
| 1 | +--- |
| 2 | +title: "Week 14: Extras" |
| 3 | +toc: true |
| 4 | +--- |
| 5 | + |
| 6 | +# Data Art |
| 7 | + |
| 8 | +### Delaunay |
| 9 | + |
| 10 | +Create a small array of 100 data points that are randomly spread across the svg: |
| 11 | +```js echo |
| 12 | +const height = 300 |
| 13 | +const points = Array.from({ length: 100 }, (_) => [ |
| 14 | + Math.random() * width, |
| 15 | + Math.random() * height |
| 16 | +]) |
| 17 | +display(points) |
| 18 | +``` |
| 19 | + |
| 20 | +These points are static, and randomly spread out: |
| 21 | +```js |
| 22 | +Plot.plot({ |
| 23 | + width, |
| 24 | + height, |
| 25 | + marks: [ |
| 26 | + Plot.dot(points) |
| 27 | + ] |
| 28 | +}) |
| 29 | +``` |
| 30 | + |
| 31 | +Given set of points in x and y, the Delaunay marks compute the [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation), its dual the [Voronoi tessellation](https://en.wikipedia.org/wiki/Voronoi_diagram), and the [convex hull](https://en.wikipedia.org/wiki/Convex_hull). The [voronoi mark](https://observablehq.com/plot/marks/delaunay#voronoi) computes the region closest to each point (its Voronoi cell). The cell can be empty if another point shares the exact same coordinates. Together, the cells cover the entire plot. |
| 32 | + |
| 33 | +The voronoi can be considered the space that "belongs" to each point. The lines that make up the voronoi mesh divide these spaces between points. |
| 34 | + |
| 35 | +```js |
| 36 | +Plot.plot({ |
| 37 | + width, |
| 38 | + height, |
| 39 | + marks: [ |
| 40 | + Plot.dot(points), |
| 41 | + Plot.voronoiMesh(points) |
| 42 | + ] |
| 43 | +}) |
| 44 | +``` |
| 45 | + |
| 46 | +This is incredibly helpful with something like tooltips. If you set the tooltip on the voronoi mesh, the user will always see some data, rather than having to carefully position their cursor over a small dot to trigger the tootlip. |
| 47 | + |
| 48 | +```js |
| 49 | +Plot.plot({ |
| 50 | + width, |
| 51 | + height, |
| 52 | + marks: [ |
| 53 | + Plot.dot(points), |
| 54 | + Plot.voronoiMesh(points, { tip: true, opacity: 0.2 }) |
| 55 | + ] |
| 56 | +}) |
| 57 | +``` |
| 58 | + |
| 59 | +We can take this further and make it more "artsy", like a wobly stained glass window effect, by coloring and manipulating the mesh itself with sinusoidal motion: |
| 60 | + |
| 61 | +```js |
| 62 | +// simulate ticking of an animation |
| 63 | +const t = (async function* () { |
| 64 | + for (let j = 0; true; ++j) { |
| 65 | + yield j; |
| 66 | + await new Promise((resolve) => setTimeout(resolve, 50)); |
| 67 | + } |
| 68 | +})(); |
| 69 | +``` |
| 70 | + |
| 71 | +```js echo |
| 72 | +const voronoi = d3.Delaunay.from( |
| 73 | + // move the points in an elliptical pattern |
| 74 | + points.map(([x, y], i) => [ |
| 75 | + x + 1.5 * Math.sin(t + i * i), // horizontal |
| 76 | + y + 2.0 * Math.cos(t + i * i) // vertical |
| 77 | + ]) |
| 78 | +).voronoi([0, 0, width, height]) |
| 79 | +// this part does what `Plot.voronoiMesh` does inherently |
| 80 | +const data = [...voronoi.cellPolygons()].flatMap((cell) => |
| 81 | + cell.map(([x, y]) => ({ x, y, i: cell.index })) |
| 82 | +) |
| 83 | +display(data) |
| 84 | +``` |
| 85 | + |
| 86 | +```js |
| 87 | +Plot.plot({ |
| 88 | + width, |
| 89 | + height, |
| 90 | + x: { axis: null }, |
| 91 | + y: { axis: null }, |
| 92 | + marks: [ |
| 93 | + Plot.dot(points), |
| 94 | + Plot.line(data, { |
| 95 | + x: "x", |
| 96 | + y: "y", |
| 97 | + fill: "i", |
| 98 | + stroke: "white", |
| 99 | + strokeWidth: 5, |
| 100 | + }) |
| 101 | + ] |
| 102 | +}) |
| 103 | +``` |
| 104 | +source: [Plot example](https://observablehq.com/@lsh/plot-voronoi) |
| 105 | + |
0 commit comments