faostat-skills 0.2.2 → 0.2.3
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/package.json +1 -1
- package/skills/analytical-brief/SKILL.md +2 -1
- package/skills/climate/SKILL.md +36 -7
- package/skills/commodity/SKILL.md +27 -9
- package/skills/compare/SKILL.md +8 -1
- package/skills/country-profile/SKILL.md +16 -5
- package/skills/explore/SKILL.md +4 -0
- package/skills/export-dataset/SKILL.md +9 -7
- package/skills/infographic/SKILL.md +649 -43
- package/skills/map/SKILL.md +4 -1
- package/skills/scientific-paper/SKILL.md +9 -7
- package/skills/story/SKILL.md +5 -1
- package/skills/trade/SKILL.md +15 -5
- package/skills/trends/SKILL.md +10 -4
- package/skills/viz/SKILL.md +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: faostat-infographic
|
|
3
|
-
description: Use when the user wants a modern, non-expert-facing visual summary of FAOSTAT data on a single page — an infographic, one-pager, visual summary, explainer card, or shareable graphic for social, pitch decks, or press use. The deliverable is a standalone HTML file with inline SVG (optional PNG/PDF export). Aesthetic
|
|
3
|
+
description: Use when the user wants a modern, non-expert-facing visual summary of FAOSTAT data on a single page — an infographic, one-pager, visual summary, explainer card, or shareable graphic for social, pitch decks, or press use. The deliverable is a standalone HTML file with inline SVG (optional PNG/PDF export). Aesthetic — Visual Capitalist / Our World in Data explainer cards / Statista — bold typography, generous whitespace, one hero stat, iconography over chart axes. Do NOT use when the user asks for an analytical brief, policy brief, FAOSTAT brief, or policymaker-facing document → route to `faostat-analytical-brief`. Do NOT use for a data story, article, long-read, or explainer with paragraphs → route to `faostat-story`. Do NOT use for academic papers or dense chart-per-finding reports. Do NOT use when the user wants a standalone interactive chart only → use `faostat-viz`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# FAOSTAT Infographic
|
|
@@ -21,14 +21,17 @@ Cross-skill invariants (all six — violations are skill bugs):
|
|
|
21
21
|
4. **TCL for national trade aggregates, TM only for partner breakdowns.** Never sum TM rows to reconstruct national totals.
|
|
22
22
|
5. **China composite default (Apr 2026 user preference).** For any country-level number or ranking, default to composite `China` (area 351). Offer `China, mainland` (41) as an opt-in. Flag the choice in the source footer. Map carve-out: if the main visual is a choropleth, the map uses the disaggregation path (area 41 on the CHN polygon; HKG 96 / MAC 128 / TWN 214 on their own polygons). Never blend the two in the same figure.
|
|
23
23
|
6. **`faostat_get_rankings` HTTP-500 fallback.** If the call fails, reconstruct by pulling `faostat_get_data` across all reporting countries and sorting client-side. Note the fallback in the source footer.
|
|
24
|
+
7. **Element and item code resolution.** Never use a hardcoded numeric element or item code as the primary value in a `faostat_get_data` call. Always resolve at runtime: `faostat_search_codes(domain_code='<dom>', dimension_id='element', query='<metric name>')` for elements; `faostat_search_codes(domain_code='<dom>', dimension_id='item', query='<item name>')` for items. Numeric codes shown in reference tables and code examples are verified hints — use them to validate the search result, not as the authoritative source. Domain letter-codes (QCL, TCL, GT, EM, FBS, FS…) are stable and may be used directly.
|
|
24
25
|
|
|
25
26
|
Infographic-specific invariants:
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
8. **One hero stat.** There is exactly one hero number on the page — the most surprising single figure. If you're torn between two, push the second into the supporting-stats row.
|
|
29
|
+
9. **One main visual per layout mode.** Two modes:
|
|
30
|
+
- **Poster mode (default):** one chart/map/flow diagram total. A second visual becomes a plain bulleted list or small numeric table — no bar-width indicators, no sparklines.
|
|
31
|
+
- **Narrative mode (opt-in):** the topic warrants multiple sections — e.g., a trade infographic with (1) global volume hero → (2) top exporters bar chart → (3) trade route flow map. Each section has its own sub-headline and one visual element. Activate when the user's topic is inherently multi-angle OR when they explicitly ask. In narrative mode the hero still appears once at the top; each section's visual is smaller than the hero section. Maximum 3 sections before the takeaway.
|
|
32
|
+
10. **Jargon only in the source footer.** `AR5`, `AR5 GWP-100`, `CAGR`, `n.e.c.`, `FILTER code`, `DISPLAY code`, `LULUCF`, bare `CO2eq`, `kt`/`Mt`/`Gt` on first reference, and numeric FAOSTAT element/item codes stay in the source footer. Not in visual titles, subtitles, chart labels, captions, or headlines.
|
|
33
|
+
11. **Ten-second test.** Read the page aloud in 10 seconds — can a non-expert recite the main point? If not, shrink the headline or enlarge the hero.
|
|
34
|
+
12. **No FAO branding.** Retain CC-BY-4.0 data attribution ("Data: FAOSTAT (FAO), CC-BY-4.0"), but do not reproduce the FAO logo, "Food and Agriculture Organization of the United Nations" masthead, ISSN, "FAO Statistics Division" stamp, or "Required citation: FAO. …" line. The infographic is the analyst's, not FAO's.
|
|
32
35
|
|
|
33
36
|
## Visual system
|
|
34
37
|
|
|
@@ -57,17 +60,173 @@ One font family loaded via Google Fonts (`https://fonts.googleapis.com/css2?fami
|
|
|
57
60
|
1. **Hero stat** — one number, as big as it dares (≥ 30 % of viewport height on desktop). Unit spelled out ("16.5 billion tonnes of CO₂-equivalent", not "16.5 Gt CO₂eq").
|
|
58
61
|
2. **Headline** — one sentence, ≤ 10 words.
|
|
59
62
|
3. **Supporting stats row** — 2–4 numbers with a Lucide icon each and a one-line caption. No more than 4.
|
|
60
|
-
4. **Main visual** — exactly one
|
|
63
|
+
4. **Main visual** — exactly one chart, map, or flow diagram per section. Default layout has one section. Narrative mode (multi-section) adds intermediate sections — see invariant 9.
|
|
61
64
|
5. **Takeaway** — one italic sentence, plain-English "so-what" framing.
|
|
62
65
|
6. **Source footer** — "Data: FAOSTAT (FAO), accessed [Month YYYY]. Licence: CC-BY-4.0. Domains: [codes]. China: [composite 351 / mainland 41 per user opt-in / disaggregated for map]."
|
|
63
66
|
|
|
64
|
-
Everything on one scrollable
|
|
67
|
+
Everything on one scrollable HTML document, mobile-responsive. The page can be as tall as the story requires — "one page" means no pagination, not one viewport. Max content width 720 px; hero and main visual full-bleed to 1200 px.
|
|
65
68
|
|
|
66
69
|
### Iconography
|
|
67
70
|
|
|
68
71
|
Inline **Lucide** SVG icons (MIT licensed, ≤ 1 kB each). Fetch from `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/<name>.svg` at build time and paste the SVG inline. Icon colour = palette accent. Icon size: 32 px next to supporting stats, 48 px next to the main visual title.
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
**Two-tier icon selection rule:** choose by *stat type first* (what kind of number is this?), then by *domain* (what is it about?) when the stat type doesn't resolve it.
|
|
74
|
+
|
|
75
|
+
Stat-type icons (use these before anything domain-specific):
|
|
76
|
+
|
|
77
|
+
| Stat type | Icon | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Monetary value (USD, EUR, etc.) | `banknote` | Any dollar/price figure. Do NOT use `ship` for export *value*. |
|
|
80
|
+
| Growth rate / percentage change | `trending-up` or `trending-down` | Sign-aware: use `trending-down` for negative. |
|
|
81
|
+
| Share / concentration / % of total | `pie-chart` | "59% shipped by top 5" → `pie-chart`, not `ship`. |
|
|
82
|
+
| Record / all-time peak | `flame` | "2024 was the peak year" → `flame`, not `calendar`. |
|
|
83
|
+
| Ranking / #1 / leader | `trophy` | "Russia is the top exporter" → `trophy`. |
|
|
84
|
+
| Milestone / threshold crossed | `zap` | Sudden change, tipping point. |
|
|
85
|
+
| Count / number of entities | `hash` | "10 countries account for…" |
|
|
86
|
+
| Year / time reference (non-record) | `calendar` | Only when the year itself is the fact, not the record it holds. |
|
|
87
|
+
| Physical quantity shipped/moved | `package` | Export *volume* (tonnes, litres). NOT `ship` — `ship` is the vessel. |
|
|
88
|
+
|
|
89
|
+
Domain icons (use when the stat type is already resolved by the Lucide icon above, or when you need a second icon):
|
|
90
|
+
|
|
91
|
+
| Domain | Icon |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Agrifood emissions / GHG | `cloud` |
|
|
94
|
+
| Crop production | `wheat` |
|
|
95
|
+
| Trade route / logistics | `ship` (only for the concept of shipping, e.g. a section header) |
|
|
96
|
+
| Temperature / warming | `thermometer-sun` |
|
|
97
|
+
| Producer price / CPI | `coins` |
|
|
98
|
+
| Yield / efficiency | `sprout` |
|
|
99
|
+
| Livestock | `beef` |
|
|
100
|
+
| Water | `droplet` |
|
|
101
|
+
| Land | `mountain` |
|
|
102
|
+
| Food security / hunger | `utensils` |
|
|
103
|
+
| Forest / land cover | `trees` |
|
|
104
|
+
|
|
105
|
+
### Design principles
|
|
106
|
+
|
|
107
|
+
These principles are extracted from best-in-class data journalism infographics. Apply them every time:
|
|
108
|
+
|
|
109
|
+
1. **Narrative arc.** Every infographic tells a complete arc: *scale the problem → show the data → land the implication*. Plan the arc in Step 2 before pulling data. If the data doesn't support the arc, reframe — don't just display numbers.
|
|
110
|
+
2. **Progressive disclosure.** A reader stopping after 5 s gets the hero. One stopping after 15 s gets the supporting stats. One reading fully gets the chart and takeaway. Each layer adds detail without requiring the previous layer to be re-read.
|
|
111
|
+
3. **Data-ink ratio.** Remove every visual element that doesn't carry a data signal. No decorative borders, no 3-D effects, no unnecessary tick marks, no legend if labels on the data suffice.
|
|
112
|
+
4. **Icon as cognitive anchor.** Icons beside supporting stats aren't decoration — they help readers recall the number later. Every supporting stat gets exactly one Lucide icon; the icon carries semantic meaning (not generic icons like `star` or `check`).
|
|
113
|
+
5. **Whitespace as structure.** Margins and padding do the job of dividers. Don't add horizontal rules or coloured bands — generous padding between sections is cleaner.
|
|
114
|
+
6. **Typography does the heavy lifting.** The hero number should be readable from arm's length. If a reader needs to lean in to read the hero, it's too small.
|
|
115
|
+
|
|
116
|
+
### Animations (HTML only)
|
|
117
|
+
|
|
118
|
+
Scroll-triggered, zero external dependencies — `IntersectionObserver` + CSS transitions + inline JS. Every animation must respect `prefers-reduced-motion: reduce` via a single early-return guard.
|
|
119
|
+
|
|
120
|
+
**Hero counter** — the hero number counts from 0 to its final value over 1.2 s (ease-out cubic). Store the numeric portion in `data-value` on `.hero-stat`; keep unit and prefix in separate `<span>` elements so only the digit string animates.
|
|
121
|
+
|
|
122
|
+
**Supporting-stats cascade** — each `.stat` card starts invisible (`opacity: 0; transform: translateY(16px)`) and transitions in with a 100 ms stagger as the section enters the viewport.
|
|
123
|
+
|
|
124
|
+
**Bar chart draw** — each bar `<rect>` starts at `width="0"` (or `height="0"` for horizontal). Set the target dimension via a CSS custom property; transition to it over 0.8 s with 50 ms per-bar stagger.
|
|
125
|
+
|
|
126
|
+
**Line chart draw** — set `stroke-dasharray` equal to the path's `getTotalLength()` at paint time; start `stroke-dashoffset` at that same value and transition to `0` over `1.4s cubic-bezier(0.25, 1, 0.5, 1)`. Triggered by an `IntersectionObserver` adding `.visible`.
|
|
127
|
+
|
|
128
|
+
**Hero glow pulse** (Ember palette only) — a repeating `box-shadow` keyframe on the hero stat container. Fades an amber glow in and out over 2 s. Omit on Meadow and Ink — those topics don't warrant urgency drama.
|
|
129
|
+
|
|
130
|
+
**Trade route arcs + moving icons** — for flow maps. Three layered animations:
|
|
131
|
+
1. Arc draw: `stroke-dashoffset` transitions from full path length to 0, staggered 200 ms per route, 1.2 s each.
|
|
132
|
+
2. Icon travel: CSS `offset-path` matching the arc's Bézier definition; `offset-rotate: auto` keeps the icon facing the direction of travel.
|
|
133
|
+
3. Pulse dot at destination: a small circle at the importer centroid scales from 0 → 1.4 → 1 (`transform: scale`) when the icon arrives, timed via `animation-delay`.
|
|
134
|
+
|
|
135
|
+
```css
|
|
136
|
+
/* Arc draw-on */
|
|
137
|
+
.trade-arc {
|
|
138
|
+
stroke-dasharray: var(--arc-len); /* set via JS: el.style.setProperty('--arc-len', path.getTotalLength()) */
|
|
139
|
+
stroke-dashoffset: var(--arc-len);
|
|
140
|
+
transition: stroke-dashoffset 1.2s cubic-bezier(.4,0,.2,1);
|
|
141
|
+
}
|
|
142
|
+
.trade-arc.visible { stroke-dashoffset: 0; }
|
|
143
|
+
|
|
144
|
+
/* Icon travel — offset-path set inline per route */
|
|
145
|
+
.trade-icon {
|
|
146
|
+
offset-rotate: auto;
|
|
147
|
+
animation: _travel var(--dur, 5s) linear var(--delay, 0s) infinite;
|
|
148
|
+
}
|
|
149
|
+
@keyframes _travel {
|
|
150
|
+
0% { offset-distance: 0%; opacity: 0; }
|
|
151
|
+
5% { opacity: 1; }
|
|
152
|
+
95% { opacity: 1; }
|
|
153
|
+
100% { offset-distance: 100%; opacity: 0; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Destination pulse */
|
|
157
|
+
.dest-dot {
|
|
158
|
+
animation: _pulse 0.4s ease-out var(--arrive-delay, 4.8s) both;
|
|
159
|
+
}
|
|
160
|
+
@keyframes _pulse {
|
|
161
|
+
0% { transform: scale(0); opacity: 1; }
|
|
162
|
+
70% { transform: scale(1.4); }
|
|
163
|
+
100% { transform: scale(1); opacity: 0.6; }
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Standard JS boilerplate (inline `<script>` at end of `<body>`):
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
171
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
172
|
+
|
|
173
|
+
const io = new IntersectionObserver((entries) => {
|
|
174
|
+
entries.forEach(e => {
|
|
175
|
+
if (!e.isIntersecting) return;
|
|
176
|
+
e.target.classList.add('visible');
|
|
177
|
+
if (e.target.classList.contains('hero-stat')) _counter(e.target);
|
|
178
|
+
if (e.target.dataset.draw) _drawPath(e.target);
|
|
179
|
+
io.unobserve(e.target);
|
|
180
|
+
});
|
|
181
|
+
}, { threshold: 0.15 });
|
|
182
|
+
|
|
183
|
+
document.querySelectorAll('.hero-stat, .stat, [data-draw]').forEach(el => io.observe(el));
|
|
184
|
+
|
|
185
|
+
function _counter(el) {
|
|
186
|
+
const end = parseFloat(el.dataset.value);
|
|
187
|
+
const decimals = (String(end).split('.')[1] ?? '').length;
|
|
188
|
+
const t0 = performance.now();
|
|
189
|
+
(function tick(now) {
|
|
190
|
+
const p = Math.min((now - t0) / 1200, 1);
|
|
191
|
+
const v = (end * (1 - Math.pow(1 - p, 3))).toFixed(decimals);
|
|
192
|
+
el.querySelector('.counter-value').textContent = v;
|
|
193
|
+
if (p < 1) requestAnimationFrame(tick);
|
|
194
|
+
})(t0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _drawPath(el) {
|
|
198
|
+
const len = el.getTotalLength ? el.getTotalLength() : parseFloat(el.dataset.draw);
|
|
199
|
+
el.style.strokeDasharray = len;
|
|
200
|
+
el.style.strokeDashoffset = len;
|
|
201
|
+
el.style.transition = 'stroke-dashoffset 1.4s cubic-bezier(.25,1,.5,1)';
|
|
202
|
+
requestAnimationFrame(() => { el.style.strokeDashoffset = 0; });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
CSS additions inside `<style>`:
|
|
208
|
+
|
|
209
|
+
```css
|
|
210
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
211
|
+
.stat { opacity: 0; transform: translateY(16px);
|
|
212
|
+
transition: opacity .5s ease, transform .5s ease; }
|
|
213
|
+
.stat.visible { opacity: 1; transform: none; }
|
|
214
|
+
.stat:nth-child(2) { transition-delay: .1s; }
|
|
215
|
+
.stat:nth-child(3) { transition-delay: .2s; }
|
|
216
|
+
.stat:nth-child(4) { transition-delay: .3s; }
|
|
217
|
+
|
|
218
|
+
/* Ember palette hero pulse */
|
|
219
|
+
body.palette-ember .hero-stat {
|
|
220
|
+
animation: _glow 2s ease-in-out infinite;
|
|
221
|
+
}
|
|
222
|
+
@keyframes _glow {
|
|
223
|
+
0%, 100% { box-shadow: 0 0 0 transparent; }
|
|
224
|
+
50% { box-shadow: 0 0 52px rgba(246,163,63,.30); }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Add `class="palette-ember"` (or `palette-meadow` / `palette-ink`) to `<body>` to activate the correct palette-scoped animation rules.
|
|
71
230
|
|
|
72
231
|
## Workflow
|
|
73
232
|
|
|
@@ -85,10 +244,10 @@ Proceed without a second clarifying round — one is enough. Pick sensible defau
|
|
|
85
244
|
### Step 2 — Design the hero message
|
|
86
245
|
|
|
87
246
|
From the topic, identify the single most striking stat. Rules of thumb:
|
|
88
|
-
- **Biggest delta over the window** (e.g., "+48.8 % in pre- and post-production emissions since 2001")
|
|
89
|
-
- **Most extreme ratio** (e.g., "4× gap in cattle-meat emissions intensity between Africa and Europe")
|
|
90
|
-
- **Most surprising ranking** (e.g., "top 10 emitters = 55 % of world total")
|
|
91
|
-
- **A number at the edge of intuition** (e.g., "16.5 billion tonnes of CO₂-equivalent")
|
|
247
|
+
- **Biggest delta over the window** (e.g., "+127 % in aquaculture output since 2000", "+48.8 % in pre- and post-production emissions since 2001", "wheat yield in the EU grew 3× faster than Sub-Saharan Africa over 30 years")
|
|
248
|
+
- **Most extreme ratio** (e.g., "4× gap in cattle-meat emissions intensity between Africa and Europe", "top 5 countries account for 72 % of global wheat production")
|
|
249
|
+
- **Most surprising ranking** (e.g., "India overtook the US as the world's top rice exporter in 2021", "top 10 emitters = 55 % of world total")
|
|
250
|
+
- **A number at the edge of intuition** (e.g., "815 million people undernourished — roughly 1 in 10", "16.5 billion tonnes of CO₂-equivalent", "Brazil's soybean exports tripled in 20 years")
|
|
92
251
|
|
|
93
252
|
This becomes the hero + headline. Draft both before pulling data — if the narrative falls apart on the numbers, rewrite.
|
|
94
253
|
|
|
@@ -99,18 +258,21 @@ This becomes the hero + headline. Draft both before pulling data — if the narr
|
|
|
99
258
|
Pick the main visual type:
|
|
100
259
|
- **Line** for a temporal story ("emissions 2001–2023")
|
|
101
260
|
- **Bar** for a ranking ("top 10 emitters in 2023") or a composition ("three pillars of agrifood emissions")
|
|
102
|
-
- **
|
|
261
|
+
- **Choropleth map** for a geographic distribution story ("emissions per capita by country"). Compose with the `faostat-map` skill → returns an SVG choropleth in the chosen palette.
|
|
262
|
+
- **Flow map** for bilateral trade routes, migration, or supply chains. See Step 5 for rendering. Use when the story is about *movement between places*, not quantity at a place. Data source: FAOSTAT TM domain (bilateral trade) for top-N flows of a commodity.
|
|
263
|
+
|
|
264
|
+
**In poster mode: exactly one main visual.** Not two, not a pair. If the user asks for both a time series and a top-producers ranking, the line chart is the main visual and the ranking is a plain bulleted list — no bar indicators. No sparklines, no small multiples, no dual-axis.
|
|
103
265
|
|
|
104
|
-
**
|
|
266
|
+
**In narrative mode: one visual per section, max 3 sections.** Each section builds on the previous. For a trade topic, the natural narrative arc is: hero volume → top exporters bar → trade route flow map. Each visual is smaller than the hero area. The page scrolls through the story.
|
|
105
267
|
|
|
106
|
-
If you feel the urge to add a second chart
|
|
268
|
+
If you feel the urge to add a second chart in poster mode:
|
|
107
269
|
- Promote it to the main visual and demote the current one to a text stat.
|
|
108
|
-
-
|
|
270
|
+
- Switch to narrative mode (if the topic warrants it).
|
|
109
271
|
- Drop it.
|
|
110
272
|
|
|
111
273
|
### Step 4 — Pull the data
|
|
112
274
|
|
|
113
|
-
Apply invariants 1–
|
|
275
|
+
Apply invariants 1–7. Log every `faostat_get_data` call if the user asked for the companion CSV.
|
|
114
276
|
|
|
115
277
|
- Use `response_format='compact'`, `show_unit=True`.
|
|
116
278
|
- Pass the element as a FILTER code.
|
|
@@ -119,15 +281,65 @@ Apply invariants 1–6. Log every `faostat_get_data` call if the user asked for
|
|
|
119
281
|
- For China in rankings / country-level numbers: composite 351 by default, unless the user opted into 41.
|
|
120
282
|
- For the map path: disaggregate — area 41 + HKG 96 + MAC 128 + TWN 214, drop 351.
|
|
121
283
|
- For top-N: prefer `faostat_get_rankings` (DISPLAY codes); if HTTP 500, fall back to `faostat_get_data` across all reporting countries and sort client-side.
|
|
284
|
+
- **Emissions indicators — always fetch from EM domain, never compute.** Resolve element codes at runtime before calling `faostat_get_data`: `faostat_search_codes(domain_code='EM', dimension_id='element', query='per capita')` for per-capita (verified `7279`); `faostat_search_codes(domain_code='EM', dimension_id='element', query='share CO2eq')` for share of national total (verified `726313`). Also filter by item — `6518` for agrifood systems total, `6996` farm gate, `6516` land-use change, `6517` pre- and post-production. Do NOT divide GT totals by population or otherwise reconstruct these metrics.
|
|
285
|
+
- **Unfamiliar topic or domain:** Call `faostat_list_domains()` to browse all available FAOSTAT domains, identify the right one(s) for the topic, then call `faostat_search_codes(domain_code='<dom>', dimension_id='element', query='<metric>')` and `faostat_search_codes(domain_code='<dom>', dimension_id='item', query='<item>')` to resolve codes. Do not guess domain codes — the infographic skill works with any FAOSTAT domain, not just emissions.
|
|
122
286
|
|
|
123
287
|
Pull only what you need for the 1 hero + 2–4 supporting stats + main visual. Do not pull a full dataset "in case" — infographics reward discipline.
|
|
124
288
|
|
|
125
289
|
### Step 5 — Compose the main visual
|
|
126
290
|
|
|
127
|
-
Inline SVG. Palette applied. Gridlines stripped. For line charts, label the endpoints directly on the line. For bar charts, label the bars directly and drop the numeric axis. For maps, call the map skill with `output_format='svg'`, `palette=<chosen>`, `disaggregate_china=true`.
|
|
291
|
+
Inline SVG. Palette applied. Gridlines stripped. For line charts, label the endpoints directly on the line. For bar charts, label the bars directly and drop the numeric axis. For choropleth maps, call the map skill with `output_format='svg'`, `palette=<chosen>`, `disaggregate_china=true`.
|
|
128
292
|
|
|
129
293
|
Size: main visual 720 px wide on desktop, 100 % width on mobile, aspect ratio ~16:9 for charts and ~2:1 for maps.
|
|
130
294
|
|
|
295
|
+
#### Flow map (trade routes, migration, supply chain)
|
|
296
|
+
|
|
297
|
+
Build as inline SVG with animated arcs. Use the top-N bilateral flows from FAOSTAT TM (typically top 10 by value or quantity).
|
|
298
|
+
|
|
299
|
+
**Flat world map variant:**
|
|
300
|
+
1. Use a simplified world outline SVG (Natural Earth 110m resolution — very small file, ≈ 30 kB). Render as light grey fills (`#e0e0e0`) on a dark or pale background.
|
|
301
|
+
2. Compute exporter and importer centroid coordinates for each country (use a precomputed lookup — standard lat/lon centroids).
|
|
302
|
+
3. Draw each trade arc as a quadratic Bézier `<path>` between centroids with the control point lifted above the midpoint (gives the arc its curve). Stroke weight proportional to flow volume.
|
|
303
|
+
4. Animate the arc with `stroke-dashoffset` (draw-on effect, 1–2 s per arc, staggered).
|
|
304
|
+
5. Add a moving transport icon (plane for air freight, ship for sea, truck for land) using CSS `offset-path`:
|
|
305
|
+
|
|
306
|
+
```css
|
|
307
|
+
.trade-icon {
|
|
308
|
+
offset-path: path("M x1,y1 Q cx,cy x2,y2"); /* same Bézier as the arc */
|
|
309
|
+
offset-rotate: auto; /* icon faces direction of travel automatically */
|
|
310
|
+
animation: move-along 4s linear infinite;
|
|
311
|
+
}
|
|
312
|
+
@keyframes move-along {
|
|
313
|
+
from { offset-distance: 0%; }
|
|
314
|
+
to { offset-distance: 100%; }
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The Lucide `plane` icon works well; rotate 45° if the route is primarily horizontal. Use `ship` for sea routes. Animate each icon with a different `animation-delay` so they don't all depart simultaneously.
|
|
319
|
+
|
|
320
|
+
**Globe variant (for dramatic single-route or top-5 flows):**
|
|
321
|
+
1. Draw a filled `<ellipse>` as the globe body (palette background or deep blue).
|
|
322
|
+
2. Overlay a simplified hemisphere outline (just the visible half of Natural Earth, clipped to the ellipse).
|
|
323
|
+
3. Define a `<clipPath>` using the same ellipse so arcs outside the visible hemisphere are hidden.
|
|
324
|
+
4. Bézier arcs stay above the globe surface — raise the control point to `cy = midpoint_y - 120` for a convincing great-circle arc appearance.
|
|
325
|
+
5. Rotate the globe centre toward the most important trade corridor for the best visual frame.
|
|
326
|
+
|
|
327
|
+
```svg
|
|
328
|
+
<defs>
|
|
329
|
+
<clipPath id="globe-clip">
|
|
330
|
+
<ellipse cx="360" cy="300" rx="280" ry="280"/>
|
|
331
|
+
</clipPath>
|
|
332
|
+
</defs>
|
|
333
|
+
<ellipse cx="360" cy="300" rx="280" ry="280" fill="#1a2a3a"/>
|
|
334
|
+
<g clip-path="url(#globe-clip)">
|
|
335
|
+
<!-- simplified world outline paths here -->
|
|
336
|
+
<!-- trade arc paths with stroke-dashoffset animation -->
|
|
337
|
+
</g>
|
|
338
|
+
<!-- moving icons outside clip so they're always visible -->
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Data note for flow maps:** pull bilateral trade from FAOSTAT TM domain — this gives reporter × partner pairs. Limit to top 10 flows by export quantity or value. Do NOT reconstruct from TCL (TCL is national totals, not bilateral).
|
|
342
|
+
|
|
131
343
|
### Step 6 — Write captions and takeaway
|
|
132
344
|
|
|
133
345
|
Plain-language rules:
|
|
@@ -138,7 +350,9 @@ Plain-language rules:
|
|
|
138
350
|
|
|
139
351
|
### Step 7 — Assemble the HTML
|
|
140
352
|
|
|
141
|
-
Single self-contained file. Inline CSS and inline SVG. One external resource allowed: the Google Fonts stylesheet.
|
|
353
|
+
Single self-contained file. Inline CSS and inline SVG. One external resource allowed: the Google Fonts stylesheet. Include the animation boilerplate from the **Animations** section of the Visual system.
|
|
354
|
+
|
|
355
|
+
Use the complete boilerplate below — copy it, fill in the data, and do not replace the CSS with placeholder comments. Placeholder comments are the primary cause of unstyled output.
|
|
142
356
|
|
|
143
357
|
```html
|
|
144
358
|
<!DOCTYPE html>
|
|
@@ -147,53 +361,445 @@ Single self-contained file. Inline CSS and inline SVG. One external resource all
|
|
|
147
361
|
<meta charset="UTF-8">
|
|
148
362
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
149
363
|
<title>[Headline]</title>
|
|
150
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family
|
|
151
|
-
<style
|
|
364
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&family=Inter:wght@400;500;700;900&family=IBM+Plex+Mono:wght@500&display=swap">
|
|
365
|
+
<style>
|
|
366
|
+
/* ── Palette tokens — set on <body> via class ── */
|
|
367
|
+
:root {
|
|
368
|
+
--bg:#0B0B0F; --hero:#F6A33F; --accent:#FF5C39;
|
|
369
|
+
--text:#F3F3F1; --secondary:#6D6D74;
|
|
370
|
+
--card-bg:rgba(255,255,255,0.06); --border:rgba(255,255,255,0.08);
|
|
371
|
+
}
|
|
372
|
+
body.palette-meadow {
|
|
373
|
+
--bg:#F6F5EF; --hero:#2E7D4F; --accent:#E8A03D;
|
|
374
|
+
--text:#1F2420; --secondary:#6B6E67;
|
|
375
|
+
--card-bg:rgba(0,0,0,0.04); --border:rgba(0,0,0,0.08);
|
|
376
|
+
}
|
|
377
|
+
body.palette-ink {
|
|
378
|
+
--bg:#FAFAFA; --hero:#1B1F3A; --accent:#E23E57;
|
|
379
|
+
--text:#1B1F3A; --secondary:#7A7D87;
|
|
380
|
+
--card-bg:rgba(0,0,0,0.04); --border:rgba(0,0,0,0.08);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ── Reset ── */
|
|
384
|
+
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
|
|
385
|
+
body {
|
|
386
|
+
background:var(--bg); color:var(--text);
|
|
387
|
+
font-family:'Inter',-apple-system,'Helvetica Neue',Arial,sans-serif;
|
|
388
|
+
line-height:1.5; -webkit-font-smoothing:antialiased;
|
|
389
|
+
}
|
|
390
|
+
main { max-width:1200px; margin:0 auto; }
|
|
391
|
+
|
|
392
|
+
/* ── Topic pill ── */
|
|
393
|
+
.topic-pill {
|
|
394
|
+
display:inline-block; font-size:11px; font-weight:700;
|
|
395
|
+
letter-spacing:.12em; text-transform:uppercase;
|
|
396
|
+
color:var(--accent); border:1px solid var(--accent);
|
|
397
|
+
border-radius:999px; padding:4px 14px; margin-bottom:32px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* ── Hero ── */
|
|
401
|
+
.hero { padding:80px 40px 64px; max-width:900px; }
|
|
402
|
+
.hero-stat {
|
|
403
|
+
font-family:'Space Grotesk',sans-serif; font-weight:700;
|
|
404
|
+
font-size:clamp(72px,14vw,180px); color:var(--hero);
|
|
405
|
+
line-height:.9; letter-spacing:-.02em; margin-bottom:12px;
|
|
406
|
+
}
|
|
407
|
+
.hero-stat .unit {
|
|
408
|
+
font-size:clamp(18px,3vw,32px); font-weight:400;
|
|
409
|
+
color:var(--secondary); display:block; margin-top:8px; letter-spacing:0;
|
|
410
|
+
}
|
|
411
|
+
h1.headline {
|
|
412
|
+
font-family:'Space Grotesk',sans-serif; font-weight:700;
|
|
413
|
+
font-size:clamp(28px,4.5vw,52px); line-height:1.1;
|
|
414
|
+
color:var(--text); margin-top:24px; max-width:720px;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* ── Supporting stats ── */
|
|
418
|
+
.supporting {
|
|
419
|
+
display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
|
|
420
|
+
gap:20px; padding:0 40px 64px; max-width:820px;
|
|
421
|
+
}
|
|
422
|
+
.stat {
|
|
423
|
+
background:var(--card-bg); border:1px solid var(--border);
|
|
424
|
+
border-radius:16px; padding:28px 24px;
|
|
425
|
+
}
|
|
426
|
+
.stat svg { color:var(--accent); margin-bottom:12px; display:block; }
|
|
427
|
+
.stat .n {
|
|
428
|
+
font-family:'Space Grotesk',sans-serif; font-weight:700;
|
|
429
|
+
font-size:36px; color:var(--hero); line-height:1;
|
|
430
|
+
}
|
|
431
|
+
.stat .cap { font-size:13px; color:var(--secondary); margin-top:6px; line-height:1.4; }
|
|
432
|
+
|
|
433
|
+
/* ── Main visual ── */
|
|
434
|
+
.visual { padding:0 0 64px; }
|
|
435
|
+
.visual-label {
|
|
436
|
+
font-size:11px; font-weight:700; letter-spacing:.1em; text-transform:uppercase;
|
|
437
|
+
color:var(--accent); padding:0 40px 8px;
|
|
438
|
+
}
|
|
439
|
+
.visual-title {
|
|
440
|
+
font-family:'Space Grotesk',sans-serif; font-weight:700;
|
|
441
|
+
font-size:22px; padding:0 40px 8px; color:var(--text);
|
|
442
|
+
}
|
|
443
|
+
.visual-sub { font-size:14px; color:var(--secondary); padding:0 40px 32px; }
|
|
444
|
+
.visual svg { width:100%; height:auto; display:block; }
|
|
445
|
+
|
|
446
|
+
/* ── Takeaway ── */
|
|
447
|
+
.takeaway {
|
|
448
|
+
font-size:18px; color:var(--secondary); font-style:italic;
|
|
449
|
+
line-height:1.6; border-top:1px solid var(--border);
|
|
450
|
+
padding:32px 40px 48px; max-width:760px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* ── Source footer ── */
|
|
454
|
+
footer.source {
|
|
455
|
+
font-size:11px; color:var(--secondary); line-height:1.6;
|
|
456
|
+
border-top:1px solid var(--border); padding:24px 40px 64px; max-width:900px;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* ── Responsive ── */
|
|
460
|
+
@media(max-width:768px){
|
|
461
|
+
.hero{padding:48px 24px 40px}
|
|
462
|
+
.supporting{padding:0 24px 40px;grid-template-columns:1fr 1fr}
|
|
463
|
+
.visual-label,.visual-title,.visual-sub{padding-left:24px;padding-right:24px}
|
|
464
|
+
.takeaway{padding:24px 24px 40px}
|
|
465
|
+
footer.source{padding:24px}
|
|
466
|
+
}
|
|
467
|
+
@media(max-width:480px){
|
|
468
|
+
.supporting{grid-template-columns:1fr}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ── Animations (prefers-reduced-motion guarded — see JS) ── */
|
|
472
|
+
.stat{opacity:0;transform:translateY(16px);transition:opacity .5s ease,transform .5s ease}
|
|
473
|
+
.stat.visible{opacity:1;transform:none}
|
|
474
|
+
.stat:nth-child(2){transition-delay:.1s}
|
|
475
|
+
.stat:nth-child(3){transition-delay:.2s}
|
|
476
|
+
.stat:nth-child(4){transition-delay:.3s}
|
|
477
|
+
[data-draw]{transition:stroke-dashoffset 1.4s cubic-bezier(.25,1,.5,1)}
|
|
478
|
+
@media(prefers-reduced-motion:no-preference){
|
|
479
|
+
body.palette-ember .hero-stat{animation:_glow 2s ease-in-out infinite}
|
|
480
|
+
}
|
|
481
|
+
@keyframes _glow{0%,100%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 52px rgba(246,163,63,.30)}}
|
|
482
|
+
</style>
|
|
152
483
|
</head>
|
|
153
|
-
<body>
|
|
484
|
+
<body class="palette-ember"> <!-- change to palette-meadow or palette-ink as needed -->
|
|
154
485
|
<main>
|
|
155
486
|
<section class="hero">
|
|
156
|
-
<div class="
|
|
157
|
-
|
|
487
|
+
<div class="topic-pill">[TOPIC · YEAR]</div>
|
|
488
|
+
<!-- data-value = numeric portion only; .unit = spelled-out unit string -->
|
|
489
|
+
<div class="hero-stat" data-value="16.5">
|
|
490
|
+
<span class="counter-value">16.5</span>
|
|
491
|
+
<span class="unit">billion tonnes of CO₂-equivalent</span>
|
|
492
|
+
</div>
|
|
493
|
+
<h1 class="headline">[Headline — plain English, ≤ 10 words]</h1>
|
|
158
494
|
</section>
|
|
495
|
+
|
|
159
496
|
<section class="supporting">
|
|
160
|
-
|
|
161
|
-
|
|
497
|
+
<!-- Repeat for each of 2–4 stats. Icons: inline Lucide SVG, 32×32 -->
|
|
498
|
+
<div class="stat">
|
|
499
|
+
<svg aria-hidden="true" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
|
500
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
501
|
+
<!-- paste Lucide icon path here -->
|
|
502
|
+
</svg>
|
|
503
|
+
<div class="n">+3.3%</div>
|
|
504
|
+
<div class="cap">rise over the decade</div>
|
|
505
|
+
</div>
|
|
162
506
|
</section>
|
|
507
|
+
|
|
163
508
|
<section class="visual">
|
|
164
|
-
<
|
|
509
|
+
<div class="visual-label">[SECTION LABEL]</div>
|
|
510
|
+
<div class="visual-title">[Chart / map title]</div>
|
|
511
|
+
<div class="visual-sub">[One-line description of what the visual shows]</div>
|
|
512
|
+
<!-- Inline SVG chart or map. For line charts add data-draw to the <path>.
|
|
513
|
+
For bar charts start each <rect> at width="0" and transition to final width.
|
|
514
|
+
For choropleth maps paste SVG returned by faostat-map skill. -->
|
|
515
|
+
<svg role="img" aria-label="[alt text describing the data]" viewBox="0 0 720 360">
|
|
516
|
+
<!-- chart content -->
|
|
517
|
+
</svg>
|
|
165
518
|
</section>
|
|
166
|
-
|
|
167
|
-
<
|
|
519
|
+
|
|
520
|
+
<p class="takeaway"><em>[One italic sentence: what this means for the reader]</em></p>
|
|
521
|
+
|
|
522
|
+
<footer class="source">
|
|
523
|
+
Data: FAOSTAT (FAO), accessed [Month YYYY]. Licence: CC-BY-4.0.
|
|
524
|
+
Domains: [codes]. Years: [range]. [China handling note]. [Any methodology note].
|
|
525
|
+
</footer>
|
|
168
526
|
</main>
|
|
527
|
+
|
|
528
|
+
<script>
|
|
529
|
+
// Animation driver — guarded by prefers-reduced-motion
|
|
530
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
531
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
532
|
+
document.querySelectorAll('.stat').forEach(el => el.classList.add('visible'));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const io = new IntersectionObserver((entries) => {
|
|
536
|
+
entries.forEach(e => {
|
|
537
|
+
if (!e.isIntersecting) return;
|
|
538
|
+
e.target.classList.add('visible');
|
|
539
|
+
if (e.target.classList.contains('hero-stat')) _counter(e.target);
|
|
540
|
+
if (e.target.dataset.draw !== undefined) _drawPath(e.target);
|
|
541
|
+
io.unobserve(e.target);
|
|
542
|
+
});
|
|
543
|
+
}, { threshold: 0.15 });
|
|
544
|
+
document.querySelectorAll('.hero-stat, .stat, [data-draw]').forEach(el => io.observe(el));
|
|
545
|
+
|
|
546
|
+
function _counter(el) {
|
|
547
|
+
const end = parseFloat(el.dataset.value);
|
|
548
|
+
const dec = (String(end).split('.')[1] ?? '').length;
|
|
549
|
+
const t0 = performance.now();
|
|
550
|
+
(function tick(now) {
|
|
551
|
+
const p = Math.min((now - t0) / 1200, 1);
|
|
552
|
+
el.querySelector('.counter-value').textContent =
|
|
553
|
+
(end * (1 - Math.pow(1 - p, 3))).toFixed(dec);
|
|
554
|
+
if (p < 1) requestAnimationFrame(tick);
|
|
555
|
+
})(t0);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function _drawPath(el) {
|
|
559
|
+
const len = el.getTotalLength ? el.getTotalLength() : 0;
|
|
560
|
+
el.style.strokeDasharray = len;
|
|
561
|
+
el.style.strokeDashoffset = len;
|
|
562
|
+
requestAnimationFrame(() => { el.style.strokeDashoffset = 0; });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
</script>
|
|
169
566
|
</body>
|
|
170
567
|
</html>
|
|
171
568
|
```
|
|
172
569
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Accessibility: every SVG gets `role="img"` and a meaningful `aria-label`. Colour contrast ≥ 4.5:1 for body text, ≥ 3:1 for large text.
|
|
570
|
+
Accessibility: every SVG gets `role="img"` and `aria-label`; decorative icons use `aria-hidden="true"`. Contrast ≥ 4.5:1 for body text, ≥ 3:1 for large text.
|
|
176
571
|
|
|
177
572
|
### Step 8 — Offer exports
|
|
178
573
|
|
|
179
574
|
After the HTML is saved, ask the user what additional formats they want. The options, with cost notes:
|
|
180
575
|
|
|
181
|
-
- **
|
|
182
|
-
- **
|
|
576
|
+
- **Social media card** — a *separate, purpose-built layout* (not a screenshot of the web infographic). See Social media card rules below.
|
|
577
|
+
- **PNG (high-DPI)** — Playwright `page.screenshot()` at `deviceScaleFactor: 3` (retina-quality, 3× pixel density). Good for Slack, social feeds, pitch decks.
|
|
578
|
+
- **PDF (vector, default)** — Playwright `page.pdf(print_background=True)`. Text and CSS shapes stay as vectors — suitable for print and email. No extra install needed.
|
|
579
|
+
- **PDF (print-grade, opt-in)** — Inkscape CLI converts the extracted inline SVG to a true resolution-independent PDF. Every element scales to any size without aliasing. Requires `inkscape` (`brew install inkscape`). Check availability with `shutil.which('inkscape')` and offer this upgrade silently if found.
|
|
183
580
|
- **Companion CSV** — the hero + supporting stats + main-visual data in a flat table. Fast (~1 s). Good for fact-checkers.
|
|
184
581
|
|
|
185
|
-
If Playwright / Chromium is unavailable
|
|
582
|
+
If Playwright / Chromium is unavailable, fall back to `weasyprint` (HTML → PDF only). Document any fallback in the output description.
|
|
186
583
|
|
|
187
584
|
Save exports with matching names:
|
|
188
585
|
|
|
189
586
|
```
|
|
190
587
|
outputs/
|
|
191
|
-
<slug>-infographic.html
|
|
192
|
-
<slug>-infographic.
|
|
193
|
-
<slug>-infographic.
|
|
194
|
-
<slug>-
|
|
588
|
+
<slug>-infographic.html # always
|
|
589
|
+
<slug>-infographic.pdf # opt-in (Playwright page.pdf or Inkscape)
|
|
590
|
+
<slug>-infographic.png # opt-in (Playwright screenshot, 3× DPI)
|
|
591
|
+
<slug>-social-portrait.html # social card source (opt-in)
|
|
592
|
+
<slug>-social-portrait.png # screenshot of the above (opt-in)
|
|
593
|
+
<slug>-social-square.html # square variant (opt-in)
|
|
594
|
+
<slug>-social-square.png # screenshot of the above (opt-in)
|
|
595
|
+
<slug>-data.csv # opt-in
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### Social media card rules
|
|
599
|
+
|
|
600
|
+
A social media card is a **separate HTML file with a fixed, single-screen viewport**. It is NOT a screenshot of the main infographic HTML. The main infographic is a scrollable web page; a social card is a single, non-scrolling frame that looks complete on its own.
|
|
601
|
+
|
|
602
|
+
**Supported formats:**
|
|
603
|
+
|
|
604
|
+
| Format | Dimensions | Best for |
|
|
605
|
+
|---|---|---|
|
|
606
|
+
| Square | 1080 × 1080 px | Instagram feed, Twitter/X |
|
|
607
|
+
| Portrait 4:5 | 1080 × 1350 px | Instagram feed (preferred) |
|
|
608
|
+
| Stories / Reels | 1080 × 1920 px | Instagram Stories, TikTok |
|
|
609
|
+
|
|
610
|
+
Default to **portrait 4:5** unless the user specifies otherwise.
|
|
611
|
+
|
|
612
|
+
**Layout inside the card (portrait 4:5 example):**
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
┌──────────────────────────┐
|
|
616
|
+
│ topic pill / eyebrow │ ← 8% height, small caps, accent color
|
|
617
|
+
│ │
|
|
618
|
+
│ HERO NUMBER │ ← 40% height, full-bleed, ≥ 200 px font
|
|
619
|
+
│ unit spelled out │
|
|
620
|
+
│ │
|
|
621
|
+
│ Headline (≤ 8 words) │ ← 15% height, bold
|
|
622
|
+
│ One-line sub-message │ ← 10% height, lighter weight
|
|
623
|
+
│ │
|
|
624
|
+
│ ┌────┐ ┌────┐ │ ← 2 supporting stats max (not 4)
|
|
625
|
+
│ │icon│ │icon│ │
|
|
626
|
+
│ │stat│ │stat│ │ ← 20% height
|
|
627
|
+
│ │cap │ │cap │ │
|
|
628
|
+
│ └────┘ └────┘ │
|
|
629
|
+
│ │
|
|
630
|
+
│ Source (tiny, muted) │ ← 7% height
|
|
631
|
+
└──────────────────────────┘
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**CSS rules that make it work:**
|
|
635
|
+
|
|
636
|
+
```css
|
|
637
|
+
html, body {
|
|
638
|
+
width: 1080px;
|
|
639
|
+
height: 1350px; /* adjust per format */
|
|
640
|
+
overflow: hidden; /* CRITICAL — no scroll; Playwright sees exactly this frame */
|
|
641
|
+
margin: 0;
|
|
642
|
+
background: var(--bg);
|
|
643
|
+
}
|
|
644
|
+
main {
|
|
645
|
+
width: 100%;
|
|
646
|
+
height: 100%;
|
|
647
|
+
display: flex;
|
|
648
|
+
flex-direction: column;
|
|
649
|
+
justify-content: space-between;
|
|
650
|
+
padding: 72px 80px;
|
|
651
|
+
box-sizing: border-box;
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Content rules (stricter than main infographic):**
|
|
656
|
+
- Hero number only — no chart. If the main visual was a line chart, replace it with the start-and-end delta ("from X to Y") as a two-number comparison or drop it entirely.
|
|
657
|
+
- Max 2 supporting stats. Pick the 2 most arresting from the main infographic's 4.
|
|
658
|
+
- No main visual (chart/map) — the hero + 2 stats fill the card. Exception: a single large donut/ring chart that illustrates a share (e.g., "59% of exports") can replace the 2 supporting stats.
|
|
659
|
+
- No takeaway sentence — the sub-message line does that job in ≤ 8 words.
|
|
660
|
+
- Source footer is 11 px, muted — just "Data: FAOSTAT (FAO), CC-BY-4.0."
|
|
661
|
+
|
|
662
|
+
**Visual quality standard — Figma-grade, not webpage-screenshot-grade:**
|
|
663
|
+
|
|
664
|
+
Social cards must look like a designer built them from scratch — not like a webpage cropped to a square. Target aesthetic: color-blocked, illustration-rich, typographically bold — the premium data journalism studio standard. Required elements:
|
|
665
|
+
|
|
666
|
+
1. **Color-blocked sections with organic edges.** The hero area is a distinct, richly colored block (not white). Section transitions use curved `clip-path` edges — no flat horizontal dividers.
|
|
667
|
+
|
|
668
|
+
2. **Background blob shapes.** One or two large abstract blobs positioned behind the hero or stats area add depth without competing with data.
|
|
669
|
+
|
|
670
|
+
3. **Large decorative SVG illustration.** One topic-appropriate SVG illustration element (100–220 px) anchors the visual — a wheat stalk silhouette for trade, a cloud/smoke form for emissions, a globe outline for geographic topics, a bowl/plate for food security. This is a *decorative* element, not a Lucide icon. It sits in the background at reduced opacity (15–25%) or in a corner at full opacity as a design accent. It gives the eye visual texture between the numbers.
|
|
671
|
+
|
|
672
|
+
4. **Rich gradient backgrounds.** Hero backgrounds use a linear or radial gradient, not a flat solid fill.
|
|
673
|
+
|
|
674
|
+
CSS patterns for organic design:
|
|
675
|
+
|
|
676
|
+
```css
|
|
677
|
+
/* Hero section: full-bleed with organic curved bottom */
|
|
678
|
+
.hero-section {
|
|
679
|
+
background: linear-gradient(145deg, var(--hero) 0%, var(--accent) 100%);
|
|
680
|
+
clip-path: ellipse(115% 78% at 50% 8%);
|
|
681
|
+
padding: 80px 80px 120px;
|
|
682
|
+
position: relative;
|
|
683
|
+
overflow: hidden;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* Background blob — large organic accent shape */
|
|
687
|
+
.blob {
|
|
688
|
+
position: absolute;
|
|
689
|
+
width: 380px; height: 380px;
|
|
690
|
+
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
|
|
691
|
+
background: var(--accent);
|
|
692
|
+
opacity: 0.18;
|
|
693
|
+
pointer-events: none;
|
|
694
|
+
}
|
|
695
|
+
.blob-tl { top: -60px; left: -80px; }
|
|
696
|
+
.blob-br { bottom: -80px; right: -60px; transform: rotate(45deg); }
|
|
697
|
+
|
|
698
|
+
/* Stats area: rounded card on contrasting background */
|
|
699
|
+
.stats-section {
|
|
700
|
+
background: rgba(255,255,255,0.07);
|
|
701
|
+
border-radius: 28px;
|
|
702
|
+
padding: 44px 48px;
|
|
703
|
+
backdrop-filter: blur(4px);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* Large decorative SVG illustration anchor */
|
|
707
|
+
.illus {
|
|
708
|
+
position: absolute;
|
|
709
|
+
opacity: 0.15;
|
|
710
|
+
right: 48px; bottom: 48px;
|
|
711
|
+
width: 180px; height: 180px;
|
|
712
|
+
fill: var(--text);
|
|
713
|
+
}
|
|
195
714
|
```
|
|
196
715
|
|
|
716
|
+
SVG illustration guidance: build a simple but bold thematic silhouette in inline SVG. Examples:
|
|
717
|
+
- **Wheat/grain trade:** Three wheat stalks, side by side, with stylised grain heads — 5–8 `<path>` elements.
|
|
718
|
+
- **Emissions/climate:** A billowing cloud/smoke column rising from a horizon line.
|
|
719
|
+
- **Food security/hunger:** A stylised bowl with a spoon, or a globe with fork and knife flanking.
|
|
720
|
+
- **Livestock:** A side-profile cow or chicken silhouette, single solid fill.
|
|
721
|
+
- **Temperature:** A thermometer with a rising mercury column and radiating lines.
|
|
722
|
+
|
|
723
|
+
These do NOT need to be photorealistic — geometric, flat-style SVG in 2–3 path shapes is the right aesthetic. Fill with palette `--hero` or `--text` at 15–25% opacity for depth without distraction.
|
|
724
|
+
|
|
725
|
+
**Screenshotting the card:** use Playwright with `viewport` matching card dimensions exactly and `deviceScaleFactor: 3` for retina-quality output (yields a 3240 × 4050 px PNG at 4:5):
|
|
726
|
+
|
|
727
|
+
```python
|
|
728
|
+
from playwright.sync_api import sync_playwright
|
|
729
|
+
with sync_playwright() as p:
|
|
730
|
+
browser = p.chromium.launch()
|
|
731
|
+
page = browser.new_page(
|
|
732
|
+
viewport={"width": 1080, "height": 1350},
|
|
733
|
+
device_scale_factor=3, # 3× retina — 3240×4050 output
|
|
734
|
+
)
|
|
735
|
+
page.goto(f"file://{path_to_social_html}")
|
|
736
|
+
page.wait_for_load_state("networkidle") # wait for Google Fonts
|
|
737
|
+
page.screenshot(path=png_path, full_page=False) # full_page=False = viewport only
|
|
738
|
+
browser.close()
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
`full_page=False` is critical — it captures exactly the viewport, not the full scrollable document.
|
|
742
|
+
|
|
743
|
+
#### Export quality pipeline
|
|
744
|
+
|
|
745
|
+
Three tiers of output, in increasing quality order. Always use the highest tier available.
|
|
746
|
+
|
|
747
|
+
| Tier | Method | Quality | Requirement |
|
|
748
|
+
|---|---|---|---|
|
|
749
|
+
| 1 — Screen PNG | Playwright `screenshot()` `deviceScaleFactor: 3` | 3× retina, ~3 MP | Playwright (already required) |
|
|
750
|
+
| 2 — Vector PDF | Playwright `page.pdf(print_background=True)` | Vectors for text + CSS shapes | Playwright (already required) |
|
|
751
|
+
| 3 — Print PDF | Inkscape CLI `--export-type=pdf` | True SVG vector, resolution-independent | `brew install inkscape` |
|
|
752
|
+
|
|
753
|
+
**Tier 2 — Playwright `page.pdf()`** (default for all PDF asks):
|
|
754
|
+
|
|
755
|
+
```python
|
|
756
|
+
from playwright.sync_api import sync_playwright
|
|
757
|
+
with sync_playwright() as p:
|
|
758
|
+
browser = p.chromium.launch()
|
|
759
|
+
page = browser.new_page()
|
|
760
|
+
page.goto(f"file://{path_to_html}")
|
|
761
|
+
page.wait_for_load_state("networkidle")
|
|
762
|
+
page.pdf(
|
|
763
|
+
path=pdf_path,
|
|
764
|
+
format="A4", # or "Letter"
|
|
765
|
+
print_background=True, # CRITICAL — without this, all fills/gradients are stripped
|
|
766
|
+
margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
|
|
767
|
+
)
|
|
768
|
+
browser.close()
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Tier 3 — Inkscape CLI** (upgrade silently when `inkscape` is installed; skip if not found):
|
|
772
|
+
|
|
773
|
+
```python
|
|
774
|
+
import subprocess, shutil
|
|
775
|
+
from bs4 import BeautifulSoup
|
|
776
|
+
|
|
777
|
+
def export_via_inkscape(html_path, svg_path, pdf_path):
|
|
778
|
+
# Extract the primary <svg> from the HTML and save standalone
|
|
779
|
+
soup = BeautifulSoup(open(html_path).read(), "html.parser")
|
|
780
|
+
svg = soup.find("svg")
|
|
781
|
+
if not svg:
|
|
782
|
+
return False # no SVG to extract; fall back to Tier 2
|
|
783
|
+
svg_path.write_text(str(svg))
|
|
784
|
+
subprocess.run([
|
|
785
|
+
"inkscape", str(svg_path),
|
|
786
|
+
"--export-type=pdf",
|
|
787
|
+
f"--export-filename={pdf_path}",
|
|
788
|
+
"--export-dpi=300",
|
|
789
|
+
], check=True)
|
|
790
|
+
return True
|
|
791
|
+
|
|
792
|
+
if shutil.which("inkscape"):
|
|
793
|
+
ok = export_via_inkscape(html_path, svg_path, pdf_path)
|
|
794
|
+
if not shutil.which("inkscape") or not ok:
|
|
795
|
+
# Fall back to Tier 2
|
|
796
|
+
...
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Quality ceiling without external design tools:**
|
|
800
|
+
|
|
801
|
+
With Tier 2/3, the output reaches 85–90% of premium data journalism studio quality. The remaining gap is hand-crafted illustration work: character figures, complex flow diagrams, and multi-element scene compositions. Those require a human designer or an interactive vector tool (Figma, Illustrator). Figma, Canva, and Adobe Express are not currently suitable for agent-driven layout generation — they require interactive editing sessions and have no practical programmatic "generate from data" API for arbitrary layouts.
|
|
802
|
+
|
|
197
803
|
### Step 9 — Save and describe
|
|
198
804
|
|
|
199
805
|
Share a `computer://` link to the HTML file and any opt-in exports. Give a 2–3 sentence description:
|
|
@@ -224,9 +830,9 @@ Offer four specific levers:
|
|
|
224
830
|
- **Playwright / Chromium unavailable** — fall back to `weasyprint` for PDF; skip PNG and tell the user the sandbox lacks the headless-browser runtime.
|
|
225
831
|
- **Google Fonts CDN blocked** — inline a system font stack as fallback: `-apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif` for body and `Georgia, 'Times New Roman', serif` for hero. Note the fallback.
|
|
226
832
|
- **User asks to add FAO branding (logo, masthead, "Required citation: FAO")** — push back. Explain that the infographic skill drops FAO branding by design so the output isn't mistaken for an FAO publication. The CC-BY-4.0 data attribution in the source footer is sufficient and legally required. Only proceed if the user explicitly overrides.
|
|
227
|
-
- **User asks for multiple hero stats** — push back. Invariant
|
|
228
|
-
- **User asks for a second chart (rankings, second time series, composition + map, etc.)** — push back. Invariant
|
|
229
|
-
- **Urge to label a chart "AR5 GWP-100", "CO2eq", "kt", etc.** — push back. Invariant
|
|
833
|
+
- **User asks for multiple hero stats** — push back. Invariant 8: one hero. Offer to turn the second would-be-hero into a supporting stat, or to build a second infographic.
|
|
834
|
+
- **User asks for a second chart (rankings, second time series, composition + map, etc.)** — push back. Invariant 9: one main visual. Offer (a) to promote the new chart to the main visual and demote the current one, (b) to render the ranking/composition as a plain numbered list or a small numeric table with no bar indicators, or (c) to split into two infographics.
|
|
835
|
+
- **Urge to label a chart "AR5 GWP-100", "CO2eq", "kt", etc.** — push back. Invariant 10: jargon belongs only in the source footer. Rewrite the label in plain English and add the methodology clause to the footer.
|
|
230
836
|
|
|
231
837
|
## Suggested citation
|
|
232
838
|
|