Skip to content

Commit 1e0cced

Browse files
feat(highcharts): migrate Highcharts from Python to JavaScript (Phase 2) (#8516)
Closes #8242. Migrates the canonical `highcharts` entry from the `highcharts-core` **Python** wrapper to the native **JavaScript** `highcharts.js`, per the "most-used variant" rule in [`docs/concepts/library-expansion.md`](docs/concepts/library-expansion.md) §6 (npm ~1 M/wk vs `highcharts-core` ~5 k/wk — ~200×). Highcharts now renders through the Phase‑1 (#8244) Node 22 + Playwright browser harness, exactly like Chart.js / D3 / ECharts. ## Approach decision: clean break (not the dual-serve window) The issue's "careful bit" offered two paths: a one-release **deprecation window** (keep Python pages, 301-redirect) or the **clean-break fallback**. Per the owner's call in the task thread (*"highcharts auch komplett löschen und einfach mit den js sachen nach und nach neu erstellen"*), this ships the **clean break** — the documented fallback: - The **322** Python `highcharts.py` impls + **322** metadata files are removed. - JS implementations are recreated **gradually** via `bulk-generate` / `daily-regen`. - Old `/{spec}/python/highcharts` deep links **gracefully fall back to the spec hub** via existing `SpecPage` logic (no matching impl → redirect to `/{spec}?language=python`). No redirect scaffolding needed. This keeps the change small and avoids the dual-language complexity (no `impl.language_id` refactor, no nginx/301 work) the deprecation window would have required. ## What changed **Registry & runtime** - `core/constants.py`: `highcharts` `language_id` `python → javascript`, `version` `1.10.0 → 12.6.0`, JS-native description (commercial license kept prominent). `framework` was already `none`. - **alembic migration** `a1c7e2f9b8d3`: `UPDATE` the existing `libraries` row (the seed upserts `ON CONFLICT DO NOTHING`, so flipping an existing row needs an explicit data migration; safe no-op on a fresh DB). Single head verified. - `package.json` / `package-lock.json`: pin `highcharts@12.6.0`; `render.mjs`: add `highcharts` to the harness `BUNDLES` (`Highcharts` global, `highcharts/highcharts.js` UMD). **Prompt & style guide** - `prompts/library/highcharts.md`: full rewrite Python → JavaScript — mount-node contract, `ANYPLOT_TOKENS` theme mapping, `deviceScaleFactor=2` canvas rule + orientation directive, `//` header, **prominent commercial-license note**, and forbidden patterns (no `highcharts-core`, no CDN/`fetch`, no add-on modules, no inline HTML/screenshot). - `prompts/default-style-guide.md`: drop the now-wrong highcharts native-pixel (dpr=1) sizing — JS Highcharts sizes in CSS px within the 1600×900 mount. **Workflows** — add `highcharts` to the `javascript` case arm in `impl-generate`/`impl-repair`/`impl-review`/`impl-merge`; remove the Selenium "Setup Chrome for Highcharts" steps and the `highcharts-core` Python version-detection special-cases; JS canvas-gate repair hint. **Frontend & docs** — `LIB_TO_LANG['highcharts'] = 'javascript'`; refresh stale library-count meta descriptions (`LibrariesPage`, `PlotsPage`, `seo.py` — they listed highcharts as Python / undercounted libraries); mark Phase 2 **shipped** in `library-expansion.md`. ## Validation - **End-to-end render via the real harness** (Playwright + Chromium): light & dark **landscape 3200×1800** and **square 2400×2400**, exact pixels from the PNG IHDR; first series in brand green `#009E73`, theme-adaptive chrome. - `npm ci --dry-run` clean (lockfile in sync); `node automation/js-render/render.mjs` renders highcharts in both themes/orientations. - **1493 Python unit tests pass**; `ruff check .` + `ruff format --check` clean; all edited workflow YAML parses; alembic resolves to a single head (`a1c7e2f9b8d3`). - `tests/unit/core/test_constants.py` now asserts `highcharts` is `language_id=="javascript"` / `framework=="none"`; `test_prompts.py` treats highcharts as a JS prompt (excluded from the Python-structure checks, like chartjs/d3/echarts). ## Notes / follow-ups - `pyproject.toml` still lists `highcharts-core` (in `plotting` + the `lib-highcharts` extra). Left untouched to avoid churning `uv.lock`; it is now unused (no Python highcharts impls) and can be pruned in a small follow-up. - Library **count unchanged at 14** — a move, not an addition. https://claude.ai/code/session_01Gz8rneNnCYjDT2aR8L2wne --- _Generated by [Claude Code](https://claude.ai/code/session_01Gz8rneNnCYjDT2aR8L2wne)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 724af4f commit 1e0cced

662 files changed

Lines changed: 335 additions & 164630 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/impl-generate.yml

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ jobs:
127127
LANGUAGE="julia"
128128
EXT=".jl"
129129
;;
130-
chartjs|d3|echarts)
130+
chartjs|d3|echarts|highcharts)
131131
LANGUAGE="javascript"
132132
EXT=".js"
133133
;;
@@ -248,12 +248,6 @@ jobs:
248248
source .venv/bin/activate
249249
uv pip install pillow pyyaml
250250
251-
- name: Setup Chrome for Highcharts
252-
if: steps.inputs.outputs.library == 'highcharts'
253-
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2
254-
with:
255-
chrome-version: stable
256-
257251
- name: Setup R + ggplot2
258252
if: steps.inputs.outputs.language == 'r'
259253
uses: ./.github/actions/setup-r
@@ -622,9 +616,6 @@ jobs:
622616
letsplot)
623617
LIBRARY_VERSION=$(get_pip_version "lets-plot")
624618
;;
625-
highcharts)
626-
LIBRARY_VERSION=$(get_pip_version "highcharts-core")
627-
;;
628619
*)
629620
LIBRARY_VERSION=$(get_pip_version "$LIBRARY")
630621
;;
@@ -637,7 +628,6 @@ jobs:
637628
case "$LIBRARY" in
638629
letsplot) PYTHON_MODULE="lets_plot" ;;
639630
plotnine) PYTHON_MODULE="plotnine" ;;
640-
highcharts) PYTHON_MODULE="highcharts_core" ;;
641631
*) PYTHON_MODULE="$LIBRARY" ;;
642632
esac
643633
LIBRARY_VERSION=$(.venv/bin/python -c "import $PYTHON_MODULE; print(getattr($PYTHON_MODULE, '__version__', 'unknown'))" 2>/dev/null || echo "unknown")
@@ -656,7 +646,7 @@ jobs:
656646
# lib/render that produced no HTML never points preview_html at a 404.
657647
HAS_HTML="false"
658648
case "$LIBRARY" in
659-
plotly|bokeh|altair|highcharts|pygal|letsplot) HAS_HTML="true" ;;
649+
plotly|bokeh|altair|pygal|letsplot) HAS_HTML="true" ;;
660650
esac
661651
if [ "$LANGUAGE" = "javascript" ] && [ -f "$IMPL_DIR/plot-light.html" ] && [ -f "$IMPL_DIR/plot-dark.html" ]; then
662652
HAS_HTML="true"
@@ -945,7 +935,7 @@ jobs:
945935
HTML_LIGHT=""
946936
HTML_DARK=""
947937
case "$LIBRARY" in
948-
plotly|bokeh|altair|highcharts|pygal|letsplot)
938+
plotly|bokeh|altair|pygal|letsplot)
949939
HTML_LIGHT="${BASE}/plot-light.html"
950940
HTML_DARK="${BASE}/plot-dark.html"
951941
;;

