Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions plots/swarm-basic/implementations/javascript/echarts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// anyplot.ai
// swarm-basic: Basic Swarm Plot
// Library: echarts 6.1.0 | JavaScript 22.23.1
// Quality: 89/100 | Created: 2026-07-26

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// CRP (C-reactive protein) biomarker levels across a dose-escalation trial.
function lcg(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
const rng = lcg(42);
function randNormal() {
const u1 = Math.max(rng(), 1e-9);
const u2 = rng();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const groups = [
{ name: "Placebo", n: 45, mean: 3.2, sd: 0.9 },
{ name: "Low Dose", n: 45, mean: 2.6, sd: 0.8 },
{ name: "Medium Dose", n: 45, mean: 1.9, sd: 0.7 },
{ name: "High Dose", n: 45, mean: 1.3, sd: 0.6 },
];
const categories = groups.map((g) => g.name);
const groupValues = groups.map((g) =>
Array.from({ length: g.n }, () => Math.max(0.1, g.mean + randNormal() * g.sd)),
);

// --- Init ---------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// Fix both axis extents up front so the coordinate system is fully
// deterministic before any point is placed.
const allValues = groupValues.flat();
const yPad = (Math.max(...allValues) - Math.min(...allValues)) * 0.08;
const yMin = Math.max(0, Math.floor(Math.min(...allValues) - yPad));
const yMax = Math.ceil(Math.max(...allValues) + yPad);
const xPad = 0.36; // 12% of the 3-unit category span, keeps swarms off the plot edge

chart.setOption({
animation: false,
backgroundColor: "transparent",
title: {
text: "swarm-basic · javascript · echarts · anyplot.ai",
left: "center",
textStyle: { color: t.ink, fontSize: 22 },
},
legend: {
data: [{ name: "Group mean", icon: "diamond" }],
right: 60,
top: 56,
itemWidth: 12,
itemHeight: 12,
textStyle: { color: t.inkSoft, fontSize: 14 },
},
grid: { left: 100, right: 60, top: 100, bottom: 80 },
tooltip: {
trigger: "item",
formatter: (p) => `${categories[Math.round(p.value[0])] ?? p.seriesName}<br/>CRP: ${p.value[1].toFixed(2)} mg/L`,
},
xAxis: {
type: "value",
min: -xPad,
max: categories.length - 1 + xPad,
interval: 1,
axisLabel: {
color: t.inkSoft,
fontSize: 14,
formatter: (v) => categories[Math.round(v)] ?? "",
showMaxLabel: false,
},
axisLine: { lineStyle: { color: t.inkSoft }, onZero: false },
axisTick: { show: false },
splitLine: { show: false },
},
yAxis: {
type: "value",
min: yMin,
max: yMax,
name: "CRP (mg/L)",
nameLocation: "middle",
nameGap: 60,
nameTextStyle: { color: t.inkSoft, fontSize: 14 },
axisLabel: { color: t.inkSoft, fontSize: 14 },
axisLine: { lineStyle: { color: t.inkSoft }, onZero: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: t.grid } },
},
});

// --- Beeswarm layout ---------------------------------------------------------
// ECharts has no native swarm/beeswarm series. Rather than bucketing points
// into fixed-width bins (which produces a blocky, grid-like silhouette), this
// greedily places each point at the nearest collision-free slot using the
// chart's own pixel projection (`convertToPixel`/`convertFromPixel`) — the
// same technique ECharts' own beeswarm/packed-scatter examples use — so the
// swarm reads as a continuous organic shape at any density.
const SYMBOL_SIZE = 14;
const GAP = SYMBOL_SIZE * 0.92; // slight overlap reads as a continuous swarm
function beeswarmLayout(ci, values) {
const basePx = values.map((v) => chart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [ci, v]));
const order = basePx.map((_, i) => i).sort((a, b) => basePx[a][1] - basePx[b][1]);
const placed = [];
const finalX = new Array(values.length);
order.forEach((i) => {
const [cx, cy] = basePx[i];
let px = cx;
let k = 0;
while (k < 200 && placed.some((p) => Math.hypot(p.x - px, p.y - cy) < GAP)) {
k += 1;
const step = Math.ceil(k / 2);
const side = k % 2 === 1 ? 1 : -1;
px = cx + side * step * GAP;
}
placed.push({ x: px, y: cy });
finalX[i] = chart.convertFromPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [px, cy])[0];
});
return finalX;
}

const pointSeries = groups.map((g, ci) => {
const values = groupValues[ci];
const xs = beeswarmLayout(ci, values);
return {
name: g.name,
type: "scatter",
data: values.map((v, i) => [xs[i], v]),
symbolSize: SYMBOL_SIZE,
itemStyle: { color: t.palette[ci], opacity: 0.8 },
};
});

const meanSeries = {
name: "Group mean",
type: "scatter",
data: groups.map((g, ci) => [ci, g.mean]),
symbol: "diamond",
symbolSize: 24,
itemStyle: { color: t.ink, borderColor: t.pageBg, borderWidth: 2 },
z: 3,
};

