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.
Files changed (106) hide show
  1. plotjs-0.0.7/.github/workflows/tests-browser.yaml +42 -0
  2. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/tests-python.yaml +2 -2
  3. {plotjs-0.0.6 → plotjs-0.0.7}/AGENTS.md +2 -6
  4. {plotjs-0.0.6/plotjs.egg-info → plotjs-0.0.7}/PKG-INFO +1 -1
  5. {plotjs-0.0.6 → plotjs-0.0.7}/package.json +0 -1
  6. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/__init__.py +1 -1
  7. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/main.py +1 -1
  8. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/static/plotparser.js +155 -22
  9. plotjs-0.0.7/plotjs/static/template.html +155 -0
  10. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/utils.py +12 -1
  11. {plotjs-0.0.6 → plotjs-0.0.7/plotjs.egg-info}/PKG-INFO +1 -1
  12. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/SOURCES.txt +7 -0
  13. {plotjs-0.0.6 → plotjs-0.0.7}/pyproject.toml +2 -1
  14. plotjs-0.0.7/pytest.ini +7 -0
  15. plotjs-0.0.7/tests/test-browser/README.md +120 -0
  16. plotjs-0.0.7/tests/test-browser/conftest.py +60 -0
  17. plotjs-0.0.7/tests/test-browser/test_interactions.py +297 -0
  18. plotjs-0.0.7/tests/test-browser/test_rendering.py +165 -0
  19. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-javascript/ParserSelectors.test.js +9 -10
  20. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-javascript/ParserSetHover.test.js +18 -12
  21. plotjs-0.0.7/tests/test-python/__init__.py +0 -0
  22. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_main.py +1 -1
  23. {plotjs-0.0.6 → plotjs-0.0.7}/uv.lock +94 -1
  24. plotjs-0.0.6/plotjs/static/template.html +0 -103
  25. {plotjs-0.0.6 → plotjs-0.0.7}/.coverage +0 -0
  26. {plotjs-0.0.6 → plotjs-0.0.7}/.gitattributes +0 -0
  27. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/doc.yaml +0 -0
  28. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/lint.yaml +0 -0
  29. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/pypi.yaml +0 -0
  30. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/tests-js.yaml +0 -0
  31. {plotjs-0.0.6 → plotjs-0.0.7}/.github/workflows/type.yaml +0 -0
  32. {plotjs-0.0.6 → plotjs-0.0.7}/.gitignore +0 -0
  33. {plotjs-0.0.6 → plotjs-0.0.7}/.pre-commit-config.yaml +0 -0
  34. {plotjs-0.0.6 → plotjs-0.0.7}/LICENSE +0 -0
  35. {plotjs-0.0.6 → plotjs-0.0.7}/README.md +0 -0
  36. {plotjs-0.0.6 → plotjs-0.0.7}/bun.lock +0 -0
  37. {plotjs-0.0.6 → plotjs-0.0.7}/coverage-badge.svg +0 -0
  38. {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/contributing.md +0 -0
  39. {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/overview.md +0 -0
  40. {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/parsing-matplotlib-svg.md +0 -0
  41. {plotjs-0.0.6 → plotjs-0.0.7}/docs/developers/svg-parser-reference.md +0 -0
  42. {plotjs-0.0.6 → plotjs-0.0.7}/docs/gallery/index.md +0 -0
  43. {plotjs-0.0.6 → plotjs-0.0.7}/docs/gallery/index.qmd +0 -0
  44. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/advanced/advanced.py +0 -0
  45. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/advanced/index.md +0 -0
  46. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/css/CSS.py +0 -0
  47. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/css/index.md +0 -0
  48. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/embed-graphs/index.md +0 -0
  49. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/javascript/index.md +0 -0
  50. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/javascript/javascript.py +0 -0
  51. {plotjs-0.0.6 → plotjs-0.0.7}/docs/guides/troubleshooting/index.md +0 -0
  52. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/CSS-2.html +0 -0
  53. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/CSS.html +0 -0
  54. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/area-natural-disasters.html +0 -0
  55. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/bug.html +0 -0
  56. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/javascript.html +0 -0
  57. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/javascript2.html +0 -0
  58. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart.html +0 -0
  59. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart10.html +0 -0
  60. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart11.html +0 -0
  61. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart2.html +0 -0
  62. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart3.html +0 -0
  63. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart4.html +0 -0
  64. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart5.html +0 -0
  65. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart6.html +0 -0
  66. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart7.html +0 -0
  67. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart8.html +0 -0
  68. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/quickstart9.html +0 -0
  69. {plotjs-0.0.6 → plotjs-0.0.7}/docs/iframes/random-walk-1.html +0 -0
  70. {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/how-it-works-1.png +0 -0
  71. {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/how-it-works-2.png +0 -0
  72. {plotjs-0.0.6 → plotjs-0.0.7}/docs/img/overview.png +0 -0
  73. {plotjs-0.0.6 → plotjs-0.0.7}/docs/index.md +0 -0
  74. {plotjs-0.0.6 → plotjs-0.0.7}/docs/index.qmd +0 -0
  75. {plotjs-0.0.6 → plotjs-0.0.7}/docs/index_files/figure-commonmark/cell-3-output-1.png +0 -0
  76. {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/css.md +0 -0
  77. {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/datasets.md +0 -0
  78. {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/javascript.md +0 -0
  79. {plotjs-0.0.6 → plotjs-0.0.7}/docs/reference/plotjs.md +0 -0
  80. {plotjs-0.0.6 → plotjs-0.0.7}/docs/static/style.css +0 -0
  81. {plotjs-0.0.6 → plotjs-0.0.7}/docs/stylesheets/style.css +0 -0
  82. {plotjs-0.0.6 → plotjs-0.0.7}/justfile +0 -0
  83. {plotjs-0.0.6 → plotjs-0.0.7}/mkdocs.yaml +0 -0
  84. {plotjs-0.0.6 → plotjs-0.0.7}/overrides/partials/footer.html +0 -0
  85. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/css.py +0 -0
  86. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/__init__.py +0 -0
  87. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/datasets.py +0 -0
  88. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/iris.csv +0 -0
  89. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/mtcars.csv +0 -0
  90. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/data/titanic.csv +0 -0
  91. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/javascript.py +0 -0
  92. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs/static/default.css +0 -0
  93. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/dependency_links.txt +0 -0
  94. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/requires.txt +0 -0
  95. {plotjs-0.0.6 → plotjs-0.0.7}/plotjs.egg-info/top_level.txt +0 -0
  96. {plotjs-0.0.6 → plotjs-0.0.7}/setup.cfg +0 -0
  97. {plotjs-0.0.6/tests/test-python → plotjs-0.0.7/tests/test-browser}/__init__.py +0 -0
  98. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/script.js +0 -0
  99. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/script2.js +0 -0
  100. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/style-invalid.css +0 -0
  101. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/static/style.css +0 -0
  102. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_css.py +0 -0
  103. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_data.py +0 -0
  104. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_js.py +0 -0
  105. {plotjs-0.0.6 → plotjs-0.0.7}/tests/test-python/test_other_utils.py +0 -0
  106. {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
@@ -26,5 +26,5 @@ jobs:
26
26
  - name: Install the project
27
27
  run: uv sync --all-groups
28
28
 
29
- - name: Run tests
30
- run: uv run pytest
29
+ - name: Run unit tests (excluding browser tests)
30
+ run: uv run pytest tests/test-python/ -v
@@ -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 (D3.js)
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 D3-based event handling
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotjs
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Turn static matplotlib charts into interactive web visualizations
5
5
  Author-email: Joseph Barbier <joseph.barbierdarnal@mail.com>
6
6
  License-Expression: MIT
@@ -5,7 +5,6 @@
5
5
  "docs:js": "jsdoc2md plotjs/static/plotparser.js > docs/developers/svg-parser-reference.md"
6
6
  },
7
7
  "devDependencies": {
8
- "d3-selection": "^3.0.0",
9
8
  "jsdoc": "^4.0.4",
10
9
  "jsdoc-to-markdown": "^9.1.2",
11
10
  "jsdom": "^24.0.0"
@@ -1,6 +1,6 @@
1
1
  from .main import PlotJS
2
2
 
3
- __version__ = "0.0.6"
3
+ __version__ = "0.0.7"
4
4
  __all__: list[str] = [
5
5
  "PlotJS",
6
6
  ]
@@ -72,7 +72,7 @@ class PlotJS:
72
72
 
73
73
  self._js_parser = _get_and_sanitize_js(
74
74
  file_path=JS_PARSER_PATH,
75
- after_pattern=r"class PlotSVGParser.*",
75
+ after_pattern=r"class Selection.*",
76
76
  )
77
77
 
78
78
  rnd = random.Random(22022001)
@@ -1,4 +1,136 @@
1
- import * as d3 from "d3-selection";
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 {d3.Selection} svg - D3 selection of the target SVG element (e.g. the entire plot).
20
- * @param {d3.Selection} tooltip - D3 selection of the tooltip container (e.g. a div).
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 {d3.Selection} svg - D3 selection of the SVG element.
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 {d3.Selection} D3 selection of bar elements.
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 = d3.select(this).select("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 {d3.Selection} svg - D3 selection of the SVG element.
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 {d3.Selection} D3 selection of point elements.
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
- d3.select(this).attr("data-group", tooltip_groups[i]);
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 {d3.Selection} svg - D3 selection of the SVG element.
221
+ * @param {Selection} svg - Selection of the SVG element.
90
222
  * @param {string} axes_class - ID of the axes group.
91
- * @returns {d3.Selection} D3 selection of line elements.
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 {d3.Selection} svg - D3 selection of the SVG element.
242
+ * @param {Selection} svg - Selection of the SVG element.
111
243
  * @param {string} axes_class - ID of the axes group.
112
- * @returns {d3.Selection} D3 selection of area elements.
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 {d3.Selection} elements - Selection of candidate elements.
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 {d3.Selection} plot_element - Selection of plot elements (points, lines, etc.).
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 [mouseX, mouseY] = d3.pointer(event);
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
- return match.group(0)
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotjs
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Turn static matplotlib charts into interactive web visualizations
5
5
  Author-email: Joseph Barbier <joseph.barbierdarnal@mail.com>
6
6
  License-Expression: MIT
@@ -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.6"
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]
@@ -0,0 +1,7 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ python_files = test_*.py
4
+ python_classes = Test*
5
+ python_functions = test_*
6
+
7
+ markers = browser: marks tests as browser tests (using Playwright, slower)