plotjs 0.0.6__tar.gz → 0.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotjs-0.0.7/.github/workflows/tests-browser.yaml +42 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/tests-python.yaml +2 -2
- {plotjs-0.0.6 → plotjs-0.0.7}/AGENTS.md +2 -6
- {plotjs-0.0.6/plotjs.egg-info → plotjs-0.0.7}/PKG-INFO +1 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/package.json +0 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/__init__.py +1 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/main.py +1 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/static/plotparser.js +155 -22
- plotjs-0.0.7/plotjs/static/template.html +155 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/utils.py +12 -1
- {plotjs-0.0.6 → plotjs-0.0.7/plotjs.egg-info}/PKG-INFO +1 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/SOURCES.txt +7 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/pyproject.toml +2 -1
- plotjs-0.0.7/pytest.ini +7 -0
- plotjs-0.0.7/tests/test-browser/README.md +120 -0
- plotjs-0.0.7/tests/test-browser/conftest.py +60 -0
- plotjs-0.0.7/tests/test-browser/test_interactions.py +297 -0
- plotjs-0.0.7/tests/test-browser/test_rendering.py +165 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-javascript/ParserSelectors.test.js +9 -10
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-javascript/ParserSetHover.test.js +18 -12
- plotjs-0.0.7/tests/test-python/__init__.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_main.py +1 -1
- {plotjs-0.0.6 → plotjs-0.0.7}/uv.lock +94 -1
- plotjs-0.0.6/plotjs/static/template.html +0 -103
- {plotjs-0.0.6 → plotjs-0.0.7}/.coverage +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.gitattributes +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/doc.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/lint.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/pypi.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/tests-js.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/type.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.gitignore +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/.pre-commit-config.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/LICENSE +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/README.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/bun.lock +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/coverage-badge.svg +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/contributing.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/overview.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/parsing-matplotlib-svg.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/svg-parser-reference.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/gallery/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/gallery/index.qmd +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/advanced/advanced.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/advanced/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/css/CSS.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/css/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/embed-graphs/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/javascript/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/javascript/javascript.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/troubleshooting/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/CSS-2.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/CSS.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/area-natural-disasters.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/bug.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/javascript.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/javascript2.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart10.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart11.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart2.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart3.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart4.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart5.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart6.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart7.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart8.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart9.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/random-walk-1.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/how-it-works-1.png +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/how-it-works-2.png +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/overview.png +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/index.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/index.qmd +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/index_files/figure-commonmark/cell-3-output-1.png +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/css.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/datasets.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/javascript.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/plotjs.md +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/static/style.css +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/docs/stylesheets/style.css +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/justfile +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/mkdocs.yaml +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/overrides/partials/footer.html +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/css.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/__init__.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/datasets.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/iris.csv +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/mtcars.csv +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/titanic.csv +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/javascript.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/static/default.css +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/dependency_links.txt +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/requires.txt +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/top_level.txt +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/setup.cfg +0 -0
- {plotjs-0.0.6/tests/test-python → plotjs-0.0.7/tests/test-browser}/__init__.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/script.js +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/script2.js +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/style-invalid.css +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/style.css +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_css.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_data.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_js.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_other_utils.py +0 -0
- {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_plotjs.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Tests Browser
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
browser-tests:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.13"]
|
|
15
|
+
env:
|
|
16
|
+
UV_PYTHON: ${{ matrix.python-version }}
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Install the project
|
|
27
|
+
run: uv sync --all-groups
|
|
28
|
+
|
|
29
|
+
- name: Install Playwright browsers
|
|
30
|
+
run: uv run playwright install --with-deps chromium
|
|
31
|
+
|
|
32
|
+
- name: Run browser tests
|
|
33
|
+
run: uv run pytest tests/test-browser/ -v
|
|
34
|
+
|
|
35
|
+
- name: Upload test artifacts on failure
|
|
36
|
+
if: failure()
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: browser-test-artifacts-${{ matrix.python-version }}
|
|
40
|
+
path: |
|
|
41
|
+
/tmp/pytest-of-*/
|
|
42
|
+
retention-days: 7
|
|
@@ -9,7 +9,7 @@ PlotJS is a Python package that transforms static matplotlib charts into interac
|
|
|
9
9
|
## Quick Architecture
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
Matplotlib Figure → SVG Export (Python) → HTML Template (Jinja2) → Interactive Browser
|
|
12
|
+
Matplotlib Figure → SVG Export (Python) → HTML Template (Jinja2) → Interactive Browser
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
### Workflow:
|
|
@@ -53,7 +53,7 @@ Matplotlib Figure → SVG Export (Python) → HTML Template (Jinja2) → Interac
|
|
|
53
53
|
**`template.html`** - Jinja2 template structure
|
|
54
54
|
|
|
55
55
|
- Injects: `{{ svg }}`, `{{ default_css }}`, `{{ additional_css }}`, `{{ js_parser }}`, `{{ plot_data_json }}`
|
|
56
|
-
- Creates tooltip container and
|
|
56
|
+
- Creates tooltip container and event handling
|
|
57
57
|
|
|
58
58
|
**`plotparser.js`** - `PlotSVGParser` JavaScript class
|
|
59
59
|
|
|
@@ -156,10 +156,6 @@ docs/ # Comprehensive documentation
|
|
|
156
156
|
- jinja2 >= 3.0.0 (HTML templating)
|
|
157
157
|
- narwhals >= 2.0.0 (dataframe abstraction)
|
|
158
158
|
|
|
159
|
-
**JavaScript (via CDN):**
|
|
160
|
-
|
|
161
|
-
- d3 v7.9.0 (DOM manipulation via d3-selection)
|
|
162
|
-
|
|
163
159
|
**Python Version:** Requires 3.10+
|
|
164
160
|
|
|
165
161
|
## Critical Patterns & Limitations
|
|
@@ -1,4 +1,136 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Selection wrapper that mimics d3-selection's chainable API.
|
|
3
|
+
* Provides basic DOM manipulation methods for working with SVG elements.
|
|
4
|
+
*/
|
|
5
|
+
class Selection {
|
|
6
|
+
constructor(elements) {
|
|
7
|
+
this.elements = Array.isArray(elements) ? elements : [elements];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
select(selector) {
|
|
11
|
+
const first = this.elements[0];
|
|
12
|
+
return first
|
|
13
|
+
? new Selection(first.querySelector(selector))
|
|
14
|
+
: new Selection([]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
selectAll(selector) {
|
|
18
|
+
const matched = [];
|
|
19
|
+
this.elements.forEach((el) => {
|
|
20
|
+
if (el && el.querySelectorAll) {
|
|
21
|
+
matched.push(...el.querySelectorAll(selector));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return new Selection(matched);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
attr(name, value) {
|
|
28
|
+
if (arguments.length === 1) {
|
|
29
|
+
return this.elements[0]?.getAttribute(name);
|
|
30
|
+
}
|
|
31
|
+
this.elements.forEach((el) => el?.setAttribute(name, value));
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
classed(name, add) {
|
|
36
|
+
if (arguments.length === 1) {
|
|
37
|
+
return this.elements[0]?.classList.contains(name);
|
|
38
|
+
}
|
|
39
|
+
this.elements.forEach((el) => {
|
|
40
|
+
if (add) el?.classList.add(name);
|
|
41
|
+
else el?.classList.remove(name);
|
|
42
|
+
});
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
style(name, value) {
|
|
47
|
+
if (arguments.length === 0) {
|
|
48
|
+
return this.elements[0]?.style[name];
|
|
49
|
+
}
|
|
50
|
+
this.elements.forEach((el) => {
|
|
51
|
+
if (el) el.style[name] = value;
|
|
52
|
+
});
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
html(content) {
|
|
57
|
+
if (arguments.length === 0) {
|
|
58
|
+
return this.elements[0]?.innerHTML;
|
|
59
|
+
}
|
|
60
|
+
this.elements.forEach((el) => {
|
|
61
|
+
if (el) el.innerHTML = content;
|
|
62
|
+
});
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
on(event, handler) {
|
|
67
|
+
this.elements.forEach((el) => {
|
|
68
|
+
if (el) el.addEventListener(event, handler);
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
filter(predicate) {
|
|
74
|
+
const filtered = this.elements.filter((el, i) =>
|
|
75
|
+
predicate.call(el, null, i),
|
|
76
|
+
);
|
|
77
|
+
return new Selection(filtered);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
each(callback) {
|
|
81
|
+
this.elements.forEach((el, i) => {
|
|
82
|
+
callback.call(el, null, i);
|
|
83
|
+
});
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
nodes() {
|
|
88
|
+
return this.elements;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
size() {
|
|
92
|
+
return this.elements.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
empty() {
|
|
96
|
+
return this.elements.length === 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a Selection from a DOM element or selector string.
|
|
102
|
+
*
|
|
103
|
+
* @param {string|Element} selector - CSS selector string or DOM element
|
|
104
|
+
* @returns {Selection} New Selection instance
|
|
105
|
+
*/
|
|
106
|
+
function select(selector) {
|
|
107
|
+
const element =
|
|
108
|
+
typeof selector === "string" ? document.querySelector(selector) : selector;
|
|
109
|
+
return new Selection(element ? [element] : []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get mouse position relative to an SVG element.
|
|
114
|
+
*
|
|
115
|
+
* @param {MouseEvent} event - The mouse event
|
|
116
|
+
* @param {Element|Selection} svgElement - The SVG element or Selection
|
|
117
|
+
* @returns {number[]} [x, y] coordinates relative to the SVG
|
|
118
|
+
*/
|
|
119
|
+
function getPointerPosition(event, svgElement) {
|
|
120
|
+
const svg =
|
|
121
|
+
svgElement instanceof Selection ? svgElement.nodes()[0] : svgElement;
|
|
122
|
+
|
|
123
|
+
if (svg && svg.createSVGPoint) {
|
|
124
|
+
const point = svg.createSVGPoint();
|
|
125
|
+
point.x = event.clientX;
|
|
126
|
+
point.y = event.clientY;
|
|
127
|
+
const transformed = point.matrixTransform(svg.getScreenCTM().inverse());
|
|
128
|
+
return [transformed.x, transformed.y];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const rect = svg.getBoundingClientRect();
|
|
132
|
+
return [event.clientX - rect.left, event.clientY - rect.top];
|
|
133
|
+
}
|
|
2
134
|
|
|
3
135
|
/**
|
|
4
136
|
* Core utility for parsing and interacting with matplotlib-generated SVG outputs.
|
|
@@ -16,14 +148,14 @@ export default class PlotSVGParser {
|
|
|
16
148
|
/**
|
|
17
149
|
* Create a new parser bound to an SVG figure.
|
|
18
150
|
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
151
|
+
* @param {Element|Selection} svg - The target SVG element or Selection (e.g. the entire plot).
|
|
152
|
+
* @param {Element|Selection} tooltip - The tooltip container element or Selection (e.g. a div).
|
|
21
153
|
* @param {number} tooltip_x_shift - Horizontal offset for tooltip positioning.
|
|
22
154
|
* @param {number} tooltip_y_shift - Vertical offset for tooltip positioning.
|
|
23
155
|
*/
|
|
24
156
|
constructor(svg, tooltip, tooltip_x_shift, tooltip_y_shift) {
|
|
25
|
-
this.svg = svg;
|
|
26
|
-
this.tooltip = tooltip;
|
|
157
|
+
this.svg = svg instanceof Selection ? svg : select(svg);
|
|
158
|
+
this.tooltip = tooltip instanceof Selection ? tooltip : select(tooltip);
|
|
27
159
|
this.tooltip_x_shift = tooltip_x_shift;
|
|
28
160
|
this.tooltip_y_shift = tooltip_y_shift;
|
|
29
161
|
}
|
|
@@ -31,16 +163,16 @@ export default class PlotSVGParser {
|
|
|
31
163
|
/**
|
|
32
164
|
* Find bar elements (`patch` groups with clipping) inside a given axes.
|
|
33
165
|
*
|
|
34
|
-
* @param {
|
|
166
|
+
* @param {Selection} svg - Selection of the SVG element.
|
|
35
167
|
* @param {string} axes_class - ID of the axes group (e.g. "axes_1").
|
|
36
|
-
* @returns {
|
|
168
|
+
* @returns {Selection} Selection of bar elements.
|
|
37
169
|
*/
|
|
38
170
|
findBars(svg, axes_class) {
|
|
39
171
|
// select all #patch within the specific axes
|
|
40
172
|
const bars = svg
|
|
41
173
|
.selectAll(`g#${axes_class} g[id^="patch"]`)
|
|
42
174
|
.filter(function () {
|
|
43
|
-
const path =
|
|
175
|
+
const path = select(this).select("path");
|
|
44
176
|
// that have a clip-path attribute
|
|
45
177
|
const clip = path.attr("clip-path");
|
|
46
178
|
// starting with "url("
|
|
@@ -58,14 +190,14 @@ export default class PlotSVGParser {
|
|
|
58
190
|
* Handles both `<use>` and `<path>` fallback cases,
|
|
59
191
|
* and assigns `data-group` attributes based on tooltip groups.
|
|
60
192
|
*
|
|
61
|
-
* @param {
|
|
193
|
+
* @param {Selection} svg - Selection of the SVG element.
|
|
62
194
|
* @param {string} axes_class - ID of the axes group (e.g. "axes_1").
|
|
63
195
|
* @param {string[]} tooltip_groups - Group identifiers for tooltips, parallel to points.
|
|
64
|
-
* @returns {
|
|
196
|
+
* @returns {Selection} Selection of point elements.
|
|
65
197
|
*/
|
|
66
198
|
findPoints(svg, axes_class, tooltip_groups) {
|
|
67
199
|
let points = svg.selectAll(
|
|
68
|
-
`g#${axes_class} g[id^="PathCollection"] g[clip-path] use
|
|
200
|
+
`g#${axes_class} g[id^="PathCollection"] g[clip-path] use`,
|
|
69
201
|
);
|
|
70
202
|
|
|
71
203
|
if (points.empty()) {
|
|
@@ -74,7 +206,7 @@ export default class PlotSVGParser {
|
|
|
74
206
|
}
|
|
75
207
|
|
|
76
208
|
points.each(function (_, i) {
|
|
77
|
-
|
|
209
|
+
select(this).attr("data-group", tooltip_groups[i]);
|
|
78
210
|
});
|
|
79
211
|
points.attr("class", "point plot-element");
|
|
80
212
|
|
|
@@ -86,9 +218,9 @@ export default class PlotSVGParser {
|
|
|
86
218
|
* Find line elements (`line2d` paths) inside a given axes,
|
|
87
219
|
* excluding axis grid lines.
|
|
88
220
|
*
|
|
89
|
-
* @param {
|
|
221
|
+
* @param {Selection} svg - Selection of the SVG element.
|
|
90
222
|
* @param {string} axes_class - ID of the axes group.
|
|
91
|
-
* @returns {
|
|
223
|
+
* @returns {Selection} Selection of line elements.
|
|
92
224
|
*/
|
|
93
225
|
findLines(svg, axes_class) {
|
|
94
226
|
// select all <path> of Line2D elements within the specific axes
|
|
@@ -107,14 +239,14 @@ export default class PlotSVGParser {
|
|
|
107
239
|
/**
|
|
108
240
|
* Find filled area elements (`FillBetweenPolyCollection` paths) inside a given axes.
|
|
109
241
|
*
|
|
110
|
-
* @param {
|
|
242
|
+
* @param {Selection} svg - Selection of the SVG element.
|
|
111
243
|
* @param {string} axes_class - ID of the axes group.
|
|
112
|
-
* @returns {
|
|
244
|
+
* @returns {Selection} Selection of area elements.
|
|
113
245
|
*/
|
|
114
246
|
findAreas(svg, axes_class) {
|
|
115
247
|
// select all <path> of FillBetweenPolyCollection elements within the specific axes
|
|
116
248
|
const areas = svg.selectAll(
|
|
117
|
-
`g#${axes_class} g[id^="FillBetweenPolyCollection"] path
|
|
249
|
+
`g#${axes_class} g[id^="FillBetweenPolyCollection"] path`,
|
|
118
250
|
);
|
|
119
251
|
areas.attr("class", "area plot-element");
|
|
120
252
|
|
|
@@ -129,7 +261,7 @@ export default class PlotSVGParser {
|
|
|
129
261
|
*
|
|
130
262
|
* @param {number} mouseX - X coordinate of the mouse relative to SVG.
|
|
131
263
|
* @param {number} mouseY - Y coordinate of the mouse relative to SVG.
|
|
132
|
-
* @param {
|
|
264
|
+
* @param {Selection} elements - Selection of candidate elements.
|
|
133
265
|
* @returns {Element|null} The nearest DOM element or `null`.
|
|
134
266
|
*/
|
|
135
267
|
nearestElementFromMouse(mouseX, mouseY, elements) {
|
|
@@ -154,7 +286,7 @@ export default class PlotSVGParser {
|
|
|
154
286
|
* Attach hover interaction and tooltip display to plot elements.
|
|
155
287
|
* Can highlight nearest element (if enabled) or hovered element directly.
|
|
156
288
|
*
|
|
157
|
-
* @param {
|
|
289
|
+
* @param {Selection} plot_element - Selection of plot elements (points, lines, etc.).
|
|
158
290
|
* @param {string} axes_class - ID of the axes group.
|
|
159
291
|
* @param {string[]} tooltip_labels - Tooltip labels for each element.
|
|
160
292
|
* @param {string[]} tooltip_groups - Group identifiers for each element.
|
|
@@ -167,18 +299,19 @@ export default class PlotSVGParser {
|
|
|
167
299
|
tooltip_labels,
|
|
168
300
|
tooltip_groups,
|
|
169
301
|
show_tooltip,
|
|
170
|
-
hover_nearest
|
|
302
|
+
hover_nearest,
|
|
171
303
|
) {
|
|
172
304
|
const self = this;
|
|
173
305
|
const axesGroup = this.svg.select(`g#${axes_class}`);
|
|
174
306
|
const getHoveredIndex = hover_nearest
|
|
175
307
|
? (event) => {
|
|
176
|
-
const
|
|
308
|
+
const svgNode = self.svg.nodes()[0];
|
|
309
|
+
const [mouseX, mouseY] = getPointerPosition(event, svgNode);
|
|
177
310
|
const allElements = axesGroup.selectAll(".plot-element");
|
|
178
311
|
const nearestElem = self.nearestElementFromMouse(
|
|
179
312
|
mouseX,
|
|
180
313
|
mouseY,
|
|
181
|
-
allElements
|
|
314
|
+
allElements,
|
|
182
315
|
);
|
|
183
316
|
return nearestElem ? allElements.nodes().indexOf(nearestElem) : null;
|
|
184
317
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>{{ document_title | safe }}</title>
|
|
6
|
+
<link rel="icon" href="{{ favicon_path | safe }}" type="image/x-icon" />
|
|
7
|
+
<style>
|
|
8
|
+
{{ default_css | safe }}
|
|
9
|
+
{{ additional_css | safe }}
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
{% set chart_id = "plot-container-" + uuid %}
|
|
14
|
+
|
|
15
|
+
<div id="{{ chart_id }}">
|
|
16
|
+
{{ svg | safe }}
|
|
17
|
+
<div class="tooltip" id="tooltip-{{ uuid }}"></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<script type="module">
|
|
21
|
+
(function () {
|
|
22
|
+
console.log("PlotJS: Initializing interactive plot");
|
|
23
|
+
|
|
24
|
+
// prettier-ignore
|
|
25
|
+
{{ js_parser | safe }}
|
|
26
|
+
|
|
27
|
+
const container = document.getElementById("{{ chart_id }}");
|
|
28
|
+
console.log(`PlotJS: Container found - ID: {{ chart_id }}`);
|
|
29
|
+
|
|
30
|
+
const tooltip = container.querySelector("#tooltip-{{ uuid }}");
|
|
31
|
+
const svg = container.querySelector("svg");
|
|
32
|
+
console.log(`PlotJS: SVG and tooltip elements loaded`);
|
|
33
|
+
|
|
34
|
+
const plot_data = JSON.parse(`{{ plot_data_json | tojson | safe }}`);
|
|
35
|
+
const tooltip_x_shift = plot_data["tooltip_x_shift"];
|
|
36
|
+
const tooltip_y_shift = -plot_data["tooltip_y_shift"];
|
|
37
|
+
const axes = plot_data["axes"];
|
|
38
|
+
console.log(
|
|
39
|
+
`PlotJS: Configuration - tooltip offset: (${tooltip_x_shift}, ${tooltip_y_shift})`,
|
|
40
|
+
);
|
|
41
|
+
console.log(
|
|
42
|
+
`PlotJS: Found ${Object.keys(axes).length} axes to process`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const plotParser = new PlotSVGParser(
|
|
46
|
+
svg,
|
|
47
|
+
tooltip,
|
|
48
|
+
tooltip_x_shift,
|
|
49
|
+
tooltip_y_shift,
|
|
50
|
+
);
|
|
51
|
+
console.log("PlotJS: Parser created successfully");
|
|
52
|
+
|
|
53
|
+
// Process each axes that has tooltip configuration
|
|
54
|
+
for (const axes_class in axes) {
|
|
55
|
+
if (axes.hasOwnProperty(axes_class)) {
|
|
56
|
+
console.log(`PlotJS: Processing axes "${axes_class}"`);
|
|
57
|
+
|
|
58
|
+
const axe_data = axes[axes_class];
|
|
59
|
+
const tooltip_labels = axe_data["tooltip_labels"];
|
|
60
|
+
const tooltip_groups = axe_data["tooltip_groups"];
|
|
61
|
+
const hover_nearest = axe_data["hover_nearest"] === "true";
|
|
62
|
+
const show_tooltip = tooltip_labels.length === 0 ? "none" : "block";
|
|
63
|
+
|
|
64
|
+
console.log(`PlotJS: - ${tooltip_labels.length} tooltip labels`);
|
|
65
|
+
console.log(`PlotJS: - ${tooltip_groups.length} tooltip groups`);
|
|
66
|
+
console.log(`PlotJS: - Hover nearest: ${hover_nearest}`);
|
|
67
|
+
console.log(
|
|
68
|
+
`PlotJS: - Show tooltips: ${show_tooltip === "block"}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const lines = plotParser.findLines(plotParser.svg, axes_class);
|
|
72
|
+
const bars = plotParser.findBars(plotParser.svg, axes_class);
|
|
73
|
+
const points = plotParser.findPoints(
|
|
74
|
+
plotParser.svg,
|
|
75
|
+
axes_class,
|
|
76
|
+
tooltip_groups,
|
|
77
|
+
);
|
|
78
|
+
const areas = plotParser.findAreas(plotParser.svg, axes_class);
|
|
79
|
+
|
|
80
|
+
const totalElements =
|
|
81
|
+
lines.size() + bars.size() + points.size() + areas.size();
|
|
82
|
+
console.log(
|
|
83
|
+
`PlotJS: - Total elements: ${totalElements} (${lines.size()} lines, ${bars.size()} bars, ${points.size()} points, ${areas.size()} areas)`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (points.size() > 0) {
|
|
87
|
+
plotParser.setHoverEffect(
|
|
88
|
+
points,
|
|
89
|
+
axes_class,
|
|
90
|
+
tooltip_labels,
|
|
91
|
+
tooltip_groups,
|
|
92
|
+
show_tooltip,
|
|
93
|
+
hover_nearest,
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
`PlotJS: - Hover effects attached to ${points.size()} points`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (lines.size() > 0) {
|
|
101
|
+
plotParser.setHoverEffect(
|
|
102
|
+
lines,
|
|
103
|
+
axes_class,
|
|
104
|
+
tooltip_labels,
|
|
105
|
+
tooltip_groups,
|
|
106
|
+
show_tooltip,
|
|
107
|
+
hover_nearest,
|
|
108
|
+
);
|
|
109
|
+
console.log(
|
|
110
|
+
`PlotJS: - Hover effects attached to ${lines.size()} lines`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (bars.size() > 0) {
|
|
115
|
+
plotParser.setHoverEffect(
|
|
116
|
+
bars,
|
|
117
|
+
axes_class,
|
|
118
|
+
tooltip_labels,
|
|
119
|
+
tooltip_groups,
|
|
120
|
+
show_tooltip,
|
|
121
|
+
hover_nearest,
|
|
122
|
+
);
|
|
123
|
+
console.log(
|
|
124
|
+
`PlotJS: - Hover effects attached to ${bars.size()} bars`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (areas.size() > 0) {
|
|
129
|
+
plotParser.setHoverEffect(
|
|
130
|
+
areas,
|
|
131
|
+
axes_class,
|
|
132
|
+
tooltip_labels,
|
|
133
|
+
tooltip_groups,
|
|
134
|
+
show_tooltip,
|
|
135
|
+
hover_nearest,
|
|
136
|
+
);
|
|
137
|
+
console.log(
|
|
138
|
+
`PlotJS: - Hover effects attached to ${areas.size()} areas`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`PlotJS: Finished processing axes "${axes_class}"`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log("PlotJS: Initialization complete - plot is interactive");
|
|
147
|
+
})();
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<script type="module">
|
|
151
|
+
// prettier-ignore
|
|
152
|
+
{{ additional_javascript | safe }}
|
|
153
|
+
</script>
|
|
154
|
+
</body>
|
|
155
|
+
</html>
|
|
@@ -33,11 +33,22 @@ def _vector_to_list(vector, name="labels and groups") -> list:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _get_and_sanitize_js(file_path, after_pattern):
|
|
36
|
+
"""
|
|
37
|
+
Extract JavaScript code starting from a pattern and remove export statements.
|
|
38
|
+
|
|
39
|
+
Export statements (export, export default) are removed because the code
|
|
40
|
+
is injected inline into HTML within an IIFE, where exports are not valid.
|
|
41
|
+
The exports remain in the source file for testing purposes.
|
|
42
|
+
"""
|
|
36
43
|
with open(file_path) as f:
|
|
37
44
|
content = f.read()
|
|
38
45
|
|
|
39
46
|
match = re.search(after_pattern, content, re.DOTALL)
|
|
40
47
|
if match:
|
|
41
|
-
|
|
48
|
+
js_code = match.group(0)
|
|
49
|
+
# Remove export statements since we're injecting inline
|
|
50
|
+
js_code = re.sub(r"^export\s+default\s+", "", js_code, flags=re.MULTILINE)
|
|
51
|
+
js_code = re.sub(r"^export\s+\{[^}]+\};?\s*$", "", js_code, flags=re.MULTILINE)
|
|
52
|
+
return js_code
|
|
42
53
|
else:
|
|
43
54
|
raise ValueError(f"Could not find '{after_pattern}' in the file")
|
|
@@ -11,10 +11,12 @@ justfile
|
|
|
11
11
|
mkdocs.yaml
|
|
12
12
|
package.json
|
|
13
13
|
pyproject.toml
|
|
14
|
+
pytest.ini
|
|
14
15
|
uv.lock
|
|
15
16
|
.github/workflows/doc.yaml
|
|
16
17
|
.github/workflows/lint.yaml
|
|
17
18
|
.github/workflows/pypi.yaml
|
|
19
|
+
.github/workflows/tests-browser.yaml
|
|
18
20
|
.github/workflows/tests-js.yaml
|
|
19
21
|
.github/workflows/tests-python.yaml
|
|
20
22
|
.github/workflows/type.yaml
|
|
@@ -81,6 +83,11 @@ plotjs/data/titanic.csv
|
|
|
81
83
|
plotjs/static/default.css
|
|
82
84
|
plotjs/static/plotparser.js
|
|
83
85
|
plotjs/static/template.html
|
|
86
|
+
tests/test-browser/README.md
|
|
87
|
+
tests/test-browser/__init__.py
|
|
88
|
+
tests/test-browser/conftest.py
|
|
89
|
+
tests/test-browser/test_interactions.py
|
|
90
|
+
tests/test-browser/test_rendering.py
|
|
84
91
|
tests/test-javascript/ParserSelectors.test.js
|
|
85
92
|
tests/test-javascript/ParserSetHover.test.js
|
|
86
93
|
tests/test-python/__init__.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "plotjs"
|
|
3
3
|
description = "Turn static matplotlib charts into interactive web visualizations"
|
|
4
|
-
version = "0.0.
|
|
4
|
+
version = "0.0.7"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
7
7
|
keywords = ["matplotlib", "interactive", "javascript", "web", "css", "d3", "mpld3", "plotnine"]
|
|
@@ -56,6 +56,7 @@ dev = [
|
|
|
56
56
|
"ipywidgets>=8.1.7",
|
|
57
57
|
"highlight-text>=0.2",
|
|
58
58
|
"drawarrow>=0.1.0",
|
|
59
|
+
"playwright>=1.40.0",
|
|
59
60
|
]
|
|
60
61
|
|
|
61
62
|
[project.urls]
|