From cf396c2ad6844e19478daaf1c10f6ecb500bb63d Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 14:48:25 +0100 Subject: [PATCH 01/16] format pyproject.toml --- pyproject.toml | 111 ++++++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5ebdca4..aeca375 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,75 +1,84 @@ [project] name = "plotjs" -description = "Turn static matplotlib charts into interactive web visualizations" version = "0.0.9" +description = "Turn static matplotlib charts into interactive web visualizations" +readme = "README.md" +requires-python = ">=3.10" license = "MIT" license-files = ["LICENSE"] -keywords = ["matplotlib", "interactive", "javascript", "web", "css", "d3", "mpld3", "plotnine"] authors = [ - { name="Joseph Barbier", email="joseph.barbierdarnal@mail.com" }, + { name = "Joseph Barbier", email = "joseph.barbierdarnal@mail.com" }, +] +keywords = [ + "css", + "d3", + "interactive", + "javascript", + "matplotlib", + "mpld3", + "plotnine", + "web" ] -readme = "README.md" -requires-python = ">=3.10" classifiers = [ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "Development Status :: 3 - Alpha" + "Development Status :: 3 - Alpha", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3" ] dependencies = [ - "jinja2>=3.0.0", - "matplotlib>=3.10.0", - "narwhals>=2.0.0", + "jinja2>=3.0.0", + "matplotlib>=3.10.0", + "narwhals>=2.0.0", +] + +[project.urls] +Documentation = "https://y-sunflower.github.io/plotjs/" +Homepage = "https://y-sunflower.github.io/plotjs/" +Issues = "https://github.com/y-sunflower/plotjs/issues" +Repository = "https://github.com/y-sunflower/plotjs" + +[dependency-groups] +dev = [ + "coverage>=7.9.1", + "drawarrow>=0.1.0", + "genbadge[coverage]>=1.1.2", + "highlight-text>=0.2", + "ipywidgets>=8.1.7", + "jupyter>=1.1.1", + "mkdocs-material>=9.6.9", + "mkdocstrings-python>=1.16.5", + "morethemes>=0.4.0", + "nbclient>=0.10.2", + "nbformat>=5.10.4", + "pandas>=2.3.1", + "playwright>=1.40.0", + "plotnine>=0.13.6", + "polars>=1.31.0", + "prek>=0.3.5", + "pyfonts>=1.0.0", + "pypalettes>=0.1.4", + "pytest>=8.3.5", + "ruff>=0.11.13", + "seaborn>=0.13.2", + "ty>=0.0.1a16", + "zensical>=0.0.27", ] [build-system] requires = [ - "setuptools", - "setuptools-scm", + "setuptools", + "setuptools-scm", ] build-backend = "setuptools.build_meta" +[tool.ruff] +extend-exclude = ["docs/index.qmd"] + [tool.setuptools] packages = ["plotjs"] -[tool.uv.sources] -plotjs = { workspace = true } - -[dependency-groups] -dev = [ - "pytest>=8.3.5", - "ruff>=0.11.13", - "mkdocs-material>=9.6.9", - "mkdocstrings-python>=1.16.5", - "coverage>=7.9.1", - "genbadge[coverage]>=1.1.2", - "pandas>=2.3.1", - "pypalettes>=0.1.4", - "pyfonts>=1.0.0", - "morethemes>=0.4.0", - "plotnine>=0.13.6", - "ty>=0.0.1a16", - "polars>=1.31.0", - "nbformat>=5.10.4", - "nbclient>=0.10.2", - "jupyter>=1.1.1", - "ipywidgets>=8.1.7", - "highlight-text>=0.2", - "drawarrow>=0.1.0", - "playwright>=1.40.0", - "prek>=0.3.5", - "seaborn>=0.13.2", - "zensical>=0.0.27", -] - -[project.urls] -Homepage = "https://y-sunflower.github.io/plotjs/" -Issues = "https://github.com/y-sunflower/plotjs/issues" -Documentation = "https://y-sunflower.github.io/plotjs/" -Repository = "https://github.com/y-sunflower/plotjs" - [tool.ty.src] include = ["plotjs"] exclude = ["tests", "sandbox"] -[tool.ruff] -extend-exclude = ["docs/index.qmd"] +[tool.uv.sources] +plotjs = { workspace = true } From 9c10be1fb925d66b6a0ae13554988d0543f7adc1 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 15:03:57 +0100 Subject: [PATCH 02/16] add tests for area chart with a legend --- tests/test-python/test_plotjs.py | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index 5ef595a..429ea4f 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -1,4 +1,5 @@ from plotjs import PlotJS, data +import numpy as np import matplotlib.pyplot as plt import os import tempfile @@ -407,3 +408,38 @@ def test_init_restores_svg_rcparams_if_savefig_fails(): assert plt.rcParams["svg.id"] == old_svg_id plt.close(fig) + + +def test_fill_between_with_legend(): + x = np.arange(10) + y1 = np.array([2, 3, 4, 3, 5, 6, 5, 7, 6, 8]) + y2 = np.array([1, 2, 2, 3, 3, 4, 4, 5, 5, 6]) + + fig, ax = plt.subplots() + + ax.fill_between(x, y1, label="Series A") + ax.fill_between(x, y2, label="Series B") + + ax.spines[["top", "right"]].set_visible(False) + ax.legend() + + plotjs = PlotJS(fig, _debug=True).add_tooltip( + labels=["Series A", "Series B"], + groups=["Series A", "Series B"], + on="area", + ) + + assert len(plotjs._axes) == 1 + assert plotjs._tooltip_labels == ["Series A", "Series B", "Series A", "Series B"] + assert plotjs._tooltip_groups == ["Series A", "Series B", "Series A", "Series B"] + + assert len(plotjs._legend_handles) == 2 + assert plotjs._legend_handles_labels == ["Series A", "Series B"] + assert plotjs._axes_tooltip == { + "axes_1": { + "tooltip_labels": ["Series A", "Series B", "Series A", "Series B"], + "tooltip_groups": ["Series A", "Series B", "Series A", "Series B"], + "hover_nearest": "false", + "on": ["area"], + } + } From 056bfdb247ca1a71ec7ba74d1c95eb74a9a61af1 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 15:10:58 +0100 Subject: [PATCH 03/16] fix string quote in d3js example --- plotjs/plotjs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotjs/plotjs.py b/plotjs/plotjs.py index 0d105dc..a0787c6 100644 --- a/plotjs/plotjs.py +++ b/plotjs/plotjs.py @@ -341,7 +341,7 @@ def add_d3js(self, version: int = 7) -> "PlotJS": ( PlotJS() .add_d3js() - .add_javascript("d3.selectAll(".point").on("click", () => alert("I wish cookies were 0 calories..."));" + .add_javascript("d3.selectAll('.point').on('click', () => alert('I wish cookies were 0 calories...'));" ) ) ``` From 571a7613447ca35752434d041af48a494e3ebf07 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 15:31:27 +0100 Subject: [PATCH 04/16] add tests and example for histogram --- docs/iframes/bug.html | 2 +- docs/iframes/quickstart.html | 2 +- docs/iframes/quickstart10.html | 2 +- docs/iframes/quickstart11.html | 2 +- docs/iframes/quickstart12.html | 1031 ++++++++++++++++++++ docs/iframes/quickstart2.html | 2 +- docs/iframes/quickstart3.html | 2 +- docs/iframes/quickstart4.html | 2 +- docs/iframes/quickstart5.html | 1548 +++++++++++++++--------------- docs/iframes/quickstart6.html | 2 +- docs/iframes/quickstart7.html | 2 +- docs/iframes/quickstart8.html | 2 +- docs/iframes/quickstart9.html | 2 +- docs/index.md | 27 +- docs/index.qmd | 27 +- tests/test-python/test_plotjs.py | 50 + 16 files changed, 1935 insertions(+), 770 deletions(-) create mode 100644 docs/iframes/quickstart12.html diff --git a/docs/iframes/bug.html b/docs/iframes/bug.html index 65014ce..128ad24 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:15.050060 + 2026-03-26T15:26:20.757980 image/svg+xml diff --git a/docs/iframes/quickstart.html b/docs/iframes/quickstart.html index 8ae12fe..c8ff6ad 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.886877 + 2026-03-26T15:26:19.436768 image/svg+xml diff --git a/docs/iframes/quickstart10.html b/docs/iframes/quickstart10.html index 7860a77..2e7a90c 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.708540 + 2026-03-26T15:26:20.396567 image/svg+xml diff --git a/docs/iframes/quickstart11.html b/docs/iframes/quickstart11.html index 4600c67..26d2e26 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.836735 + 2026-03-26T15:26:20.533903 image/svg+xml diff --git a/docs/iframes/quickstart12.html b/docs/iframes/quickstart12.html new file mode 100644 index 0000000..4a3f531 --- /dev/null +++ b/docs/iframes/quickstart12.html @@ -0,0 +1,1031 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-26T15:26:19.772224 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 166ffd4..23e523e 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.909929 + 2026-03-26T15:26:19.461958 image/svg+xml diff --git a/docs/iframes/quickstart3.html b/docs/iframes/quickstart3.html index efd64bf..8dac4ca 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.929348 + 2026-03-26T15:26:19.482519 image/svg+xml diff --git a/docs/iframes/quickstart4.html b/docs/iframes/quickstart4.html index ac933f9..606980a 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.954602 + 2026-03-26T15:26:19.509885 image/svg+xml diff --git a/docs/iframes/quickstart5.html b/docs/iframes/quickstart5.html index deb522a..5063974 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.979453 + 2026-03-26T15:26:19.536201 image/svg+xml @@ -343,12 +343,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + @@ -367,41 +367,40 @@ - + - - - - - + + + - + - - - + + + + - + - - - + + + @@ -409,759 +408,794 @@ - + - - - + + + - - - diff --git a/docs/iframes/quickstart6.html b/docs/iframes/quickstart6.html index bd9c5c0..050a303 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.119874 + 2026-03-26T15:26:19.685815 image/svg+xml diff --git a/docs/iframes/quickstart7.html b/docs/iframes/quickstart7.html index a645be7..c35589d 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.198616 + 2026-03-26T15:26:19.857726 image/svg+xml diff --git a/docs/iframes/quickstart8.html b/docs/iframes/quickstart8.html index 535bce9..6265461 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.344118 + 2026-03-26T15:26:20.014295 image/svg+xml diff --git a/docs/iframes/quickstart9.html b/docs/iframes/quickstart9.html index 1d85252..7b52b8b 100644 --- a/docs/iframes/quickstart9.html +++ b/docs/iframes/quickstart9.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.503003 + 2026-03-26T15:26:20.183536 image/svg+xml diff --git a/docs/index.md b/docs/index.md index 27e7a7f..da8a8fa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ interactive charts with minimum user inputs. You can: ???+ warning - `plotjs` is in very early stage: expect regular breaking changes. + Consider that `plotjs` is still unstable. ## Get started @@ -227,6 +227,31 @@ ax.barh( +### Histogram + +``` python +import matplotlib.pyplot as plt +import numpy as np + +from plotjs import PlotJS + +x = np.random.normal(loc=0.0, scale=1.0, size=100) + +fig, ax = plt.subplots() +counts, bins, _ = ax.hist(x, color="#2a9d8f") + +labels = [ + f"Lower bound: {lo:.2f}
Upper bound:{hi:.2f}
n: {int(n)}" + for lo, hi, n in zip(bins[:-1], bins[1:], counts) +] + +PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") +``` + + + ### Connect legend and plot elements: - Scatter plot diff --git a/docs/index.qmd b/docs/index.qmd index 8ac3f99..6bb69aa 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -9,8 +9,10 @@ execute: ```{python} # | include: false import matplotlib.pyplot as plt +import numpy as np plt.rcParams["figure.dpi"] = 300 +np.random.seed(0) ``` plotjs logo @@ -25,7 +27,7 @@ plt.rcParams["figure.dpi"] = 300 ???+ warning - `plotjs` is in very early stage: expect regular breaking changes. + Consider that `plotjs` is still unstable. ## Get started @@ -213,6 +215,29 @@ ax.barh( +### Histogram + +```{python} +import matplotlib.pyplot as plt +import numpy as np + +from plotjs import PlotJS + +x = np.random.normal(loc=0.0, scale=1.0, size=100) + +fig, ax = plt.subplots() +counts, bins, _ = ax.hist(x, color="#2a9d8f") + +labels = [ + f"Lower bound: {lo:.2f}
Upper bound:{hi:.2f}
n: {int(n)}" + for lo, hi, n in zip(bins[:-1], bins[1:], counts) +] + +PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") +``` + + + ### Connect legend and plot elements: - Scatter plot diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index 429ea4f..80a88cf 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -443,3 +443,53 @@ def test_fill_between_with_legend(): "on": ["area"], } } + + +def test_histogram_with_custom_labels(): + np.random.seed(0) + x = np.random.normal(loc=0.0, scale=1.0, size=100) + + fig, ax = plt.subplots() + counts, bins, _ = ax.hist(x, color="#2a9d8f") + + labels = [ + f"Lower bound: {lo:.2f}
Upper bound:{hi:.2f}
n: {int(n)}" + for lo, hi, n in zip(bins[:-1], bins[1:], counts) + ] + + plotjs = PlotJS(fig=fig).add_tooltip(labels=labels) + + assert len(plotjs._axes) == 1 + assert plotjs._tooltip_labels == [ + "Lower bound: -2.55
Upper bound:-2.07
n: 1", + "Lower bound: -2.07
Upper bound:-1.59
n: 5", + "Lower bound: -1.59
Upper bound:-1.11
n: 7", + "Lower bound: -1.11
Upper bound:-0.62
n: 13", + "Lower bound: -0.62
Upper bound:-0.14
n: 17", + "Lower bound: -0.14
Upper bound:0.34
n: 18", + "Lower bound: 0.34
Upper bound:0.82
n: 16", + "Lower bound: 0.82
Upper bound:1.31
n: 11", + "Lower bound: 1.31
Upper bound:1.79
n: 7", + "Lower bound: 1.79
Upper bound:2.27
n: 5", + ] + assert plotjs._tooltip_groups == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + assert plotjs._axes_tooltip == { + "axes_1": { + "tooltip_labels": [ + "Lower bound: -2.55
Upper bound:-2.07
n: 1", + "Lower bound: -2.07
Upper bound:-1.59
n: 5", + "Lower bound: -1.59
Upper bound:-1.11
n: 7", + "Lower bound: -1.11
Upper bound:-0.62
n: 13", + "Lower bound: -0.62
Upper bound:-0.14
n: 17", + "Lower bound: -0.14
Upper bound:0.34
n: 18", + "Lower bound: 0.34
Upper bound:0.82
n: 16", + "Lower bound: 0.82
Upper bound:1.31
n: 11", + "Lower bound: 1.31
Upper bound:1.79
n: 7", + "Lower bound: 1.79
Upper bound:2.27
n: 5", + ], + "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "hover_nearest": "false", + "on": None, + } + } From 7c9ac76f917f19cc17f69db1f600071171f5428f Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 16:22:02 +0100 Subject: [PATCH 05/16] init pie element parser --- docs/index.qmd | 2 -- plotjs/plotjs.py | 7 +++--- plotjs/static/plotparser.js | 17 +++++++++++++ plotjs/static/template.html | 25 +++++++++++++++++-- tests/test-python/test_plotjs.py | 43 ++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/docs/index.qmd b/docs/index.qmd index 6bb69aa..2f49726 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -276,8 +276,6 @@ import matplotlib.pyplot as plt import numpy as np from plotjs import PlotJS -np.random.seed(0) - length = 500 walk1 = np.cumsum(np.random.choice([-1, 1], size=length)) walk2 = np.cumsum(np.random.choice([-1, 1], size=length)) diff --git a/plotjs/plotjs.py b/plotjs/plotjs.py index a0787c6..efa2eb1 100644 --- a/plotjs/plotjs.py +++ b/plotjs/plotjs.py @@ -118,8 +118,8 @@ def add_tooltip( hover_nearest: When `True`, hover the nearest plot element. on: Which plot elements to apply interactivity to. Can be a single element type or a list. Valid values are "point", - "line", "bar", "area" (plurals like "points" also accepted). - If `None` (default), applies to all element types. + "line", "bar", "area", "pie (plurals like "points" also + accepted). If `None` (default), applies to all element types. ax: A matplotlib Axes. If `None` (default), uses first Axes. Returns: @@ -164,12 +164,13 @@ def add_tooltip( self._tooltip_y_shift = tooltip_y_shift # Normalize and validate the `on` parameter - valid_elements = {"point", "line", "bar", "area"} + valid_elements = {"point", "line", "bar", "area", "pie"} plural_to_singular = { "points": "point", "lines": "line", "bars": "bar", "areas": "area", + "pies": "pie", } if on is None: diff --git a/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index 0502872..c259b5d 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -229,6 +229,23 @@ export default class PlotSVGParser { return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // select all of Patch elements within the specific axes + const pies = svg.selectAll(`g#${axes_class} g[id^="patch_"] path`); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. diff --git a/plotjs/static/template.html b/plotjs/static/template.html index ec071c0..4313eb3 100644 --- a/plotjs/static/template.html +++ b/plotjs/static/template.html @@ -77,6 +77,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -92,9 +95,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -125,6 +132,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index 80a88cf..c798fd9 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -493,3 +493,46 @@ def test_histogram_with_custom_labels(): "on": None, } } + + +def test_pie_chart(): + labels = ["A", "B", "C", "D"] + sizes = [15, 10, 25, 10] + + fig, ax = plt.subplots() + ax.pie(sizes, labels=labels, autopct="%1.1f%%") + + tooltip = [f"{lab} (n = {size})" for lab, size in zip(labels, sizes)] + + plotjs = PlotJS(fig, _debug=True).add_tooltip(labels=tooltip) + + assert len(plotjs._axes) == 1 + assert plotjs._tooltip_labels == [ + "A (n = 15)", + "B (n = 10)", + "C (n = 25)", + "D (n = 10)", + "A", + "B", + "C", + "D", + ] + assert plotjs._tooltip_groups == [0, 1, 2, 3, 4, 5, 6, 7] + + assert plotjs._axes_tooltip == { + "axes_1": { + "tooltip_labels": [ + "A (n = 15)", + "B (n = 10)", + "C (n = 25)", + "D (n = 10)", + "A", + "B", + "C", + "D", + ], + "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7], + "hover_nearest": "false", + "on": None, + } + } From 089f47ac04294e97d36b18637a3e9d9ecce9fd03 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 16:32:21 +0100 Subject: [PATCH 06/16] update pie elements parser with precise filter --- plotjs/static/plotparser.js | 20 +++++- tests/test-javascript/ParserSelectors.test.js | 62 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index c259b5d..aeb4d8c 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -237,8 +237,24 @@ export default class PlotSVGParser { * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // select all of Patch elements within the specific axes - const pies = svg.selectAll(`g#${axes_class} g[id^="patch_"] path`); + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); pies.attr("class", "pie plot-element"); diff --git a/tests/test-javascript/ParserSelectors.test.js b/tests/test-javascript/ParserSelectors.test.js index 85e2050..6436a45 100644 --- a/tests/test-javascript/ParserSelectors.test.js +++ b/tests/test-javascript/ParserSelectors.test.js @@ -201,6 +201,68 @@ describe("findPoints", () => { }); }); +describe("findPies", () => { + test("should find pie slice patch paths", () => { + const dom = new JSDOM(` + + + + + + + + + `); + + const svg = dom.window.document.querySelector("svg"); + const parser = new PlotSVGParser(svg, null, 0, 0); + const pies = parser.findPies(parser.svg, "axes_1"); + + expect(pies.size()).toBe(2); + pies.each(function () { + expect(this.getAttribute("class")).toBe("pie plot-element"); + }); + }); + + test("should ignore scatter plot patch paths", () => { + const dom = new JSDOM(` + + + + + + + + + + + + `); + + const svg = dom.window.document.querySelector("svg"); + const parser = new PlotSVGParser(svg, null, 0, 0); + const pies = parser.findPies(parser.svg, "axes_1"); + + expect(pies.size()).toBe(0); + expect(pies.empty()).toBe(true); + }); +}); + describe("findLines", () => { test("should find line2d path elements", () => { const dom = new JSDOM(` From b2972fce681b0d7a6e509620220a308dec161a3b Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 16:34:34 +0100 Subject: [PATCH 07/16] document new pie elements parser --- docs/developers/svg-parser-reference.md | 101 +++++++++++++++--------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/docs/developers/svg-parser-reference.md b/docs/developers/svg-parser-reference.md index 9ebc6fe..2b51d4a 100644 --- a/docs/developers/svg-parser-reference.md +++ b/docs/developers/svg-parser-reference.md @@ -27,6 +27,9 @@ Provides basic DOM manipulation methods for working with SVG elements.

