@samizdata/create-mozaic 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +19 -2
  3. package/bin/create-mozaic.js +10 -0
  4. package/package.json +32 -3
  5. package/src/index.js +157 -0
  6. package/template/.claude/skills/mozaic-chart/SKILL.md +27 -0
  7. package/template/.github/workflows/deploy.yml +38 -0
  8. package/template/AGENTS.md +32 -0
  9. package/template/CLAUDE.md +1 -0
  10. package/template/README.md +57 -0
  11. package/template/_gitignore +13 -0
  12. package/template/analysis/R/01_prepare.R +21 -0
  13. package/template/analysis/py/prepare.py +37 -0
  14. package/template/data/processed/example.csv +31 -0
  15. package/template/data/raw/.gitkeep +0 -0
  16. package/template/justfile +25 -0
  17. package/template/mozaic.config.ts +15 -0
  18. package/template/package.json +41 -0
  19. package/template/pyproject.toml +6 -0
  20. package/template/src/app.d.ts +13 -0
  21. package/template/src/app.html +12 -0
  22. package/template/src/charts/example/Chart.svelte +100 -0
  23. package/template/src/charts/example/data.ts +12 -0
  24. package/template/src/charts/example/messages.ts +51 -0
  25. package/template/src/charts/index.ts +27 -0
  26. package/template/src/lib/theme.ts +11 -0
  27. package/template/src/routes/+layout.svelte +5 -0
  28. package/template/src/routes/+layout.ts +3 -0
  29. package/template/src/routes/+page.svelte +120 -0
  30. package/template/src/routes/embed/[chart]/[locale]/+page.svelte +20 -0
  31. package/template/src/routes/embed/[chart]/[locale]/+page.ts +14 -0
  32. package/template/src/routes/embed/manifest.json/+server.ts +26 -0
  33. package/template/src/routes/embed.js/+server.ts +9 -0
  34. package/template/tsconfig.json +7 -0
  35. package/template/vite.config.ts +18 -0
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # create-mozaic
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scaffold a mozaic analysis project: R/Python data scripts plus Svelte charts built as static embeds.
4
+
5
+ ```bash
6
+ pnpm create @samizdata/mozaic budget-2026 --locales ro,ru --title "Budget 2026"
7
+ ```
8
+
9
+ | Option | |
10
+ | ------------------- | ------------------------------------------------------------------------------- |
11
+ | `--locales` | comma-separated `en`, `ro`, `ru` (default `en`) |
12
+ | `--title` | project title |
13
+ | `--name` | package name (default: from the folder) |
14
+ | `--mozaic`, `--cli` | a version range, or a local path to `packages/mozaic` / `packages/cli` (linked) |
15
+ | `--no-git` | skip `git init` |
16
+ | `--install` | run `pnpm install` |
17
+
18
+ The generated project contains `analysis/` (R and Python), `data/`, `src/charts/` with an example chart,
19
+ a preview page, embed routes, `mozaic.config.ts`, a house style in `src/lib/theme.ts` (mozaic's `base`; swap in an outlet's brand theme), a `justfile`, `AGENTS.md`, a Claude Code skill and a
20
+ GitHub Pages deploy workflow.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/index.js';
3
+
4
+ run(process.argv.slice(2)).then(
5
+ (code) => process.exit(code),
6
+ (error) => {
7
+ console.error(`create-mozaic: ${error instanceof Error ? error.message : error}`);
8
+ process.exit(1);
9
+ }
10
+ );
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@samizdata/create-mozaic",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Scaffold a mozaic analysis project: R/Python data scripts plus Svelte charts built as static embeds.",
5
+ "license": "GPL-3.0-or-later",
6
+ "author": "Nicu Calcea",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/samizdata-co/mozaic.git",
10
+ "directory": "packages/create-mozaic"
11
+ },
12
+ "homepage": "https://github.com/samizdata-co/mozaic#readme",
13
+ "bugs": "https://github.com/samizdata-co/mozaic/issues",
14
+ "type": "module",
15
+ "bin": {
16
+ "create-mozaic": "./bin/create-mozaic.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "template"
22
+ ],
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "typescript": "^6.0.3",
26
+ "vitest": "^4.1.12"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.17"
30
+ },
31
+ "scripts": {
32
+ "test": "vitest --run",
33
+ "check": "tsc -p jsconfig.json"
34
+ }
6
35
  }
