diff --git a/core/package.json b/core/package.json
index d4780e8..d1ccc5b 100644
--- a/core/package.json
+++ b/core/package.json
@@ -6,6 +6,9 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
+ },
+ "./elements": {
+ "import": "./dist/elements.js"
}
},
"repository": {
diff --git a/core/rslib.config.ts b/core/rslib.config.ts
index 1492d60..e08fe0d 100644
--- a/core/rslib.config.ts
+++ b/core/rslib.config.ts
@@ -16,5 +16,26 @@ export default defineConfig({
externals: ["vega", "vega-lite", "vega-embed"],
},
},
+ // Self-contained browser bundle (`./elements` export). Loading it registers
+ // the `` custom element, so a docs page needs only a single
+ //
+ *
+ * ```
+ *
+ * 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/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/pyproject.toml b/python/pyproject.toml
index 44507fe..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
@@ -45,6 +49,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/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_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("