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/docs/developers/svg-parser-reference.md b/docs/developers/svg-parser-reference.md index 9ebc6fe..2686b42 100644 --- a/docs/developers/svg-parser-reference.md +++ b/docs/developers/svg-parser-reference.md @@ -27,6 +27,12 @@ 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.

+
findLines(svg, axes_class)Selection

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

@@ -120,6 +126,32 @@ 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) +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) diff --git a/docs/gallery/index.md b/docs/gallery/index.md index de90d31..20d6c30 100644 --- a/docs/gallery/index.md +++ b/docs/gallery/index.md @@ -54,10 +54,22 @@ This page contains all the `plotjs` examples from this website.
+ + +
+ +
+ + + @@ -66,11 +78,11 @@ This page contains all the `plotjs` examples from this website.
- - @@ -78,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 2262d4e..179ca87 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) @@ -192,85 +194,69 @@ def remove_agg_rows(entity: str): ) ).add_tooltip(labels=columns).save("docs/iframes/area-natural-disasters.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) -size = 10000 +cmap = mcolors.LinearSegmentedColormap.from_list("", ["#2a9d8f", "#e9c46a", "#e76f51"]) -labels = ["S&P500", "CAC40", "Bitcoin", "Livret A", "Default"] -groups = ["safe", "safe", "safe", "not safe", "not safe"] +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", + ) -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], +ax.text( + 0, + 12.4, + "Consommation d'énergie par habitant", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -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], +ax.text( + 0, + -0.2, + "2011", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], +ax.text( + 10, + -0.2, + "2021", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -axs[1].legend() - +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, groups=labels, ax=axs[0]) - .add_tooltip(labels=labels, groups=labels, ax=axs[1]) - .save("docs/iframes/random-walk-1.html") + .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..184af53 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,90 +202,73 @@ PlotJS(fig, bbox_inches="tight").add_css( -## Random walks +## Energy consumption in France ```python -import numpy as np -import matplotlib.pyplot as plt -from plotjs import PlotJS +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) -size = 10000 +cmap = mcolors.LinearSegmentedColormap.from_list("", ["#2a9d8f", "#e9c46a", "#e76f51"]) -labels = ["S&P500", "CAC40", "Bitcoin", "Livret A", "Default"] -groups = ["safe", "safe", "safe", "not safe", "not safe"] +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", + ) -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], +ax.text( + 0, + 12.4, + "Consommation d'énergie par habitant", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -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], +ax.text( + 0, + -0.2, + "2011", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -axs[1].plot( - np.cumsum(np.random.choice([-1, 1], size=size)), - linewidth=5, - color="#14213d", - label=labels[4], +ax.text( + 10, + -0.2, + "2021", + ha="left", + va="center", + fontsize=12, + fontweight="light", ) -axs[1].legend() - +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, groups=labels, ax=axs[0]) - .add_tooltip(labels=labels, groups=labels, ax=axs[1]) - .save("docs/iframes/random-walk-1.html") + .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/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/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/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/CSS-2.html b/docs/iframes/CSS-2.html index 25a5efa..42e2ad4 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-27T15:17:31.081686 image/svg+xml @@ -1587,6 +1587,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1860,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1822,9 +1881,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1855,6 +1919,34 @@ ); } + 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, + 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..8fcd5dc 100644 --- a/docs/iframes/CSS.html +++ b/docs/iframes/CSS.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:18.640742 + 2026-03-27T15:17:31.027219 image/svg+xml @@ -1583,6 +1583,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1856,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1818,9 +1877,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1851,6 +1915,34 @@ ); } + 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, + 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..5ab69ec 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-27T15:17:29.956186 image/svg+xml @@ -3891,6 +3891,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +4164,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -4126,9 +4185,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -4159,6 +4223,34 @@ ); } + 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, + 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 65014ce..17ebd32 100644 --- a/docs/iframes/bug.html +++ b/docs/iframes/bug.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:15.050060 + 2026-03-27T15:17:26.650954 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,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1229,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", "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"]; @@ -1222,6 +1275,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1237,9 +1296,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1270,6 +1334,34 @@ ); } + 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, + 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/energy-consumption-france.html b/docs/iframes/energy-consumption-france.html new file mode 100644 index 0000000..47939ee --- /dev/null +++ b/docs/iframes/energy-consumption-france.html @@ -0,0 +1,2702 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-27T15:17:30.229360 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/javascript.html b/docs/iframes/javascript.html index 93738ec..e97e547 100644 --- a/docs/iframes/javascript.html +++ b/docs/iframes/javascript.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:19.351940 + 2026-03-27T15:17:31.742979 image/svg+xml @@ -1266,6 +1266,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1539,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1560,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1534,6 +1598,34 @@ ); } + 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, + 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..02f6b8e 100644 --- a/docs/iframes/javascript2.html +++ b/docs/iframes/javascript2.html @@ -68,7 +68,7 @@ - 2026-03-15T20:57:19.393021 + 2026-03-27T15:17:31.785615 image/svg+xml @@ -1337,6 +1337,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1610,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1572,9 +1631,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1605,6 +1669,34 @@ ); } + 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, + 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 8ae12fe..df4dff6 100644 --- a/docs/iframes/quickstart.html +++ b/docs/iframes/quickstart.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.886877 + 2026-03-27T15:17:24.471069 image/svg+xml @@ -1266,6 +1266,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1539,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1560,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1534,6 +1598,34 @@ ); } + 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, + 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 7860a77..681f279 100644 --- a/docs/iframes/quickstart10.html +++ b/docs/iframes/quickstart10.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.708540 + 2026-03-27T15:17:26.308079 image/svg+xml @@ -1002,6 +1002,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1275,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1237,9 +1296,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1270,6 +1334,34 @@ ); } + 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, + 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 4600c67..e4f66e8 100644 --- a/docs/iframes/quickstart11.html +++ b/docs/iframes/quickstart11.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.836735 + 2026-03-27T15:17:26.438056 image/svg+xml @@ -2420,6 +2420,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +2693,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -2655,9 +2714,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -2688,6 +2752,34 @@ ); } + 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, + 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 new file mode 100644 index 0000000..7da1449 --- /dev/null +++ b/docs/iframes/quickstart12.html @@ -0,0 +1,1123 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-27T15:17:24.795041 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/quickstart13.html b/docs/iframes/quickstart13.html new file mode 100644 index 0000000..2c690ae --- /dev/null +++ b/docs/iframes/quickstart13.html @@ -0,0 +1,3293 @@ + + + + + Made with plotjs + + + + + + +
+ + + + + + + + 2026-03-27T15:17:25.559529 + image/svg+xml + + + Matplotlib v3.10.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/docs/iframes/quickstart2.html b/docs/iframes/quickstart2.html index 166ffd4..52a21a1 100644 --- a/docs/iframes/quickstart2.html +++ b/docs/iframes/quickstart2.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.909929 + 2026-03-27T15:17:24.494966 image/svg+xml @@ -1266,6 +1266,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1539,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1560,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1534,6 +1598,34 @@ ); } + 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, + 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 efd64bf..0cda1d2 100644 --- a/docs/iframes/quickstart3.html +++ b/docs/iframes/quickstart3.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.929348 + 2026-03-27T15:17:24.515804 image/svg+xml @@ -1266,6 +1266,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1539,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1560,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1534,6 +1598,34 @@ ); } + 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, + 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 ac933f9..e60076b 100644 --- a/docs/iframes/quickstart4.html +++ b/docs/iframes/quickstart4.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.954602 + 2026-03-27T15:17:24.542468 image/svg+xml @@ -1266,6 +1266,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1539,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1501,9 +1560,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1534,6 +1598,34 @@ ); } + 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, + 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 deb522a..4ed581c 100644 --- a/docs/iframes/quickstart5.html +++ b/docs/iframes/quickstart5.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:13.979453 + 2026-03-27T15:17:24.568853 image/svg+xml @@ -343,12 +343,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - +
- - + + - + @@ -367,41 +367,40 @@ - + - - - - - + + + - + - - - + + + + - + - - - + + + @@ -409,759 +408,794 @@ - + - - - + + + - - - @@ -1428,6 +1462,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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. @@ -1648,6 +1735,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1663,9 +1756,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1696,6 +1794,34 @@ ); } + 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, + 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 bd9c5c0..31e257c 100644 --- a/docs/iframes/quickstart6.html +++ b/docs/iframes/quickstart6.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.119874 + 2026-03-27T15:17:24.716534 image/svg+xml @@ -1047,6 +1047,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1320,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1282,9 +1341,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1315,6 +1379,34 @@ ); } + 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, + 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 a645be7..4243919 100644 --- a/docs/iframes/quickstart7.html +++ b/docs/iframes/quickstart7.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.198616 + 2026-03-27T15:17:25.791413 image/svg+xml @@ -1330,6 +1330,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +1603,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -1565,9 +1624,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -1598,6 +1662,34 @@ ); } + 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, + 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 535bce9..c797bb3 100644 --- a/docs/iframes/quickstart8.html +++ b/docs/iframes/quickstart8.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.344118 + 2026-03-27T15:17:25.941743 image/svg+xml @@ -343,12 +343,12 @@ " style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + @@ -367,40 +367,41 @@ - + - - - + + + + + - + - - - - + + + - + - - - + + + @@ -408,794 +409,754 @@ - + - - - + + + - - - @@ -1220,27 +1181,27 @@ - - - + - - + - - + 0) { @@ -2058,6 +2083,34 @@ ); } + 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, + 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 1d85252..27997ae 100644 --- a/docs/iframes/quickstart9.html +++ b/docs/iframes/quickstart9.html @@ -61,7 +61,7 @@ - 2026-03-15T20:57:14.503003 + 2026-03-27T15:17:26.099517 image/svg+xml @@ -1965,6 +1965,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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 +2238,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -2200,9 +2259,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -2233,6 +2297,34 @@ ); } + 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, + 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 deleted file mode 100644 index e82f213..0000000 --- a/docs/iframes/random-walk-1.html +++ /dev/null @@ -1,39233 +0,0 @@ - - - - - Made with plotjs - - - - - - -
- - - - - - - - 2026-03-15T20:57:17.902450 - image/svg+xml - - - Matplotlib v3.10.3, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - diff --git a/docs/index.md b/docs/index.md index 27e7a7f..8a6fbc4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,26 @@ 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. + +## 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 @@ -227,6 +246,67 @@ 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") +``` + + + +### Boxplot + +``` 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 @@ -267,8 +347,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)) @@ -419,27 +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 -``` - -## Dependencies - -- 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/docs/index.qmd b/docs/index.qmd index 8ac3f99..528ce81 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,24 @@ plt.rcParams["figure.dpi"] = 300 ???+ warning - `plotjs` is in very early stage: expect regular breaking changes. + 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 @@ -213,6 +232,63 @@ 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") +``` + + + +### Boxplot + +```{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 @@ -251,8 +327,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)) @@ -391,26 +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 -``` - -## Dependencies - -- 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/plotjs.py b/plotjs/plotjs.py index 0d105dc..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 @@ -29,7 +30,7 @@ class PlotJS: """ - Class to convert static matplotlib plots to interactive charts. + Main class to convert static matplotlib plots to interactive charts. """ def __init__( @@ -118,8 +119,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: @@ -160,16 +161,20 @@ 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 # 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: @@ -341,7 +346,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...'));" ) ) ``` @@ -426,7 +431,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: @@ -453,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/plotjs/static/plotparser.js b/plotjs/static/plotparser.js index 0502872..8c9736b 100755 --- a/plotjs/static/plotparser.js +++ b/plotjs/static/plotparser.js @@ -229,6 +229,59 @@ 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. + * + * @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) { + 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") ?? ""; + + 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" && + /[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. diff --git a/plotjs/static/template.html b/plotjs/static/template.html index ec071c0..c7ea6d7 100644 --- a/plotjs/static/template.html +++ b/plotjs/static/template.html @@ -77,6 +77,12 @@ 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([]); const bars = shouldProcess("bar") ? plotParser.findBars(plotParser.svg, axes_class) : new Selection([]); @@ -92,9 +98,14 @@ : new Selection([]); const totalElements = - lines.size() + bars.size() + points.size() + areas.size(); + lines.size() + + 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)`, + `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) { @@ -125,6 +136,34 @@ ); } + 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, + 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/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/pyproject.toml b/pyproject.toml index 5ebdca4..107e1f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,75 +1,85 @@ [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, and export them to HTML files" +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", + "pywaffle>=1.1.1", + "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 } 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-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(` diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index 5ef595a..ede9f39 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -1,10 +1,12 @@ -from plotjs import PlotJS, data +import numpy as np import matplotlib.pyplot as plt import os import tempfile from unittest.mock import patch import pytest +from plotjs import PlotJS, data + def test_add_css_method_chaining(): df = data.load_iris() @@ -387,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) @@ -407,3 +409,150 @@ 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).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"], + } + } + + +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, + } + } + + +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).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, + } + } + + +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() 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" 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",