From 33d667c2fba0cfb5af25dea31818822edd5d497b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 09:11:35 +0000 Subject: [PATCH 1/3] feat: embed Vega-Lite charts in Markdown via a Web Component Adds an HTML-in-Markdown authoring path for the Zensical docs: an author writes a plain Vega-Lite spec (the existing intermediate language) and it renders live as an interactive chart, with the unified preset and light/dark theme tracking injected automatically. No bespoke config schema. Two authoring syntaxes, both producing the same element: - Raw `` custom element with the spec in a nested ` + * + * ``` + * + * Attributes: `preset` (unified preset name, default the molplot preset), + * `theme` (`auto` | `light` | `dark`, default `auto`), and `spec` (inline JSON, + * a one-line alternative to the script block). + */ + +/** + * Parse the Vega-Lite spec an author embedded in a ``. Reads the + * first child ` + + +``` + +
+ + + +
+ +Attributes mirror the fence options (`preset`, `theme`), plus a `spec` attribute +holding inline JSON as a one-line alternative to the script block. The element +renders through the same [`RawChart`](web.md) the library exposes, so a spec that +carries its own `config` is respected verbatim. + +## Enabling it + +Two small additions to `zensical.toml` — already configured in this repo: + +1. **Load the component** once for the whole site: + + ```toml + extra_javascript = [ + "https://cdn.jsdelivr.net/npm/@molcrafts/molplot@0.1/dist/elements.js", + ] + ``` + + Loading it registers `` cheaply; the ~350 KB Vega runtime is a + lazy chunk that downloads only when a chart actually mounts. + +2. **Register the fence** as a `pymdownx.superfences` custom fence pointing at the + formatter shipped with the Python package (`molplot.mdx`): + + ```toml + [[project.markdown_extensions.pymdownx.superfences.custom_fences]] + name = "molplot" + class = "molplot" + format = "molplot.mdx.molplot_fence" + validator = "molplot.mdx.molplot_validator" + ``` + + The formatter runs at build time and is a pure text transform: it turns the + fenced Vega-Lite spec into the `` element above — it does not + draw the chart. Because declaring `markdown_extensions` replaces Zensical's + defaults, this repo's `zensical.toml` re-lists the full default set alongside + this fence. The `molcrafts-molplot` package must be importable at build time + (it is in the `doc` dependency group). diff --git a/python/pyproject.toml b/python/pyproject.toml index 44507fe..50268e4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -45,6 +45,11 @@ doc = [ "zensical>=0.0.45", # The molcrafts docs theme (zensical.toml sets `theme.name = "molcrafts"`). "molcrafts-zensical-theme>=0.1.0", + # This package itself, so the `molplot` custom-fence formatter + # (molplot.mdx.molplot_fence) is importable while the site compiles. + "molcrafts-molplot", + # YAML authoring for the `molplot` fenced block (molplot.mdx). + "pyyaml>=6.0", ] [project.urls] diff --git a/python/src/molplot/mdx.py b/python/src/molplot/mdx.py new file mode 100644 index 0000000..e4027f1 --- /dev/null +++ b/python/src/molplot/mdx.py @@ -0,0 +1,144 @@ +"""Markdown authoring sugar — a ``molplot`` fenced block for docs. + +Lets a documentation author embed a chart by writing a Vega-Lite spec directly +in a fenced code block instead of raw HTML:: + + ```molplot + mark: line + data: + values: + - {step: 0, energy: 1} + - {step: 1, energy: 2} + encoding: + x: {field: step, type: quantitative} + y: {field: energy, type: quantitative} + ``` + +This is a `pymdownx.superfences` *custom fence* formatter — a build-time +**text → text** transform that runs while the site is compiled. It does **not** +draw the chart: it parses the fenced body (a plain Vega-Lite spec, YAML or JSON) +and returns a ```` custom-element string carrying that spec in a +nested ``' + f"" + f"" + ) + + +def molplot_validator( + language: str, + inputs: dict[str, str], + options: dict[str, Any], + attrs: dict[str, Any], + md: Any, +) -> bool: + """`pymdownx.superfences` custom-fence validator. + + The default validator rejects any fence-header options, so ``preset=…`` / + ``theme=…`` / ``type=…`` would fall back to a plain code block. This accepts + exactly those keys and forwards them to the formatter via ``options``; + anything else fails validation (so a typo surfaces rather than silently + vanishing). + """ + for key, value in inputs.items(): + if key not in _ALLOWED_OPTIONS: + return False + options[key] = value + return True + + +def molplot_fence( + source: str, + language: str, + css_class: str, + options: dict[str, Any], + md: Any, + **kwargs: Any, +) -> str: + """`pymdownx.superfences` custom-fence formatter (see the module docstring). + + Signature follows the superfences ``format`` contract; ``options`` holds the + validated ``key=value`` pairs from the fence header (e.g. + ``preset=molplot-paper``) that :func:`molplot_validator` allowed through. + """ + return render_element( + source, + preset=options.get("preset"), + theme=options.get("theme"), + ) diff --git a/python/tests/test_mdx.py b/python/tests/test_mdx.py new file mode 100644 index 0000000..5e34ad6 --- /dev/null +++ b/python/tests/test_mdx.py @@ -0,0 +1,74 @@ +import json + +from molplot.mdx import molplot_fence, molplot_validator, render_element + + +def _extract_spec(el: str) -> dict: + start = el.index(">", el.index("") + return json.loads(el[start:end]) + + +def test_render_element_wraps_yaml_spec_in_a_block_div(): + el = render_element( + "mark: line\n" + "data:\n" + " values:\n" + " - {x: 0, y: 1}\n" + " - {x: 1, y: 2}\n" + "encoding:\n" + " x: {field: x, type: quantitative}\n" + ) + # A block-level
wrapper keeps Markdown/md_in_html from wrapping the + # element in

or reprocessing the JSON. + assert el.startswith('

') + assert el.endswith("
") + assert "" in el + spec = _extract_spec(el) + assert spec["mark"] == "line" + assert spec["data"]["values"] == [{"x": 0, "y": 1}, {"x": 1, "y": 2}] + + +def test_render_element_accepts_json_spec(): + spec = _extract_spec(render_element('{"mark": "bar", "data": {"values": []}}')) + assert spec == {"mark": "bar", "data": {"values": []}} + + +def test_render_element_forwards_preset_and_theme(): + el = render_element("mark: point", preset="molplot-paper", theme="dark") + assert 'preset="molplot-paper"' in el + assert 'theme="dark"' in el + + +def test_render_element_reports_parse_error_inline(): + el = render_element("mark: [unterminated") + assert "molplot-error" in el + assert "` Web Component (the `elements` +# bundle of @molcrafts/molplot). It registers the custom element cheaply; the +# vega runtime is a lazy chunk that downloads only when a chart mounts. This is +# what makes the `` element and the ```molplot fence render live. +extra_javascript = [ + "https://cdn.jsdelivr.net/npm/@molcrafts/molplot@0.1/dist/elements.js", +] nav = [ { "Home" = "index.md" }, @@ -21,10 +23,51 @@ nav = [ "getting-started/preset.md", { "Web (Vega-Lite)" = "getting-started/web.md" }, { "Python (scienceplots)" = "getting-started/python.md" }, + { "Charts in Markdown" = "getting-started/markdown.md" }, ] }, { "API Reference" = "api/index.md" }, ] +# Markdown extensions. Declaring this section REPLACES Zensical's built-in +# default set (it is not merged), so the full default set is reproduced verbatim +# below — the only addition is the `molplot` custom fence (last custom_fence +# entry), which compiles a ```molplot block to a element via +# molplot.mdx.molplot_fence (the Python package must be importable at build +# time; see the `doc` group in python/pyproject.toml). Keep this in sync with +# Zensical's defaults on upgrade. +[project.markdown_extensions] +abbr = {} +admonition = {} +attr_list = {} +def_list = {} +footnotes = {} +md_in_html = {} + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.pymdownx] +betterem = {} +caret = {} +details = {} +inlinehilite = {} +keys = {} +magiclink = {} +mark = {} +smartsymbols = {} +tilde = {} +arithmatex = { generic = true } +emoji = { emoji_generator = "zensical.extensions.emoji.to_svg", emoji_index = "zensical.extensions.emoji.twemoji" } +highlight = { anchor_linenums = true, line_spans = "__span", pygments_lang_class = true } +tabbed = { alternate_style = true, combine_header_slug = true } +tasklist = { custom_checkbox = true } + +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid" }, + { name = "molplot", class = "molplot", format = "molplot.mdx.molplot_fence", validator = "molplot.mdx.molplot_validator" }, +] + # Shared MolCrafts theme (github.com/MolCrafts/molcrafts-zensical-theme). Provides # the brand palette, light/dark schemes, navigation features, and the home-page # hero/manual-home component system used by docs/index.md. The package must be From 6478e660814ed3890af41dae0eb201c250fcc2bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 09:23:46 +0000 Subject: [PATCH 2/3] feat(presets): align the unified preset with scienceplots' science base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preset's categorical palette drives both the web Vega range and the matplotlib prop_cycle, but it shipped d3's category20 — which the generated mplstyle then layered *over* scienceplots' own colours, so browser and paper figures didn't actually match. Re-derive the tokens from scienceplots so the "one preset, two renderers" promise holds. - Categorical palette → scienceplots' standard seven-colour "science" cycle (0c5da5, 00b945, ff9500, ff2c00, 845b97, 474747, 9e9e9e), shared by both presets so web and paper stay colour-consistent. - `molplot` default now matches the science base: serif type, 1.0 pt lines, black-on-white axes. `molplot-paper` keeps its tighter nature-sized figure and higher DPI, with markers at 3 pt. - The Python side still layers this overlay on the scienceplots base, so it also inherits science's tick geometry (direction in, minor ticks, top/right). Regenerated core/src/presets/generated.ts, the Python _generated.py, and the four .mplstyle files from the two hand-edited presets/*.json. Updated the palette assertions in the TS/Python tests and the palette references in the docs. Full stack verified: molplot.use("molplot") renders lines in the science cycle with serif type and 1.0 pt strokes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FwRZiMJXPcHWZLDUhduxmC --- core/src/presets/generated.ts | 84 +++++++------------ core/src/theme.ts | 7 +- core/tests/preset.test.ts | 4 +- docs/getting-started/python.md | 2 +- presets/molplot-paper.json | 33 +++----- presets/molplot.json | 51 +++++------ python/README.md | 2 +- python/src/molplot/presets/_generated.py | 84 +++++++------------ .../src/molplot/presets/molplot-dark.mplstyle | 20 ++--- .../presets/molplot-paper-dark.mplstyle | 4 +- .../molplot/presets/molplot-paper.mplstyle | 4 +- python/src/molplot/presets/molplot.mplstyle | 20 ++--- python/tests/test_preset.py | 4 +- python/tests/test_specs.py | 4 +- python/tests/test_style.py | 4 +- 15 files changed, 125 insertions(+), 202 deletions(-) diff --git a/core/src/presets/generated.ts b/core/src/presets/generated.ts index 8add7be..8dd3e50 100644 --- a/core/src/presets/generated.ts +++ b/core/src/presets/generated.ts @@ -52,13 +52,13 @@ export const PRESETS = { "molplot": { "name": "molplot", "label": "MolPlot", - "description": "MolCrafts unified scientific charting preset — the single source of truth shared by the Observable Plot + D3 web renderer and the scienceplots/matplotlib Python renderer.", + "description": "MolCrafts unified scientific charting preset, faithful to scienceplots' 'science' base — its standard seven-colour cycle, thin 1.0 pt lines, serif type, and black-on-white axes — so a browser chart and a matplotlib figure share the same scienceplots look.", "sciencePlotsBase": [ "science", "no-latex" ], "typography": { - "family": "Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif", + "family": "Times New Roman, Times, Nimbus Roman, serif", "familyMono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", "familySerif": "Times New Roman, Times, Nimbus Roman, serif", "size": { @@ -71,33 +71,20 @@ export const PRESETS = { }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5" + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e" ], - "defaultColor": "#60a5fa", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu" }, "geometry": { - "lineWidth": 2, + "lineWidth": 1, "markerSize": 6, "barGap": 0.2, "figSize": [ @@ -116,19 +103,19 @@ export const PRESETS = { "light": { "background": "transparent", "figureFace": "#ffffff", - "foreground": "#52525b", - "gridColor": "rgba(120,120,120,0.18)", - "gridColorSolid": "#d4d4d8", - "tickColor": "#a1a1aa", + "foreground": "#000000", + "gridColor": "rgba(0,0,0,0.10)", + "gridColorSolid": "#cccccc", + "tickColor": "#000000", "highlightRing": "#111827" }, "dark": { "background": "transparent", "figureFace": "#111113", - "foreground": "#d4d4d8", - "gridColor": "rgba(220,220,220,0.12)", - "gridColorSolid": "#3f3f46", - "tickColor": "#71717a", + "foreground": "#e5e5e5", + "gridColor": "rgba(255,255,255,0.10)", + "gridColorSolid": "#444444", + "tickColor": "#e5e5e5", "highlightRing": "#f9fafb" } } @@ -136,7 +123,7 @@ export const PRESETS = { "molplot-paper": { "name": "molplot-paper", "label": "MolPlot Paper", - "description": "Publication variant of the MolPlot preset — serif type, tighter figure, high DPI, layered on scienceplots' 'nature' base. Same categorical palette as the default so web and paper figures stay colour-consistent.", + "description": "Publication variant layered on scienceplots' 'science' + 'nature' bases — the same standard seven-colour cycle as the default, thin 1.0 pt lines and serif type, but a tighter single-column figure and higher DPI. Web and paper figures stay colour-consistent.", "sciencePlotsBase": [ "science", "nature", @@ -156,34 +143,21 @@ export const PRESETS = { }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5" + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e" ], - "defaultColor": "#1f77b4", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu" }, "geometry": { "lineWidth": 1, - "markerSize": 4, + "markerSize": 3, "barGap": 0.2, "figSize": [ 3.3, diff --git a/core/src/theme.ts b/core/src/theme.ts index 8db2213..0830d72 100644 --- a/core/src/theme.ts +++ b/core/src/theme.ts @@ -2,9 +2,10 @@ import { getPreset } from "./preset"; import type { ThemeMode } from "./types"; /** - * 20-entry categorical palette (d3 category20). Re-exported for API - * compatibility with the former plotly build; the authoritative copy now - * lives in `presets/molplot.json` and flows through {@link getPreset}. + * Categorical palette — scienceplots' standard seven-colour "science" cycle. + * Re-exported for API compatibility with the former plotly build; the + * authoritative copy now lives in `presets/molplot.json` and flows through + * {@link getPreset}. */ export const CHART_PALETTE: readonly string[] = getPreset().palette.categorical; diff --git a/core/tests/preset.test.ts b/core/tests/preset.test.ts index 23ca74a..8aa15a3 100644 --- a/core/tests/preset.test.ts +++ b/core/tests/preset.test.ts @@ -14,7 +14,7 @@ describe("preset", () => { it("CHART_PALETTE matches the default preset tokens", () => { expect(CHART_PALETTE).toEqual(getPreset("molplot").palette.categorical); - expect(CHART_PALETTE[0]).toBe("#1f77b4"); + expect(CHART_PALETTE[0]).toBe("#0c5da5"); }); }); @@ -39,7 +39,7 @@ describe("vegaConfig", () => { const cfg = vegaConfig(resolveTheme("light")); // biome-ignore lint/suspicious/noExplicitAny: loose VL config shape const c = cfg as any; - expect(c.range.category[0]).toBe("#1f77b4"); + expect(c.range.category[0]).toBe("#0c5da5"); expect(c.axis.grid).toBe(true); expect(typeof c.axis.labelFontSize).toBe("number"); }); diff --git a/docs/getting-started/python.md b/docs/getting-started/python.md index ab5a9e3..805dfa7 100644 --- a/docs/getting-started/python.md +++ b/docs/getting-started/python.md @@ -20,7 +20,7 @@ molplot.use("molplot-paper") # serif, high-DPI, 'nature' base with molplot.style("molplot", mode="dark"): plt.plot(x, y) # scoped; restores on exit -molplot.palette()[0] # '#1f77b4' — same colours as the web +molplot.palette()[0] # '#0c5da5' — same colours as the web ``` `plt.style.use("molplot")` also works directly (the `.mplstyle` files register diff --git a/presets/molplot-paper.json b/presets/molplot-paper.json index 404bf9b..73df3fb 100644 --- a/presets/molplot-paper.json +++ b/presets/molplot-paper.json @@ -2,7 +2,7 @@ "$schema": "./preset.schema.json", "name": "molplot-paper", "label": "MolPlot Paper", - "description": "Publication variant of the MolPlot preset — serif type, tighter figure, high DPI, layered on scienceplots' 'nature' base. Same categorical palette as the default so web and paper figures stay colour-consistent.", + "description": "Publication variant layered on scienceplots' 'science' + 'nature' bases — the same standard seven-colour cycle as the default, thin 1.0 pt lines and serif type, but a tighter single-column figure and higher DPI. Web and paper figures stay colour-consistent.", "sciencePlotsBase": ["science", "nature", "no-latex"], "typography": { "family": "Times New Roman, Times, Nimbus Roman, serif", @@ -18,34 +18,21 @@ }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5" + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e" ], - "defaultColor": "#1f77b4", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu" }, "geometry": { "lineWidth": 1, - "markerSize": 4, + "markerSize": 3, "barGap": 0.2, "figSize": [3.3, 2.5], "dpi": 600, diff --git a/presets/molplot.json b/presets/molplot.json index 78d2685..bc6e6e9 100644 --- a/presets/molplot.json +++ b/presets/molplot.json @@ -2,10 +2,10 @@ "$schema": "./preset.schema.json", "name": "molplot", "label": "MolPlot", - "description": "MolCrafts unified scientific charting preset — the single source of truth shared by the Observable Plot + D3 web renderer and the scienceplots/matplotlib Python renderer.", + "description": "MolCrafts unified scientific charting preset, faithful to scienceplots' 'science' base — its standard seven-colour cycle, thin 1.0 pt lines, serif type, and black-on-white axes — so a browser chart and a matplotlib figure share the same scienceplots look.", "sciencePlotsBase": ["science", "no-latex"], "typography": { - "family": "Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif", + "family": "Times New Roman, Times, Nimbus Roman, serif", "familyMono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", "familySerif": "Times New Roman, Times, Nimbus Roman, serif", "size": { @@ -18,33 +18,20 @@ }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5" + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e" ], - "defaultColor": "#60a5fa", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu" }, "geometry": { - "lineWidth": 2, + "lineWidth": 1, "markerSize": 6, "barGap": 0.2, "figSize": [3.5, 2.625], @@ -55,19 +42,19 @@ "light": { "background": "transparent", "figureFace": "#ffffff", - "foreground": "#52525b", - "gridColor": "rgba(120,120,120,0.18)", - "gridColorSolid": "#d4d4d8", - "tickColor": "#a1a1aa", + "foreground": "#000000", + "gridColor": "rgba(0,0,0,0.10)", + "gridColorSolid": "#cccccc", + "tickColor": "#000000", "highlightRing": "#111827" }, "dark": { "background": "transparent", "figureFace": "#111113", - "foreground": "#d4d4d8", - "gridColor": "rgba(220,220,220,0.12)", - "gridColorSolid": "#3f3f46", - "tickColor": "#71717a", + "foreground": "#e5e5e5", + "gridColor": "rgba(255,255,255,0.10)", + "gridColorSolid": "#444444", + "tickColor": "#e5e5e5", "highlightRing": "#f9fafb" } } diff --git a/python/README.md b/python/README.md index 50ccb1e..079c262 100644 --- a/python/README.md +++ b/python/README.md @@ -33,7 +33,7 @@ molplot.use("molplot-paper") with molplot.style("molplot", mode="dark"): plt.plot(x, y) # scoped; restores rcParams on exit -molplot.palette()[0] # '#1f77b4' — same categorical colours as the web +molplot.palette()[0] # '#0c5da5' — same categorical colours as the web ``` `plt.style.use("molplot")` also works directly (the `.mplstyle` files are diff --git a/python/src/molplot/presets/_generated.py b/python/src/molplot/presets/_generated.py index 02aee41..0ed42b7 100644 --- a/python/src/molplot/presets/_generated.py +++ b/python/src/molplot/presets/_generated.py @@ -9,13 +9,13 @@ "molplot": { "name": "molplot", "label": "MolPlot", - "description": "MolCrafts unified scientific charting preset — the single source of truth shared by the Observable Plot + D3 web renderer and the scienceplots/matplotlib Python renderer.", + "description": "MolCrafts unified scientific charting preset, faithful to scienceplots' 'science' base — its standard seven-colour cycle, thin 1.0 pt lines, serif type, and black-on-white axes — so a browser chart and a matplotlib figure share the same scienceplots look.", "sciencePlotsBase": [ "science", "no-latex", ], "typography": { - "family": "Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif", + "family": "Times New Roman, Times, Nimbus Roman, serif", "familyMono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", "familySerif": "Times New Roman, Times, Nimbus Roman, serif", "size": { @@ -28,33 +28,20 @@ }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5", + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e", ], - "defaultColor": "#60a5fa", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu", }, "geometry": { - "lineWidth": 2, + "lineWidth": 1, "markerSize": 6, "barGap": 0.2, "figSize": [ @@ -73,19 +60,19 @@ "light": { "background": "transparent", "figureFace": "#ffffff", - "foreground": "#52525b", - "gridColor": "rgba(120,120,120,0.18)", - "gridColorSolid": "#d4d4d8", - "tickColor": "#a1a1aa", + "foreground": "#000000", + "gridColor": "rgba(0,0,0,0.10)", + "gridColorSolid": "#cccccc", + "tickColor": "#000000", "highlightRing": "#111827", }, "dark": { "background": "transparent", "figureFace": "#111113", - "foreground": "#d4d4d8", - "gridColor": "rgba(220,220,220,0.12)", - "gridColorSolid": "#3f3f46", - "tickColor": "#71717a", + "foreground": "#e5e5e5", + "gridColor": "rgba(255,255,255,0.10)", + "gridColorSolid": "#444444", + "tickColor": "#e5e5e5", "highlightRing": "#f9fafb", }, }, @@ -93,7 +80,7 @@ "molplot-paper": { "name": "molplot-paper", "label": "MolPlot Paper", - "description": "Publication variant of the MolPlot preset — serif type, tighter figure, high DPI, layered on scienceplots' 'nature' base. Same categorical palette as the default so web and paper figures stay colour-consistent.", + "description": "Publication variant layered on scienceplots' 'science' + 'nature' bases — the same standard seven-colour cycle as the default, thin 1.0 pt lines and serif type, but a tighter single-column figure and higher DPI. Web and paper figures stay colour-consistent.", "sciencePlotsBase": [ "science", "nature", @@ -113,34 +100,21 @@ }, "palette": { "categorical": [ - "#1f77b4", - "#ff7f0e", - "#2ca02c", - "#d62728", - "#9467bd", - "#8c564b", - "#e377c2", - "#7f7f7f", - "#bcbd22", - "#17becf", - "#aec7e8", - "#ffbb78", - "#98df8a", - "#ff9896", - "#c5b0d5", - "#c49c94", - "#f7b6d2", - "#c7c7c7", - "#dbdb8d", - "#9edae5", + "#0c5da5", + "#00b945", + "#ff9500", + "#ff2c00", + "#845b97", + "#474747", + "#9e9e9e", ], - "defaultColor": "#1f77b4", + "defaultColor": "#0c5da5", "sequential": "viridis", "diverging": "RdBu", }, "geometry": { "lineWidth": 1, - "markerSize": 4, + "markerSize": 3, "barGap": 0.2, "figSize": [ 3.3, diff --git a/python/src/molplot/presets/molplot-dark.mplstyle b/python/src/molplot/presets/molplot-dark.mplstyle index 6044532..c4f1095 100644 --- a/python/src/molplot/presets/molplot-dark.mplstyle +++ b/python/src/molplot/presets/molplot-dark.mplstyle @@ -9,8 +9,8 @@ figure.facecolor: 111113 axes.facecolor: 111113 savefig.facecolor: 111113 -font.family: sans-serif -font.sans-serif: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif +font.family: serif +font.serif: Times New Roman, Times, Nimbus Roman, serif font.size: 10 axes.titlesize: 12 axes.labelsize: 10 @@ -18,18 +18,18 @@ xtick.labelsize: 9 ytick.labelsize: 9 legend.fontsize: 9 -axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf', 'aec7e8', 'ffbb78', '98df8a', 'ff9896', 'c5b0d5', 'c49c94', 'f7b6d2', 'c7c7c7', 'dbdb8d', '9edae5']) -lines.linewidth: 2 +axes.prop_cycle: cycler('color', ['0c5da5', '00b945', 'ff9500', 'ff2c00', '845b97', '474747', '9e9e9e']) +lines.linewidth: 1 lines.markersize: 6 -text.color: d4d4d8 -axes.edgecolor: d4d4d8 -axes.labelcolor: d4d4d8 -xtick.color: d4d4d8 -ytick.color: d4d4d8 +text.color: e5e5e5 +axes.edgecolor: e5e5e5 +axes.labelcolor: e5e5e5 +xtick.color: e5e5e5 +ytick.color: e5e5e5 axes.grid: True -grid.color: 3f3f46 +grid.color: 444444 grid.linewidth: 0.5 grid.alpha: 1.0 axes.axisbelow: True diff --git a/python/src/molplot/presets/molplot-paper-dark.mplstyle b/python/src/molplot/presets/molplot-paper-dark.mplstyle index 15634a5..e70903b 100644 --- a/python/src/molplot/presets/molplot-paper-dark.mplstyle +++ b/python/src/molplot/presets/molplot-paper-dark.mplstyle @@ -18,9 +18,9 @@ xtick.labelsize: 8 ytick.labelsize: 8 legend.fontsize: 8 -axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf', 'aec7e8', 'ffbb78', '98df8a', 'ff9896', 'c5b0d5', 'c49c94', 'f7b6d2', 'c7c7c7', 'dbdb8d', '9edae5']) +axes.prop_cycle: cycler('color', ['0c5da5', '00b945', 'ff9500', 'ff2c00', '845b97', '474747', '9e9e9e']) lines.linewidth: 1 -lines.markersize: 4 +lines.markersize: 3 text.color: e5e5e5 axes.edgecolor: e5e5e5 diff --git a/python/src/molplot/presets/molplot-paper.mplstyle b/python/src/molplot/presets/molplot-paper.mplstyle index 67d6565..97d4367 100644 --- a/python/src/molplot/presets/molplot-paper.mplstyle +++ b/python/src/molplot/presets/molplot-paper.mplstyle @@ -18,9 +18,9 @@ xtick.labelsize: 8 ytick.labelsize: 8 legend.fontsize: 8 -axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf', 'aec7e8', 'ffbb78', '98df8a', 'ff9896', 'c5b0d5', 'c49c94', 'f7b6d2', 'c7c7c7', 'dbdb8d', '9edae5']) +axes.prop_cycle: cycler('color', ['0c5da5', '00b945', 'ff9500', 'ff2c00', '845b97', '474747', '9e9e9e']) lines.linewidth: 1 -lines.markersize: 4 +lines.markersize: 3 text.color: 000000 axes.edgecolor: 000000 diff --git a/python/src/molplot/presets/molplot.mplstyle b/python/src/molplot/presets/molplot.mplstyle index 443d2b6..eda89e1 100644 --- a/python/src/molplot/presets/molplot.mplstyle +++ b/python/src/molplot/presets/molplot.mplstyle @@ -9,8 +9,8 @@ figure.facecolor: ffffff axes.facecolor: ffffff savefig.facecolor: ffffff -font.family: sans-serif -font.sans-serif: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif +font.family: serif +font.serif: Times New Roman, Times, Nimbus Roman, serif font.size: 10 axes.titlesize: 12 axes.labelsize: 10 @@ -18,18 +18,18 @@ xtick.labelsize: 9 ytick.labelsize: 9 legend.fontsize: 9 -axes.prop_cycle: cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2', '7f7f7f', 'bcbd22', '17becf', 'aec7e8', 'ffbb78', '98df8a', 'ff9896', 'c5b0d5', 'c49c94', 'f7b6d2', 'c7c7c7', 'dbdb8d', '9edae5']) -lines.linewidth: 2 +axes.prop_cycle: cycler('color', ['0c5da5', '00b945', 'ff9500', 'ff2c00', '845b97', '474747', '9e9e9e']) +lines.linewidth: 1 lines.markersize: 6 -text.color: 52525b -axes.edgecolor: 52525b -axes.labelcolor: 52525b -xtick.color: 52525b -ytick.color: 52525b +text.color: 000000 +axes.edgecolor: 000000 +axes.labelcolor: 000000 +xtick.color: 000000 +ytick.color: 000000 axes.grid: True -grid.color: d4d4d8 +grid.color: cccccc grid.linewidth: 0.5 grid.alpha: 1.0 axes.axisbelow: True diff --git a/python/tests/test_preset.py b/python/tests/test_preset.py index c4a3e44..edad096 100644 --- a/python/tests/test_preset.py +++ b/python/tests/test_preset.py @@ -12,7 +12,7 @@ def test_unknown_preset_falls_back(): def test_palette_matches_tokens(): - assert molplot.palette("molplot")[0] == "#1f77b4" + assert molplot.palette("molplot")[0] == "#0c5da5" assert molplot.palette() == get_preset("molplot")["palette"]["categorical"] @@ -26,7 +26,7 @@ def test_resolve_light_vs_dark(): def test_rc_params_inject_palette_and_type_scale(): rc = rc_params("molplot", "light") colors = rc["axes.prop_cycle"].by_key()["color"] - assert colors[0] == "#1f77b4" + assert colors[0] == "#0c5da5" assert rc["font.size"] == 10 assert rc["axes.grid"] is True diff --git a/python/tests/test_specs.py b/python/tests/test_specs.py index bc8400b..8792370 100644 --- a/python/tests/test_specs.py +++ b/python/tests/test_specs.py @@ -13,7 +13,7 @@ def test_line_spec_shape_and_config(): assert spec["$schema"].endswith("vega-lite/v5.json") assert spec["data"]["values"][0] == {"s": "a", "key": "A", "i": 0, "x": 0, "y": 1} # Unified preset injected as the VL config. - assert spec["config"]["range"]["category"][0] == "#1f77b4" + assert spec["config"]["range"]["category"][0] == "#0c5da5" color = spec["layer"][0]["encoding"]["color"] assert color["scale"]["domain"] == ["A"] assert color["legend"] == {"title": None} @@ -52,7 +52,7 @@ def test_gantt_spec_spans_and_status_colours(): def test_vega_config_matches_preset_palette(): cfg = molplot.vega_config("molplot") - assert cfg["range"]["category"][0] == "#1f77b4" + assert cfg["range"]["category"][0] == "#0c5da5" assert cfg["axis"]["grid"] is True diff --git a/python/tests/test_style.py b/python/tests/test_style.py index 2248dcd..1046049 100644 --- a/python/tests/test_style.py +++ b/python/tests/test_style.py @@ -18,14 +18,14 @@ def test_style_context_applies_and_restores(): before = plt.rcParams["axes.prop_cycle"].by_key()["color"] with molplot.style("molplot"): inside = plt.rcParams["axes.prop_cycle"].by_key()["color"] - assert inside[0] == "#1f77b4" + assert inside[0] == "#0c5da5" after = plt.rcParams["axes.prop_cycle"].by_key()["color"] assert after == before def test_use_applies_persistently(): molplot.use("molplot") - assert plt.rcParams["axes.prop_cycle"].by_key()["color"][0] == "#1f77b4" + assert plt.rcParams["axes.prop_cycle"].by_key()["color"][0] == "#0c5da5" assert plt.rcParams["font.size"] == 10 From 979ded88a9069b36a7f58fab91c0d77e7e887351 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 09:38:25 +0000 Subject: [PATCH 3/3] fix(ci): install pyyaml for the mdx tests The test-python CI job installs `.[dev]`, but pyyaml lived only in the PEP-735 `doc` dependency group, so `import yaml` failed in molplot.mdx during CI and the YAML-fed test_mdx.py cases hit the error branch (3 failed, 40 passed). It passed locally only because pyyaml happened to be installed system-wide. Declare pyyaml where it belongs: a new `mdx` extra (the fence formatter's runtime dep) and add it to `dev` so the test env installs it. The `doc` group already lists pyyaml for the Zensical build. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FwRZiMJXPcHWZLDUhduxmC --- python/pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/pyproject.toml b/python/pyproject.toml index 50268e4..49ec413 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -29,11 +29,15 @@ dependencies = [ [project.optional-dependencies] convert = ["vl-convert-python>=1.6"] +# YAML authoring for the `molplot` Markdown fence formatter (molplot.mdx). +mdx = ["pyyaml>=6.0"] dev = [ "build>=1.0", "hatchling", "pytest>=7.0", "pytest-cov", + # mdx's runtime dep, so test_mdx.py can exercise the YAML path. + "pyyaml>=6.0", ] # Docs are built with Zensical from the single site config at ../zensical.toml