chart.setOption({ series: [...pointSeries, meanSeries] });
229 changes: 229 additions & 0 deletions plots/swarm-basic/metadata/javascript/echarts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
library: echarts
language: javascript
specification_id: swarm-basic
created: '2026-07-26T07:25:48Z'
updated: '2026-07-26T07:51:49Z'
generated_by: claude-sonnet
workflow_run: 30192609034
issue: 974
language_version: 22.23.1
library_version: 6.1.0
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/javascript/echarts/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/javascript/echarts/plot-dark.png
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/javascript/echarts/plot-light.html
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/javascript/echarts/plot-dark.html
quality_score: 89
review:
strengths:
- 'All three Attempt 1 weaknesses fixed: legend entry now labels the mean-diamond
markers, y-axis tick marks hidden to match x-axis chrome, and beeswarm layout
rewritten to a greedy nearest-slot placement in pixel space for an organic swarm
silhouette'
- Correct Imprint palette assignment across all 4 categories in canonical order
(Placebo=#009E73, Low Dose=#C475FD, Medium Dose=#4467A3, High Dose=#BD8233)
- Theme-adaptive chrome verified correct in both renders — no dark-on-dark or light-on-light
legibility failures
- Deterministic seeded LCG data generation, fully reproducible
- Realistic, neutral CRP dose-escalation clinical trial dataset with a clear downward
dose-response trend, varied spread per group, and visible outliers
weaknesses:
- Library Mastery is still fundamentally a generic scatter series; the custom layout
uses ECharts' coordinate-conversion API but doesn't showcase a truly ECharts-distinctive
series type (e.g. custom renderItem)
- Data storytelling is solid but static — no annotation or callout emphasizes the
dose-response trend beyond the visual pattern itself
image_description: |-
Light render (plot-light.png):
Background: Warm off-white matching #FAF8F1, not pure white.
Chrome: Title "swarm-basic · javascript · echarts · anyplot.ai" centered, bold dark charcoal text, fully legible. "Group mean" legend with diamond icon top-right. Y-axis "CRP (mg/L)" label with units. X-axis category labels (Placebo, Low Dose, Medium Dose, High Dose) directly beneath each swarm. Subtle horizontal-only gridlines; both axis tick marks hidden. All readable against the light background.
Data: Four swarms in Imprint canonical order — green #009E73 (Placebo), lavender #C475FD (Low Dose), blue #4467A3 (Medium Dose), ochre #BD8233 (High Dose) — each with a dark diamond (light halo border) marking the group mean. Clear downward dose-response trend visible.
Legibility verdict: PASS

Dark render (plot-dark.png):
Background: Warm near-black matching #1A1A17, not pure black.
Chrome: Title, legend text, y-axis title, and all tick labels switch to light off-white/gray and remain fully readable — no dark-on-dark failures anywhere. Gridlines remain subtle but visible.
Data: Colors are pixel-identical to the light render — only chrome flipped. Mean-marker diamonds swap to a light fill with dark border to stay visible against the darker page, an appropriate theme-adaptive treatment.
Legibility verdict: PASS
criteria_checklist:
visual_quality:
score: 28
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: All text readable in both themes, correctly sized
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: No text or marker collisions
- id: VQ-03
name: Element Visibility
score: 5
max: 6
passed: true
comment: Marker size/alpha appropriate for ~45 points per group
- id: VQ-04
name: Color Accessibility
score: 2
max: 2
passed: true
comment: CVD-safe Imprint palette, adequate contrast
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Good proportions, nothing cut off
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Descriptive with units (CRP mg/L)
- id: VQ-07
name: Palette Compliance
score: 2
max: 2
passed: true
comment: Canonical Imprint order, correct theme backgrounds both renders
design_excellence:
score: 15
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Custom pixel-space beeswarm layout + themed mean markers
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: Subtle grid, generous whitespace, both axis ticks hidden (fixed from
Attempt 1)
- id: DE-03
name: Data Storytelling
score: 4
max: 6
passed: true
comment: Dose-response trend + labeled mean markers create readable hierarchy
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct swarm/beeswarm plot
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: All spec features present
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X/Y correct, axes show all data
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Exact title format; legend now identifies mean markers (fixed from
Attempt 1)
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Shows distribution shape, outliers, mean markers
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Realistic, neutral CRP clinical trial data
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Sensible CRP mg/L values
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: Helper functions justified by lack of native ECharts swarm series
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: Seeded LCG RNG, deterministic
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: No unused imports
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Appropriate complexity, no fake functionality
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Follows mount-node contract correctly
library_mastery:
score: 6
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: Correct token usage, idiomatic setOption pattern
- id: LM-02
name: Distinctive Features
score: 2
max: 5
passed: false
comment: Uses convertToPixel/convertFromPixel coordinate API but still a plain
scatter series, not a distinctive ECharts technique like custom renderItem
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- custom-legend
patterns:
- data-generation
- iteration-over-groups
dataprep: []
styling:
- alpha-blending
Loading