package/src/index.js ADDED
@@ -0,0 +1,157 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { cp, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { basename, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createRequire } from 'node:module';
6
+
7
+ export const LOCALES = ['en', 'ro', 'ru'];
8
+ const TEMPLATE = fileURLToPath(new URL('../template', import.meta.url));
9
+ // mozaic, @samizdata/mozaic-cli and create-mozaic are released together with the same version.
10
+ const { version } = createRequire(import.meta.url)('../package.json');
11
+ const TEXT = /\.(svelte|ts|js|json|md|yml|yaml|toml|R|py|csv|html|txt)$|^(justfile|_gitignore)$/;
12
+
13
+ /**
14
+ * @typedef {{ dir: string, name?: string, title?: string, locales?: string[], mozaic?: string, cli?: string }} ScaffoldOptions
15
+ */
16
+
17
+ /** @param {string} value */
18
+ export function slugify(value) {
19
+ return value
20
+ .normalize('NFKD')
21
+ .replace(/[̀-ͯ]/g, '')
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9]+/g, '-')
24
+ .replace(/^-|-$/g, '');
25
+ }
26
+
27
+ /**
28
+ * A dependency spec for mozaic packages: a version range, or `link:` for a local path.
29
+ * @param {string} value
30
+ */
31
+ function spec(value) {
32
+ return /^[.~/]/.test(value) ? `link:${resolve(value)}` : value;
33
+ }
34
+
35
+ /**
36
+ * @param {string} dir
37
+ * @returns {AsyncGenerator<string>}
38
+ */
39
+ async function* walk(dir) {
40
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
41
+ const path = join(dir, entry.name);
42
+ if (entry.isDirectory()) yield* walk(path);
43
+ else yield path;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Copy the template into `dir` and fill in its placeholders.
49
+ * @param {ScaffoldOptions} options
50
+ * @returns {Promise<{ dir: string, name: string }>}
51
+ */
52
+ export async function scaffold(options) {
53
+ const dir = resolve(options.dir);
54
+ const name = slugify(options.name ?? basename(dir));
55
+ if (!name) throw new Error('Could not derive a project name; pass --name.');
56
+ const locales = options.locales ?? ['en'];
57
+ const unknown = locales.filter((l) => !LOCALES.includes(l));
58
+ if (unknown.length || !locales.length)
59
+ throw new Error(`Unknown locale(s) ${unknown.join(', ')}. Locales: ${LOCALES.join(', ')}.`);
60
+
61
+ const existing = await readdir(dir).catch(() => []);
62
+ if (existing.length) throw new Error(`${dir} is not empty.`);
63
+
64
+ await cp(TEMPLATE, dir, {
65
+ recursive: true,
66
+ filter: (src) => !/[/\\](node_modules|\.svelte-kit|build)([/\\]|$)/.test(src)
67
+ });
68
+ await rename(join(dir, '_gitignore'), join(dir, '.gitignore'));
69
+
70
+ /** @type {Record<string, string>} */
71
+ const replacements = {
72
+ __PROJECT_NAME__: name,
73
+ __PROJECT_TITLE__: options.title ?? name,
74
+ __LOCALES__: JSON.stringify(locales).replace(/"/g, "'"),
75
+ __MOZAIC_SPEC__: spec(options.mozaic ?? `^${version}`),
76
+ __CLI_SPEC__: spec(options.cli ?? `^${version}`)
77
+ };
78
+ for await (const file of walk(dir)) {
79
+ if (!TEXT.test(basename(file))) continue;
80
+ const source = await readFile(file, 'utf8');
81
+ const filled = source.replace(/__[A-Z_]+__/g, (token) => replacements[token] ?? token);
82
+ if (filled !== source) await writeFile(file, filled);
83
+ }
84
+ return { dir, name };
85
+ }
86
+
87
+ /**
88
+ * @param {string} command
89
+ * @param {string[]} args
90
+ * @param {string} cwd
91
+ */
92
+ function exec(command, args, cwd) {
93
+ return new Promise((ok, fail) => {
94
+ const child = spawn(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
95
+ child.on('exit', (code) => (code === 0 ? ok(undefined) : fail(new Error(`${command} ${args.join(' ')} failed`))));
96
+ child.on('error', fail);
97
+ });
98
+ }
99
+
100
+ const HELP = `create-mozaic — start a new mozaic analysis project
101
+
102
+ Usage:
103
+ pnpm create @samizdata/mozaic <dir> [options]
104
+
105
+ Options:
106
+ --locales <list> comma-separated: ${LOCALES.join(',')} (default: en)
107
+ --title <text> project title (default: the folder name)
108
+ --name <slug> package name (default: from the folder name)
109
+ --mozaic <spec|path> mozaic version, or a local path to packages/mozaic
110
+ --cli <spec|path> @samizdata/mozaic-cli version, or a local path to packages/cli
111
+ --git / --no-git initialise a git repository (default: yes)
112
+ --install run pnpm install afterwards
113
+ `;
114
+
115
+ /**
116
+ * @param {string[]} argv
117
+ * @returns {Promise<number>}
118
+ */
119
+ export async function run(argv) {
120
+ /** @type {Record<string, string | boolean>} */
121
+ const options = {};
122
+ /** @type {string[]} */
123
+ const positionals = [];
124
+ for (let i = 0; i < argv.length; i++) {
125
+ const arg = argv[i];
126
+ if (!arg.startsWith('--')) positionals.push(arg);
127
+ else if (arg.startsWith('--no-')) options[arg.slice(5)] = false;
128
+ else if (arg.includes('=')) options[arg.slice(2, arg.indexOf('='))] = arg.slice(arg.indexOf('=') + 1);
129
+ else if (argv[i + 1] && !argv[i + 1].startsWith('--')) options[arg.slice(2)] = argv[++i];
130
+ else options[arg.slice(2)] = true;
131
+ }
132
+ if (options.help || !positionals[0]) {
133
+ console.log(HELP);
134
+ return options.help ? 0 : 1;
135
+ }
136
+
137
+ const str = (/** @type {string} */ key) =>
138
+ typeof options[key] === 'string' ? /** @type {string} */ (options[key]) : undefined;
139
+ const { dir, name } = await scaffold({
140
+ dir: positionals[0],
141
+ name: str('name'),
142
+ title: str('title'),
143
+ locales: str('locales')?.split(','),
144
+ mozaic: str('mozaic'),
145
+ cli: str('cli')
146
+ });
147
+ if (options.git !== false)
148
+ await exec('git', ['init', '-q'], dir).catch(() => console.warn('git init failed; skipping.'));
149
+ if (options.install) await exec('pnpm', ['install'], dir);
150
+
151
+ const relative = positionals[0];
152
+ console.log(
153
+ `\nCreated ${name} in ${dir}\n\nNext:\n cd ${relative}\n${options.install ? '' : ' pnpm install\n'} pnpm dev\n`
154
+ );
155
+ await stat(dir);
156
+ return 0;
157
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: mozaic-chart
3
+ description: Build or change a chart in this mozaic analysis project. Use when asked to add a chart, change how a chart looks, or chart new data.
4
+ ---
5
+
6
+ # Build a mozaic chart
7
+
8
+ 1. **Data.** Find or create the tidy CSV in `data/processed/`. If it does not exist, add a step to
9
+ the analysis script (`analysis/R` or `analysis/py`) that writes it, and run it.
10
+ 2. **Form.** Pick the chart from what the data must show: magnitude → bars; change over time →
11
+ lines; parts of a whole → stacked bars (`StackedBarChart`) or a waffle; parts of a whole across groups of different sizes → mosaic (`MosaicChart`); distribution → beeswarm or histogram;
12
+ geography → choropleth; two values per item → dumbbell or slope; one item at two dates → arrow plot (`ArrowChart`), or bars
13
+ with a dot for the earlier date (`BarDotChart`) when the latest value matters most. One number → no chart.
14
+ 3. **Files.** Copy `src/charts/example/` to `src/charts/<slug>/`. Edit `data.ts` (import the CSV
15
+ with `?raw`, parse with `csvParse(raw, autoType)`, type the rows), `messages.ts` (title,
16
+ subtitle, alt, notes, source per language) and `Chart.svelte`.
17
+ 4. **Compose.** If a recipe in `@samizdata/mozaic/recipes` fits (`BarChart`, `ColumnChart`, `LineChart`,
18
+ `ScatterChart`, `ArrowChart`, `BarDotChart`, `StackedBarChart`, `MosaicChart`), wrap it: pass the data, accessors and strings. A new
19
+ form that other charts could reuse goes into mozaic as a recipe, not into this project.
20
+ Otherwise use `Chart` / `Header` / `Plot` / `Svg` / marks / guides / `Footer` from mozaic.
21
+ Prefer existing marks (`Bars`, `Line`, `Area`, `Points`, `Rects`, `Text`, `GeoPath`, `Treemap`,
22
+ `Waffle`, `Dumbbell`, `Slope`, `Beeswarm`) and annotations (`Callout`, `RefLine`, `RefBand`,
23
+ `Label`). Write raw SVG with `usePlot()` scales only when no mark fits.
24
+ 5. **Register** the chart in `src/charts/index.ts` (slug, title, component, print widths).
25
+ 6. **Check.** Run `pnpm check`. Open the preview with `pnpm dev` and look at the chart at 320 px
26
+ and 1000 px, in every language and in print; fix overlaps, clipped labels and empty states.
27
+ 7. **Build.** `pnpm build` must succeed; `pnpm export` produces print files when needed.
@@ -0,0 +1,38 @@
1
+ # Builds the embeds and deploys them to GitHub Pages on every push to main.
2
+ # Enable Pages (Settings → Pages → Source: GitHub Actions) before the first run.
3
+ # Pages serves the site from a sub-path; embed pages use relative links, so that works.
4
+ name: Deploy
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+ pages: write
14
+ id-token: write
15
+
16
+ jobs:
17
+ deploy:
18
+ runs-on: ubuntu-latest
19
+ environment:
20
+ name: github-pages
21
+ url: ${{ steps.deployment.outputs.page_url }}
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: pnpm/action-setup@v4
25
+ with:
26
+ version: 12
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: 24
30
+ cache: pnpm
31
+ - run: pnpm install --frozen-lockfile
32
+ - run: pnpm check
33
+ - run: pnpm build
34
+ - uses: actions/upload-pages-artifact@v3
35
+ with:
36
+ path: build
37
+ - id: deployment
38
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,32 @@
1
+ # AGENTS.md
2
+
3
+ This repository holds the data analysis and charts for one story, built with
4
+ the mozaic chart library (Svelte 5 + d3). House style: `src/lib/theme.ts`. Languages: see
5
+ `mozaic.config.ts`.
6
+
7
+ ## Where things go
8
+
9
+ - Data wrangling: `analysis/R` or `analysis/py`. Output tidy CSV to `data/processed/`.
10
+ - Charts: `src/charts/<slug>/` with `Chart.svelte` (the chart), `data.ts` (load and type the
11
+ CSV), `messages.ts` (every user-facing string, per language). Register in `src/charts/index.ts`.
12
+ - Do not edit `src/routes/embed/**`, `manifest.json` or `embed.js` routes; they are generic.
13
+
14
+ ## Chart conventions
15
+
16
+ - Wrap everything in `<Chart {style} {locale} {medium} {width} alt="…">` with `Header`
17
+ (`Title`, `Subtitle`), a `Plot`, and `Footer` (`Notes`, `Source`).
18
+ - Scales are plain d3 scales. Set the domain; `<Plot>` sets the range.
19
+ - Draw in `<Svg>`; tooltips go in `<Html>`. Use `{#snippet children(plot)}` on `<Plot>`
20
+ to get ranged scales for tooltip positions.
21
+ - Colours: `style.palettes` (the theme each chart receives); assign categorical colours by entity in order.
22
+ Text uses text colours (`var(--mz-color-text)`), never data colours.
23
+ - Every chart needs `alt` text, a legend for 2+ series, and strings in every language.
24
+ - Use the theme's breakpoints (`plot.breakpoint`) for small-screen layouts.
25
+
26
+ ## Before you finish
27
+
28
+ - `pnpm check` passes.
29
+ - Look at the preview (`pnpm dev`) at 320 px and 1000 px, in every language, and in print medium.
30
+ - `pnpm build` succeeds.
31
+
32
+ The full component reference and examples are in the mozaic docs site (the library's `apps/docs`).
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,57 @@
1
+ # __PROJECT_TITLE__
2
+
3
+ Charts for one piece of analysis, built with mozaic. The house style is in
4
+ `src/lib/theme.ts`.
5
+
6
+ ## Layout
7
+
8
+ ```
9
+ analysis/ R and Python scripts: data/raw → data/processed
10
+ data/raw/ source files (not committed)
11
+ data/processed/ tidy CSV the charts read (committed)
12
+ src/charts/ one folder per chart (Chart.svelte, data.ts, messages.ts) + index.ts registry
13
+ src/lib/theme.ts the house style
14
+ src/routes/ preview page, embed pages, manifest, embed.js (no need to edit)
15
+ mozaic.config.ts project name, languages, public URL
16
+ ```
17
+
18
+ ## Workflow
19
+
20
+ ```bash
21
+ pnpm install
22
+ just data # run the analysis scripts (or: Rscript analysis/R/01_prepare.R)
23
+ pnpm dev # preview at http://localhost:5173
24
+ pnpm check # type-check
25
+ pnpm build # static embeds in build/
26
+ pnpm export # SVG, PDF and PNG in exports/ (needs a Chromium; see below)
27
+ ```
28
+
29
+ ## Adding a chart
30
+
31
+ 1. Write the data to `data/processed/<name>.csv` from your analysis script (tidy, one row per observation).
32
+ 2. Copy `src/charts/example/` to `src/charts/<name>/` and edit `data.ts`, `messages.ts` and `Chart.svelte`.
33
+ 3. Register it in `src/charts/index.ts`.
34
+ 4. Check it in the preview at small (320 px) and large widths, in each language, and in print.
35
+
36
+ See `AGENTS.md` for the conventions, and the mozaic gallery for examples to copy.
37
+
38
+ ## Publishing
39
+
40
+ `pnpm build` writes a static site to `build/`. Upload it anywhere (the included
41
+ GitHub Actions workflow deploys to GitHub Pages). Set `baseUrl` in
42
+ `mozaic.config.ts` to the public URL so the preview page shows correct embed codes.
43
+
44
+ Each chart is at `embed/<slug>/<locale>/`. To embed it, paste the code from the
45
+ preview page into the CMS. It is an iframe plus `embed.js`, which resizes the
46
+ iframe to fit the chart.
47
+
48
+ ## Print and image export
49
+
50
+ `pnpm export` renders every embed with a headless Chromium and writes, per chart
51
+ and language:
52
+
53
+ - `web.png` (2×) and `web.svg` at 640 px
54
+ - `print-<width>.svg` and `print-<width>.pdf` for each print width in `src/charts/index.ts`
55
+
56
+ SVG text stays editable. If no Chromium is found, run `npx playwright install chromium`
57
+ or pass `--browser /path/to/chrome`.
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ .svelte-kit
3
+ build
4
+ exports
5
+ .DS_Store
6
+ .env
7
+ .env.*
8
+ data/raw/*
9
+ !data/raw/.gitkeep
10
+ .venv
11
+ __pycache__
12
+ .Rhistory
13
+ renv/library
@@ -0,0 +1,21 @@
1
+ # Prepare data for the charts.
2
+ # Reads raw files from data/raw/ and writes tidy, long-format CSV to data/processed/.
3
+ # Run from the project root: Rscript analysis/R/01_prepare.R
4
+ # Uses base R only; add packages with renv (renv::init()) if you need them.
5
+
6
+ raw_dir <- "data/raw"
7
+ out_dir <- "data/processed"
8
+ dir.create(out_dir, showWarnings = FALSE, recursive = TRUE)
9
+
10
+ # Replace this block with your own wrangling. It writes the example dataset.
11
+ years <- 2015:2024
12
+ start <- c(North = 42, South = 35, Capital = 55)
13
+ growth <- c(North = 1.6, South = 0.95, Capital = 3.9)
14
+ dip_2020 <- c(North = 4, South = 3, Capital = 7)
15
+ rows <- do.call(rbind, lapply(names(start), function(region) {
16
+ value <- start[[region]] + growth[[region]] * (years - 2015) - ifelse(years == 2020, dip_2020[[region]], 0)
17
+ data.frame(year = years, region = region, value = round(value))
18
+ }))
19
+
20
+ write.csv(rows, file.path(out_dir, "example.csv"), row.names = FALSE, quote = FALSE)
21
+ message("Wrote ", nrow(rows), " rows to ", file.path(out_dir, "example.csv"))
@@ -0,0 +1,37 @@
1
+ """Prepare data for the charts.
2
+
3
+ Reads raw files from data/raw/ and writes tidy, long-format CSV to
4
+ data/processed/. Run from the project root: uv run analysis/py/prepare.py
5
+ Uses the standard library only; add pandas/polars to pyproject.toml if needed.
6
+ """
7
+
8
+ import csv
9
+ from pathlib import Path
10
+
11
+ OUT = Path("data/processed")
12
+
13
+
14
+ def main() -> None:
15
+ OUT.mkdir(parents=True, exist_ok=True)
16
+ # Replace this with your own wrangling. It writes the example dataset.
17
+ start = {"North": 42, "South": 35, "Capital": 55}
18
+ growth = {"North": 1.6, "South": 0.95, "Capital": 3.9}
19
+ dip_2020 = {"North": 4, "South": 3, "Capital": 7}
20
+ rows = [
21
+ {
22
+ "year": year,
23
+ "region": region,
24
+ "value": round(start[region] + growth[region] * (year - 2015) - (dip_2020[region] if year == 2020 else 0)),
25
+ }
26
+ for region in start
27
+ for year in range(2015, 2025)
28
+ ]
29
+ with (OUT / "example.csv").open("w", newline="", encoding="utf-8") as f:
30
+ writer = csv.DictWriter(f, fieldnames=["year", "region", "value"], lineterminator="\n")
31
+ writer.writeheader()
32
+ writer.writerows(rows)
33
+ print(f"Wrote {len(rows)} rows to {OUT / 'example.csv'}")
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,31 @@
1
+ year,region,value
2
+ 2015,North,42
3
+ 2016,North,44
4
+ 2017,North,45
5
+ 2018,North,47
6
+ 2019,North,48
7
+ 2020,North,46
8
+ 2021,North,52
9
+ 2022,North,53
10
+ 2023,North,55
11
+ 2024,North,56
12
+ 2015,South,35
13
+ 2016,South,36
14
+ 2017,South,37
15
+ 2018,South,38
16
+ 2019,South,39
17
+ 2020,South,37
18
+ 2021,South,41
19
+ 2022,South,42
20
+ 2023,South,43
21
+ 2024,South,44
22
+ 2015,Capital,55
23
+ 2016,Capital,59
24
+ 2017,Capital,63
25
+ 2018,Capital,67
26
+ 2019,Capital,71
27
+ 2020,Capital,68
28
+ 2021,Capital,78
29
+ 2022,Capital,82
30
+ 2023,Capital,86
31
+ 2024,Capital,90
File without changes
@@ -0,0 +1,25 @@
1
+ # Common tasks. Install just: https://just.systems
2
+
3
+ # Run the data pipeline (R and Python scripts) to refresh data/processed/
4
+ data:
5
+ Rscript analysis/R/01_prepare.R
6
+ # uv run analysis/py/prepare.py
7
+
8
+ # Preview every chart at http://localhost:5173
9
+ dev:
10
+ pnpm dev
11
+
12
+ # Type-check the charts
13
+ check:
14
+ pnpm check
15
+
16
+ # Build static embeds into build/
17
+ build:
18
+ pnpm build
19
+
20
+ # Export SVG, PDF and PNG files into exports/ (needs a Chromium)
21
+ export: build
22
+ pnpm export
23
+
24
+ # Everything: data, build, export
25
+ all: data build export
@@ -0,0 +1,15 @@
1
+ import type { LocaleCode } from '@samizdata/mozaic/locale';
2
+
3
+ /** Project settings. Charts are registered in src/charts/index.ts. */
4
+ export const config = {
5
+ /** Used in URLs and file names. */
6
+ project: '__PROJECT_NAME__',
7
+ title: '__PROJECT_TITLE__',
8
+ /** Languages to build. The first is the default. */
9
+ locales: __LOCALES__ as LocaleCode[],
10
+ /**
11
+ * Public URL where `build/` will be deployed, with a trailing slash, e.g.
12
+ * `https://charts.example.com/__PROJECT_NAME__/`. Used for embed codes.
13
+ */
14
+ baseUrl: ''
15
+ };
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11
+ "export": "mozaic export build",
12
+ "prepare": "svelte-kit sync || echo ''"
13
+ },
14
+ "dependencies": {
15
+ "d3-array": "^3.2.4",
16
+ "d3-dsv": "^3.0.1",
17
+ "d3-format": "^3.1.0",
18
+ "d3-geo": "^3.1.1",
19
+ "d3-scale": "^4.0.2",
20
+ "d3-shape": "^3.2.0",
21
+ "d3-time-format": "^4.1.0",
22
+ "@samizdata/mozaic": "__MOZAIC_SPEC__"
23
+ },
24
+ "devDependencies": {
25
+ "@samizdata/mozaic-cli": "__CLI_SPEC__",
26
+ "@sveltejs/adapter-static": "^4.0.0",
27
+ "@sveltejs/kit": "^3.1.0",
28
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
29
+ "@types/d3-array": "^3.2.1",
30
+ "@types/d3-dsv": "^3.0.7",
31
+ "@types/d3-scale": "^4.0.8",
32
+ "@types/d3-shape": "^3.1.6",
33
+ "svelte": "^5.57.2",
34
+ "svelte-check": "^4.7.7",
35
+ "typescript": "^6.0.3",
36
+ "vite": "^8.3.5"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.17"
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ [project]
2
+ name = "__PROJECT_NAME__-analysis"
3
+ version = "0.1.0"
4
+ description = "Data preparation for __PROJECT_TITLE__"
5
+ requires-python = ">=3.12"
6
+ dependencies = []
@@ -0,0 +1,13 @@
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
+ }
12
+
13
+ export {};
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="text-scale" content="scale" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>