babelplot 2025.6__tar.gz → 2025.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 (44) hide show
  1. {babelplot-2025.6 → babelplot-2025.7}/PKG-INFO +1 -1
  2. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/catalog/bokeh_.py +6 -3
  3. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/catalog/matplotlib_.py +87 -73
  4. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/catalog/plotly_.py +220 -147
  5. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/catalog/vedo_.py +4 -1
  6. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/helper/validation.py +2 -0
  7. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/task/showing.py +33 -30
  8. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/backend.py +26 -9
  9. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/figure.py +12 -12
  10. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/plot_type.py +2 -2
  11. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/version.py +1 -1
  12. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot.egg-info/PKG-INFO +1 -1
  13. {babelplot-2025.6 → babelplot-2025.7}/MANIFEST.in +0 -0
  14. {babelplot-2025.6 → babelplot-2025.7}/README-COPYRIGHT-utf8.txt +0 -0
  15. {babelplot-2025.6 → babelplot-2025.7}/README-LICENCE-utf8.txt +0 -0
  16. {babelplot-2025.6 → babelplot-2025.7}/README.rst +0 -0
  17. {babelplot-2025.6 → babelplot-2025.7}/documentation/wiki/description.asciidoc +0 -0
  18. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/__init__.py +0 -0
  19. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/api/backend.py +0 -0
  20. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/api/definition.py +0 -0
  21. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/api/help.py +0 -0
  22. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/backend/helper/creation.py +0 -0
  23. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/constant/backend.py +0 -0
  24. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/constant/path.py +0 -0
  25. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/constant/project.py +0 -0
  26. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/extension/enum_.py +0 -0
  27. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/extension/function.py +0 -0
  28. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/runtime/backends.py +0 -0
  29. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/runtime/figure.py +0 -0
  30. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/task/help.py +0 -0
  31. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/task/instantiation.py +0 -0
  32. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/dimension.py +0 -0
  33. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/ffp_base.py +0 -0
  34. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/frame.py +0 -0
  35. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/plot.py +0 -0
  36. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot/type/translator.py +0 -0
  37. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot.egg-info/SOURCES.txt +0 -0
  38. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot.egg-info/dependency_links.txt +0 -0
  39. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot.egg-info/requires.txt +0 -0
  40. {babelplot-2025.6 → babelplot-2025.7}/package/babelplot.egg-info/top_level.txt +0 -0
  41. {babelplot-2025.6 → babelplot-2025.7}/pyproject.toml +0 -0
  42. {babelplot-2025.6 → babelplot-2025.7}/requirements.txt +0 -0
  43. {babelplot-2025.6 → babelplot-2025.7}/setup.cfg +0 -0
  44. {babelplot-2025.6 → babelplot-2025.7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: babelplot
3
- Version: 2025.6
3
+ Version: 2025.7
4
4
  Summary: A Meta Plotting Library That Speaks Several Backends
5
5
  Home-page: https://src.koda.cnrs.fr/eric.debreuve/babelplot/
6
6
  Author: Eric Debreuve
@@ -7,7 +7,7 @@ SEE COPYRIGHT NOTICE BELOW
7
7
  import dataclasses as d
8
8
  import typing as h
9
9
 
10
- from babelplot.task.showing import Show
10
+ from babelplot.task.showing import ShowHTMLWithPyQt
11
11
  from babelplot.type.dimension import dim_e
12
12
  from babelplot.type.figure import figure_t as base_figure_t
13
13
  from babelplot.type.frame import frame_t as base_frame_t
@@ -59,8 +59,11 @@ class frame_t(base_frame_t):
59
59
  class figure_t(base_figure_t):
60
60
  layout: h.Any = None
61
61
 
62
- _NewBackendFigure = backend_figure_t
63
- _BackendShow = Show
62
+ _BackendShow = ShowHTMLWithPyQt
63
+
64
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
65
+ """"""
66
+ return backend_figure_t(*args, **kwargs)
64
67
 
65
68
  def _NewFrame(
66
69
  self,
@@ -124,10 +124,13 @@ class figure_t(base_figure_t):
124
124
  _BackendSetProperty: h.ClassVar[h.Callable] = _SetProperty
125
125
  _BackendProperty: h.ClassVar[h.Callable] = _Property
126
126
 
127
- @staticmethod
128
- def _NewBackendFigure(*args, **kwargs) -> backend_figure_t:
127
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
129
128
  """"""
130
- return NewBackendFigure(*args, num=None, **kwargs)
129
+ output = NewBackendFigure(*args, num=None, **kwargs)
130
+
131
+ output.canvas.mpl_connect("close_event", lambda *_, **__: _OnCloseEvent(self))
132
+
133
+ return output
131
134
 
132
135
  def _NewFrame(
133
136
  self,
@@ -199,27 +202,27 @@ class figure_t(base_figure_t):
199
202
  )
200
203
  frame.raw.set_position((left, bottom, width, height))
201
204
 
202
- def _BackendShow(self) -> None:
205
+ def _BackendShow(self, modal: bool, /) -> None:
203
206
  """"""
204
207
  raw = self.raw
205
-
206
208
  raw.show()
207
209
 
208
- event_manager = raw.canvas
209
- event_manager.mpl_connect(
210
- "close_event", lambda _: event_manager.stop_event_loop()
211
- )
212
- event_manager.start_event_loop()
210
+ if modal:
211
+ title = raw.get_suptitle()
212
+ if title.__len__() > 0:
213
+ title = f"{title} [MASTER FIGURE]"
214
+ else:
215
+ title = "MASTER FIGURE"
216
+ raw.suptitle(title)
213
217
 
218
+ raw.canvas.start_event_loop()
214
219
 
215
- def _Polygon(
216
- frame: backend_frame_2d_t, xs: array_t, ys: array_t, *_, **kwargs
217
- ) -> polygon_t:
218
- """"""
219
- output = polygon_t(nmpy.vstack((xs, ys)).T, **kwargs)
220
- frame.add_patch(output)
221
220
 
222
- return output
221
+ def _OnCloseEvent(figure: figure_t, /) -> None:
222
+ """"""
223
+ # Hopefully, the event loop does not mind being stopped several times.
224
+ figure.raw.canvas.stop_event_loop()
225
+ figure.AcknowledgeClosure()
223
226
 
224
227
 
225
228
  def _Arrows2(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plot_t:
@@ -268,6 +271,54 @@ def _Arrows3(frame: backend_frame_3d_t, *args, **kwargs) -> backend_plot_t:
268
271
  return frame.quiver(x, y, z, u, v, w, **kwargs)
269
272
 
270
273
 
274
+ def _Bar3(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plot_t:
275
+ """"""
276
+ if args.__len__() == 1:
277
+ counts = nmpy.asarray(args[0])
278
+ x, y = counts.shape
279
+ else:
280
+ x, y, counts = args
281
+ counts = nmpy.asarray(counts)
282
+ if isinstance(x, int):
283
+ x, y = nmpy.meshgrid(range(x), range(y), indexing="ij")
284
+ if counts.ndim == 1:
285
+ x = x.ravel()
286
+ y = y.ravel()
287
+
288
+ width = kwargs.get("width", 0.8)
289
+ depth = kwargs.get("depth", 0.8)
290
+ offset = kwargs.get("offset", 0.0)
291
+ kwargs = {
292
+ _key: _vle
293
+ for _key, _vle in kwargs.items()
294
+ if _key not in ("width", "depth", "offset")
295
+ }
296
+
297
+ return frame.bar3d(x, y, offset, width, depth, counts, **kwargs)
298
+
299
+
300
+ def _BarH(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plots_t:
301
+ """"""
302
+ if args.__len__() == 1:
303
+ counts = args[0]
304
+ positions = range(counts.__len__())
305
+ else:
306
+ positions, counts = args
307
+
308
+ return frame.barh(positions, counts, **kwargs)
309
+
310
+
311
+ def _BarV(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plots_t:
312
+ """"""
313
+ if args.__len__() == 1:
314
+ counts = args[0]
315
+ positions = range(counts.__len__())
316
+ else:
317
+ positions, counts = args
318
+
319
+ return frame.bar(positions, counts, **kwargs)
320
+
321
+
271
322
  def _ElevationSurface(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plot_t:
272
323
  """"""
273
324
  if args.__len__() == 1:
@@ -319,52 +370,14 @@ def _Mesh(
319
370
  )
320
371
 
321
372
 
322
- def _BarH(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plots_t:
323
- """"""
324
- if args.__len__() == 1:
325
- counts = args[0]
326
- positions = range(counts.__len__())
327
- else:
328
- positions, counts = args
329
-
330
- return frame.barh(positions, counts, **kwargs)
331
-
332
-
333
- def _BarV(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plots_t:
334
- """"""
335
- if args.__len__() == 1:
336
- counts = args[0]
337
- positions = range(counts.__len__())
338
- else:
339
- positions, counts = args
340
-
341
- return frame.bar(positions, counts, **kwargs)
342
-
343
-
344
- def _Bar3(frame: backend_frame_2d_t, *args, **kwargs) -> backend_plot_t:
373
+ def _Polygon(
374
+ frame: backend_frame_2d_t, xs: array_t, ys: array_t, *_, **kwargs
375
+ ) -> polygon_t:
345
376
  """"""
346
- if args.__len__() == 1:
347
- counts = nmpy.asarray(args[0])
348
- x, y = counts.shape
349
- else:
350
- x, y, counts = args
351
- counts = nmpy.asarray(counts)
352
- if isinstance(x, int):
353
- x, y = nmpy.meshgrid(range(x), range(y), indexing="ij")
354
- if counts.ndim == 1:
355
- x = x.ravel()
356
- y = y.ravel()
357
-
358
- width = kwargs.get("width", 0.8)
359
- depth = kwargs.get("depth", 0.8)
360
- offset = kwargs.get("offset", 0.0)
361
- kwargs = {
362
- _key: _vle
363
- for _key, _vle in kwargs.items()
364
- if _key not in ("width", "depth", "offset")
365
- }
377
+ output = polygon_t(nmpy.vstack((xs, ys)).T, **kwargs)
378
+ frame.add_patch(output)
366
379
 
367
- return frame.bar3d(x, y, offset, width, depth, counts, **kwargs)
380
+ return output
368
381
 
369
382
 
370
383
  def _Text2(frame: backend_frame_2d_t, text, x, y, *_, **kwargs) -> backend_plot_t:
@@ -379,24 +392,23 @@ def _Text3(frame: backend_frame_2d_t, text, x, y, z, *_, **kwargs) -> backend_pl
379
392
 
380
393
  PLOTS = plot_e.NewPlotsTemplate()
381
394
 
382
- PLOTS[plot_e.SCATTER][1] = backend_frame_2d_t.scatter
383
- PLOTS[plot_e.POLYLINE][1] = backend_frame_2d_t.plot
384
- PLOTS[plot_e.POLYGON][1] = _Polygon
385
395
  PLOTS[plot_e.ARROWS][1] = _Arrows2
386
- PLOTS[plot_e.ISOSET][1] = _IsoContour
396
+ PLOTS[plot_e.ARROWS][2] = _Arrows3
397
+ PLOTS[plot_e.BAR3][2] = _Bar3
387
398
  PLOTS[plot_e.BARH][1] = _BarH
388
399
  PLOTS[plot_e.BARV][1] = _BarV
389
- PLOTS[plot_e.PIE][1] = backend_frame_2d_t.pie
390
- PLOTS[plot_e.IMAGE][1] = backend_frame_2d_t.matshow
391
- PLOTS[plot_e.TEXT][1] = _Text2
392
-
393
- PLOTS[plot_e.SCATTER][2] = backend_frame_3d_t.scatter
394
- PLOTS[plot_e.POLYLINE][2] = backend_frame_3d_t.plot
395
- PLOTS[plot_e.ARROWS][2] = _Arrows3
396
400
  PLOTS[plot_e.ELEVATION][2] = _ElevationSurface
401
+ PLOTS[plot_e.IMAGE][1] = backend_frame_2d_t.matshow
402
+ PLOTS[plot_e.ISOSET][1] = _IsoContour
397
403
  PLOTS[plot_e.ISOSET][2] = _IsoSurface
398
404
  PLOTS[plot_e.MESH][2] = _Mesh
399
- PLOTS[plot_e.BAR3][2] = _Bar3
405
+ PLOTS[plot_e.PIE][1] = backend_frame_2d_t.pie
406
+ PLOTS[plot_e.POLYGON][1] = _Polygon
407
+ PLOTS[plot_e.POLYLINE][1] = backend_frame_2d_t.plot
408
+ PLOTS[plot_e.POLYLINE][2] = backend_frame_3d_t.plot
409
+ PLOTS[plot_e.SCATTER][1] = backend_frame_2d_t.scatter
410
+ PLOTS[plot_e.SCATTER][2] = backend_frame_3d_t.scatter
411
+ PLOTS[plot_e.TEXT][1] = _Text2
400
412
  PLOTS[plot_e.TEXT][2] = _Text3
401
413
 
402
414
 
@@ -407,7 +419,9 @@ TRANSLATIONS = {
407
419
  "color_max": "vmax",
408
420
  "color_min": "vmin",
409
421
  "color_scaling": "norm",
422
+ "colormap": "cmap",
410
423
  "depth_shade": "depthshade",
424
+ "opacity": "alpha",
411
425
  "plot_non_finite": "plotnonfinite",
412
426
  "size": "s",
413
427
  "width_edge": "linewidths",
@@ -5,12 +5,15 @@ SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
7
  import dataclasses as d
8
+ import gzip
9
+ import typing as h
10
+ from multiprocessing import Process as process_t
8
11
  from pathlib import Path as path_t
9
12
 
10
13
  import numpy as nmpy
11
14
  import plotly.figure_factory as fcry # noqa
12
15
  import plotly.graph_objects as plly # noqa
13
- from babelplot.task.showing import Show
16
+ from babelplot.task.showing import ShowHTMLWithPyQt
14
17
  from babelplot.type.dimension import dim_e
15
18
  from babelplot.type.figure import figure_t as base_figure_t
16
19
  from babelplot.type.frame import frame_t as base_frame_t
@@ -64,8 +67,10 @@ class frame_t(base_frame_t):
64
67
 
65
68
  @d.dataclass(slots=True, repr=False, eq=False)
66
69
  class figure_t(base_figure_t):
67
- _NewBackendFigure = backend_figure_t
68
- _BackendShow = Show
70
+
71
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
72
+ """"""
73
+ return backend_figure_t(*args, **kwargs)
69
74
 
70
75
  def _NewFrame(
71
76
  self,
@@ -128,7 +133,7 @@ class figure_t(base_figure_t):
128
133
  if self.title is not None:
129
134
  raw.update_layout(title_text=self.title + title_postfix)
130
135
 
131
- def AsHTML(self) -> str:
136
+ def _BackendShow(self, modal: bool, /) -> None:
132
137
  """
133
138
  Note on include_plotlyjs:
134
139
  - "cdn": works but must be online.
@@ -136,35 +141,16 @@ class figure_t(base_figure_t):
136
141
  PySide6.QtWebEngineWidgets.QWebEngineView.setHtml because of html size limit.
137
142
  See note in babelplot.task.html.Show.
138
143
  """
139
- return self.raw.to_html(include_plotlyjs=True, config=_FIGURE_CONFIG)
140
-
141
-
142
- def _Scatter2(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
143
- """"""
144
- return plly.Scatter(x=x, y=y, mode="markers", **kwargs)
145
-
146
-
147
- def _Scatter3(x: array_t, y: array_t, z: array_t, *_, **kwargs) -> backend_plot_t:
148
- """"""
149
- return plly.Scatter3d(x=x, y=y, z=z, mode="markers", **kwargs)
150
-
151
-
152
- def _Polyline2(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
153
- """"""
154
- return plly.Scatter(x=x, y=y, mode="lines", **kwargs)
155
-
156
-
157
- def _Polyline3(x: array_t, y: array_t, z: array_t, *_, **kwargs) -> backend_plot_t:
158
- """"""
159
- return plly.Scatter3d(x=x, y=y, z=z, mode="lines", **kwargs)
160
-
161
-
162
- def _Polygon(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
163
- """"""
164
- x = nmpy.concatenate((x, [x[0]]))
165
- y = nmpy.concatenate((y, [y[0]]))
166
-
167
- return plly.Scatter(x=x, y=y, mode="lines", fill="toself", **kwargs)
144
+ html = self.raw.to_html(include_plotlyjs=True, config=_FIGURE_CONFIG)
145
+ uid = id(self)
146
+ if modal:
147
+ ShowHTMLWithPyQt(html, uid)
148
+ self.AcknowledgeClosure()
149
+ else:
150
+ html = gzip.compress(html.encode())
151
+ process = process_t(target=lambda: ShowHTMLWithPyQt(html, uid))
152
+ process.start()
153
+ self.showing_process = process
168
154
 
169
155
 
170
156
  def _Arrows2(*args, **kwargs) -> backend_plot_t:
@@ -205,105 +191,6 @@ def _Arrows3(*args, **kwargs) -> backend_plot_t:
205
191
  )
206
192
 
207
193
 
208
- def _ElevationSurface(*args, **kwargs) -> backend_plot_t:
209
- """"""
210
- if args.__len__() == 1:
211
- elevation = args[0]
212
- x, y = nmpy.meshgrid(
213
- range(elevation.shape[0]), range(elevation.shape[1]), indexing="ij"
214
- )
215
- else:
216
- x, y, elevation = args
217
-
218
- return plly.Surface(contours={}, x=x, y=y, z=elevation, **kwargs)
219
-
220
-
221
- def _IsoContour(*args, **kwargs) -> backend_plot_t:
222
- """"""
223
- parameters = {
224
- "contours_coloring": "lines",
225
- "line_width": 2,
226
- }
227
-
228
- if args.__len__() == 2:
229
- values, value = args
230
- else:
231
- x, y, values, value = args
232
- parameters["x"] = x
233
- parameters["y"] = y
234
- parameters["z"] = values
235
- parameters["contours"] = {
236
- "start": value,
237
- "end": value,
238
- "size": 1,
239
- "showlabels": True,
240
- }
241
-
242
- parameters.update(kwargs)
243
-
244
- return plly.Contour(**parameters)
245
-
246
-
247
- def _IsoSurface(values: array_t, value: float, *args, **kwargs) -> backend_plot_t:
248
- """"""
249
- if args.__len__() == 3:
250
- x, y, z = args
251
- else:
252
- x, y, z = nmpy.meshgrid(
253
- range(values.shape[0]),
254
- range(values.shape[1]),
255
- range(values.shape[2]),
256
- indexing="ij",
257
- )
258
-
259
- parameters = {
260
- "surface": {"count": 1},
261
- "caps": {"x_show": False, "y_show": False, "z_show": False},
262
- }
263
- parameters.update(kwargs)
264
-
265
- return plly.Isosurface(
266
- x=x.ravel(),
267
- y=y.ravel(),
268
- z=z.ravel(),
269
- value=values.ravel(),
270
- isomin=value,
271
- isomax=value,
272
- **parameters,
273
- )
274
-
275
-
276
- def _Mesh(triangles: array_t, vertices: array_t, *_, **kwargs) -> backend_plot_t:
277
- """"""
278
- return plly.Mesh3d(
279
- x=vertices[:, 0],
280
- y=vertices[:, 1],
281
- z=vertices[:, 2],
282
- i=triangles[:, 0],
283
- j=triangles[:, 1],
284
- k=triangles[:, 2],
285
- **kwargs,
286
- )
287
-
288
-
289
- def _BarH(*args, **kwargs) -> backend_plot_t:
290
- """"""
291
- return _BarV(*args, orientation="h", **kwargs)
292
-
293
-
294
- def _BarV(*args, **kwargs) -> backend_plot_t:
295
- """"""
296
- if args.__len__() == 1:
297
- counts = args[0]
298
- positions = tuple(range(counts.__len__()))
299
- else:
300
- positions, counts = args
301
- if kwargs.get("orientation") == "h":
302
- positions, counts = counts, positions
303
-
304
- return plly.Bar(x=positions, y=counts, **kwargs)
305
-
306
-
307
194
  def _Bar3(*args, **kwargs) -> backend_plot_t:
308
195
  """"""
309
196
  if args.__len__() == 1:
@@ -406,9 +293,35 @@ def _Bar3(*args, **kwargs) -> backend_plot_t:
406
293
  )
407
294
 
408
295
 
409
- def _Pie(values: array_t, *_, **kwargs) -> backend_plot_t:
296
+ def _BarH(*args, **kwargs) -> backend_plot_t:
410
297
  """"""
411
- return plly.Pie(values=values, **kwargs)
298
+ return _BarV(*args, orientation="h", **kwargs)
299
+
300
+
301
+ def _BarV(*args, **kwargs) -> backend_plot_t:
302
+ """"""
303
+ if args.__len__() == 1:
304
+ counts = args[0]
305
+ positions = tuple(range(counts.__len__()))
306
+ else:
307
+ positions, counts = args
308
+ if kwargs.get("orientation") == "h":
309
+ positions, counts = counts, positions
310
+
311
+ return plly.Bar(x=positions, y=counts, **kwargs)
312
+
313
+
314
+ def _ElevationSurface(*args, **kwargs) -> backend_plot_t:
315
+ """"""
316
+ if args.__len__() == 1:
317
+ elevation = args[0]
318
+ x, y = nmpy.meshgrid(
319
+ range(elevation.shape[0]), range(elevation.shape[1]), indexing="ij"
320
+ )
321
+ else:
322
+ x, y, elevation = args
323
+
324
+ return plly.Surface(contours={}, x=x, y=y, z=elevation, **kwargs)
412
325
 
413
326
 
414
327
  def _Image(image: array_t, *_, **kwargs) -> backend_plot_t:
@@ -419,6 +332,107 @@ def _Image(image: array_t, *_, **kwargs) -> backend_plot_t:
419
332
  return plly.Image(z=image, **kwargs)
420
333
 
421
334
 
335
+ def _IsoContour(*args, **kwargs) -> backend_plot_t:
336
+ """"""
337
+ parameters = {
338
+ "contours_coloring": "lines",
339
+ "line_width": 2,
340
+ }
341
+
342
+ if args.__len__() == 2:
343
+ values, value = args
344
+ else:
345
+ x, y, values, value = args
346
+ parameters["x"] = x
347
+ parameters["y"] = y
348
+ parameters["z"] = values
349
+ parameters["contours"] = {
350
+ "start": value,
351
+ "end": value,
352
+ "size": 1,
353
+ "showlabels": True,
354
+ }
355
+
356
+ parameters.update(kwargs)
357
+
358
+ return plly.Contour(**parameters)
359
+
360
+
361
+ def _IsoSurface(values: array_t, value: float, *args, **kwargs) -> backend_plot_t:
362
+ """"""
363
+ if args.__len__() == 3:
364
+ x, y, z = args
365
+ else:
366
+ x, y, z = nmpy.meshgrid(
367
+ range(values.shape[0]),
368
+ range(values.shape[1]),
369
+ range(values.shape[2]),
370
+ indexing="ij",
371
+ )
372
+
373
+ parameters = {
374
+ "surface": {"count": 1},
375
+ "caps": {"x_show": False, "y_show": False, "z_show": False},
376
+ }
377
+ parameters.update(kwargs)
378
+
379
+ return plly.Isosurface(
380
+ x=x.ravel(),
381
+ y=y.ravel(),
382
+ z=z.ravel(),
383
+ value=values.ravel(),
384
+ isomin=value,
385
+ isomax=value,
386
+ **parameters,
387
+ )
388
+
389
+
390
+ def _Mesh(triangles: array_t, vertices: array_t, *_, **kwargs) -> backend_plot_t:
391
+ """"""
392
+ return plly.Mesh3d(
393
+ x=vertices[:, 0],
394
+ y=vertices[:, 1],
395
+ z=vertices[:, 2],
396
+ i=triangles[:, 0],
397
+ j=triangles[:, 1],
398
+ k=triangles[:, 2],
399
+ **kwargs,
400
+ )
401
+
402
+
403
+ def _Pie(values: array_t, *_, **kwargs) -> backend_plot_t:
404
+ """"""
405
+ return plly.Pie(values=values, **kwargs)
406
+
407
+
408
+ def _Polygon(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
409
+ """"""
410
+ x = nmpy.concatenate((x, [x[0]]))
411
+ y = nmpy.concatenate((y, [y[0]]))
412
+
413
+ return plly.Scatter(x=x, y=y, mode="lines", fill="toself", **kwargs)
414
+
415
+
416
+ def _Polyline2(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
417
+ """"""
418
+ return plly.Scatter(x=x, y=y, mode="lines", **kwargs)
419
+
420
+
421
+ def _Polyline3(x: array_t, y: array_t, z: array_t, *_, **kwargs) -> backend_plot_t:
422
+ """"""
423
+ return plly.Scatter3d(x=x, y=y, z=z, mode="lines", **kwargs)
424
+
425
+
426
+ def _Scatter2(x: array_t, y: array_t, *_, **kwargs) -> backend_plot_t:
427
+ """"""
428
+ return plly.Scatter(x=x, y=y, mode="markers", **kwargs)
429
+
430
+
431
+ def _Scatter3(x: array_t, y: array_t, z: array_t, *_, **kwargs) -> backend_plot_t:
432
+ """"""
433
+ return plly.Scatter3d(x=x, y=y, z=z, mode="markers", **kwargs)
434
+
435
+
422
436
  def _Text2(text: str, x: float, y: float, *_, **kwargs) -> backend_plot_t:
423
437
  """"""
424
438
  parameters = {"textposition": "top right"}
@@ -444,32 +458,91 @@ def _Text3(text: str, x: float, y: float, z: float, *_, **kwargs) -> backend_plo
444
458
 
445
459
  PLOTS = plot_e.NewPlotsTemplate()
446
460
 
447
- PLOTS[plot_e.SCATTER][1] = _Scatter2
448
- PLOTS[plot_e.POLYLINE][1] = _Polyline2
449
- PLOTS[plot_e.POLYGON][1] = _Polygon
450
461
  PLOTS[plot_e.ARROWS][1] = _Arrows2
451
- PLOTS[plot_e.ISOSET][1] = _IsoContour
462
+ PLOTS[plot_e.ARROWS][2] = _Arrows3
463
+ PLOTS[plot_e.BAR3][2] = _Bar3
452
464
  PLOTS[plot_e.BARH][1] = _BarH
453
465
  PLOTS[plot_e.BARV][1] = _BarV
454
- PLOTS[plot_e.PIE][1] = _Pie
455
- PLOTS[plot_e.IMAGE][1] = _Image
456
- PLOTS[plot_e.TEXT][1] = _Text2
457
-
458
- PLOTS[plot_e.SCATTER][2] = _Scatter3
459
- PLOTS[plot_e.POLYLINE][2] = _Polyline3
460
- PLOTS[plot_e.ARROWS][2] = _Arrows3
461
466
  PLOTS[plot_e.ELEVATION][2] = _ElevationSurface
467
+ PLOTS[plot_e.IMAGE][1] = _Image
468
+ PLOTS[plot_e.ISOSET][1] = _IsoContour
462
469
  PLOTS[plot_e.ISOSET][2] = _IsoSurface
463
470
  PLOTS[plot_e.MESH][2] = _Mesh
464
- PLOTS[plot_e.BAR3][2] = _Bar3
471
+ PLOTS[plot_e.PIE][1] = _Pie
472
+ PLOTS[plot_e.POLYGON][1] = _Polygon
473
+ PLOTS[plot_e.POLYLINE][1] = _Polyline2
474
+ PLOTS[plot_e.POLYLINE][2] = _Polyline3
475
+ PLOTS[plot_e.SCATTER][1] = _Scatter2
476
+ PLOTS[plot_e.SCATTER][2] = _Scatter3
477
+ PLOTS[plot_e.TEXT][1] = _Text2
465
478
  PLOTS[plot_e.TEXT][2] = _Text3
466
479
 
467
480
 
481
+ def _ApplyGlobalTranslation(
482
+ kwargs: dict[str, h.Any], who_s_asking: str | h.Callable | None, /
483
+ ) -> None:
484
+ """"""
485
+ if who_s_asking in (_Scatter2, _Scatter3):
486
+ marker = {}
487
+ for passed, wanted in zip(
488
+ ("color", "opacity", "size", "shape"),
489
+ ("color", "opacity", "size", "symbol"),
490
+ ):
491
+ if (value := kwargs.get(passed)) is not None:
492
+ marker[wanted] = value
493
+ del kwargs[passed]
494
+ if who_s_asking is _Scatter3:
495
+ # /!\\ Passing one size per sample makes all of them disappear.
496
+ if ("size" in marker) and not isinstance(marker["size"], int | float):
497
+ marker["size"] = 0.5 * (min(marker["size"]) + max(marker["size"]))
498
+ # /!\\ Opacity seems to be all-or-nothing.
499
+ if "opacity" in marker:
500
+ del marker["opacity"]
501
+
502
+ kwargs["marker"] = marker
503
+ #
504
+ elif who_s_asking in (_Arrows2, _Polyline2, _Polyline3):
505
+ line = {}
506
+ for passed, wanted in zip(
507
+ ("color", "opacity", "line_width", "line_style"),
508
+ ("color", "opacity", "width", "symbol"),
509
+ ):
510
+ if (value := kwargs.get(passed)) is not None:
511
+ line[wanted] = value
512
+ del kwargs[passed]
513
+
514
+ kwargs["line"] = line
515
+ #
516
+ elif who_s_asking is _Pie:
517
+ marker = {}
518
+ for passed, wanted in zip(("color",), ("colors",)):
519
+ if (value := kwargs.get(passed)) is not None:
520
+ marker[wanted] = value
521
+ del kwargs[passed]
522
+
523
+ kwargs["marker"] = marker
524
+
525
+
468
526
  TRANSLATIONS = {
469
- "alpha": "opacity",
470
527
  "color_face": "surfacecolor",
528
+ (_Arrows3, "color"): None,
529
+ (_BarH, "color"): None,
530
+ (_BarV, "color"): None,
531
+ (_ElevationSurface, "color_edge"): None,
532
+ (_ElevationSurface, "color_face"): None,
533
+ (_ElevationSurface, "width_edge"): None,
534
+ (_Image, "colormap"): None,
535
+ (_IsoContour, "color"): "fillcolor",
536
+ (_IsoSurface, "color_edge"): None,
471
537
  (_IsoSurface, "color_face"): None,
538
+ (_IsoSurface, "step_size"): None,
539
+ (_IsoSurface, "width_edge"): None,
540
+ (_Mesh, "color_edge"): None,
472
541
  (_Mesh, "color_face"): "facecolor",
542
+ (_Mesh, "width_edge"): None,
543
+ (_Polygon, "color_edge"): None,
544
+ (_Polygon, "color_face"): "fillcolor",
545
+ None: _ApplyGlobalTranslation,
473
546
  }
474
547
 
475
548
 
@@ -51,7 +51,10 @@ class frame_t(base_frame_t):
51
51
 
52
52
  @d.dataclass(slots=True, repr=False, eq=False)
53
53
  class figure_t(base_figure_t):
54
- _NewBackendFigure = backend_figure_t
54
+
55
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
56
+ """"""
57
+ return backend_figure_t(*args, **kwargs)
55
58
 
56
59
  def _NewFrame(
57
60
  self,
@@ -46,6 +46,8 @@ def CheckBackend(backend: str | path_t, /) -> None:
46
46
  else:
47
47
  print('--- Missing "PLOTS" dictionary')
48
48
 
49
+ # TODO: Check that translations respect the format (to be documented by the way).
50
+
49
51
 
50
52
  """
51
53
  COPYRIGHT NOTICE
@@ -4,31 +4,33 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2022
4
4
  SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
+ import gzip
7
8
  import os as osys
8
9
  import sys as s
9
10
  import tempfile as tmpf
10
11
 
11
12
  from babelplot.constant.project import NAME
12
13
  from babelplot.runtime.figure import SHOWN_FIGURES, UN_SHOWN_FIGURES
13
- from babelplot.type.figure import figure_t
14
14
  from PyQt6.QtCore import QUrl as url_t
15
15
  from PyQt6.QtWebEngineWidgets import QWebEngineView as widget_t
16
16
  from PyQt6.QtWidgets import QApplication as application_t
17
17
 
18
18
 
19
- def Show(figure: figure_t, /) -> None:
19
+ def ShowHTMLWithPyQt(html: str | bytes, uid: int, /) -> None:
20
20
  """"""
21
+ if isinstance(html, bytes):
22
+ html = gzip.decompress(html).decode()
23
+
21
24
  # The application must be instantiated in the same thread/process as the one running
22
25
  # exec().
23
26
  if (application := application_t.instance()) is None:
24
27
  application = application_t(s.argv)
25
- application.setApplicationName(f"{NAME}-{id(figure)}")
26
- widget = widget_t()
28
+ application.setApplicationName(f"{NAME}-{uid}")
27
29
 
28
- html = figure.AsHTML()
29
- SetHTMLFromFile = lambda _: _SetHTMLFromFile(_, widget, html)
30
- widget.loadFinished.connect(SetHTMLFromFile)
31
- widget.setHtml(html)
30
+ widget = widget_t()
31
+ SetURLIfNeeded = lambda _: _SetURLIfNeeded(_, html, widget, application)
32
+ widget.loadFinished.connect(SetURLIfNeeded)
33
+ widget.setHtml(html) # Now attempt to set HTML; If failure, then SetURLIfNeeded.
32
34
 
33
35
  widget.show()
34
36
  application.exec()
@@ -39,35 +41,34 @@ def ShowAllFigures(
39
41
  modal: bool = True,
40
42
  ) -> None:
41
43
  """"""
42
- # TODO: Is this related to backend thread support (see associated constant)?
43
- if all(_.backend_name == "matplotlib" for _ in UN_SHOWN_FIGURES):
44
- for figure in UN_SHOWN_FIGURES:
45
- figure.AdjustLayout()
46
-
47
- SHOWN_FIGURES.extend(UN_SHOWN_FIGURES) # /!\ Must be removed when closed.
48
- UN_SHOWN_FIGURES.clear()
49
-
50
- import matplotlib.pyplot as pypl # noqa
44
+ n_figures = UN_SHOWN_FIGURES.__len__()
45
+ while n_figures > 0:
46
+ UN_SHOWN_FIGURES[0].Show(modal=modal and (n_figures == 1))
47
+ n_figures -= 1
51
48
 
52
- pypl.show()
53
- else:
54
- while UN_SHOWN_FIGURES.__len__() > 1:
55
- UN_SHOWN_FIGURES[0].Show(modal=False)
56
- if UN_SHOWN_FIGURES.__len__() > 0:
57
- UN_SHOWN_FIGURES[0].Show(modal=modal)
49
+ assert UN_SHOWN_FIGURES.__len__() == 0
58
50
 
51
+ if modal:
52
+ WaitForAllFiguresToBeClosed()
59
53
 
60
- # def CloseFigure(figure: figure_t, /)->None:
61
- # """"""
62
54
 
55
+ def WaitForAllFiguresToBeClosed() -> None:
56
+ """"""
57
+ if SHOWN_FIGURES.__len__() == 0:
58
+ return
63
59
 
64
- # def CloseAllFigures()->None:
65
- # """"""
60
+ with_processes = tuple(_ for _ in SHOWN_FIGURES if _.showing_process is not None)
61
+ for figure in with_processes:
62
+ figure.showing_process.join()
63
+ figure.AcknowledgeClosure()
66
64
 
67
65
 
68
- def _SetHTMLFromFile(success: bool, widget: widget_t, html: str, /) -> None:
66
+ def _SetURLIfNeeded(
67
+ success: bool, html: str, widget: widget_t, application: application_t, /
68
+ ) -> None:
69
69
  """
70
- From: https://doc.qt.io/qtforpython-6/PySide6/QtWebEngineWidgets/QWebEngineView.html#PySide6.QtWebEngineWidgets.PySide6.QtWebEngineWidgets.QWebEngineView.setHtml
70
+ From: https://doc.qt.io/qtforpython-6/PySide6/QtWebEngineWidgets/QWebEngineView.html
71
+ #PySide6.QtWebEngineWidgets.PySide6.QtWebEngineWidgets.QWebEngineView.setHtml
71
72
  Content larger than 2 MB cannot be displayed...
72
73
  ...
73
74
  Thereby, the provided code becomes a URL that exceeds the 2 MB limit set by
@@ -82,9 +83,11 @@ def _SetHTMLFromFile(success: bool, widget: widget_t, html: str, /) -> None:
82
83
  with open(transfer.name, "w") as accessor:
83
84
  accessor.write(html)
84
85
  url = url_t.fromLocalFile(transfer.name)
86
+
85
87
  widget.setUrl(url)
88
+
86
89
  DeleteTemporaryFile = lambda: osys.remove(transfer.name)
87
- application_t.instance().lastWindowClosed.connect(DeleteTemporaryFile)
90
+ application.lastWindowClosed.connect(DeleteTemporaryFile)
88
91
 
89
92
 
90
93
  """
@@ -74,25 +74,42 @@ class backend_t:
74
74
  out_kwargs = {}
75
75
 
76
76
  for idx, value in enumerate(args):
77
- if (translation := translations.get((who_s_asking, idx))) is None:
78
- # The passed arg should indeed be an arg.
77
+ if (translation := translations.get((who_s_asking, idx), -1)) == -1:
78
+ # No translation needed; Keeping the passed arg.
79
79
  out_args.append(value)
80
- else:
80
+ elif translation is None:
81
+ # The argument should be discarded.
82
+ pass
83
+ else: # isinstance(translation, str)
81
84
  # The passed arg should actually be a kwarg with name "translation".
82
85
  out_kwargs[translation] = value
83
86
 
84
87
  from_kwargs_to_args = []
85
88
  for key, value in kwargs.items():
86
89
  # Search for specific translation (who_s_asking, key) before key alone.
87
- if (translation := translations.get((who_s_asking, key))) is None:
88
- out_kwargs[translations.get(key, key)] = value
89
- elif isinstance(translation, int):
90
- from_kwargs_to_args.append((translation, value))
91
- else:
90
+ if (translation := translations.get((who_s_asking, key), -1)) == -1:
91
+ if (translation := translations.get(key, -1)) == -1:
92
+ # No translation needed; Keeping the passed arg.
93
+ out_kwargs[key] = value
94
+ elif translation is None:
95
+ # The argument should be discarded.
96
+ pass
97
+ else: # isinstance(translation, str)
98
+ out_kwargs[translation] = value
99
+ elif translation is None:
100
+ # The argument should be discarded.
101
+ pass
102
+ elif isinstance(translation, str):
92
103
  out_kwargs[translation] = value
93
- for value, idx in sorted(from_kwargs_to_args, key=ItemAt(0)):
104
+ else: # isinstance(translation, int)
105
+ from_kwargs_to_args.append((translation, value))
106
+
107
+ for idx, value in sorted(from_kwargs_to_args, key=ItemAt(0)):
94
108
  out_args.insert(idx, value)
95
109
 
110
+ if (ApplyGlobalTranslation := translations.get(None)) is not None:
111
+ ApplyGlobalTranslation(out_kwargs, who_s_asking)
112
+
96
113
  return out_args, out_kwargs
97
114
 
98
115
  def __str__(self) -> str:
@@ -30,6 +30,7 @@ class figure_t(base_t):
30
30
  frames: list[frame_t] = d.field(init=False, default_factory=list)
31
31
  locations: list[tuple[int, int]] = d.field(init=False, default_factory=list)
32
32
  shape: list[int] = d.field(init=False, default_factory=_DefaultShape)
33
+ showing_process: process_t | None = d.field(init=False, default=None)
33
34
 
34
35
  def __post_init__(self) -> None:
35
36
  """"""
@@ -62,8 +63,7 @@ class figure_t(base_t):
62
63
 
63
64
  return output
64
65
 
65
- @staticmethod
66
- def _NewBackendFigure(*args, **kwargs) -> backend_figure_h:
66
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_h:
67
67
  """"""
68
68
  raise NotImplementedError
69
69
 
@@ -180,10 +180,6 @@ class figure_t(base_t):
180
180
  """"""
181
181
  raise NotImplementedError
182
182
 
183
- def AsHTML(self) -> str:
184
- """"""
185
- raise NotImplementedError
186
-
187
183
  def Clear(self) -> None:
188
184
  """"""
189
185
  # Do not use a for-loop since self.frames will be modified during looping.
@@ -211,16 +207,20 @@ class figure_t(base_t):
211
207
  SHOWN_FIGURES.append(self) # /!\ Must be removed when closed.
212
208
  UN_SHOWN_FIGURES.remove(self)
213
209
 
214
- if modal:
215
- self._BackendShow()
216
- else:
217
- thread = process_t(target=lambda: self._BackendShow())
218
- thread.start()
210
+ self._BackendShow(modal)
219
211
 
220
- def _BackendShow(self) -> None:
212
+ def _BackendShow(self, modal: bool, /) -> None:
221
213
  """"""
222
214
  raise NotImplementedError
223
215
 
216
+ def AcknowledgeClosure(self) -> None:
217
+ """"""
218
+ if self in SHOWN_FIGURES:
219
+ SHOWN_FIGURES.remove(self)
220
+ else:
221
+ assert self in UN_SHOWN_FIGURES
222
+ UN_SHOWN_FIGURES.remove(self)
223
+
224
224
 
225
225
  """
226
226
  COPYRIGHT NOTICE
@@ -145,10 +145,10 @@ class plot_e(enum_t):
145
145
  )
146
146
  SCATTER = (
147
147
  "Set of Points",
148
- "A set of points X, (X, Y), or (X, Y, Z) depending on the dimension. They are plotted using a so-called "
148
+ "A set of points (X, Y) or (X, Y, Z) depending on the dimension. They are plotted using a so-called "
149
149
  "marker.\n"
150
150
  "Required arguments: X coordinates, Y coordinates, Z coordinates (depending on the dimension).",
151
- (1, 2, 3),
151
+ (2, 3),
152
152
  )
153
153
  TEXT = (
154
154
  "Text annotation",
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2022
4
4
  SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
- __version__ = "2025.6"
7
+ __version__ = "2025.7"
8
8
 
9
9
  """
10
10
  COPYRIGHT NOTICE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: babelplot
3
- Version: 2025.6
3
+ Version: 2025.7
4
4
  Summary: A Meta Plotting Library That Speaks Several Backends
5
5
  Home-page: https://src.koda.cnrs.fr/eric.debreuve/babelplot/
6
6
  Author: Eric Debreuve
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes