Skip to content

Commit dedcf49

Browse files
feat(chartjs): implement pyramid-basic (#11585)
## Implementation: `pyramid-basic` - javascript/chartjs Implements the **javascript/chartjs** version of `pyramid-basic`. **File:** `plots/pyramid-basic/implementations/javascript/chartjs.js` **Parent Issue:** #1000 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33965287063)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 8fc6cf7 commit dedcf49

2 files changed

Lines changed: 394 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// anyplot.ai
2+
// pyramid-basic: Basic Pyramid Chart
3+
// Library: chartjs 4.4.7 | JavaScript 22.23.2
4+
// Quality: 94/100 | Created: 2026-09-05
5+
6+
//# anyplot-orientation: landscape
7+
const t = window.ANYPLOT_TOKENS;
8+
9+
// --- Data (in-memory, deterministic) ---------------------------------------
10+
const ageGroups = ["0-9", "10-19", "20-29", "30-39", "40-49", "50-59", "60-69", "70-79", "80+"];
11+
const malePopulation = [42, 45, 40, 38, 41, 35, 28, 15, 6];
12+
const femalePopulation = [40, 43, 39, 39, 42, 37, 31, 19, 9];
13+
const axisMax = 50;
14+
15+
// Oldest two cohorts show the widest female/male gap — give them a subtle
16+
// accent border so the eye lands on the chart's own insight.
17+
const highlightedRows = new Set([7, 8]);
18+
const accentBorder = (color) => ageGroups.map((_, i) => (highlightedRows.has(i) ? color : "transparent"));
19+
const accentWidth = ageGroups.map((_, i) => (highlightedRows.has(i) ? 2 : 0));
20+
21+
// --- Mount -------------------------------------------------------------------
22+
const canvas = document.createElement("canvas");
23+
document.getElementById("container").appendChild(canvas);
24+
25+
// --- Chart.js-specific plugin --------------------------------------------
26+
// A small local plugin (Chart.js's own extensibility hook, not a cross-library
27+
// pattern) draws a dashed center axis line plus a bracket + label calling out
28+
// the widening gender gap in the two oldest cohorts.
29+
const pyramidAnnotations = {
30+
id: "pyramidAnnotations",
31+
afterDraw(chart) {
32+
const { ctx, chartArea, scales } = chart;
33+
const centerX = scales.x.getPixelForValue(0);
34+
35+
ctx.save();
36+
37+
ctx.setLineDash([4, 4]);
38+
ctx.strokeStyle = t.grid;
39+
ctx.lineWidth = 1;
40+
ctx.beginPath();
41+
ctx.moveTo(centerX, chartArea.top);
42+
ctx.lineTo(centerX, chartArea.bottom);
43+
ctx.stroke();
44+
ctx.setLineDash([]);
45+
46+
const rowHeight = scales.y.getPixelForTick(1) - scales.y.getPixelForTick(0);
47+
const topY = scales.y.getPixelForTick(7) - rowHeight / 2;
48+
const bottomY = scales.y.getPixelForTick(8) + rowHeight / 2;
49+
const bracketX = chartArea.right + 10;
50+
51+
ctx.strokeStyle = t.amber;
52+
ctx.lineWidth = 2;
53+
ctx.beginPath();
54+
ctx.moveTo(bracketX - 6, topY);
55+
ctx.lineTo(bracketX, topY);
56+
ctx.lineTo(bracketX, bottomY);
57+
ctx.lineTo(bracketX - 6, bottomY);
58+
ctx.stroke();
59+
60+
ctx.fillStyle = t.amber;
61+
ctx.font = "600 13px sans-serif";
62+
ctx.textAlign = "left";
63+
ctx.textBaseline = "middle";
64+
ctx.fillText("gap widens", bracketX + 8, (topY + bottomY) / 2);
65+
66+
ctx.restore();
67+
},
68+
};
69+
70+
// --- Chart -------------------------------------------------------------------
71+
new Chart(canvas, {
72+
type: "bar",
73+
data: {
74+
labels: ageGroups,
75+
datasets: [
76+
{
77+
label: "Male",
78+
data: malePopulation.map((v) => -v),
79+
backgroundColor: t.palette[0],
80+
borderColor: accentBorder(t.amber),
81+
borderWidth: accentWidth,
82+
borderRadius: 4,
83+
categoryPercentage: 0.85,
84+
barPercentage: 0.9,
85+
},
86+
{
87+
label: "Female",
88+
data: femalePopulation,
89+
backgroundColor: t.palette[1],
90+
borderColor: accentBorder(t.amber),
91+
borderWidth: accentWidth,
92+
borderRadius: 4,
93+
categoryPercentage: 0.85,
94+
barPercentage: 0.9,
95+
},
96+
],
97+
},
98+
plugins: [pyramidAnnotations],
99+
options: {
100+
indexAxis: "y",
101+
responsive: true,
102+
maintainAspectRatio: false,
103+
animation: false,
104+
layout: { padding: { top: 0, right: 100 } },
105+
plugins: {
106+
title: {
107+
display: true,
108+
text: "pyramid-basic · javascript · chartjs · anyplot.ai",
109+
color: t.ink,
110+
font: { size: 22, weight: "600" },
111+
},
112+
subtitle: {
113+
display: true,
114+
text: "Female population increasingly exceeds male past age 70",
115+
color: t.inkSoft,
116+
font: { size: 14, style: "italic" },
117+
padding: { bottom: 8 },
118+
},
119+
legend: {
120+
position: "top",
121+
labels: { color: t.ink, font: { size: 16 } },
122+
padding: 8,
123+
},
124+
tooltip: {
125+
callbacks: {
126+
label: (ctx) => `${ctx.dataset.label}: ${Math.abs(ctx.parsed.x)}k`,
127+
},
128+
},
129+
},
130+
scales: {
131+
x: {
132+
stacked: true,
133+
min: -axisMax,
134+
max: axisMax,
135+
ticks: {
136+
color: t.inkSoft,
137+
font: { size: 14 },
138+
stepSize: 10,
139+
callback: (value) => Math.abs(value),
140+
},
141+
grid: { color: t.grid },
142+
title: { display: true, text: "Population (thousands)", color: t.ink, font: { size: 18 } },
143+
},
144+
y: {
145+
stacked: true,
146+
ticks: { color: t.inkSoft, font: { size: 14 } },
147+
grid: { display: false },
148+
title: { display: true, text: "Age Group", color: t.ink, font: { size: 18 } },
149+
},
150+
},
151+
},
152+
});
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
library: chartjs
2+
language: javascript
3+
specification_id: pyramid-basic
4+
created: '2026-09-05T12:14:48Z'
5+
updated: '2026-09-05T12:28:03Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 33965287063
8+
issue: 1000
9+
language_version: 22.23.2
10+
library_version: 4.4.7
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/pyramid-basic/javascript/chartjs/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/pyramid-basic/javascript/chartjs/plot-dark.png
13+
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/pyramid-basic/javascript/chartjs/plot-light.html
14+
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/pyramid-basic/javascript/chartjs/plot-dark.html
15+
quality_score: 94
16+
review:
17+
strengths:
18+
- Custom Chart.js plugin (afterDraw hook) draws a dashed center axis, a bracket,
19+
and a "gap widens" annotation that directly calls out the widening 70-79/80+ gender
20+
gap — a genuinely library-distinctive technique that resolves the prior LM-02
21+
and DE-03 weaknesses
22+
- Accent border highlighting on the two oldest rows reinforces the same insight
23+
without breaking Imprint palette compliance
24+
- New subtitle ("Female population increasingly exceeds male past age 70") adds
25+
narrative context and visual hierarchy above the chart
26+
- 'Full spec compliance retained: canonical Imprint palette order (#009E73 Male,
27+
#C475FD Female), symmetric -50..50 axis, tooltip callback reporting absolute values,
28+
correct mount-node contract with animation: false'
29+
- Clean, deterministic, KISS code — hard-coded data arrays, no unused imports, no
30+
fake functionality
31+
weaknesses:
32+
- Visual refinement (spines/grid/whitespace) is largely unchanged from attempt 1
33+
— aside from the new plugin, the chart still relies on fairly default Chart.js
34+
chrome; further subtle grid/typography polish would raise DE-02
35+
- 'Minor: noticeable whitespace gap remains between the legend row and the first
36+
data bar (age 0-9) — could be trimmed for a tighter composition'
37+
image_description: |-
38+
Light render (plot-light.png):
39+
Background: Warm off-white, consistent with #FAF8F1 — not pure white, not dark.
40+
Chrome: Bold dark title "pyramid-basic · javascript · chartjs · anyplot.ai" at top, italic dark-gray subtitle "Female population increasingly exceeds male past age 70" beneath it, green/lavender "Male"/"Female" legend swatches below that. X-axis title "Population (thousands)" and Y-axis title "Age Group" both present with units. A new dashed vertical line marks the center axis, and an amber bracket + "gap widens" label sit to the right of the 70-79/80+ rows, which also carry a thin amber accent border on their bars. All text (title, subtitle, legend, axis titles, tick labels, category labels, "gap widens" annotation) is clearly readable against the light background — no clipping at the canvas edges.
41+
Data: Male bars extend left in brand green (#009E73), Female bars extend right in lavender (#C475FD) — matches canonical Imprint palette order (position 1 = brand green, position 2 = lavender). Bars widen through middle age groups and narrow at the extremes, with Female visibly and increasingly longer than Male from age 70 onward, exactly the insight called out by the subtitle and the bracket annotation.
42+
Legibility verdict: PASS
43+
44+
Dark render (plot-dark.png):
45+
Background: Warm near-black, consistent with #1A1A17 — not pure black, not light.
46+
Chrome: Identical layout to the light render; title, subtitle, legend text, axis titles, and tick labels all flipped to light/off-white ink for contrast against the dark surface. The dashed center line, amber bracket, and "gap widens" label remain amber and clearly visible against the dark background. No dark-on-dark issues found — every text element is legible.
47+
Data: Male in #009E73, Female in #C475FD — identical to the light render, confirming only chrome (not data colors) flipped between themes.
48+
Legibility verdict: PASS
49+
criteria_checklist:
50+
visual_quality:
51+
score: 30
52+
max: 30
53+
items:
54+
- id: VQ-01
55+
name: Text Legibility
56+
score: 8
57+
max: 8
58+
passed: true
59+
comment: All font sizes explicit (title 22px, axis titles 18px, legend 16px,
60+
ticks 14px); readable in both themes including the new annotation text
61+
- id: VQ-02
62+
name: No Overlap
63+
score: 6
64+
max: 6
65+
passed: true
66+
comment: No collisions; bracket and annotation sit cleanly in the reserved
67+
right-side padding
68+
- id: VQ-03
69+
name: Element Visibility
70+
score: 6
71+
max: 6
72+
passed: true
73+
comment: Bar thickness well-adapted to 9 categories; accent borders don't
74+
obscure data
75+
- id: VQ-04
76+
name: Color Accessibility
77+
score: 2
78+
max: 2
79+
passed: true
80+
comment: Green/lavender pairing plus redundant left/right positional encoding
81+
- id: VQ-05
82+
name: Layout & Canvas
83+
score: 4
84+
max: 4
85+
passed: true
86+
comment: Chart fills a large, balanced portion of the canvas; right padding
87+
correctly reserved for the bracket annotation, nothing cut off
88+
- id: VQ-06
89+
name: Axis Labels & Title
90+
score: 2
91+
max: 2
92+
passed: true
93+
comment: X-axis has units ("Population (thousands)"), Y-axis descriptive
94+
- id: VQ-07
95+
name: Palette Compliance
96+
score: 2
97+
max: 2
98+
passed: true
99+
comment: 'First series #009E73, second series canonical Imprint position 2
100+
(#C475FD); theme-correct chrome in both renders'
101+
design_excellence:
102+
score: 15
103+
max: 20
104+
items:
105+
- id: DE-01
106+
name: Aesthetic Sophistication
107+
score: 6
108+
max: 8
109+
passed: true
110+
comment: Custom plugin (center line, bracket, annotation) and subtitle add
111+
deliberate polish and hierarchy beyond library defaults
112+
- id: DE-02
113+
name: Visual Refinement
114+
score: 4
115+
max: 6
116+
passed: false
117+
comment: Spines/grid/whitespace unchanged from attempt 1 — still fairly default
118+
aside from the new plugin
119+
- id: DE-03
120+
name: Data Storytelling
121+
score: 5
122+
max: 6
123+
passed: true
124+
comment: Bracket + "gap widens" annotation + accent borders directly emphasize
125+
the widening older-age gender gap called out in the subtitle
126+
spec_compliance:
127+
score: 15
128+
max: 15
129+
items:
130+
- id: SC-01
131+
name: Plot Type
132+
score: 5
133+
max: 5
134+
passed: true
135+
comment: Correct diverging stacked-bar pyramid
136+
- id: SC-02
137+
name: Required Features
138+
score: 4
139+
max: 4
140+
passed: true
141+
comment: Symmetric axis, distinct colors per side, legend, centered category
142+
axis all present
143+
- id: SC-03
144+
name: Data Mapping
145+
score: 3
146+
max: 3
147+
passed: true
148+
comment: X = population (diverging), Y = age group categories
149+
- id: SC-04
150+
name: Title & Legend
151+
score: 3
152+
max: 3
153+
passed: true
154+
comment: Title matches mandated format exactly; legend labels match dataset
155+
labels
156+
data_quality:
157+
score: 15
158+
max: 15
159+
items:
160+
- id: DQ-01
161+
name: Feature Coverage
162+
score: 6
163+
max: 6
164+
passed: true
165+
comment: Full age range with realistic older-age gender divergence
166+
- id: DQ-02
167+
name: Realistic Context
168+
score: 5
169+
max: 5
170+
passed: true
171+
comment: Population pyramid by age/gender is the canonical neutral demographic
172+
example named in the spec
173+
- id: DQ-03
174+
name: Appropriate Scale
175+
score: 4
176+
max: 4
177+
passed: true
178+
comment: Values and the gender gap at older ages are factually plausible
179+
code_quality:
180+
score: 10
181+
max: 10
182+
items:
183+
- id: CQ-01
184+
name: KISS Structure
185+
score: 3
186+
max: 3
187+
passed: true
188+
comment: Flat script; the plugin object and small helper arrow functions are
189+
idiomatic Chart.js extensibility, not gratuitous abstraction
190+
- id: CQ-02
191+
name: Reproducibility
192+
score: 2
193+
max: 2
194+
passed: true
195+
comment: Hard-coded deterministic arrays
196+
- id: CQ-03
197+
name: Clean Imports
198+
score: 2
199+
max: 2
200+
passed: true
201+
comment: No unused imports
202+
- id: CQ-04
203+
name: Code Elegance
204+
score: 2
205+
max: 2
206+
passed: true
207+
comment: Appropriately complex, no fake functionality — the highlighted rows
208+
are computed from an explicit, data-driven set
209+
- id: CQ-05
210+
name: Output & API
211+
score: 1
212+
max: 1
213+
passed: true
214+
comment: 'Correct mount-node contract, animation: false set, current Chart.js
215+
v4 API'
216+
library_mastery:
217+
score: 9
218+
max: 10
219+
items:
220+
- id: LM-01
221+
name: Idiomatic Usage
222+
score: 5
223+
max: 5
224+
passed: true
225+
comment: Solid use of stacked scales, plugin system, and tick callbacks
226+
- id: LM-02
227+
name: Distinctive Features
228+
score: 4
229+
max: 5
230+
passed: true
231+
comment: Custom afterDraw plugin is a genuinely Chart.js-specific extensibility
232+
hook, directly addressing the prior generic-usage weakness
233+
verdict: APPROVED
234+
impl_tags:
235+
dependencies: []
236+
techniques:
237+
- annotations
238+
patterns:
239+
- data-generation
240+
dataprep: []
241+
styling:
242+
- edge-highlighting

0 commit comments

Comments
 (0)