Handles both
<use> and <path> fallback cases, and assigns data-group attributes based on tooltip groups.

+
findPies(svg, axes_class)Selection
+

Find pie elements (patch paths) inside a given axes.

+
findLines(svg, axes_class)Selection

Find line elements (line2d paths) inside a given axes, excluding axis grid lines.

@@ -50,6 +53,7 @@ Can highlight nearest element (if enabled) or hovered element directly.

## Selection + Lightweight Selection wrapper that mimics d3-selection's chainable API. Provides basic DOM manipulation methods for working with SVG elements. @@ -57,56 +61,61 @@ Provides basic DOM manipulation methods for working with SVG elements. ## select(selector) ⇒ [Selection](#Selection) + Create a Selection from a DOM element or selector string. **Kind**: global function **Returns**: [Selection](#Selection) - New Selection instance -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| -------- | ------------------------------------------- | ---------------------------------- | | selector | string \| Element | CSS selector string or DOM element | ## getPointerPosition(event, svgElement) ⇒ Array.<number> + Get mouse position relative to an SVG element. **Kind**: global function **Returns**: Array.<number> - [x, y] coordinates relative to the SVG -| Param | Type | Description | -| --- | --- | --- | -| event | MouseEvent | The mouse event | +| Param | Type | Description | +| ---------- | ------------------------------------------------------------ | ---------------------------- | +| event | MouseEvent | The mouse event | | svgElement | Element \| [Selection](#Selection) | The SVG element or Selection | ## getFillValue(element) ⇒ string + Extract the raw fill value from an SVG element. **Kind**: global function **Returns**: string - Normalized fill value, or empty string if absent. -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| ------- | -------------------- | ----------------------- | | element | Element | SVG element to inspect. | ## findBars(svg, axes_class) ⇒ [Selection](#Selection) + Find bar elements (`patch` groups with clipping) inside a given axes. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of bar elements. -| Param | Type | Description | -| --- | --- | --- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group (e.g. "axes_1"). | +| Param | Type | Description | +| ---------- | ------------------------------------ | ------------------------------------- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group (e.g. "axes_1"). | ## findPoints(svg, axes_class, tooltip_groups) ⇒ [Selection](#Selection) + Find scatter plot points inside a given axes. Handles both `` and `` fallback cases, and assigns `data-group` attributes based on tooltip groups. @@ -114,29 +123,45 @@ and assigns `data-group` attributes based on tooltip groups. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of point elements. -| Param | Type | Description | -| --- | --- | --- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group (e.g. "axes_1"). | -| tooltip_groups | Array.<string> | Group identifiers for tooltips, parallel to points. | +| Param | Type | Description | +| -------------- | ------------------------------------ | --------------------------------------------------- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group (e.g. "axes_1"). | +| tooltip_groups | Array.<string> | Group identifiers for tooltips, parallel to points. | + + + +## findPies(svg, axes_class) ⇒ [Selection](#Selection) + +Find pie elements (`patch` paths) inside a given axes. + +**Kind**: global function +**Returns**: [Selection](#Selection) - Selection of pie elements. + +| Param | Type | Description | +| ---------- | ------------------------------------ | ----------------------------- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## findLines(svg, axes_class) ⇒ [Selection](#Selection) + Find line elements (`line2d` paths) inside a given axes, excluding axis grid lines. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of line elements. -| Param | Type | Description | -| --- | --- | --- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group. | +| Param | Type | Description | +| ---------- | ------------------------------------ | ----------------------------- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## findAreas(svg, axes_class) ⇒ [Selection](#Selection) + Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes. Also includes legend swatches whose fill matches the plotted areas so legend hover can target the same series as the chart area. @@ -144,14 +169,15 @@ can target the same series as the chart area. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of area elements. -| Param | Type | Description | -| --- | --- | --- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group. | +| Param | Type | Description | +| ---------- | ------------------------------------ | ----------------------------- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## nearestElementFromMouse(mouseX, mouseY, elements) ⇒ Element \| null + Compute the nearest element to the mouse cursor from a set of elements. Uses bounding box centers for distance. This function is used when the `hover_nearest` argument is true. @@ -159,25 +185,26 @@ This function is used when the `hover_nearest` argument is true. **Kind**: global function **Returns**: Element \| null - The nearest DOM element or `null`. -| Param | Type | Description | -| --- | --- | --- | -| mouseX | number | X coordinate of the mouse relative to SVG. | -| mouseY | number | Y coordinate of the mouse relative to SVG. | -| elements | [Selection](#Selection) | Selection of candidate elements. | +| Param | Type | Description | +| -------- | ------------------------------------ | ------------------------------------------ | +| mouseX | number | X coordinate of the mouse relative to SVG. | +| mouseY | number | Y coordinate of the mouse relative to SVG. | +| elements | [Selection](#Selection) | Selection of candidate elements. | ## setHoverEffect(plot_element, axes_class, tooltip_labels, tooltip_groups, show_tooltip, hover_nearest) + Attach hover interaction and tooltip display to plot elements. Can highlight nearest element (if enabled) or hovered element directly. **Kind**: global function -| Param | Type | Description | -| --- | --- | --- | -| plot_element | [Selection](#Selection) | Selection of plot elements (points, lines, etc.). | -| axes_class | string | ID of the axes group. | -| tooltip_labels | Array.<string> | Tooltip labels for each element. | -| tooltip_groups | Array.<string> | Group identifiers for each element. | -| show_tooltip | "block" \| "none" | Whether to display tooltips. | -| hover_nearest | boolean | If true, highlight nearest element instead of hovered one. | +| Param | Type | Description | +| -------------- | --------------------------------------------------------------- | ---------------------------------------------------------- | +| plot_element | [Selection](#Selection) | Selection of plot elements (points, lines, etc.). | +| axes_class | string | ID of the axes group. | +| tooltip_labels | Array.<string> | Tooltip labels for each element. | +| tooltip_groups | Array.<string> | Group identifiers for each element. | +| show_tooltip | "block" \| "none" | Whether to display tooltips. | +| hover_nearest | boolean | If true, highlight nearest element instead of hovered one. | From 89153e83749dbed902593808580071984ce6abd3 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Thu, 26 Mar 2026 16:35:30 +0100 Subject: [PATCH 08/16] document everything --- docs/developers/svg-parser-reference.md | 93 +- docs/gallery/index.md | 24 +- docs/iframes/CSS-2.html | 60 +- docs/iframes/CSS.html | 60 +- docs/iframes/area-natural-disasters.html | 60 +- docs/iframes/bug.html | 352 +- docs/iframes/javascript.html | 60 +- docs/iframes/javascript2.html | 60 +- docs/iframes/quickstart.html | 60 +- docs/iframes/quickstart10.html | 60 +- docs/iframes/quickstart11.html | 60 +- docs/iframes/quickstart12.html | 60 +- docs/iframes/quickstart2.html | 60 +- docs/iframes/quickstart3.html | 60 +- docs/iframes/quickstart4.html | 60 +- docs/iframes/quickstart5.html | 60 +- docs/iframes/quickstart6.html | 60 +- docs/iframes/quickstart7.html | 60 +- docs/iframes/quickstart8.html | 1677 +- docs/iframes/quickstart9.html | 60 +- docs/iframes/random-walk-1.html | 74809 ++++++++++----------- docs/index.md | 2 - 22 files changed, 39450 insertions(+), 38467 deletions(-) diff --git a/docs/developers/svg-parser-reference.md b/docs/developers/svg-parser-reference.md index 2b51d4a..6a7277f 100644 --- a/docs/developers/svg-parser-reference.md +++ b/docs/developers/svg-parser-reference.md @@ -53,7 +53,6 @@ Can highlight nearest element (if enabled) or hovered element directly.

