release: v0.1.4 — VL annotations + layer0 zoom - #9
Merged
Roy-Kid merged 20 commits intoAug 5, 2026
Conversation
…ter (MolCrafts#1) Replace the ad-hoc per-mark render.py prototype with a small interpreter package (molplot.vlmpl) structured as four compiler passes: normalize → bind scales → dispatch marks → finalize axes. Mark encoders read typed channels instead of hardcoded field names, so marks and scales are extensible. Fixes two Vega-Lite → matplotlib parity gaps the prototype had: - positional scales are applied: log axes (scale.type) and explicit domains (scale.domain → axis limits) now reach the figure. - per-layer transform filters are honoured, so a plain line chart no longer draws the stray marker-layer points; the detail channel splits series that share a legend colour. render.py becomes a thin shim re-exporting molplot.vlmpl.render, preserving the public molplot.render API. Adds test_vlmpl.py for the fixed behaviour.
Every chart now declares one scale-bound interval selection per continuous axis. Drag pans, a wheel over an axis gutter zooms that axis alone, shift+wheel zooms every bound axis, double click resets. A bare wheel over the plot is left untouched so an enclosing panel still scrolls. Band scales (bar's category axis, gantt's task axis) get no param at all — Vega-Lite refuses to bind a discrete domain — so they are inert by construction rather than by special case. Three Vega constraints drove the shape of this: - Selection params must sit on exactly one unit layer. At the top level of a layered spec Vega-Lite copies them into every layer and Vega then throws "Duplicate signal name" at parse time, while vl.compile() stays silent. specs.test.ts compiles each spec and asserts no duplicate signal names. - `bind: "scales"` defaults its wheel stream to `source: "scope"` — the plot group. Axes render with pointer-events: none, so an axis wheel falls through to the SVG root and never enters that group. Switching to `view:` catches it, and keeps sibling charts on the page from reacting. - A Vega event filter is compiled without the signal scope object: it may call x()/y() but throws `ReferenceError: _ is not defined` on any signal read. That rules out `y() > height`, so VegaChart resolves the pointer against the scenegraph's axis bounds and hands the spec a boolean flag. `|| event.shiftKey` keeps the spec self-sufficient for a raw vegaEmbed. The classification is a pure function (axisChannelAt) tested with plain rectangles: grid boxes coincide with the plot rect, legends never carry role "axis", and a wheel below a bottom legend must stay inert. Marks are now clipped, without which a zoomed mark paints over the axes. `params` is inert on paper: render.py reads only encoding/layer/mark, so the matplotlib translator and vl-convert draw the initial view.
`npm run dev` now aliases dev:page, so the gallery starts from the repo root like every other MolCrafts package. Zoom is gated on hovering an axis or holding shift, neither of which is discoverable. The header names the four gestures, and each card says which of its axes can be zoomed — bar and gantt zoom only one, because the other is a band scale.
Moves zensical.toml onto the [project] schema and gives docs/index.md a hero front matter block (kicker, install command, npm/PyPI/licence badges). Adds a `doc` dependency group to python/pyproject.toml pinning zensical and the shared molcrafts-zensical-theme, and a Pages workflow that builds the site from that group alone — the docs are hand-written Markdown, so the job needs neither Node, the preset compiler, nor a wheel.
Lock the npm package, the Python package and the workspace root to one version — the v* tag fires release-core.yml and release-python.yml together, so a mismatch would publish two different versions under one tag.
The workflow was copied from molpack, which still publishes to molcrafts.github.io. This repo has no Pages site, so `actions/configure-pages` failed on the first push to master with "Get Pages site failed". Docs here deploy the way molpy and molvis do: Cloudflare builds the Zensical site straight from the repo, and neither of those ships a docs workflow. So remove ours and move site_url onto the same <name>.molcrafts.org convention. The `doc` dependency group stays — the Cloudflare build installs zensical and the shared molcrafts theme from it.
The v0.1.1 release PR was rebase-merged, which rewrote dev's commits onto master under new SHAs. dev and master then held the same tree under two histories, so the next dev->master PR replayed five duplicate commits. Merging master back in makes it an ancestor of dev again; future dev->master merges fast-forward. Use a merge commit for the release PR next time.
merge: reconcile dev with master
Unify local hooks and CI on `uv run --extra dev tox -e lint|py`. tox lives in optional-dependencies dev; prek language: system.
Add portable annotation layers (scaleBar |———|, arrow) in TS and Python. RawChart/fence top-level `annotations` expand before render; matplotlib gains rule/text mark encoders. Bump to 0.1.4.
Support absolute x2/y2 ends and geometric tickRatio for log axes; region labels use Times italic.
fontScaleForWidth is 2–3× (200% at design width). Axis/text marks use normal Times style; annotation labels no longer italic or fixed px.
scaleBar orientation=along draws a chord with log-space perpendicular caps (matplotlib-style). fontScaleForWidth floor 3× paper, cap 4.5×.
Translate the bar along the log-space path normal (default 0.42) so it does not overlap the curve; end-caps remain ⊥ to the bar.
One scale_bar() owns spine + both end-caps + label — never hand-draw caps. Mirrors FancyArrowPatch(arrowstyle='|-|') / AnchoredSizeBar.
Zoom params bind at top-level when encoding is shared (docs layered specs); layer0-only placement left bind:scales inert. Scale bars/arrows are drawn as a screen-space SVG overlay from Vega scales after layout and on pan/zoom — not pre-baked VL geometry.
Expand scaleBar/arrow into rule/text layers that share chart scales so they pan/zoom with the plot. Put bind:scales params on layer[0] only — top-level params with multi-layer specs make Vega-Lite emit duplicate zoom signals and vega.parse fails (chart invisible). Move interactive unit test out of src/ so dist stays free of *.test.js. Ignore staged docs/assets in biome.
Annotations as pure Vega-Lite layers (scaleBar/arrow); zoom params on layer[0] so multi-layer charts parse without duplicate signals. Docs pan/zoom with regime markers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
annotations(scaleBar/arrow) into pure Vega-Literule/textlayers so regime markers pan/zoom with the chart.bind:scaleszoom params onlayer[0]only — top-level params + multi-layer specs caused duplicate VL selection signals and silent render failure.src/sodiststays free of*.test.js; ignore stageddocs/assetsin biome.Test plan
v0.1.4→ npm + GitHub release