Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 15 additions & 26 deletions .github/workflows/doc.yaml
Original file line number Diff line number Diff line change
@@ -1,41 +1,30 @@
name: Build and deploy doc

name: Deploy
on:
push:
branches: [main]
branches:
- main

permissions:
contents: write

concurrency:
group: gh-pages
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install zensical
- run: zensical build --clean

- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
with:
enable-cache: true

- name: Install the project
run: uv sync --all-groups

- name: Deploy MkDocs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: PYTHONPATH=$(pwd) uv run mkdocs gh-deploy --force
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: site
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ release.sh
node_modules
bun.lockb
.claude/
site/
File renamed without changes.
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

<img src="https://github.com/JosephBARBIERDARNAL/static/blob/main/python-libs/plotjs/image.png?raw=true" alt="plotjs logo" align="right" width="150px"/>

`plotjs` is a Python package that transform matplotlib plots into interactive charts with minimum user inputs. You can:
`plotjs` is a Python package that transform matplotlib plots into interactive charts with minimum user inputs. It's very easy to use and highly extensible! It lets you:

- control tooltip labels and grouping
- add CSS
- add JavaScript
- and many more

> Consider that the project is still **very unstable**.
> [!IMPORTANT]
> Consider that the project is still **unstable**.

[Online demo](https://y-sunflower.github.io/plotjs/)

Expand All @@ -28,3 +29,70 @@ Latest dev version:
```
pip install git+https://github.com/y-sunflower/plotjs.git
```

<br>

## Quickstart

`plotjs` mainly provides a `PlotJS` class

```python
import matplotlib.pyplot as plt
from plotjs import PlotJS, data

df = data.load_iris()

fig, ax = plt.subplots()
ax.scatter(
df["sepal_length"],
df["sepal_width"],
c=df["species"].astype("category").cat.codes,
s=180,
alpha=0.6,
ec="black",
)

(
PlotJS(fig)
.add_tooltip(labels=df["species"])
.save("iris-scatter.html")
)
```

![](./docs/img/quick-start-readme.png)

Open `iris-scatter.html` in your browser to get hover tooltips and default highlight/fade behavior.

<br>

## Why `plotjs`?

`plotjs` keeps your existing matplotlib workflow and adds interactivity on top of the SVG that matplotlib already knows how to generate. Instead of rebuilding the chart in another library, you keep the same `Figure`, export it to HTML, and control the browser-side behavior with CSS and JavaScript.

Learn more in the [Q&A](https://y-sunflower.github.io/plotjs/#qa).

<br>

## Features Overview

- Keep your existing matplotlib figure and export it as a standalone interactive HTML file
- Add hover tooltips from any iterable of labels
- Highlight related elements together with `groups=...`
- Restrict interactivity to specific element types with `on=...`
- Use direct hover or nearest-element hover with `hover_nearest=True`
- Add custom CSS with strings, dictionaries, or files
- Add custom JavaScript with strings or files, and optionally load D3.js
- Work with multiple matplotlib axes in the same figure
- Export either to disk with `save()` or to an HTML string with `as_html()`

<br>

## Documentation

- [Getting started](https://y-sunflower.github.io/plotjs/)
- [PlotJS API reference](https://y-sunflower.github.io/plotjs/reference/plotjs)
- [CSS guide](https://y-sunflower.github.io/plotjs/guides/css/)
- [JavaScript guide](https://y-sunflower.github.io/plotjs/guides/javascript/)
- [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)
2 changes: 1 addition & 1 deletion docs/developers/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ git checkout -b my-feature

```bash
uv sync --all-groups --dev
uv run pre-commit install
uv run prek install -f
uv pip install -e .
```

Expand Down
21 changes: 20 additions & 1 deletion docs/developers/svg-parser-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Provides basic DOM manipulation methods for working with SVG elements.</p>
<dt><a href="#getPointerPosition">getPointerPosition(event, svgElement)</a> ⇒ <code>Array.&lt;number&gt;</code></dt>
<dd><p>Get mouse position relative to an SVG element.</p>
</dd>
<dt><a href="#getFillValue">getFillValue(element)</a> ⇒ <code>string</code></dt>
<dd><p>Extract the raw fill value from an SVG element.</p>
</dd>
<dt><a href="#findBars">findBars(svg, axes_class)</a> ⇒ <code><a href="#Selection">Selection</a></code></dt>
<dd><p>Find bar elements (<code>patch</code> groups with clipping) inside a given axes.</p>
</dd>
Expand All @@ -29,7 +32,9 @@ and assigns <code>data-group</code> attributes based on tooltip groups.</p>
excluding axis grid lines.</p>
</dd>
<dt><a href="#findAreas">findAreas(svg, axes_class)</a> ⇒ <code><a href="#Selection">Selection</a></code></dt>
<dd><p>Find filled area elements (<code>FillBetweenPolyCollection</code> paths) inside a given axes.</p>
<dd><p>Find filled area elements (<code>FillBetweenPolyCollection</code> paths) inside a given axes.
Also includes legend swatches whose fill matches the plotted areas so legend hover
can target the same series as the chart area.</p>
</dd>
<dt><a href="#nearestElementFromMouse">nearestElementFromMouse(mouseX, mouseY, elements)</a> ⇒ <code>Element</code> | <code>null</code></dt>
<dd><p>Compute the nearest element to the mouse cursor from a set of elements.
Expand Down Expand Up @@ -74,6 +79,18 @@ Get mouse position relative to an SVG element.
| event | <code>MouseEvent</code> | The mouse event |
| svgElement | <code>Element</code> \| [<code>Selection</code>](#Selection) | The SVG element or Selection |

<a name="getFillValue"></a>

## getFillValue(element) ⇒ <code>string</code>
Extract the raw fill value from an SVG element.

**Kind**: global function
**Returns**: <code>string</code> - Normalized fill value, or empty string if absent.

| Param | Type | Description |
| --- | --- | --- |
| element | <code>Element</code> | SVG element to inspect. |

<a name="findBars"></a>

## findBars(svg, axes_class) ⇒ [<code>Selection</code>](#Selection)
Expand Down Expand Up @@ -121,6 +138,8 @@ excluding axis grid lines.

## findAreas(svg, axes_class) ⇒ [<code>Selection</code>](#Selection)
Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes.
Also includes legend swatches whose fill matches the plotted areas so legend hover
can target the same series as the chart area.

**Kind**: global function
**Returns**: [<code>Selection</code>](#Selection) - Selection of area elements.
Expand Down
42 changes: 39 additions & 3 deletions docs/iframes/CSS-2.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-02-02T14:52:23.438279</dc:date>
<dc:date>2026-03-15T20:57:18.692897</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
Expand Down Expand Up @@ -1518,6 +1518,23 @@
this.tooltip_y_shift = tooltip_y_shift;
}

/**
* Extract the raw fill value from an SVG element.
*
* @param {Element} element - SVG element to inspect.
* @returns {string} Normalized fill value, or empty string if absent.
*/
getFillValue(element) {
const style = element?.getAttribute("style") ?? "";
const styleMatch = style.match(/(?:^|;)\s*fill:\s*([^;]+)/i);

if (styleMatch) {
return styleMatch[1].trim().toLowerCase();
}

return (element?.getAttribute("fill") ?? "").trim().toLowerCase();
}

/**
* Find bar elements (`patch` groups with clipping) inside a given axes.
*
Expand Down Expand Up @@ -1594,16 +1611,35 @@

/**
* Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes.
* Also includes legend swatches whose fill matches the plotted areas so legend hover
* can target the same series as the chart area.
*
* @param {Selection} svg - Selection of the SVG element.
* @param {string} axes_class - ID of the axes group.
* @returns {Selection} Selection of area elements.
*/
findAreas(svg, axes_class) {
// select all <path> of FillBetweenPolyCollection elements within the specific axes
const areas = svg.selectAll(
const parser = this;
const plottedAreas = svg.selectAll(
`g#${axes_class} g[id^="FillBetweenPolyCollection"] path`,
);
const areaFills = new Set(
plottedAreas
.nodes()
.map((element) => this.getFillValue(element))
.filter((fill) => fill && fill !== "none"),
);

const legendAreas = svg
.selectAll(`g#${axes_class} g[id^="legend"] g[id^="patch"] path`)
.filter(function () {
return areaFills.size > 0 && areaFills.has(parser.getFillValue(this));
});

const areas = new Selection([
...plottedAreas.nodes(),
...legendAreas.nodes(),
]);
areas.attr("class", "area plot-element");

console.log(`Found ${areas.size()} "area" element`);
Expand Down
42 changes: 39 additions & 3 deletions docs/iframes/CSS.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<cc:Work>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:date>2026-02-02T14:52:23.343865</dc:date>
<dc:date>2026-03-15T20:57:18.640742</dc:date>
<dc:format>image/svg+xml</dc:format>
<dc:creator>
<cc:Agent>
Expand Down Expand Up @@ -1514,6 +1514,23 @@
this.tooltip_y_shift = tooltip_y_shift;
}

/**
* Extract the raw fill value from an SVG element.
*
* @param {Element} element - SVG element to inspect.
* @returns {string} Normalized fill value, or empty string if absent.
*/
getFillValue(element) {
const style = element?.getAttribute("style") ?? "";
const styleMatch = style.match(/(?:^|;)\s*fill:\s*([^;]+)/i);

if (styleMatch) {
return styleMatch[1].trim().toLowerCase();
}

return (element?.getAttribute("fill") ?? "").trim().toLowerCase();
}

/**
* Find bar elements (`patch` groups with clipping) inside a given axes.
*
Expand Down Expand Up @@ -1590,16 +1607,35 @@

/**
* Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes.
* Also includes legend swatches whose fill matches the plotted areas so legend hover
* can target the same series as the chart area.
*
* @param {Selection} svg - Selection of the SVG element.
* @param {string} axes_class - ID of the axes group.
* @returns {Selection} Selection of area elements.
*/
findAreas(svg, axes_class) {
// select all <path> of FillBetweenPolyCollection elements within the specific axes
const areas = svg.selectAll(
const parser = this;
const plottedAreas = svg.selectAll(
`g#${axes_class} g[id^="FillBetweenPolyCollection"] path`,
);
const areaFills = new Set(
plottedAreas
.nodes()
.map((element) => this.getFillValue(element))
.filter((fill) => fill && fill !== "none"),
);

const legendAreas = svg
.selectAll(`g#${axes_class} g[id^="legend"] g[id^="patch"] path`)
.filter(function () {
return areaFills.size > 0 && areaFills.has(parser.getFillValue(this));
});

const areas = new Selection([
...plottedAreas.nodes(),
...legendAreas.nodes(),
]);
areas.attr("class", "area plot-element");

console.log(`Found ${areas.size()} "area" element`);
Expand Down
Loading
Loading