## Selection - Lightweight Selection wrapper that mimics d3-selection's chainable API. Provides basic DOM manipulation methods for working with SVG elements. @@ -61,61 +60,56 @@ Provides basic DOM manipulation methods for working with SVG elements. ## select(selector) ⇒ [Selection](#Selection) - Create a Selection from a DOM element or selector string. **Kind**: global function **Returns**: [Selection](#Selection) - New Selection instance -| Param | Type | Description | -| -------- | ------------------------------------------- | ---------------------------------- | +| Param | Type | Description | +| --- | --- | --- | | selector | string \| Element | CSS selector string or DOM element | ## getPointerPosition(event, svgElement) ⇒ Array.<number> - Get mouse position relative to an SVG element. **Kind**: global function **Returns**: Array.<number> - [x, y] coordinates relative to the SVG -| Param | Type | Description | -| ---------- | ------------------------------------------------------------ | ---------------------------- | -| event | MouseEvent | The mouse event | +| Param | Type | Description | +| --- | --- | --- | +| event | MouseEvent | The mouse event | | svgElement | Element \| [Selection](#Selection) | The SVG element or Selection | ## getFillValue(element) ⇒ string - Extract the raw fill value from an SVG element. **Kind**: global function **Returns**: string - Normalized fill value, or empty string if absent. -| Param | Type | Description | -| ------- | -------------------- | ----------------------- | +| Param | Type | Description | +| --- | --- | --- | | element | Element | SVG element to inspect. | ## findBars(svg, axes_class) ⇒ [Selection](#Selection) - Find bar elements (`patch` groups with clipping) inside a given axes. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of bar elements. -| Param | Type | Description | -| ---------- | ------------------------------------ | ------------------------------------- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group (e.g. "axes_1"). | +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group (e.g. "axes_1"). | ## findPoints(svg, axes_class, tooltip_groups) ⇒ [Selection](#Selection) - Find scatter plot points inside a given axes. Handles both `` and `` fallback cases, and assigns `data-group` attributes based on tooltip groups. @@ -123,45 +117,42 @@ and assigns `data-group` attributes based on tooltip groups. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of point elements. -| Param | Type | Description | -| -------------- | ------------------------------------ | --------------------------------------------------- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group (e.g. "axes_1"). | -| tooltip_groups | Array.<string> | Group identifiers for tooltips, parallel to points. | +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group (e.g. "axes_1"). | +| tooltip_groups | Array.<string> | Group identifiers for tooltips, parallel to points. | ## findPies(svg, axes_class) ⇒ [Selection](#Selection) - Find pie elements (`patch` paths) inside a given axes. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of pie elements. -| Param | Type | Description | -| ---------- | ------------------------------------ | ----------------------------- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group. | +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## findLines(svg, axes_class) ⇒ [Selection](#Selection) - Find line elements (`line2d` paths) inside a given axes, excluding axis grid lines. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of line elements. -| Param | Type | Description | -| ---------- | ------------------------------------ | ----------------------------- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group. | +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## findAreas(svg, axes_class) ⇒ [Selection](#Selection) - Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes. Also includes legend swatches whose fill matches the plotted areas so legend hover can target the same series as the chart area. @@ -169,15 +160,14 @@ can target the same series as the chart area. **Kind**: global function **Returns**: [Selection](#Selection) - Selection of area elements. -| Param | Type | Description | -| ---------- | ------------------------------------ | ----------------------------- | -| svg | [Selection](#Selection) | Selection of the SVG element. | -| axes_class | string | ID of the axes group. | +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group. | ## nearestElementFromMouse(mouseX, mouseY, elements) ⇒ Element \| null - Compute the nearest element to the mouse cursor from a set of elements. Uses bounding box centers for distance. This function is used when the `hover_nearest` argument is true. @@ -185,26 +175,25 @@ This function is used when the `hover_nearest` argument is true. **Kind**: global function **Returns**: Element \| null - The nearest DOM element or `null`. -| Param | Type | Description | -| -------- | ------------------------------------ | ------------------------------------------ | -| mouseX | number | X coordinate of the mouse relative to SVG. | -| mouseY | number | Y coordinate of the mouse relative to SVG. | -| elements | [Selection](#Selection) | Selection of candidate elements. | +| Param | Type | Description | +| --- | --- | --- | +| mouseX | number | X coordinate of the mouse relative to SVG. | +| mouseY | number | Y coordinate of the mouse relative to SVG. | +| elements | [Selection](#Selection) | Selection of candidate elements. | ## setHoverEffect(plot_element, axes_class, tooltip_labels, tooltip_groups, show_tooltip, hover_nearest) - Attach hover interaction and tooltip display to plot elements. Can highlight nearest element (if enabled) or hovered element directly. **Kind**: global function -| Param | Type | Description | -| -------------- | --------------------------------------------------------------- | ---------------------------------------------------------- | -| plot_element | [Selection](#Selection) | Selection of plot elements (points, lines, etc.). | -| axes_class | string | ID of the axes group. | -| tooltip_labels | Array.<string> | Tooltip labels for each element. | -| tooltip_groups | Array.<string> | Group identifiers for each element. | -| show_tooltip | "block" \| "none" | Whether to display tooltips. | -| hover_nearest | boolean | If true, highlight nearest element instead of hovered one. | +| Param | Type | Description | +| --- | --- | --- | +| plot_element | [Selection](#Selection) | Selection of plot elements (points, lines, etc.). | +| axes_class | string | ID of the axes group. | +| tooltip_labels | Array.<string> | Tooltip labels for each element. | +| tooltip_groups | Array.<string> | Group identifiers for each element. | +| show_tooltip | "block" \| "none" | Whether to display tooltips. | +| hover_nearest | boolean | If true, highlight nearest element instead of hovered one. | diff --git a/docs/gallery/index.md b/docs/gallery/index.md index de90d31..fadfdb1 100644 --- a/docs/gallery/index.md +++ b/docs/gallery/index.md @@ -54,11 +54,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -66,11 +66,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -78,11 +78,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -90,11 +90,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -102,10 +102,18 @@ This page contains all the `plotjs` examples from this website.
+ + +
+ +
+ diff --git a/docs/iframes/CSS-2.html b/docs/iframes/CSS-2.html index 25a5efa..6910781 100644 --- a/docs/iframes/CSS-2.html +++ b/docs/iframes/CSS-2.html @@ -65,7 +65,7 @@ - 2026-03-15T20:57:18.692897 + 2026-03-26T16:35:14.208731 image/svg+xml @@ -1587,6 +1587,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1807,6 +1840,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1822,9 +1858,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1855,6 +1895,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/CSS.html b/docs/iframes/CSS.html index 5c466a3..f1f0a90 100644 --- a/docs/iframes/CSS.html +++ b/docs/iframes/CSS.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:18.640742 + 2026-03-26T16:35:14.156729 image/svg+xml @@ -1583,6 +1583,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1803,6 +1836,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1818,9 +1854,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1851,6 +1891,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/area-natural-disasters.html b/docs/iframes/area-natural-disasters.html index c6759e2..34acbbf 100644 --- a/docs/iframes/area-natural-disasters.html +++ b/docs/iframes/area-natural-disasters.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:17.845442 + 2026-03-26T16:35:13.369122 image/svg+xml @@ -3891,6 +3891,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -4111,6 +4144,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -4126,9 +4162,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -4159,6 +4199,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/bug.html b/docs/iframes/bug.html index 128ad24..9ff3c6f 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:20.757980 + 2026-03-26T16:35:10.513958 image/svg+xml @@ -107,56 +107,56 @@ " style="stroke: #000000"/> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + + @@ -174,56 +174,56 @@ " style="stroke: #000000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -241,56 +241,56 @@ " style="stroke: #000000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1002,6 +1002,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1176,7 +1209,7 @@ const svg = container.querySelector("svg"); console.log(`PlotJS: SVG and tooltip elements loaded`); - const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": ["versicolor", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "versicolor", "setosa", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "setosa", "versicolor", "virginica", "versicolor", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "versicolor", "virginica", "setosa", "setosa", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "virginica", "virginica", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "virginica"], "tooltip_labels": ["versicolor", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "versicolor", "setosa", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "setosa", "versicolor", "virginica", "versicolor", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "versicolor", "virginica", "setosa", "setosa", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "virginica", "virginica", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "virginica"]}}, "hover_nearest": false, "tooltip_groups": ["versicolor", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "versicolor", "setosa", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "setosa", "versicolor", "virginica", "versicolor", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "versicolor", "virginica", "setosa", "setosa", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "virginica", "virginica", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "virginica"], "tooltip_labels": ["versicolor", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "versicolor", "setosa", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "setosa", "versicolor", "virginica", "versicolor", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "versicolor", "virginica", "setosa", "setosa", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "virginica", "virginica", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "setosa", "virginica", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "virginica"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); + const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_labels": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"]}}, "hover_nearest": false, "tooltip_groups": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_labels": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); const tooltip_x_shift = plot_data["tooltip_x_shift"]; const tooltip_y_shift = -plot_data["tooltip_y_shift"]; const axes = plot_data["axes"]; @@ -1222,6 +1255,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1237,9 +1273,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1270,6 +1310,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/javascript.html b/docs/iframes/javascript.html index 93738ec..69ef8f8 100644 --- a/docs/iframes/javascript.html +++ b/docs/iframes/javascript.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:19.351940 + 2026-03-26T16:35:14.857482 image/svg+xml @@ -1266,6 +1266,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1486,6 +1519,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1537,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1534,6 +1574,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/javascript2.html b/docs/iframes/javascript2.html index 40af739..7527111 100644 --- a/docs/iframes/javascript2.html +++ b/docs/iframes/javascript2.html @@ -68,7 +68,7 @@ - 2026-03-15T20:57:19.393021 + 2026-03-26T16:35:14.902606 image/svg+xml @@ -1337,6 +1337,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1557,6 +1590,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1572,9 +1608,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1605,6 +1645,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart.html b/docs/iframes/quickstart.html index c8ff6ad..4b47a48 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.436768 + 2026-03-26T16:35:09.251584 image/svg+xml @@ -1266,6 +1266,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1486,6 +1519,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1537,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1534,6 +1574,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart10.html b/docs/iframes/quickstart10.html index 2e7a90c..ed9ddf6 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:20.396567 + 2026-03-26T16:35:10.168781 image/svg+xml @@ -1002,6 +1002,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1222,6 +1255,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1237,9 +1273,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1270,6 +1310,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart11.html b/docs/iframes/quickstart11.html index 26d2e26..c3e1536 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:20.533903 + 2026-03-26T16:35:10.299185 image/svg+xml @@ -2420,6 +2420,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -2640,6 +2673,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -2655,9 +2691,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -2688,6 +2728,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart12.html b/docs/iframes/quickstart12.html index 4a3f531..10097e0 100644 --- a/docs/iframes/quickstart12.html +++ b/docs/iframes/quickstart12.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.772224 + 2026-03-26T16:35:09.568998 image/svg+xml @@ -719,6 +719,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -939,6 +972,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -954,9 +990,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -987,6 +1027,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 23e523e..0644e80 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.461958 + 2026-03-26T16:35:09.275133 image/svg+xml @@ -1266,6 +1266,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1486,6 +1519,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1537,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1534,6 +1574,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart3.html b/docs/iframes/quickstart3.html index 8dac4ca..7047a96 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.482519 + 2026-03-26T16:35:09.296416 image/svg+xml @@ -1266,6 +1266,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1486,6 +1519,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1537,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1534,6 +1574,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart4.html b/docs/iframes/quickstart4.html index 606980a..2745269 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.509885 + 2026-03-26T16:35:09.322696 image/svg+xml @@ -1266,6 +1266,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1486,6 +1519,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1537,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1534,6 +1574,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart5.html b/docs/iframes/quickstart5.html index 5063974..597871f 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.536201 + 2026-03-26T16:35:09.348319 image/svg+xml @@ -1462,6 +1462,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1682,6 +1715,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1697,9 +1733,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1730,6 +1770,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart6.html b/docs/iframes/quickstart6.html index 050a303..244f3e1 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.685815 + 2026-03-26T16:35:09.490854 image/svg+xml @@ -1047,6 +1047,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1267,6 +1300,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1282,9 +1318,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1315,6 +1355,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart7.html b/docs/iframes/quickstart7.html index c35589d..765fada 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:19.857726 + 2026-03-26T16:35:09.648263 image/svg+xml @@ -1330,6 +1330,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -1550,6 +1583,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1565,9 +1601,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -1598,6 +1638,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart8.html b/docs/iframes/quickstart8.html index 6265461..c5012ee 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:20.014295 + 2026-03-26T16:35:09.796799 image/svg+xml @@ -343,12 +343,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + @@ -367,40 +367,41 @@ - + - - - + + + + + - + - - - - + + + - + - - - + + + @@ -408,794 +409,816 @@ - + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + - - + - - + - - + @@ -1232,13 +1255,13 @@ z " style="fill: #ffffff; opacity: 0.8; stroke: #cccccc; stroke-linejoin: miter"/> - + - + @@ -1343,13 +1366,13 @@ - + - + @@ -1398,13 +1421,13 @@ - + - + @@ -1790,6 +1813,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -2010,6 +2066,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -2025,9 +2084,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -2058,6 +2121,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/quickstart9.html b/docs/iframes/quickstart9.html index 7b52b8b..b70446b 100644 --- a/docs/iframes/quickstart9.html +++ b/docs/iframes/quickstart9.html @@ -61,7 +61,7 @@ - 2026-03-26T15:26:20.183536 + 2026-03-26T16:35:09.960250 image/svg+xml @@ -1965,6 +1965,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -2185,6 +2218,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -2200,9 +2236,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -2233,6 +2273,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/iframes/random-walk-1.html b/docs/iframes/random-walk-1.html index e82f213..775041f 100644 --- a/docs/iframes/random-walk-1.html +++ b/docs/iframes/random-walk-1.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:17.902450 + 2026-03-26T16:35:13.425491 image/svg+xml @@ -361,12 +361,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + @@ -427,12 +427,12 @@ - + - + @@ -442,12 +442,12 @@ - + - + @@ -455,12 +455,12 @@ - + - + @@ -469,12 +469,12 @@ - + - + @@ -484,18697 +484,18540 @@ - + - + + + + + + + + + + + + + + + + - - + - - + - - + - - + - - + @@ -19211,13 +19054,13 @@ z " style="fill: #ffffff; opacity: 0.8; stroke: #cccccc; stroke-linejoin: miter"/> - + - + @@ -19322,13 +19165,13 @@ - + - + @@ -19377,13 +19220,13 @@ - + - + @@ -19524,13 +19367,13 @@ - + - + @@ -19607,13 +19450,13 @@ - + - + @@ -19742,12 +19585,12 @@ - + - + @@ -19755,12 +19598,12 @@ - + - + @@ -19771,12 +19614,12 @@ - + - + @@ -19787,12 +19630,12 @@ - + - + @@ -19803,12 +19646,12 @@ - + - + @@ -19819,12 +19662,12 @@ - + - + @@ -19837,31 +19680,15 @@ - - - - - - - - - - - - - - - - - + - + @@ -19872,18683 +19699,18795 @@ - + - - - + + + + + - + - - - - - + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + - - + - - + - - + @@ -38573,27 +38512,27 @@ - - - + - + - + @@ -38602,15 +38541,15 @@ - - + - + - + @@ -38618,15 +38557,15 @@ - - + - + - + @@ -38636,15 +38575,15 @@ - - + - + - + @@ -38655,15 +38594,15 @@ - - + - + - + @@ -38921,6 +38860,39 @@ return points; } + /** + * Find pie elements (`patch` paths) inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group. + * @returns {Selection} Selection of pie elements. + */ + findPies(svg, axes_class) { + // Matplotlib also uses patch_* groups for axes backgrounds and spines. + // Pie wedges are the patch paths with a non-empty fill and curve commands. + const parser = this; + const pies = svg + .selectAll(`g#${axes_class} g[id^="patch_"] path`) + .filter(function () { + const element = this; + const clipPath = element.getAttribute("clip-path"); + const normalizedFill = parser.getFillValue(element); + const pathData = element.getAttribute("d") ?? ""; + + return ( + !clipPath && + normalizedFill !== "" && + normalizedFill !== "none" && + /[CQAST]/.test(pathData) + ); + }); + + pies.attr("class", "pie plot-element"); + + console.log(`Found ${pies.size()} "pie" element`); + return pies; + } + /** * Find line elements (`line2d` paths) inside a given axes, * excluding axis grid lines. @@ -39141,6 +39113,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const pies = shouldProcess("pie") + ? plotParser.findPies(plotParser.svg, axes_class) + : new Selection([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -39156,9 +39131,13 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + bars.size() + + points.size() + + areas.size() + + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, ); if (points.size() > 0) { @@ -39189,6 +39168,20 @@ ); } + if (pies.size() > 0) { + plotParser.setHoverEffect( + pies, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${pies.size()} pies`, + ); + } + if (bars.size() > 0) { plotParser.setHoverEffect( bars, diff --git a/docs/index.md b/docs/index.md index da8a8fa..6e1d2ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -292,8 +292,6 @@ import matplotlib.pyplot as plt import numpy as np from plotjs import PlotJS -np.random.seed(0) - length = 500 walk1 = np.cumsum(np.random.choice([-1, 1], size=length)) walk2 = np.cumsum(np.random.choice([-1, 1], size=length)) From b8ddcdeef2df5398e2f013d1d5974b7a4a096fe1 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 10:31:10 +0100 Subject: [PATCH 09/16] fix JSON parsing NaN errors + tests --- AGENTS.md | 1 + README.md | 6 +++++ plotjs/plotjs.py | 4 ++-- plotjs/utils.py | 24 +++++++++++++++++-- tests/test-browser/test_rendering.py | 36 ++++++++++++++++++++++++++++ tests/test-python/test_plotjs.py | 7 +++--- 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 81ec47c..f17d8e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,7 @@ Always use `uv` and/or `just` for running commands: - `findBars(svg, axes_class)` - Select bar chart elements - `findPoints(svg, axes_class, tooltip_groups)` - Select scatter points - `findLines(svg, axes_class)` - Select line chart elements +- `findPies(svg, axes_class)` - Select pie chart elements - `findAreas(svg, axes_class)` - Select filled area elements - `nearestElementFromMouse(mouseX, mouseY, elements)` - Hover nearest detection - `setHoverEffect(...)` - Attach mouseover handlers, show tooltips diff --git a/README.md b/README.md index a413584..6351177 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,9 @@ Learn more in the [Q&A](https://y-sunflower.github.io/plotjs/#qa). - [Embedding in Quarto, marimo, or websites](https://y-sunflower.github.io/plotjs/guides/embed-graphs/) - [Troubleshooting](https://y-sunflower.github.io/plotjs/guides/troubleshooting/) - [Developer architecture overview](https://y-sunflower.github.io/plotjs/developers/overview) + +
+ +## Contribution + +Looking to contribute? Check out the [contributing guide](https://y-sunflower.github.io/plotjs/developers/contributing/). You can get an overview of how the project works [here](https://y-sunflower.github.io/plotjs/developers/overview/), and in the [AGENTS.md file](https://github.com/y-sunflower/plotjs/blob/main/AGENTS.md). diff --git a/plotjs/plotjs.py b/plotjs/plotjs.py index efa2eb1..ea4712a 100644 --- a/plotjs/plotjs.py +++ b/plotjs/plotjs.py @@ -29,7 +29,7 @@ class PlotJS: """ - Class to convert static matplotlib plots to interactive charts. + Main class to convert static matplotlib plots to interactive charts. """ def __init__( @@ -427,7 +427,7 @@ def as_html(self) -> str: def show(self) -> "PlotJS": """ - Open the HTML file in the default browser. + Open the HTML file in the default browser, or inside your editor. If the file hasn't been saved yet, it will be saved to a temporary file. Returns: diff --git a/plotjs/utils.py b/plotjs/utils.py index 85d48a1..0246caf 100644 --- a/plotjs/utils.py +++ b/plotjs/utils.py @@ -1,9 +1,24 @@ +import numpy as np import narwhals.stable.v2 as nw from narwhals.stable.v2.dependencies import is_numpy_array, is_into_series import re +def _is_missing(value) -> bool: + if value is None: + return True + try: + return bool(np.isnan(value)) + except TypeError: + pass + try: + return value != value + except Exception: + return False + return False + + def _vector_to_list(vector, name="labels and groups") -> list: """ Function used to easily convert various kind of iterables to @@ -23,14 +38,19 @@ def _vector_to_list(vector, name="labels and groups") -> list: A list """ if isinstance(vector, (list, tuple)) or is_numpy_array(vector): - return list(vector) + vector_sanitized: list = list(vector) elif is_into_series(vector): - return nw.from_native(vector, allow_series=True).to_list() + vector_sanitized: list = nw.from_native(vector, allow_series=True).to_list() else: raise ValueError( f"{name} must be a Series or a valid iterable (list, tuple, ndarray...)." ) + # Drop NaNs to avoid JSON parsing error + # https://github.com/y-sunflower/plotjs/issues/67 + vector_sanitized = [x for x in vector_sanitized if not _is_missing(x)] + return vector_sanitized + def _get_and_sanitize_js(file_path, after_pattern): """ diff --git a/tests/test-browser/test_rendering.py b/tests/test-browser/test_rendering.py index 00ffd2c..84f6be5 100644 --- a/tests/test-browser/test_rendering.py +++ b/tests/test-browser/test_rendering.py @@ -1,4 +1,5 @@ import pytest +import seaborn as sns import matplotlib.pyplot as plt from plotjs import PlotJS, data @@ -96,6 +97,41 @@ def test_multiple_axes_render(page, tmp_output_dir, load_html): assert axes_groups.count() == 2 +def test_seaborn_with_nans_renders(page, tmp_output_dir, load_html): + mpg = sns.load_dataset("mpg").sort_values("origin") + sns.relplot( + x="horsepower", + y="mpg", + hue="origin", + size="weight", + sizes=(40, 400), + alpha=0.5, + palette="muted", + height=6, + data=mpg, + ) + + html_path = tmp_output_dir / "bar.html" + ( + PlotJS() + .add_tooltip(labels=mpg["horsepower"], groups=mpg["origin"]) + .save(str(html_path)) + ) + + plt.close() + + load_html(page, html_path) + + svg = page.locator("svg") + assert svg.count() == 1 + + # Check for no error messages with NaNs + console_messages = [] + page.on("console", lambda msg: console_messages.append(msg)) + errors = [msg for msg in console_messages if msg.type == "error"] + assert len(errors) == 0, f"JavaScript errors found: {[msg.text for msg in errors]}" + + def test_custom_css_applies(page, tmp_output_dir, load_html): """Test that custom CSS is applied correctly.""" fig, ax = plt.subplots() diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index c798fd9..bbafcb6 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -1,4 +1,3 @@ -from plotjs import PlotJS, data import numpy as np import matplotlib.pyplot as plt import os @@ -6,6 +5,8 @@ from unittest.mock import patch import pytest +from plotjs import PlotJS, data + def test_add_css_method_chaining(): df = data.load_iris() @@ -423,7 +424,7 @@ def test_fill_between_with_legend(): ax.spines[["top", "right"]].set_visible(False) ax.legend() - plotjs = PlotJS(fig, _debug=True).add_tooltip( + plotjs = PlotJS(fig).add_tooltip( labels=["Series A", "Series B"], groups=["Series A", "Series B"], on="area", @@ -504,7 +505,7 @@ def test_pie_chart(): tooltip = [f"{lab} (n = {size})" for lab, size in zip(labels, sizes)] - plotjs = PlotJS(fig, _debug=True).add_tooltip(labels=tooltip) + plotjs = PlotJS(fig).add_tooltip(labels=tooltip) assert len(plotjs._axes) == 1 assert plotjs._tooltip_labels == [ From 23cf46e768b7be9229d91c8066c9d35c757d3ab1 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 13:55:21 +0100 Subject: [PATCH 10/16] add boxplot example --- docs/iframes/bug.html | 280 +-- docs/iframes/quickstart.html | 2 +- docs/iframes/quickstart10.html | 2 +- docs/iframes/quickstart11.html | 2 +- docs/iframes/quickstart12.html | 4 +- docs/iframes/quickstart13.html | 3255 ++++++++++++++++++++++++++++++++ docs/iframes/quickstart2.html | 2 +- docs/iframes/quickstart3.html | 2 +- docs/iframes/quickstart4.html | 2 +- docs/iframes/quickstart5.html | 2 +- docs/iframes/quickstart6.html | 2 +- docs/iframes/quickstart7.html | 2 +- docs/iframes/quickstart8.html | 1602 ++++++++-------- docs/iframes/quickstart9.html | 2 +- docs/index.md | 40 +- docs/index.qmd | 38 +- 16 files changed, 4249 insertions(+), 990 deletions(-) create mode 100644 docs/iframes/quickstart13.html diff --git a/docs/iframes/bug.html b/docs/iframes/bug.html index 9ff3c6f..2dff2d2 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:10.513958 + 2026-03-27T13:53:08.660974 image/svg+xml @@ -107,56 +107,56 @@ " style="stroke: #000000"/>
- - - - - - - - - - - - + - - - - - + + + + + - - - - - - - - - + + - - + + + - - - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + - - - + +
@@ -174,56 +174,56 @@ " style="stroke: #000000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -241,56 +241,56 @@ " style="stroke: #000000"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1209,7 +1209,7 @@ const svg = container.querySelector("svg"); console.log(`PlotJS: SVG and tooltip elements loaded`); - const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_labels": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"]}}, "hover_nearest": false, "tooltip_groups": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_labels": ["virginica", "virginica", "setosa", "virginica", "setosa", "setosa", "versicolor", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "virginica", "setosa", "virginica", "virginica", "versicolor", "setosa", "versicolor", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "setosa", "versicolor", "setosa", "setosa", "versicolor", "setosa", "virginica", "virginica", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "versicolor", "versicolor", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "virginica", "versicolor", "setosa", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "setosa", "virginica", "versicolor", "setosa", "setosa", "setosa", "virginica", "versicolor", "setosa", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "setosa", "versicolor", "setosa", "versicolor", "versicolor", "virginica", "setosa"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); + const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": ["virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "virginica", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "versicolor", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa"], "tooltip_labels": ["virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "virginica", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "versicolor", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa"]}}, "hover_nearest": false, "tooltip_groups": ["virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "virginica", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "versicolor", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa"], "tooltip_labels": ["virginica", "versicolor", "virginica", "setosa", "virginica", "setosa", "virginica", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "setosa", "versicolor", "virginica", "virginica", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "virginica", "versicolor", "virginica", "versicolor", "versicolor", "virginica", "setosa", "setosa", "virginica", "virginica", "virginica", "versicolor", "setosa", "virginica", "virginica", "setosa", "virginica", "virginica", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "virginica", "versicolor", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "versicolor", "versicolor", "versicolor", "setosa", "versicolor", "versicolor", "setosa", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "versicolor", "versicolor", "virginica", "versicolor", "setosa", "setosa", "virginica", "versicolor", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "virginica", "virginica", "setosa", "virginica", "setosa", "virginica", "setosa", "virginica", "virginica", "virginica", "versicolor", "versicolor", "setosa", "virginica", "setosa", "setosa", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "versicolor", "versicolor", "versicolor", "virginica", "virginica", "setosa", "setosa", "setosa", "virginica", "setosa", "versicolor", "virginica", "setosa", "versicolor", "versicolor", "versicolor", "setosa"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); const tooltip_x_shift = plot_data["tooltip_x_shift"]; const tooltip_y_shift = -plot_data["tooltip_y_shift"]; const axes = plot_data["axes"]; diff --git a/docs/iframes/quickstart.html b/docs/iframes/quickstart.html index 4b47a48..f1ca934 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.251584 + 2026-03-27T13:53:06.183530 image/svg+xml diff --git a/docs/iframes/quickstart10.html b/docs/iframes/quickstart10.html index ed9ddf6..8f7b042 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:10.168781 + 2026-03-27T13:53:08.299300 image/svg+xml diff --git a/docs/iframes/quickstart11.html b/docs/iframes/quickstart11.html index c3e1536..1aeba46 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:10.299185 + 2026-03-27T13:53:08.435035 image/svg+xml diff --git a/docs/iframes/quickstart12.html b/docs/iframes/quickstart12.html index 10097e0..7dd3b55 100644 --- a/docs/iframes/quickstart12.html +++ b/docs/iframes/quickstart12.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.568998 + 2026-03-27T13:53:06.518751 image/svg+xml @@ -926,7 +926,7 @@ const svg = container.querySelector("svg"); console.log(`PlotJS: SVG and tooltip elements loaded`); - const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "tooltip_labels": ["Lower bound: -3.05\u003cbr\u003eUpper bound:-2.57\u003cbr\u003en: 1", "Lower bound: -2.57\u003cbr\u003eUpper bound:-2.10\u003cbr\u003en: 1", "Lower bound: -2.10\u003cbr\u003eUpper bound:-1.63\u003cbr\u003en: 5", "Lower bound: -1.63\u003cbr\u003eUpper bound:-1.16\u003cbr\u003en: 8", "Lower bound: -1.16\u003cbr\u003eUpper bound:-0.69\u003cbr\u003en: 10", "Lower bound: -0.69\u003cbr\u003eUpper bound:-0.22\u003cbr\u003en: 23", "Lower bound: -0.22\u003cbr\u003eUpper bound:0.26\u003cbr\u003en: 19", "Lower bound: 0.26\u003cbr\u003eUpper bound:0.73\u003cbr\u003en: 20", "Lower bound: 0.73\u003cbr\u003eUpper bound:1.20\u003cbr\u003en: 8", "Lower bound: 1.20\u003cbr\u003eUpper bound:1.67\u003cbr\u003en: 5"]}}, "hover_nearest": false, "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "tooltip_labels": ["Lower bound: -3.05\u003cbr\u003eUpper bound:-2.57\u003cbr\u003en: 1", "Lower bound: -2.57\u003cbr\u003eUpper bound:-2.10\u003cbr\u003en: 1", "Lower bound: -2.10\u003cbr\u003eUpper bound:-1.63\u003cbr\u003en: 5", "Lower bound: -1.63\u003cbr\u003eUpper bound:-1.16\u003cbr\u003en: 8", "Lower bound: -1.16\u003cbr\u003eUpper bound:-0.69\u003cbr\u003en: 10", "Lower bound: -0.69\u003cbr\u003eUpper bound:-0.22\u003cbr\u003en: 23", "Lower bound: -0.22\u003cbr\u003eUpper bound:0.26\u003cbr\u003en: 19", "Lower bound: 0.26\u003cbr\u003eUpper bound:0.73\u003cbr\u003en: 20", "Lower bound: 0.73\u003cbr\u003eUpper bound:1.20\u003cbr\u003en: 8", "Lower bound: 1.20\u003cbr\u003eUpper bound:1.67\u003cbr\u003en: 5"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); + const plot_data = JSON.parse(`{"axes": {"axes_1": {"hover_nearest": "false", "on": null, "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "tooltip_labels": ["Lower bound: -3.05\u003cbr\u003eUpper bound: -2.57\u003cbr\u003en: 1", "Lower bound: -2.57\u003cbr\u003eUpper bound: -2.10\u003cbr\u003en: 1", "Lower bound: -2.10\u003cbr\u003eUpper bound: -1.63\u003cbr\u003en: 5", "Lower bound: -1.63\u003cbr\u003eUpper bound: -1.16\u003cbr\u003en: 8", "Lower bound: -1.16\u003cbr\u003eUpper bound: -0.69\u003cbr\u003en: 10", "Lower bound: -0.69\u003cbr\u003eUpper bound: -0.22\u003cbr\u003en: 23", "Lower bound: -0.22\u003cbr\u003eUpper bound: 0.26\u003cbr\u003en: 19", "Lower bound: 0.26\u003cbr\u003eUpper bound: 0.73\u003cbr\u003en: 20", "Lower bound: 0.73\u003cbr\u003eUpper bound: 1.20\u003cbr\u003en: 8", "Lower bound: 1.20\u003cbr\u003eUpper bound: 1.67\u003cbr\u003en: 5"]}}, "hover_nearest": false, "tooltip_groups": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "tooltip_labels": ["Lower bound: -3.05\u003cbr\u003eUpper bound: -2.57\u003cbr\u003en: 1", "Lower bound: -2.57\u003cbr\u003eUpper bound: -2.10\u003cbr\u003en: 1", "Lower bound: -2.10\u003cbr\u003eUpper bound: -1.63\u003cbr\u003en: 5", "Lower bound: -1.63\u003cbr\u003eUpper bound: -1.16\u003cbr\u003en: 8", "Lower bound: -1.16\u003cbr\u003eUpper bound: -0.69\u003cbr\u003en: 10", "Lower bound: -0.69\u003cbr\u003eUpper bound: -0.22\u003cbr\u003en: 23", "Lower bound: -0.22\u003cbr\u003eUpper bound: 0.26\u003cbr\u003en: 19", "Lower bound: 0.26\u003cbr\u003eUpper bound: 0.73\u003cbr\u003en: 20", "Lower bound: 0.73\u003cbr\u003eUpper bound: 1.20\u003cbr\u003en: 8", "Lower bound: 1.20\u003cbr\u003eUpper bound: 1.67\u003cbr\u003en: 5"], "tooltip_x_shift": 0, "tooltip_y_shift": 0}`); const tooltip_x_shift = plot_data["tooltip_x_shift"]; const tooltip_y_shift = -plot_data["tooltip_y_shift"]; const axes = plot_data["axes"]; diff --git a/docs/iframes/quickstart13.html b/docs/iframes/quickstart13.html new file mode 100644 index 0000000..912e6bc --- /dev/null +++ b/docs/iframes/quickstart13.html @@ -0,0 +1,3255 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-27T13:53:07.436346 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 0644e80..22ccb55 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.275133 + 2026-03-27T13:53:06.210138 image/svg+xml diff --git a/docs/iframes/quickstart3.html b/docs/iframes/quickstart3.html index 7047a96..0b66e69 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.296416 + 2026-03-27T13:53:06.232171 image/svg+xml diff --git a/docs/iframes/quickstart4.html b/docs/iframes/quickstart4.html index 2745269..f6a7163 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.322696 + 2026-03-27T13:53:06.259708 image/svg+xml diff --git a/docs/iframes/quickstart5.html b/docs/iframes/quickstart5.html index 597871f..a38a1b1 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.348319 + 2026-03-27T13:53:06.286232 image/svg+xml diff --git a/docs/iframes/quickstart6.html b/docs/iframes/quickstart6.html index 244f3e1..020011f 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.490854 + 2026-03-27T13:53:06.432989 image/svg+xml diff --git a/docs/iframes/quickstart7.html b/docs/iframes/quickstart7.html index 765fada..5fefc0e 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.648263 + 2026-03-27T13:53:07.759038 image/svg+xml diff --git a/docs/iframes/quickstart8.html b/docs/iframes/quickstart8.html index c5012ee..9d72019 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:09.796799 + 2026-03-27T13:53:07.913735 image/svg+xml @@ -343,12 +343,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - +
- + - + - + @@ -382,12 +382,12 @@ - + - + @@ -395,12 +395,12 @@ - + - + @@ -409,816 +409,754 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + @@ -1243,27 +1181,27 @@ - - - + - + - + - - + - + - + - - + - + - + - 2026-03-26T16:35:09.960250 + 2026-03-27T13:53:08.077589 image/svg+xml diff --git a/docs/index.md b/docs/index.md index 6e1d2ad..33155b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -241,7 +241,7 @@ fig, ax = plt.subplots() counts, bins, _ = ax.hist(x, color="#2a9d8f") labels = [ - f"Lower bound: {lo:.2f}
Upper bound:{hi:.2f}
n: {int(n)}" + f"Lower bound: {lo:.2f}
Upper bound: {hi:.2f}
n: {int(n)}" for lo, hi, n in zip(bins[:-1], bins[1:], counts) ] @@ -252,6 +252,42 @@ PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") +### Boxplot (seaborn) + +``` python +from plotjs import PlotJS +import seaborn as sns +import matplotlib.pyplot as plt + +planets = sns.load_dataset("planets") + +fig, ax = plt.subplots(figsize=(7, 6)) +ax.set_xscale("log") +sns.boxplot( + planets, + x="distance", + y="method", + hue="method", + whis=[0, 100], + width=0.6, + palette="vlag", +) +sns.stripplot(planets, x="distance", y="method", size=4, color=".3", alpha=0.4) +ax.xaxis.grid(True) +ax.set(ylabel="") +sns.despine() + +( + PlotJS(_debug=True, bbox_inches="tight") + .add_tooltip(labels=planets["method"].unique(), on="bar", hover_nearest=True) + .save("iframes/quickstart13.html") +) +``` + + + ### Connect legend and plot elements: - Scatter plot @@ -456,8 +492,6 @@ pip install plotjs pip install git+https://github.com/y-sunflower/plotjs.git ``` -## Dependencies - - Python 3.10+ - [matplotlib](https://matplotlib.org/), [jinja2](https://jinja.palletsprojects.com/en/stable/) and diff --git a/docs/index.qmd b/docs/index.qmd index 2f49726..a6d73e4 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -229,7 +229,7 @@ fig, ax = plt.subplots() counts, bins, _ = ax.hist(x, color="#2a9d8f") labels = [ - f"Lower bound: {lo:.2f}
Upper bound:{hi:.2f}
n: {int(n)}" + f"Lower bound: {lo:.2f}
Upper bound: {hi:.2f}
n: {int(n)}" for lo, hi, n in zip(bins[:-1], bins[1:], counts) ] @@ -238,6 +238,40 @@ PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") +### Boxplot (seaborn) + +```{python} +from plotjs import PlotJS +import seaborn as sns +import matplotlib.pyplot as plt + +planets = sns.load_dataset("planets") + +fig, ax = plt.subplots(figsize=(7, 6)) +ax.set_xscale("log") +sns.boxplot( + planets, + x="distance", + y="method", + hue="method", + whis=[0, 100], + width=0.6, + palette="vlag", +) +sns.stripplot(planets, x="distance", y="method", size=4, color=".3", alpha=0.4) +ax.xaxis.grid(True) +ax.set(ylabel="") +sns.despine() + +( + PlotJS(bbox_inches="tight") + .add_tooltip(labels=planets["method"].unique(), on="bar", hover_nearest=True) + .save("iframes/quickstart13.html") +) +``` + + + ### Connect legend and plot elements: - Scatter plot @@ -429,8 +463,6 @@ pip install plotjs pip install git+https://github.com/y-sunflower/plotjs.git ``` -## Dependencies - - Python 3.10+ - [matplotlib](https://matplotlib.org/), [jinja2](https://jinja.palletsprojects.com/en/stable/) and [narwhals](https://narwhals-dev.github.io/narwhals/) From ea8007aa7798990d2b8252554cc79439db7ae2ec Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 14:00:56 +0100 Subject: [PATCH 11/16] fix regression that detects a pie element in legends --- plotjs/static/plotparser.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index aeb4d8c..0e880d2 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -237,8 +237,6 @@ export default class PlotSVGParser { * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -248,7 +246,15 @@ export default class PlotSVGParser { const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && From 772218047537e14e9e753b33658cd229a0b4f4b7 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 14:03:31 +0100 Subject: [PATCH 12/16] render doc --- docs/gallery/index.md | 20 +- docs/iframes/CSS-2.html | 12 +- docs/iframes/CSS.html | 12 +- docs/iframes/area-natural-disasters.html | 12 +- docs/iframes/bug.html | 12 +- docs/iframes/javascript.html | 12 +- docs/iframes/javascript2.html | 12 +- docs/iframes/quickstart.html | 12 +- docs/iframes/quickstart10.html | 12 +- docs/iframes/quickstart11.html | 12 +- docs/iframes/quickstart12.html | 12 +- docs/iframes/quickstart13.html | 12 +- docs/iframes/quickstart2.html | 12 +- docs/iframes/quickstart3.html | 12 +- docs/iframes/quickstart4.html | 12 +- docs/iframes/quickstart5.html | 12 +- docs/iframes/quickstart6.html | 12 +- docs/iframes/quickstart7.html | 12 +- docs/iframes/quickstart8.html | 12 +- docs/iframes/quickstart9.html | 12 +- docs/iframes/random-walk-1.html | 75356 +++++++++++---------- docs/index.md | 2 +- 22 files changed, 38161 insertions(+), 37445 deletions(-) diff --git a/docs/gallery/index.md b/docs/gallery/index.md index fadfdb1..e05e7ea 100644 --- a/docs/gallery/index.md +++ b/docs/gallery/index.md @@ -66,11 +66,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -78,11 +78,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -90,11 +90,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -102,11 +102,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -114,6 +114,10 @@ This page contains all the `plotjs` examples from this website.
+ + diff --git a/docs/iframes/CSS-2.html b/docs/iframes/CSS-2.html index 6910781..adebc86 100644 --- a/docs/iframes/CSS-2.html +++ b/docs/iframes/CSS-2.html @@ -65,7 +65,7 @@ - 2026-03-26T16:35:14.208731 + 2026-03-27T14:03:16.131771 image/svg+xml @@ -1595,8 +1595,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1606,7 +1604,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/CSS.html b/docs/iframes/CSS.html index f1f0a90..834827d 100644 --- a/docs/iframes/CSS.html +++ b/docs/iframes/CSS.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:14.156729 + 2026-03-27T14:03:16.078328 image/svg+xml @@ -1591,8 +1591,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1602,7 +1600,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/area-natural-disasters.html b/docs/iframes/area-natural-disasters.html index 34acbbf..8311d71 100644 --- a/docs/iframes/area-natural-disasters.html +++ b/docs/iframes/area-natural-disasters.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:13.369122 + 2026-03-27T14:03:15.213408 image/svg+xml @@ -3899,8 +3899,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -3910,7 +3908,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/bug.html b/docs/iframes/bug.html index 2dff2d2..a5fa8e4 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:08.660974 + 2026-03-27T14:03:13.131146 image/svg+xml @@ -1010,8 +1010,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1021,7 +1019,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/javascript.html b/docs/iframes/javascript.html index 69ef8f8..5f7cf67 100644 --- a/docs/iframes/javascript.html +++ b/docs/iframes/javascript.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:14.857482 + 2026-03-27T14:03:16.868968 image/svg+xml @@ -1274,8 +1274,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1285,7 +1283,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/javascript2.html b/docs/iframes/javascript2.html index 7527111..7a7c25b 100644 --- a/docs/iframes/javascript2.html +++ b/docs/iframes/javascript2.html @@ -68,7 +68,7 @@ - 2026-03-26T16:35:14.902606 + 2026-03-27T14:03:16.915233 image/svg+xml @@ -1345,8 +1345,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1356,7 +1354,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart.html b/docs/iframes/quickstart.html index f1ca934..88719cd 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.183530 + 2026-03-27T14:03:11.485439 image/svg+xml @@ -1274,8 +1274,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1285,7 +1283,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart10.html b/docs/iframes/quickstart10.html index 8f7b042..9dfd3e9 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:08.299300 + 2026-03-27T14:03:12.786393 image/svg+xml @@ -1010,8 +1010,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1021,7 +1019,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart11.html b/docs/iframes/quickstart11.html index 1aeba46..2747185 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:08.435035 + 2026-03-27T14:03:12.916453 image/svg+xml @@ -2428,8 +2428,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -2439,7 +2437,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart12.html b/docs/iframes/quickstart12.html index 7dd3b55..5949bfd 100644 --- a/docs/iframes/quickstart12.html +++ b/docs/iframes/quickstart12.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.518751 + 2026-03-27T14:03:11.837370 image/svg+xml @@ -727,8 +727,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -738,7 +736,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart13.html b/docs/iframes/quickstart13.html index 912e6bc..ef8d7ca 100644 --- a/docs/iframes/quickstart13.html +++ b/docs/iframes/quickstart13.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:07.436346 + 2026-03-27T14:03:12.042453 image/svg+xml @@ -2897,8 +2897,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -2908,7 +2906,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 22ccb55..2b91016 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.210138 + 2026-03-27T14:03:11.506158 image/svg+xml @@ -1274,8 +1274,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1285,7 +1283,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart3.html b/docs/iframes/quickstart3.html index 0b66e69..c410d83 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.232171 + 2026-03-27T14:03:11.526675 image/svg+xml @@ -1274,8 +1274,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1285,7 +1283,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart4.html b/docs/iframes/quickstart4.html index f6a7163..1e3ba7d 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.259708 + 2026-03-27T14:03:11.554235 image/svg+xml @@ -1274,8 +1274,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1285,7 +1283,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart5.html b/docs/iframes/quickstart5.html index a38a1b1..f00c3d8 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.286232 + 2026-03-27T14:03:11.580403 image/svg+xml @@ -1470,8 +1470,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1481,7 +1479,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart6.html b/docs/iframes/quickstart6.html index 020011f..0ed4d27 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:06.432989 + 2026-03-27T14:03:11.753152 image/svg+xml @@ -1055,8 +1055,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1066,7 +1064,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart7.html b/docs/iframes/quickstart7.html index 5fefc0e..8897204 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:07.759038 + 2026-03-27T14:03:12.268597 image/svg+xml @@ -1338,8 +1338,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1349,7 +1347,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart8.html b/docs/iframes/quickstart8.html index 9d72019..1a4dc5c 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:07.913735 + 2026-03-27T14:03:12.418184 image/svg+xml @@ -1759,8 +1759,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1770,7 +1768,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/quickstart9.html b/docs/iframes/quickstart9.html index d8d2c76..2c6bfa1 100644 --- a/docs/iframes/quickstart9.html +++ b/docs/iframes/quickstart9.html @@ -61,7 +61,7 @@ - 2026-03-27T13:53:08.077589 + 2026-03-27T14:03:12.577765 image/svg+xml @@ -1973,8 +1973,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -1984,7 +1982,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/iframes/random-walk-1.html b/docs/iframes/random-walk-1.html index 775041f..1b5004c 100644 --- a/docs/iframes/random-walk-1.html +++ b/docs/iframes/random-walk-1.html @@ -61,7 +61,7 @@ - 2026-03-26T16:35:13.425491 + 2026-03-27T14:03:15.268619 image/svg+xml @@ -361,12 +361,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + @@ -411,14 +411,14 @@ - + - - + + - + @@ -427,18597 +427,18974 @@ - + - - + + - - + + + - + - - - + + + + + + - + - - - - + + + + + - + - - - - - + + + - + - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + - - + - - + - - + - - + - - + @@ -19042,27 +19419,27 @@ - - - + - + - + - - + - + - + - - + - + - + - - + - + - + - - + - + - + - + - + @@ -19598,12 +19975,12 @@ - + - + @@ -19614,12 +19991,12 @@ - + - + @@ -19630,12 +20007,12 @@ - + - + @@ -19646,12 +20023,12 @@ - + - + @@ -19662,12 +20039,12 @@ - + - + @@ -19680,18814 +20057,19029 @@ - - - - - - - - - - - - - - - - - + - - + + - + + - + - - - + + + + + + - + - - - - + + + + + + - + - - - - - + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + - - + - - + - - + - - + @@ -38512,27 +39104,27 @@ - - - + - + - + @@ -38541,15 +39133,15 @@ - - + - + - + @@ -38557,15 +39149,15 @@ - - + - + - + @@ -38575,15 +39167,15 @@ - - + - + - + @@ -38594,15 +39186,15 @@ - - + - + - + @@ -38868,8 +39460,6 @@ * @returns {Selection} Selection of pie elements. */ findPies(svg, axes_class) { - // Matplotlib also uses patch_* groups for axes backgrounds and spines. - // Pie wedges are the patch paths with a non-empty fill and curve commands. const parser = this; const pies = svg .selectAll(`g#${axes_class} g[id^="patch_"] path`) @@ -38879,7 +39469,15 @@ const normalizedFill = parser.getFillValue(element); const pathData = element.getAttribute("d") ?? ""; + const parent = element.parentElement; + const grandparent = parent?.parentElement; + const isInLegend = + parent?.tagName?.toLowerCase() === "g" && + grandparent?.tagName?.toLowerCase() === "g" && + /^legend_\d+$/.test(grandparent.id); + return ( + !isInLegend && !clipPath && normalizedFill !== "" && normalizedFill !== "none" && diff --git a/docs/index.md b/docs/index.md index 33155b4..78d207e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -278,7 +278,7 @@ ax.set(ylabel="") sns.despine() ( - PlotJS(_debug=True, bbox_inches="tight") + PlotJS(bbox_inches="tight") .add_tooltip(labels=planets["method"].unique(), on="bar", hover_nearest=True) .save("iframes/quickstart13.html") ) From 472e71f18f70aaea3a183221d30adae38bf1c363 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 14:44:02 +0100 Subject: [PATCH 13/16] init rect parsing --- plotjs/static/plotparser.js | 16 ++++++++++++++++ plotjs/static/template.html | 20 +++++++++++++++++++- pyproject.toml | 3 ++- uv.lock | 23 +++++++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index 0e880d2..b464a0d 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -229,6 +229,22 @@ export default class PlotSVGParser { return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + + rectangles.attr("class", "rectangle plot-element"); + + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * diff --git a/plotjs/static/template.html b/plotjs/static/template.html index 4313eb3..c7ea6d7 100644 --- a/plotjs/static/template.html +++ b/plotjs/static/template.html @@ -77,6 +77,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -99,9 +102,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -132,6 +136,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/pyproject.toml b/pyproject.toml index aeca375..107e1f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "plotjs" version = "0.0.9" -description = "Turn static matplotlib charts into interactive web visualizations" +description = "Turn static matplotlib charts into interactive web visualizations, and export them to HTML files" readme = "README.md" requires-python = ">=3.10" license = "MIT" @@ -57,6 +57,7 @@ dev = [ "pyfonts>=1.0.0", "pypalettes>=0.1.4", "pytest>=8.3.5", + "pywaffle>=1.1.1", "ruff>=0.11.13", "seaborn>=0.13.2", "ty>=0.0.1a16", diff --git a/uv.lock b/uv.lock index b3fc884..92afc46 100644 --- a/uv.lock +++ b/uv.lock @@ -708,6 +708,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, ] +[[package]] +name = "fontawesomefree" +version = "6.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/e9/d43f5133b73e7ef9047bda28daaa6905e00b7d39f093b547f7e78ee2fc40/fontawesomefree-6.6.0-py3-none-any.whl", hash = "sha256:599b574431c9bd92ed5fc054d1045a07c42335da36c17884f2b934755eef9089", size = 25645298, upload-time = "2024-07-16T18:35:44.818Z" }, +] + [[package]] name = "fonttools" version = "4.59.0" @@ -2264,6 +2272,7 @@ dev = [ { name = "pyfonts" }, { name = "pypalettes" }, { name = "pytest" }, + { name = "pywaffle" }, { name = "ruff" }, { name = "seaborn" }, { name = "ty" }, @@ -2298,6 +2307,7 @@ dev = [ { name = "pyfonts", specifier = ">=1.0.0" }, { name = "pypalettes", specifier = ">=0.1.4" }, { name = "pytest", specifier = ">=8.3.5" }, + { name = "pywaffle", specifier = ">=1.1.1" }, { name = "ruff", specifier = ">=0.11.13" }, { name = "seaborn", specifier = ">=0.13.2" }, { name = "ty", specifier = ">=0.0.1a16" }, @@ -2549,6 +2559,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywaffle" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fontawesomefree" }, + { name = "matplotlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/4b/c0f432b1287ce2df12841929e406a0b466484255b88ca1e1fd6ca684a759/pywaffle-1.1.1.tar.gz", hash = "sha256:d3d98178b76bcf1e179fd3c9d77b18fa231e32d628710441ceddb4451ddf3c4a", size = 37110, upload-time = "2024-06-16T04:18:09.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/99/e564248b5a2ec8f005a6209e2e59a458ce4850281fb28e4aa3571fe91f52/pywaffle-1.1.1-py2.py3-none-any.whl", hash = "sha256:48b15575639d2ddf5565a1768520f2b26cc3e92f778edf10fb29ecf27bcdecab", size = 30911, upload-time = "2024-06-16T04:18:08.303Z" }, +] + [[package]] name = "pywin32" version = "311" From 26fdbf8111c7a082a6f2f3a77a61cbffa15f455b Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 15:10:45 +0100 Subject: [PATCH 14/16] fix navbar --- docs/guides/advanced/advanced.py | 67 + docs/guides/advanced/index.md | 75 +- docs/guides/css/index.md | 4 + docs/guides/embed-graphs/index.md | 4 + docs/guides/javascript/index.md | 4 + docs/guides/troubleshooting/index.md | 4 + docs/iframes/area-natural-disasters.html | 36 +- docs/iframes/energy-consumption-france.html | 2702 + docs/iframes/random-walk-1.html | 75671 +++++++++--------- docs/index.qmd | 37 +- plotjs/static/plotparser.js | 2 - zensical.toml | 1 + 12 files changed, 40615 insertions(+), 37992 deletions(-) create mode 100644 docs/iframes/energy-consumption-france.html diff --git a/docs/guides/advanced/advanced.py b/docs/guides/advanced/advanced.py index 2262d4e..7f559da 100644 --- a/docs/guides/advanced/advanced.py +++ b/docs/guides/advanced/advanced.py @@ -6,6 +6,8 @@ from highlight_text import fig_text, ax_text from pyfonts import load_google_font from drawarrow import ax_arrow +import matplotlib.colors as mcolors +import seaborn as sns url = "https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/disaster-events.csv" df = pd.read_csv(url) @@ -274,3 +276,68 @@ def remove_agg_rows(entity: str): ) ########################################## + +path = "https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/heatmap_data_norm.csv" +heatmap_data_norm = pd.read_csv(path, index_col=0) + +cmap = mcolors.LinearSegmentedColormap.from_list("", ["#2a9d8f", "#e9c46a", "#e76f51"]) + +fig, ax = plt.subplots(figsize=(10, 10)) +sns.heatmap(heatmap_data_norm, ax=ax, cmap=cmap, cbar=False) +ax.set_axis_off() +for j, region in enumerate(heatmap_data_norm.index): + ax.text( + 0.4, # x axis position + j + 0.5, # y axis position + f"{region}", # text + ha="left", + va="center", + fontsize=17, + fontweight="light", + ) + +ax.text( + 0, + 12.4, + "Consommation d'énergie par habitant", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) + +ax.text( + 0, + -0.2, + "2011", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) +ax.text( + 10, + -0.2, + "2021", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) + +labels = [ + f"{val:.1f} MWh/hab - {region} en {i + 2011}" + for region, row in heatmap_data_norm.iterrows() + for i, val in enumerate(row) +] +( + PlotJS(fig=fig) + .add_tooltip(labels=labels) + .add_css( + from_dict={ + ".tooltip": {"font-size": "1.2em"}, + ".plot-element.not-hovered": {"opacity": 0.8}, + } + ) + .save("docs/iframes/energy-consumption-france.html") +) diff --git a/docs/guides/advanced/index.md b/docs/guides/advanced/index.md index 425f62e..0fc30ae 100644 --- a/docs/guides/advanced/index.md +++ b/docs/guides/advanced/index.md @@ -1,4 +1,6 @@ -# TODO: find cool examples to showcase here +--- +title: Advanced examples +--- ## Natural disasters @@ -200,6 +202,77 @@ PlotJS(fig, bbox_inches="tight").add_css( +## Energy consumption in France + +```python +path = "https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/heatmap_data_norm.csv" +heatmap_data_norm = pd.read_csv(path, index_col=0) + +cmap = mcolors.LinearSegmentedColormap.from_list("", ["#2a9d8f", "#e9c46a", "#e76f51"]) + +fig, ax = plt.subplots(figsize=(10, 10)) +sns.heatmap(heatmap_data_norm, ax=ax, cmap=cmap, cbar=False) +ax.set_axis_off() +for j, region in enumerate(heatmap_data_norm.index): + ax.text( + 0.4, # x axis position + j + 0.5, # y axis position + f"{region}", # text + ha="left", + va="center", + fontsize=17, + fontweight="light", + ) + +ax.text( + 0, + 12.4, + "Consommation d'énergie par habitant", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) + +ax.text( + 0, + -0.2, + "2011", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) +ax.text( + 10, + -0.2, + "2021", + ha="left", + va="center", + fontsize=12, + fontweight="light", +) + +labels = [ + f"{val:.1f} MWh/hab - {region} en {i + 2011}" + for region, row in heatmap_data_norm.iterrows() + for i, val in enumerate(row) +] +( + PlotJS(fig=fig) + .add_tooltip(labels=labels) + .add_css( + from_dict={ + ".tooltip": {"font-size": "1.2em"}, + ".plot-element.not-hovered": {"opacity": 0.8}, + } + ) + .save("docs/iframes/energy-consumption-france.html") +) +``` + + + ## Random walks ```python diff --git a/docs/guides/css/index.md b/docs/guides/css/index.md index 8275ae0..50cc9c5 100644 --- a/docs/guides/css/index.md +++ b/docs/guides/css/index.md @@ -1,3 +1,7 @@ +--- +title: CSS +--- + With `plotjs`, you can add your own CSS for advanced plot customization. Here's how it works. ## What is CSS? diff --git a/docs/guides/embed-graphs/index.md b/docs/guides/embed-graphs/index.md index a6cae24..27ae93f 100644 --- a/docs/guides/embed-graphs/index.md +++ b/docs/guides/embed-graphs/index.md @@ -1,3 +1,7 @@ +--- +title: Marimo, Quarto, Jupyter, websites... +--- + Most of the time you'll want to embed your interactive chart into an app or a website. For this purpose, `plotjs` offers a few utility tools to make this easy, depending on the tool you're using. ## Quarto diff --git a/docs/guides/javascript/index.md b/docs/guides/javascript/index.md index f863f2f..87a4af0 100644 --- a/docs/guides/javascript/index.md +++ b/docs/guides/javascript/index.md @@ -1,3 +1,7 @@ +--- +title: JavaScript +--- + Under the hood, JavaScript is what is used to make the charts interactive. But `plotjs` allows anyone to add some more JavaScript for finer control of what is happening and basically do whatever you want! ## Basic example diff --git a/docs/guides/troubleshooting/index.md b/docs/guides/troubleshooting/index.md index 722c3d8..8e318dc 100644 --- a/docs/guides/troubleshooting/index.md +++ b/docs/guides/troubleshooting/index.md @@ -1,3 +1,7 @@ +--- +title: Troubleshooting +--- + Since `plotjs` does many things via JavaScript (e.g., in your browser when you open your HTML file), you may easily encounter "silent" errors. In practice, you will run your Python and everything will seem fine, but that does not mean what you'll see in the output is what you expected. There may be multiple reasons for this. Here I'll explain common things that can happen, and how to debug them. diff --git a/docs/iframes/area-natural-disasters.html b/docs/iframes/area-natural-disasters.html index 8311d71..e358d8f 100644 --- a/docs/iframes/area-natural-disasters.html +++ b/docs/iframes/area-natural-disasters.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:15.213408 + 2026-03-27T15:04:22.838319 image/svg+xml @@ -3891,6 +3891,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -4150,6 +4164,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -4172,9 +4189,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -4205,6 +4223,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/energy-consumption-france.html b/docs/iframes/energy-consumption-france.html new file mode 100644 index 0000000..8a5df6c --- /dev/null +++ b/docs/iframes/energy-consumption-france.html @@ -0,0 +1,2702 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-27T15:04:23.236113 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/random-walk-1.html b/docs/iframes/random-walk-1.html index 1b5004c..b3e24c2 100644 --- a/docs/iframes/random-walk-1.html +++ b/docs/iframes/random-walk-1.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:15.268619 + 2026-03-27T15:04:22.898323 image/svg+xml @@ -361,12 +361,12 @@ " style="stroke: #000000; stroke-width: 0.8"/>
- +
- - + + - - - + + @@ -411,12 +418,12 @@ - + - + @@ -427,15 +434,15 @@ - + - - + + - + @@ -443,18958 +450,18755 @@ - + - - - - - - + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - + - - + - - + - - + - - + @@ -19431,13 +19235,13 @@ z " style="fill: #ffffff; opacity: 0.8; stroke: #cccccc; stroke-linejoin: miter"/> - + - + @@ -19532,6 +19336,31 @@ L 628 0 L 628 4666 z +" transform="scale(0.015625)"/> + @@ -19542,13 +19371,13 @@ - + - + @@ -19597,13 +19426,13 @@ - + - + @@ -19744,13 +19573,13 @@ - + - + @@ -19827,13 +19656,13 @@ - + - + @@ -19962,12 +19791,12 @@ - + - + @@ -19975,12 +19804,12 @@ - + - + @@ -19991,12 +19820,12 @@ - + - + @@ -20007,12 +19836,12 @@ - + - + @@ -20023,12 +19852,12 @@ - + - + @@ -20039,12 +19868,12 @@ - + - + @@ -20057,47 +19886,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - + @@ -20105,18981 +19902,18887 @@ - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - - - - - - - - - - - - - - - - + - - + - - + - - + - - + @@ -39116,13 +38819,13 @@ z " style="fill: #ffffff; opacity: 0.8; stroke: #cccccc; stroke-linejoin: miter"/> - + - + @@ -39133,13 +38836,13 @@ - + - + @@ -39149,13 +38852,13 @@ - + - + @@ -39167,13 +38870,13 @@ - + - + @@ -39186,13 +38889,13 @@ - + - + @@ -39452,6 +39155,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -39711,6 +39428,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -39733,9 +39453,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -39766,6 +39487,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/index.qmd b/docs/index.qmd index a6d73e4..528ce81 100644 --- a/docs/index.qmd +++ b/docs/index.qmd @@ -29,6 +29,23 @@ np.random.seed(0) Consider that `plotjs` is still unstable. +## Installation + +- From PyPI (recommended): + +```bash +pip install plotjs +``` + +- Latest dev version: + +```bash +pip install git+https://github.com/y-sunflower/plotjs.git +``` + +- Python 3.10+ +- [matplotlib](https://matplotlib.org/), [jinja2](https://jinja.palletsprojects.com/en/stable/) and [narwhals](https://narwhals-dev.github.io/narwhals/) + ## Get started Matplotlib is **great**[^1]: you can draw anything with it. @@ -238,7 +255,7 @@ PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") -### Boxplot (seaborn) +### Boxplot ```{python} from plotjs import PlotJS @@ -448,24 +465,6 @@ ax3.scatter(**args) - -## Installation - -- From PyPI (recommended): - -```bash -pip install plotjs -``` - -- Latest dev version: - -```bash -pip install git+https://github.com/y-sunflower/plotjs.git -``` - -- Python 3.10+ -- [matplotlib](https://matplotlib.org/), [jinja2](https://jinja.palletsprojects.com/en/stable/) and [narwhals](https://narwhals-dev.github.io/narwhals/) - ## Important limitation ### Plotting order diff --git a/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index b464a0d..8c9736b 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -238,9 +238,7 @@ export default class PlotSVGParser { */ findRectangles(svg, axes_class) { let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); - rectangles.attr("class", "rectangle plot-element"); - console.log(`Found ${rectangles.size()} "rectangle" element`); return rectangles; } diff --git a/zensical.toml b/zensical.toml index c407405..932418f 100644 --- a/zensical.toml +++ b/zensical.toml @@ -12,6 +12,7 @@ nav = [ { "Home" = "index.md" }, { "Gallery" = "gallery/index.md" }, { "Guides" = [ + "guides/index.md", "guides/css/index.md", "guides/javascript/index.md", "guides/embed-graphs/index.md", From db6bb0e351eb51eb366b0bb81a82d65c952c6a63 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 15:17:48 +0100 Subject: [PATCH 15/16] bunch of updates on the doc --- docs/developers/svg-parser-reference.md | 16 + docs/gallery/index.md | 6 +- docs/guides/advanced/advanced.py | 83 +- docs/guides/advanced/index.md | 88 - docs/guides/index.md | 9 + docs/iframes/CSS-2.html | 36 +- docs/iframes/CSS.html | 36 +- docs/iframes/area-natural-disasters.html | 2 +- docs/iframes/bug.html | 36 +- docs/iframes/energy-consumption-france.html | 2 +- docs/iframes/javascript.html | 36 +- docs/iframes/javascript2.html | 36 +- docs/iframes/quickstart.html | 36 +- docs/iframes/quickstart10.html | 36 +- docs/iframes/quickstart11.html | 36 +- docs/iframes/quickstart12.html | 36 +- docs/iframes/quickstart13.html | 36 +- docs/iframes/quickstart2.html | 36 +- docs/iframes/quickstart3.html | 36 +- docs/iframes/quickstart4.html | 36 +- docs/iframes/quickstart5.html | 36 +- docs/iframes/quickstart6.html | 36 +- docs/iframes/quickstart7.html | 36 +- docs/iframes/quickstart8.html | 36 +- docs/iframes/quickstart9.html | 36 +- docs/iframes/random-walk-1.html | 39559 ------------------ docs/index.md | 40 +- 27 files changed, 663 insertions(+), 39790 deletions(-) create mode 100644 docs/guides/index.md delete mode 100644 docs/iframes/random-walk-1.html diff --git a/docs/developers/svg-parser-reference.md b/docs/developers/svg-parser-reference.md index 6a7277f..2686b42 100644 --- a/docs/developers/svg-parser-reference.md +++ b/docs/developers/svg-parser-reference.md @@ -27,6 +27,9 @@ Provides basic DOM manipulation methods for working with SVG elements.

Handles both <use> and <path> fallback cases, and assigns data-group attributes based on tooltip groups.

+
findRectangles(svg, axes_class)Selection
+

Find rectangle elements inside a given axes.

+
findPies(svg, axes_class)Selection

Find pie elements (patch paths) inside a given axes.

@@ -123,6 +126,19 @@ and assigns `data-group` attributes based on tooltip groups. | axes_class | string | ID of the axes group (e.g. "axes_1"). | | tooltip_groups | Array.<string> | Group identifiers for tooltips, parallel to points. | + + +## findRectangles(svg, axes_class) ⇒ [Selection](#Selection) +Find rectangle elements inside a given axes. + +**Kind**: global function +**Returns**: [Selection](#Selection) - Selection of rectangle elements. + +| Param | Type | Description | +| --- | --- | --- | +| svg | [Selection](#Selection) | Selection of the SVG element. | +| axes_class | string | ID of the axes group (e.g. "axes_1"). | + ## findPies(svg, axes_class) ⇒ [Selection](#Selection) diff --git a/docs/gallery/index.md b/docs/gallery/index.md index e05e7ea..20d6c30 100644 --- a/docs/gallery/index.md +++ b/docs/gallery/index.md @@ -78,11 +78,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -90,7 +90,7 @@ This page contains all the `plotjs` examples from this website.
- diff --git a/docs/guides/advanced/advanced.py b/docs/guides/advanced/advanced.py index 7f559da..179ca87 100644 --- a/docs/guides/advanced/advanced.py +++ b/docs/guides/advanced/advanced.py @@ -194,88 +194,7 @@ def remove_agg_rows(entity: str): ) ).add_tooltip(labels=columns).save("docs/iframes/area-natural-disasters.html") -##################################################### - - -size = 10000 - -labels = ["S&P500", "CAC40", "Bitcoin", "Livret A", "Default"] -groups = ["safe", "safe", "safe", "not safe", "not safe"] - -fig, axs = plt.subplots(figsize=(10, 10), nrows=2) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#264653", - label=labels[0], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#2a9d8f", - label=labels[1], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#e9c46a", - label=labels[2], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#0077b6", - label=labels[3], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], -) -axs[0].legend() - -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#264653", - label=labels[0], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#2a9d8f", - label=labels[1], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#e9c46a", - label=labels[2], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#0077b6", - label=labels[3], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], -) -axs[1].legend() - - -( - PlotJS(fig=fig) - .add_tooltip(labels=labels, groups=labels, ax=axs[0]) - .add_tooltip(labels=labels, groups=labels, ax=axs[1]) - .save("docs/iframes/random-walk-1.html") -) - -########################################## +##########################################################################@ path = "https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/heatmap_data_norm.csv" heatmap_data_norm = pd.read_csv(path, index_col=0) diff --git a/docs/guides/advanced/index.md b/docs/guides/advanced/index.md index 0fc30ae..184af53 100644 --- a/docs/guides/advanced/index.md +++ b/docs/guides/advanced/index.md @@ -272,91 +272,3 @@ labels = [ ``` - -## Random walks - -```python -import numpy as np -import matplotlib.pyplot as plt -from plotjs import PlotJS - -size = 10000 - -labels = ["S&P500", "CAC40", "Bitcoin", "Livret A", "Default"] -groups = ["safe", "safe", "safe", "not safe", "not safe"] - -fig, axs = plt.subplots(figsize=(10, 10), nrows=2) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#264653", - label=labels[0], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#2a9d8f", - label=labels[1], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#e9c46a", - label=labels[2], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#0077b6", - label=labels[3], -) -axs[0].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], -) -axs[0].legend() - -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#264653", - label=labels[0], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#2a9d8f", - label=labels[1], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#e9c46a", - label=labels[2], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#0077b6", - label=labels[3], -) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], -) -axs[1].legend() - - -( - PlotJS(fig=fig) - .add_tooltip(labels=labels, groups=labels, ax=axs[0]) - .add_tooltip(labels=labels, groups=labels, ax=axs[1]) - .save("docs/iframes/random-walk-1.html") -) -``` - - diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 0000000..83477cd --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,9 @@ +--- +title: Guides +--- + +- How to add some [CSS](./css/index.md) +- How to add some [JavaScript](./javascript/index.md) +- How to embed your charts [in Marimo, Quarto, Jupyter, etc](./embed-graphs/index.md) +- How to [troubleshoot](./troubleshooting/index.md) +- [Advanced examples](./advanced/index.md) diff --git a/docs/iframes/CSS-2.html b/docs/iframes/CSS-2.html index adebc86..42e2ad4 100644 --- a/docs/iframes/CSS-2.html +++ b/docs/iframes/CSS-2.html @@ -65,7 +65,7 @@ - 2026-03-27T14:03:16.131771 + 2026-03-27T15:17:31.081686 image/svg+xml @@ -1587,6 +1587,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1846,6 +1860,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1868,9 +1885,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1901,6 +1919,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/CSS.html b/docs/iframes/CSS.html index 834827d..8fcd5dc 100644 --- a/docs/iframes/CSS.html +++ b/docs/iframes/CSS.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:16.078328 + 2026-03-27T15:17:31.027219 image/svg+xml @@ -1583,6 +1583,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1842,6 +1856,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1864,9 +1881,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1897,6 +1915,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/area-natural-disasters.html b/docs/iframes/area-natural-disasters.html index e358d8f..5ab69ec 100644 --- a/docs/iframes/area-natural-disasters.html +++ b/docs/iframes/area-natural-disasters.html @@ -61,7 +61,7 @@ - 2026-03-27T15:04:22.838319 + 2026-03-27T15:17:29.956186 image/svg+xml diff --git a/docs/iframes/bug.html b/docs/iframes/bug.html index a5fa8e4..17ebd32 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:13.131146 + 2026-03-27T15:17:26.650954 image/svg+xml @@ -1002,6 +1002,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1261,6 +1275,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1283,9 +1300,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1316,6 +1334,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/energy-consumption-france.html b/docs/iframes/energy-consumption-france.html index 8a5df6c..47939ee 100644 --- a/docs/iframes/energy-consumption-france.html +++ b/docs/iframes/energy-consumption-france.html @@ -61,7 +61,7 @@ - 2026-03-27T15:04:23.236113 + 2026-03-27T15:17:30.229360 image/svg+xml diff --git a/docs/iframes/javascript.html b/docs/iframes/javascript.html index 5f7cf67..e97e547 100644 --- a/docs/iframes/javascript.html +++ b/docs/iframes/javascript.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:16.868968 + 2026-03-27T15:17:31.742979 image/svg+xml @@ -1266,6 +1266,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1525,6 +1539,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1547,9 +1564,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1580,6 +1598,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/javascript2.html b/docs/iframes/javascript2.html index 7a7c25b..02f6b8e 100644 --- a/docs/iframes/javascript2.html +++ b/docs/iframes/javascript2.html @@ -68,7 +68,7 @@ - 2026-03-27T14:03:16.915233 + 2026-03-27T15:17:31.785615 image/svg+xml @@ -1337,6 +1337,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1596,6 +1610,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1618,9 +1635,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1651,6 +1669,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart.html b/docs/iframes/quickstart.html index 88719cd..df4dff6 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.485439 + 2026-03-27T15:17:24.471069 image/svg+xml @@ -1266,6 +1266,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1525,6 +1539,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1547,9 +1564,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1580,6 +1598,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart10.html b/docs/iframes/quickstart10.html index 9dfd3e9..681f279 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.786393 + 2026-03-27T15:17:26.308079 image/svg+xml @@ -1002,6 +1002,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1261,6 +1275,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1283,9 +1300,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1316,6 +1334,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart11.html b/docs/iframes/quickstart11.html index 2747185..e4f66e8 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.916453 + 2026-03-27T15:17:26.438056 image/svg+xml @@ -2420,6 +2420,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -2679,6 +2693,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -2701,9 +2718,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -2734,6 +2752,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart12.html b/docs/iframes/quickstart12.html index 5949bfd..7da1449 100644 --- a/docs/iframes/quickstart12.html +++ b/docs/iframes/quickstart12.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.837370 + 2026-03-27T15:17:24.795041 image/svg+xml @@ -719,6 +719,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -978,6 +992,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1000,9 +1017,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1033,6 +1051,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart13.html b/docs/iframes/quickstart13.html index ef8d7ca..2c690ae 100644 --- a/docs/iframes/quickstart13.html +++ b/docs/iframes/quickstart13.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.042453 + 2026-03-27T15:17:25.559529 image/svg+xml @@ -2889,6 +2889,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -3148,6 +3162,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -3170,9 +3187,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -3203,6 +3221,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 2b91016..52a21a1 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.506158 + 2026-03-27T15:17:24.494966 image/svg+xml @@ -1266,6 +1266,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1525,6 +1539,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1547,9 +1564,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1580,6 +1598,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart3.html b/docs/iframes/quickstart3.html index c410d83..0cda1d2 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.526675 + 2026-03-27T15:17:24.515804 image/svg+xml @@ -1266,6 +1266,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1525,6 +1539,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1547,9 +1564,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1580,6 +1598,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart4.html b/docs/iframes/quickstart4.html index 1e3ba7d..e60076b 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.554235 + 2026-03-27T15:17:24.542468 image/svg+xml @@ -1266,6 +1266,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1525,6 +1539,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1547,9 +1564,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1580,6 +1598,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart5.html b/docs/iframes/quickstart5.html index f00c3d8..4ed581c 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.580403 + 2026-03-27T15:17:24.568853 image/svg+xml @@ -1462,6 +1462,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1721,6 +1735,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1743,9 +1760,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1776,6 +1794,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart6.html b/docs/iframes/quickstart6.html index 0ed4d27..31e257c 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:11.753152 + 2026-03-27T15:17:24.716534 image/svg+xml @@ -1047,6 +1047,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1306,6 +1320,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1328,9 +1345,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1361,6 +1379,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart7.html b/docs/iframes/quickstart7.html index 8897204..4243919 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.268597 + 2026-03-27T15:17:25.791413 image/svg+xml @@ -1330,6 +1330,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -1589,6 +1603,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -1611,9 +1628,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -1644,6 +1662,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart8.html b/docs/iframes/quickstart8.html index 1a4dc5c..c797bb3 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.418184 + 2026-03-27T15:17:25.941743 image/svg+xml @@ -1751,6 +1751,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -2010,6 +2024,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -2032,9 +2049,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -2065,6 +2083,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/quickstart9.html b/docs/iframes/quickstart9.html index 2c6bfa1..27997ae 100644 --- a/docs/iframes/quickstart9.html +++ b/docs/iframes/quickstart9.html @@ -61,7 +61,7 @@ - 2026-03-27T14:03:12.577765 + 2026-03-27T15:17:26.099517 image/svg+xml @@ -1965,6 +1965,20 @@ return points; } + /** + * Find rectangle elements inside a given axes. + * + * @param {Selection} svg - Selection of the SVG element. + * @param {string} axes_class - ID of the axes group (e.g. "axes_1"). + * @returns {Selection} Selection of rectangle elements. + */ + findRectangles(svg, axes_class) { + let rectangles = svg.selectAll(`g#${axes_class} g[id^="QuadMesh"] path`); + rectangles.attr("class", "rectangle plot-element"); + console.log(`Found ${rectangles.size()} "rectangle" element`); + return rectangles; + } + /** * Find pie elements (`patch` paths) inside a given axes. * @@ -2224,6 +2238,9 @@ const lines = shouldProcess("line") ? plotParser.findLines(plotParser.svg, axes_class) : new Selection([]); + const rectangles = shouldProcess("rect") + ? plotParser.findRectangles(plotParser.svg, axes_class) + : new Selection([]); const pies = shouldProcess("pie") ? plotParser.findPies(plotParser.svg, axes_class) : new Selection([]); @@ -2246,9 +2263,10 @@ bars.size() + points.size() + areas.size() + + rectangles.size() + pies.size(); console.log( - `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies)`, + `PlotJS: Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas, ${pies.size()} pies, ${rectangles.size()} rectangles)`, ); if (points.size() > 0) { @@ -2279,6 +2297,20 @@ ); } + if (rectangles.size() > 0) { + plotParser.setHoverEffect( + rectangles, + axes_class, + tooltip_labels, + tooltip_groups, + show_tooltip, + hover_nearest, + ); + console.log( + `PlotJS: Hover effects attached to ${rectangles.size()} rectangles`, + ); + } + if (pies.size() > 0) { plotParser.setHoverEffect( pies, diff --git a/docs/iframes/random-walk-1.html b/docs/iframes/random-walk-1.html deleted file mode 100644 index b3e24c2..0000000 --- a/docs/iframes/random-walk-1.html +++ /dev/null @@ -1,39559 +0,0 @@ - - - - - Made with plotjs - - - - - - -
- - - - - - - - 2026-03-27T15:04:22.898323 - image/svg+xml - - - Matplotlib v3.10.3, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - diff --git a/docs/index.md b/docs/index.md index 78d207e..8a6fbc4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,6 +15,25 @@ interactive charts with minimum user inputs. You can: Consider that `plotjs` is still unstable. +## Installation + +- From PyPI (recommended): + +``` bash +pip install plotjs +``` + +- Latest dev version: + +``` bash +pip install git+https://github.com/y-sunflower/plotjs.git +``` + +- Python 3.10+ +- [matplotlib](https://matplotlib.org/), + [jinja2](https://jinja.palletsprojects.com/en/stable/) and + [narwhals](https://narwhals-dev.github.io/narwhals/) + ## Get started Matplotlib is **great**[^1]: you can draw anything with it. @@ -252,7 +271,7 @@ PlotJS(fig=fig).add_tooltip(labels=labels).save("iframes/quickstart12.html") -### Boxplot (seaborn) +### Boxplot ``` python from plotjs import PlotJS @@ -478,25 +497,6 @@ ax3.scatter(**args) -## Installation - -- From PyPI (recommended): - -``` bash -pip install plotjs -``` - -- Latest dev version: - -``` bash -pip install git+https://github.com/y-sunflower/plotjs.git -``` - -- Python 3.10+ -- [matplotlib](https://matplotlib.org/), - [jinja2](https://jinja.palletsprojects.com/en/stable/) and - [narwhals](https://narwhals-dev.github.io/narwhals/) - ## Important limitation ### Plotting order From 7afa1030ef6bd707a9cd1ca48469d1e7fde07129 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Fri, 27 Mar 2026 15:37:12 +0100 Subject: [PATCH 16/16] add warning when both labels and groups are None #68 --- plotjs/plotjs.py | 6 +++++- tests/test-python/test_plotjs.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/plotjs/plotjs.py b/plotjs/plotjs.py index ea4712a..92f5389 100644 --- a/plotjs/plotjs.py +++ b/plotjs/plotjs.py @@ -4,6 +4,7 @@ import uuid import webbrowser import tempfile +import warnings from typing import Optional import numpy as np @@ -160,6 +161,9 @@ def add_tooltip( ) ``` """ + if labels is None and groups is None: + warnings.warn("Either `labels` or `groups` must not be `None`.") + self._tooltip_x_shift = tooltip_x_shift self._tooltip_y_shift = tooltip_y_shift @@ -454,7 +458,7 @@ def show(self) -> "PlotJS": def _set_plot_data_json(self) -> None: if not hasattr(self, "_tooltip_labels"): if self._axes: - self.add_tooltip() + self.add_tooltip(labels=[]) else: self._tooltip_labels = [] self._tooltip_groups = [] diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index bbafcb6..ede9f39 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -389,7 +389,7 @@ def test_add_tooltip_without_axes_raises_clear_error(): with pytest.raises( ValueError, match=r"Cannot add tooltip because the figure has no Axes\." ): - PlotJS(fig=fig).add_tooltip() + PlotJS(fig=fig).add_tooltip(labels=[]) plt.close(fig) @@ -537,3 +537,22 @@ def test_pie_chart(): "on": None, } } + + +def test_warning_message_for_labels_and_groups(): + df = data.load_iris() + + fig, ax = plt.subplots() + ax.scatter( + df["sepal_length"], + df["sepal_width"], + c=df["species"].astype("category").cat.codes, + s=300, + alpha=0.5, + ec="black", + ) + + with pytest.warns( + UserWarning, match="Either `labels` or `groups` must not be `None`." + ): + PlotJS(fig=fig).add_tooltip()