Skip to content

Commit 661e0ff

Browse files
Filpalewire
andauthored
New example: d3/canvas to png (#1514)
* Add canvas to png example * scale up * add a link in examples/README --------- Co-authored-by: palewire <[email protected]>
1 parent 474220b commit 661e0ff

File tree

8 files changed

+183
-25
lines changed

8 files changed

+183
-25
lines changed

examples/README.md

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,39 +45,40 @@
4545

4646
### Charts
4747

48-
- [`geotiff`](https://observablehq.observablehq.cloud/framework-example-geotiff/) - Parsing GeoTIFF with geotiff.js, then visualizing with Observable Plot
49-
- [`netcdf`](https://observablehq.observablehq.cloud/framework-example-netcdf/) - Parsing NetCDF with `netcdfjs`, then visualizing with Observable Plot
48+
- [`geotiff`](https://observablehq.observablehq.cloud/framework-example-geotiff/) - Parse GeoTIFF with geotiff.js, then visualize with Observable Plot
49+
- [`netcdf`](https://observablehq.observablehq.cloud/framework-example-netcdf/) - Parse NetCDF with `netcdfjs`, then visualize with Observable Plot
5050
- [`vega-dark`](https://observablehq.observablehq.cloud/framework-example-vega-dark/) - Responsive dark mode in Vega-Lite
5151
- [`vega-responsive`](https://observablehq.observablehq.cloud/framework-example-vega-responsive/) - Responsive width in Vega-Lite using ResizeObserver
5252

5353
### Data loaders
5454

55-
- [`loader-airtable`](https://observablehq.observablehq.cloud/framework-example-loader-airtable/) - Loading data from Airtable
56-
- [`loader-arrow`](https://observablehq.observablehq.cloud/framework-example-loader-arrow/) - Generating Apache Arrow IPC files
57-
- [`loader-census`](https://observablehq.observablehq.cloud/framework-example-loader-census/) - Loading and transform shapefiles from the U.S. Census Bureau
58-
- [`loader-databricks`](https://observablehq.observablehq.cloud/framework-example-loader-databricks/) - Loading data from Databricks
59-
- [`loader-duckdb`](https://observablehq.observablehq.cloud/framework-example-loader-duckdb/) - Processing data with DuckDB
60-
- [`loader-elasticsearch`](https://observablehq.observablehq.cloud/framework-example-loader-elasticsearch/) - Loading data from Elasticsearch
61-
- [`loader-github`](https://observablehq.observablehq.cloud/framework-example-loader-github/) - Loading data from GitHub
62-
- [`loader-google-analytics`](https://observablehq.observablehq.cloud/framework-example-loader-google-analytics/) - Loading data from Google Analytics
63-
- [`loader-julia-to-txt`](https://observablehq.observablehq.cloud/framework-example-loader-julia-to-txt/) - Generating TXT from Julia
64-
- [`loader-parquet`](https://observablehq.observablehq.cloud/framework-example-loader-parquet/) - Generating Apache Parquet files
65-
- [`loader-postgres`](https://observablehq.observablehq.cloud/framework-example-loader-postgres/) - Loading data from PostgreSQL
66-
- [`loader-python-to-csv`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-csv/) - Generating CSV from Python
67-
- [`loader-python-to-png`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-png/) - Generating PNG from Python
68-
- [`loader-python-to-zip`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-zip/) - Generating ZIP from Python
69-
- [`loader-r-to-csv`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-csv/) - Generating CSV from R
70-
- [`loader-r-to-jpeg`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-jpeg/) - Generating JPEG from R
71-
- [`loader-r-to-json`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-json/) - Generating JSON from R
72-
- [`loader-rust-to-json`](https://observablehq.observablehq.cloud/framework-example-loader-rust-to-json/) - Generating JSON from Rust
73-
- [`loader-snowflake`](https://observablehq.observablehq.cloud/framework-example-loader-snowflake/) - Loading data from Snowflake
74-
- [`netcdf-contours`](https://observablehq.observablehq.cloud/framework-example-netcdf-contours/) - Converting NetCDF to GeoJSON with `netcdfjs` and `d3-geo-voronoi`
55+
- [`loader-airtable`](https://observablehq.observablehq.cloud/framework-example-loader-airtable/) - Load data from Airtable
56+
- [`loader-arrow`](https://observablehq.observablehq.cloud/framework-example-loader-arrow/) - Generate Apache Arrow IPC files
57+
- [`loader-canvas-to-png`](https://observablehq.observablehq.cloud/framework-example-loader-canvas-to-png/) - Generate PNG using node-canvas
58+
- [`loader-census`](https://observablehq.observablehq.cloud/framework-example-loader-census/) - Load and transform shapefiles from the U.S. Census Bureau
59+
- [`loader-databricks`](https://observablehq.observablehq.cloud/framework-example-loader-databricks/) - Load data from Databricks
60+
- [`loader-duckdb`](https://observablehq.observablehq.cloud/framework-example-loader-duckdb/) - Process data with DuckDB
61+
- [`loader-elasticsearch`](https://observablehq.observablehq.cloud/framework-example-loader-elasticsearch/) - Load data from Elasticsearch
62+
- [`loader-github`](https://observablehq.observablehq.cloud/framework-example-loader-github/) - Load data from GitHub
63+
- [`loader-google-analytics`](https://observablehq.observablehq.cloud/framework-example-loader-google-analytics/) - Load data from Google Analytics
64+
- [`loader-julia-to-txt`](https://observablehq.observablehq.cloud/framework-example-loader-julia-to-txt/) - Generate TXT from Julia
65+
- [`loader-parquet`](https://observablehq.observablehq.cloud/framework-example-loader-parquet/) - Generat Apache Parquet files
66+
- [`loader-postgres`](https://observablehq.observablehq.cloud/framework-example-loader-postgres/) - Load data from PostgreSQL
67+
- [`loader-python-to-csv`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-csv/) - Generate CSV from Python
68+
- [`loader-python-to-png`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-png/) - Generate PNG from Python
69+
- [`loader-python-to-zip`](https://observablehq.observablehq.cloud/framework-example-loader-python-to-zip/) - Generate ZIP from Python
70+
- [`loader-r-to-csv`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-csv/) - Generate CSV from R
71+
- [`loader-r-to-jpeg`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-jpeg/) - Generate JPEG from R
72+
- [`loader-r-to-json`](https://observablehq.observablehq.cloud/framework-example-loader-r-to-json/) - Generate JSON from R
73+
- [`loader-rust-to-json`](https://observablehq.observablehq.cloud/framework-example-loader-rust-to-json/) - Generate JSON from Rust
74+
- [`loader-snowflake`](https://observablehq.observablehq.cloud/framework-example-loader-snowflake/) - Load data from Snowflake
75+
- [`netcdf-contours`](https://observablehq.observablehq.cloud/framework-example-netcdf-contours/) - Convert NetCDF to GeoJSON with `netcdfjs` and `d3-geo-voronoi`
7576

7677
### Inputs
7778

7879
- [`codemirror`](https://observablehq.observablehq.cloud/framework-example-codemirror/) - A text input powered by CodeMirror
7980
- [`custom-input-2d`](https://observablehq.observablehq.cloud/framework-example-custom-input-2d/) - A custom 2D input with bidirectional binding
80-
- [`input-select-file`](https://observablehq.observablehq.cloud/framework-example-input-select-file/) - Selecting a file from a drop-down menu
81+
- [`input-select-file`](https://observablehq.observablehq.cloud/framework-example-input-select-file/) - Select a file from a drop-down menu
8182

8283
### Markdown
8384

@@ -87,8 +88,8 @@
8788

8889
### Other
8990

90-
- [`chess`](https://observablehq.observablehq.cloud/framework-example-chess/) - Loading Zip data from FIDE; creating a bump chart with Observable Plot
91-
- [`custom-stylesheet`](https://observablehq.observablehq.cloud/framework-example-custom-stylesheet/) - Defining a custom stylesheet (custom theme)
91+
- [`chess`](https://observablehq.observablehq.cloud/framework-example-chess/) - Load Zip data from FIDE; create a bump chart with Observable Plot
92+
- [`custom-stylesheet`](https://observablehq.observablehq.cloud/framework-example-custom-stylesheet/) - Define a custom stylesheet (custom theme)
9293
- [`google-analytics`](https://observablehq.observablehq.cloud/framework-example-google-analytics/) - A Google Analytics dashboard with numbers and charts
9394
- [`hello-world`](https://observablehq.observablehq.cloud/framework-example-hello-world/) - A minimal Framework project
9495
- [`intersection-observer`](https://observablehq.observablehq.cloud/framework-example-intersection-observer/) - Scrollytelling with IntersectionObserver
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/dist/
3+
node_modules/
4+
yarn-error.log
5+
.venv
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[Framework examples →](../)
2+
3+
# Data loader to generate PNG from canvas
4+
5+
View live: <https://observablehq.observablehq.cloud/framework-example-loader-canvas-to-png/>
6+
7+
This Observable Framework example demonstrates how to write a Node.js data loader that creates a map using [node-canvas](https://github.com/Automattic/node-canvas) and D3, then writes the map to standard out as a PNG. The data loader lives in [`src/data/us-map.png.js`](./src/data/us-map.png.js).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
root: "src"
3+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"type": "module",
3+
"private": true,
4+
"scripts": {
5+
"clean": "rimraf src/.observablehq/cache",
6+
"build": "rimraf dist && observable build",
7+
"dev": "observable preview",
8+
"deploy": "observable deploy",
9+
"observable": "observable"
10+
},
11+
"dependencies": {
12+
"@observablehq/framework": "latest",
13+
"topojson-client": "^3.0.0",
14+
"d3": "^7.0.0",
15+
"canvas": "^2.8.0"
16+
},
17+
"devDependencies": {
18+
"rimraf": "^5.0.5"
19+
},
20+
"engines": {
21+
"node": ">=18"
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.observablehq/cache/
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {createCanvas} from "canvas";
2+
import {geoPath} from "d3";
3+
import * as topojson from "topojson-client";
4+
5+
// Get the map file from the US Atlas package
6+
// https://github.com/topojson/us-atlas
7+
const url = "https://cdn.jsdelivr.net/npm/us-atlas@3/counties-albers-10m.json";
8+
const us = await fetch(url).then(response => response.json());
9+
10+
// Create and configure a canvas
11+
const width = 975;
12+
const height = 610;
13+
const canvas = createCanvas(width * 2, height * 2);
14+
const context = canvas.getContext("2d");
15+
context.scale(2, 2);
16+
17+
// https://observablehq.com/@d3/u-s-map-canvas
18+
context.lineJoin = "round";
19+
context.lineCap = "round";
20+
// Use the null projection, since coordinates in US Atlas are already projected.
21+
const path = geoPath(null, context);
22+
23+
context.fillStyle = "#fff";
24+
context.fillRect(0, 0, width, height);
25+
26+
context.beginPath();
27+
path(topojson.mesh(us, us.objects.counties, (a, b) => a !== b && (a.id / 1000 | 0) === (b.id / 1000 | 0)));
28+
context.lineWidth = 0.5;
29+
context.strokeStyle = "#aaa";
30+
context.stroke();
31+
32+
context.beginPath();
33+
path(topojson.mesh(us, us.objects.states, (a, b) => a !== b));
34+
context.lineWidth = 0.5;
35+
context.strokeStyle = "#000";
36+
context.stroke();
37+
38+
context.beginPath();
39+
path(topojson.feature(us, us.objects.nation));
40+
context.lineWidth = 1;
41+
context.strokeStyle = "#000";
42+
context.stroke();
43+
44+
// Write the canvas to a PNG buffer
45+
const buffer = canvas.toBuffer("image/png");
46+
47+
// Pipe the buffer to process.stdout
48+
process.stdout.write(buffer);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Data loader to generate PNG from canvas
2+
3+
Here’s a Node.js data loader that creates a map using [node-canvas](https://github.com/Automattic/node-canvas) and D3, then writes the map to standard out as a PNG.
4+
5+
This pattern can be used to render maps that require with a large amount of data as lightweight static files.
6+
7+
```js run=false
8+
import {createCanvas} from "canvas";
9+
import {geoPath} from "d3";
10+
import * as topojson from "topojson-client";
11+
12+
// Get the map file from the US Atlas package
13+
// https://github.com/topojson/us-atlas
14+
const url = "https://cdn.jsdelivr.net/npm/us-atlas@3/counties-albers-10m.json";
15+
const us = await fetch(url).then(response => response.json());
16+
17+
// Create and configure a canvas
18+
const width = 975;
19+
const height = 610;
20+
const canvas = createCanvas(width * 2, height * 2);
21+
const context = canvas.getContext("2d");
22+
context.scale(2, 2);
23+
24+
// https://observablehq.com/@d3/u-s-map-canvas
25+
context.lineJoin = "round";
26+
context.lineCap = "round";
27+
// Use the null projection, since coordinates in US Atlas are already projected.
28+
const path = geoPath(null, context);
29+
30+
context.fillStyle = "#fff";
31+
context.fillRect(0, 0, width, height);
32+
33+
context.beginPath();
34+
path(topojson.mesh(us, us.objects.counties, (a, b) => a !== b && (a.id / 1000 | 0) === (b.id / 1000 | 0)));
35+
context.lineWidth = 0.5;
36+
context.strokeStyle = "#aaa";
37+
context.stroke();
38+
39+
context.beginPath();
40+
path(topojson.mesh(us, us.objects.states, (a, b) => a !== b));
41+
context.lineWidth = 0.5;
42+
context.strokeStyle = "#000";
43+
context.stroke();
44+
45+
context.beginPath();
46+
path(topojson.feature(us, us.objects.nation));
47+
context.lineWidth = 1;
48+
context.strokeStyle = "#000";
49+
context.stroke();
50+
51+
// Write the canvas to a PNG buffer
52+
const buffer = canvas.toBuffer("image/png");
53+
54+
// Pipe the buffer to process.stdout
55+
process.stdout.write(buffer);
56+
```
57+
58+
<div class="note">
59+
60+
To run this data loader, you’ll need to add the `d3`, `topojson-client` and `canvas` libraries as dependencies with `npm add d3 topojson-client canvas` or `yarn add d3 topojson-client canvas`.
61+
62+
</div>
63+
64+
The above data loader lives in `data/us-map.png.js`, so we can access the image using `data/us-map.png`:
65+
66+
```html run=false
67+
<img src="data/us-map.png" style="max-width: 975px;">
68+
```
69+
70+
<img src="data/us-map.png" style="max-width: 975px;">

0 commit comments

Comments
 (0)