.github/workflows/impl-merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ jobs:
113113
LANGUAGE="julia"
114114
EXT=".jl"
115115
;;
116-
chartjs|d3|echarts)
116+
chartjs|d3|echarts|highcharts)
117117
LANGUAGE="javascript"
118118
EXT=".js"
119119
;;

.github/workflows/impl-repair.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
LANGUAGE="julia"
8080
EXT=".jl"
8181
;;
82-
chartjs|d3|echarts)
82+
chartjs|d3|echarts|highcharts)
8383
LANGUAGE="javascript"
8484
EXT=".js"
8585
;;
@@ -117,12 +117,6 @@ jobs:
117117
source .venv/bin/activate
118118
uv pip install pillow pyyaml
119119
120-
- name: Setup Chrome for Highcharts
121-
if: inputs.library == 'highcharts'
122-
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2
123-
with:
124-
chrome-version: stable
125-
126120
- name: Setup R + ggplot2
127121
if: steps.lang.outputs.language == 'r'
128122
uses: ./.github/actions/setup-r

.github/workflows/impl-review.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ jobs:
109109
LANGUAGE="julia"
110110
EXT=".jl"
111111
;;
112-
chartjs|d3|echarts)
112+
chartjs|d3|echarts|highcharts)
113113
LANGUAGE="javascript"
114114
EXT=".js"
115115
;;
@@ -266,7 +266,7 @@ jobs:
266266
"altair": "Most likely cause: vl-convert padding title/legend outside `width`/`height`. Add `configure_view(continuousWidth=…, continuousHeight=…)` + zero `padding` and normalize the saved PNG with the PIL crop/pad snippet in `prompts/library/altair.md`.",
267267
"plotly": "Most likely cause: `autosize=True` or implicit margin growth. Set `autosize=False` and pin `margin=dict(...)` explicitly.",
268268
"bokeh": "Most likely cause: bokeh toolbar adding ~30-50 px above the figure. Set `toolbar_location=None`.",
269-
"highcharts": "Most likely cause: Chrome chrome eating viewport space. Use `Emulation.setDeviceMetricsOverride` via CDP (see `prompts/library/highcharts.md` Save section) — `--window-size` alone is not authoritative in headless mode.",
269+
"highcharts": "The harness owns exact pixels (Playwright deviceScaleFactor 2 over a fixed mount). Call `Highcharts.chart('container', {...})` and let it auto-size to `#container` — do NOT set `chart.width`/`chart.height`. Use `//# anyplot-orientation: square` only for square specs.",
270270
"pygal": "Check that `pygal.<Chart>(width=…, height=…)` and the SVG→PNG conversion don't override dims.",
271271
"plotnine": "Check `ggsave(width=…, height=…, units='in', dpi=400)` and `theme(figure_size=…)`; do not pass `bbox_inches='tight'`.",
272272
"letsplot": "Check `ggsize(W, H)` and `ggsave(..., scale=4)` pair — only the two canonical pairs land on target.",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""migrate_highcharts_to_javascript
2+
3+
Phase 2 of the JavaScript rollout (docs/concepts/library-expansion.md §6, the
4+
"most-used variant" rule): native highcharts.js (~1 M npm downloads/wk) vastly
5+
outweighs the highcharts-core Python wrapper (~5 k/wk), so the canonical
6+
`highcharts` registry entry moves from Python to JavaScript.
7+
8+
The libraries seed in automation/scripts/sync_to_postgres.py upserts with
9+
ON CONFLICT DO NOTHING, so it inserts *new* libraries but never rewrites an
10+
existing row. `highcharts` predates the language column (it was backfilled to
11+
'python'), so flipping its language requires an explicit data migration here.
12+
13+
This is a data-only migration (no schema change): it points the existing
14+
`highcharts` row at the 'javascript' language and refreshes its version to the
15+
vendored highcharts.js (package.json / package-lock.json). The 'javascript'
16+
language row already exists (seeded in Phase 1). On a fresh database the
17+
libraries table is empty when migrations run, so the UPDATE simply matches zero
18+
rows and the seed later inserts `highcharts` straight as JavaScript — making the
19+
migration a safe no-op in that case.
20+
21+
Revision ID: a1c7e2f9b8d3
22+
Revises: f7a2c9d4e8b1
23+
Create Date: 2026-06-09 16:45:00.000000
24+
25+
"""
26+
27+
from typing import Sequence
28+
29+
from alembic import op
30+
31+
32+
# revision identifiers, used by Alembic.
33+
revision: str = "a1c7e2f9b8d3"
34+
down_revision: str | None = "f7a2c9d4e8b1"
35+
branch_labels: str | Sequence[str] | None = None
36+
depends_on: str | Sequence[str] | None = None
37+
38+
39+
def upgrade() -> None:
40+
op.execute(
41+
"""
42+
UPDATE libraries
43+
SET language_id = 'javascript',
44+
framework = 'none',
45+
version = '12.6.0'
46+
WHERE id = 'highcharts'
47+
"""
48+
)
49+
50+
51+
def downgrade() -> None:
52+
op.execute(
53+
"""
54+
UPDATE libraries
55+
SET language_id = 'python',
56+
framework = 'none',
57+
version = '1.10.0'
58+
WHERE id = 'highcharts'
59+
"""
60+
)

api/routers/seo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def seo_plots():
175175
return HTMLResponse(
176176
BOT_HTML_TEMPLATE.format(
177177
title="plots | anyplot.ai",
178-
description="Browse and filter Python visualization examples across 9 libraries: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot.",
178+
description="Browse and filter visualization examples across 14 libraries in Python, R, Julia, and JavaScript: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, lets-plot, ggplot2, Makie.jl, Chart.js, D3.js, ECharts, Highcharts.",
179179
image=DEFAULT_PLOTS_IMAGE,
180180
url="https://anyplot.ai/plots",
181181
)

app/src/constants/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const LIB_TO_LANG: Record<string, string> = {
4343
d3: 'javascript',
4444
echarts: 'javascript',
4545
ggplot2: 'r',
46-
highcharts: 'python',
46+
highcharts: 'javascript',
4747
letsplot: 'python',
4848
makie: 'julia',
4949
matplotlib: 'python',

app/src/pages/LibrariesPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export function LibrariesPage() {
3232
<title>libraries | anyplot.ai</title>
3333
<meta
3434
name="description"
35-
content="Eleven plotting libraries across Python, R, and Julia — matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot, ggplot2, Makie.jl. Same specs, every library."
35+
content="Fourteen plotting libraries across Python, R, Julia, and JavaScript — matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, lets-plot, ggplot2, Makie.jl, Chart.js, D3.js, ECharts, Highcharts. Same specs, every library."
3636
/>
3737
<meta property="og:title" content="libraries | anyplot.ai" />
3838
<meta
3939
property="og:description"
40-
content="Eleven plotting libraries across Python, R, and Julia — same specs, every library."
40+
content="Fourteen plotting libraries across Python, R, Julia, and JavaScript — same specs, every library."
4141
/>
4242
<link rel="canonical" href="https://anyplot.ai/libraries" />
4343
</Helmet>

app/src/pages/PlotsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export function PlotsPage() {
173173
<Box onClick={handleContainerClick}>
174174
<Helmet>
175175
<title>plots | anyplot.ai</title>
176-
<meta name="description" content="Browse and filter 2,600+ visualization examples across 11 libraries in Python, R, and Julia. Search by plot type, domain, features, and more." />
176+
<meta name="description" content="Browse and filter 2,600+ visualization examples across 14 libraries in Python, R, Julia, and JavaScript. Search by plot type, domain, features, and more." />
177177
<link rel="canonical" href="https://anyplot.ai/plots" />
178178
</Helmet>
179179

automation/js-render/render.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ const BUNDLES = {
101101
chartjs: { global: "Chart", dist: "chart.js/dist/chart.umd.js" },
102102
d3: { global: "d3", dist: "d3/dist/d3.min.js" },
103103
echarts: { global: "echarts", dist: "echarts/dist/echarts.min.js" },
104+
// Highcharts ships its UMD bundle at the package root (highcharts.js); loaded
105+
// as a <script> it defines the `Highcharts` global. Commercial license, free
106+
// for non-commercial use — only the core bundle is vendored (no highcharts-more
107+
// / modules), matching the framework=none single-global model of the Phase-1 libs.
108+
highcharts: { global: "Highcharts", dist: "highcharts/highcharts.js" },
104109
};
105110

106111
function parseArgs(argv) {

0 commit comments

Comments
 (0)