@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.
- package/LICENSE +674 -0
- package/README.md +19 -2
- package/bin/create-mozaic.js +10 -0
- package/package.json +32 -3
- package/src/index.js +157 -0
- package/template/.claude/skills/mozaic-chart/SKILL.md +27 -0
- package/template/.github/workflows/deploy.yml +38 -0
- package/template/AGENTS.md +32 -0
- package/template/CLAUDE.md +1 -0
- package/template/README.md +57 -0
- package/template/_gitignore +13 -0
- package/template/analysis/R/01_prepare.R +21 -0
- package/template/analysis/py/prepare.py +37 -0
- package/template/data/processed/example.csv +31 -0
- package/template/data/raw/.gitkeep +0 -0
- package/template/justfile +25 -0
- package/template/mozaic.config.ts +15 -0
- package/template/package.json +41 -0
- package/template/pyproject.toml +6 -0
- package/template/src/app.d.ts +13 -0
- package/template/src/app.html +12 -0
- package/template/src/charts/example/Chart.svelte +100 -0
- package/template/src/charts/example/data.ts +12 -0
- package/template/src/charts/example/messages.ts +51 -0
- package/template/src/charts/index.ts +27 -0
- package/template/src/lib/theme.ts +11 -0
- package/template/src/routes/+layout.svelte +5 -0
- package/template/src/routes/+layout.ts +3 -0
- package/template/src/routes/+page.svelte +120 -0
- package/template/src/routes/embed/[chart]/[locale]/+page.svelte +20 -0
- package/template/src/routes/embed/[chart]/[locale]/+page.ts +14 -0
- package/template/src/routes/embed/manifest.json/+server.ts +26 -0
- package/template/src/routes/embed.js/+server.ts +9 -0
- package/template/tsconfig.json +7 -0
- package/template/vite.config.ts +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-mozaic
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samizdata/create-mozaic",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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,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,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>
|