babelplot 2025.6__tar.gz → 2025.8__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.8}/PKG-INFO +1 -1
  2. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/catalog/bokeh_.py +48 -9
  3. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/catalog/matplotlib_.py +87 -73
  4. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/catalog/plotly_.py +221 -148
  5. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/catalog/vedo_.py +37 -6
  6. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/helper/validation.py +2 -0
  7. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/task/showing.py +33 -30
  8. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/backend.py +49 -12
  9. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/figure.py +13 -13
  10. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/plot_type.py +2 -2
  11. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/version.py +1 -1
  12. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot.egg-info/PKG-INFO +1 -1
  13. {babelplot-2025.6 → babelplot-2025.8}/MANIFEST.in +0 -0
  14. {babelplot-2025.6 → babelplot-2025.8}/README-COPYRIGHT-utf8.txt +0 -0
  15. {babelplot-2025.6 → babelplot-2025.8}/README-LICENCE-utf8.txt +0 -0
  16. {babelplot-2025.6 → babelplot-2025.8}/README.rst +0 -0
  17. {babelplot-2025.6 → babelplot-2025.8}/documentation/wiki/description.asciidoc +0 -0
  18. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/__init__.py +0 -0
  19. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/api/backend.py +0 -0
  20. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/api/definition.py +0 -0
  21. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/api/help.py +0 -0
  22. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/backend/helper/creation.py +0 -0
  23. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/constant/backend.py +0 -0
  24. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/constant/path.py +0 -0
  25. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/constant/project.py +0 -0
  26. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/extension/enum_.py +0 -0
  27. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/extension/function.py +0 -0
  28. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/runtime/backends.py +0 -0
  29. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/runtime/figure.py +0 -0
  30. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/task/help.py +0 -0
  31. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/task/instantiation.py +0 -0
  32. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/dimension.py +0 -0
  33. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/ffp_base.py +0 -0
  34. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/frame.py +0 -0
  35. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/plot.py +0 -0
  36. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot/type/translator.py +0 -0
  37. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot.egg-info/SOURCES.txt +0 -0
  38. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot.egg-info/dependency_links.txt +0 -0
  39. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot.egg-info/requires.txt +0 -0
  40. {babelplot-2025.6 → babelplot-2025.8}/package/babelplot.egg-info/top_level.txt +0 -0
  41. {babelplot-2025.6 → babelplot-2025.8}/pyproject.toml +0 -0
  42. {babelplot-2025.6 → babelplot-2025.8}/requirements.txt +0 -0
  43. {babelplot-2025.6 → babelplot-2025.8}/setup.cfg +0 -0
  44. {babelplot-2025.6 → babelplot-2025.8}/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.8
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
@@ -5,9 +5,11 @@ SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
7
  import dataclasses as d
8
+ import gzip
8
9
  import typing as h
10
+ from multiprocessing import Process as process_t
9
11
 
10
- from babelplot.task.showing import Show
12
+ from babelplot.task.showing import ShowHTMLWithPyQt
11
13
  from babelplot.type.dimension import dim_e
12
14
  from babelplot.type.figure import figure_t as base_figure_t
13
15
  from babelplot.type.frame import frame_t as base_frame_t
@@ -21,8 +23,6 @@ from bokeh.models.renderers import GlyphRenderer as backend_plot_t # noqa
21
23
  from bokeh.plotting import figure as backend_figure_t # noqa
22
24
  from bokeh.resources import INLINE # noqa
23
25
 
24
- # from bokeh.layouts import LayoutDOM as backend_content_t # noqa
25
-
26
26
  NAME = "bokeh"
27
27
 
28
28
 
@@ -59,8 +59,9 @@ 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
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
63
+ """"""
64
+ return backend_figure_t(*args, **kwargs)
64
65
 
65
66
  def _NewFrame(
66
67
  self,
@@ -78,17 +79,34 @@ class figure_t(base_figure_t):
78
79
  backend_name=self.backend_name,
79
80
  )
80
81
 
81
- output.raw = backend_frame_t(*args, title=title, **kwargs)
82
+ output.raw = backend_frame_t(title=title, **kwargs)
82
83
 
83
84
  return output
84
85
 
85
86
  def AdjustLayout(self) -> None:
86
87
  """"""
88
+ # from bokeh.layouts import LayoutDOM as backend_content_t # noqa
89
+
90
+ # arranged_frames must be composed of list since Bokeh does not support tuples
91
+ # here!
87
92
  n_rows, n_cols = self.shape
88
93
  arranged_frames = [n_cols * [None] for _ in range(n_rows)]
89
94
  for frame, (row, col) in zip(self.frames, self.locations):
90
95
  arranged_frames[row][col] = frame.raw
91
- arranged_frames: list[list[backend_frame_t]]
96
+
97
+ # Bokeh does not support inserting None as an indicator of empty space. As a
98
+ # workaround, the frames are currently flattened (and None_s are filtered out).
99
+ should_be_filtered = False
100
+ for one_row in arranged_frames:
101
+ if any(_ is None for _ in one_row):
102
+ should_be_filtered = True
103
+ break
104
+ if should_be_filtered:
105
+ arranged_frames = [_ for one_row in arranged_frames for _ in one_row]
106
+ arranged_frames = list(filter(lambda _: _ is not None, arranged_frames))
107
+ arranged_frames = [arranged_frames]
108
+ n_rows = 1
109
+ n_cols = arranged_frames[0].__len__()
92
110
 
93
111
  if n_rows > 1:
94
112
  if n_cols > 1:
@@ -101,9 +119,29 @@ class figure_t(base_figure_t):
101
119
 
102
120
  self.layout = layout
103
121
 
104
- def AsHTML(self) -> str:
122
+ def _BackendShow(self, modal: bool, /) -> None:
105
123
  """"""
106
- return HTMLofBackendContent(self.layout, INLINE)
124
+ try:
125
+ html = HTMLofBackendContent(self.layout, INLINE)
126
+ except ValueError as exception:
127
+ html = f"""
128
+ <!DOCTYPE html>
129
+ <html>
130
+ <body>
131
+ <h1>Figure cannot be shown</h1>
132
+ <p>{str(exception)}</p>
133
+ </body>
134
+ </html>
135
+ """
136
+ uid = id(self)
137
+ if modal:
138
+ ShowHTMLWithPyQt(html, uid)
139
+ self.AcknowledgeClosure()
140
+ else:
141
+ html = gzip.compress(html.encode())
142
+ process = process_t(target=lambda: ShowHTMLWithPyQt(html, uid))
143
+ process.start()
144
+ self.showing_process = process
107
145
 
108
146
 
109
147
  PLOTS = plot_e.NewPlotsTemplate()
@@ -112,6 +150,7 @@ PLOTS[plot_e.SCATTER][1] = backend_frame_t.scatter
112
150
 
113
151
  TRANSLATIONS = {
114
152
  "color_face": "fill_color",
153
+ "opacity": "alpha",
115
154
  }
116
155
 
117
156
 
@@ -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
@@ -27,7 +30,7 @@ array_t = nmpy.ndarray
27
30
 
28
31
 
29
32
  _FIGURE_CONFIG = {
30
- "toImageButtonOptions": { # TODO: Does not work: no PNG export produced
33
+ "toImageButtonOptions": { # TODO: No PNG export produced actually.
31
34
  "filename": str(path_t.home() / "plotly_figure"),
32
35
  "height": None,
33
36
  "width": None,
@@ -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
 
@@ -5,6 +5,7 @@ SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
7
  import dataclasses as d
8
+ from multiprocessing import Process as process_t
8
9
 
9
10
  import vedo # noqa
10
11
  from babelplot.type.dimension import dim_e
@@ -42,7 +43,8 @@ class frame_t(base_frame_t):
42
43
  )
43
44
 
44
45
  raw = plot_function(*args, **kwargs)
45
- self.raw.__iadd__(raw)
46
+ if raw is not None:
47
+ self.raw.__iadd__(raw)
46
48
 
47
49
  output.raw = raw
48
50
 
@@ -51,7 +53,10 @@ class frame_t(base_frame_t):
51
53
 
52
54
  @d.dataclass(slots=True, repr=False, eq=False)
53
55
  class figure_t(base_figure_t):
54
- _NewBackendFigure = backend_figure_t
56
+
57
+ def _NewBackendFigure(self, *args, **kwargs) -> backend_figure_t:
58
+ """"""
59
+ return backend_figure_t(*args, **kwargs)
55
60
 
56
61
  def _NewFrame(
57
62
  self,
@@ -69,13 +74,19 @@ class figure_t(base_figure_t):
69
74
  backend_name=self.backend_name,
70
75
  )
71
76
 
72
- output.raw = self
77
+ output.raw = self.raw
73
78
 
74
79
  return output
75
80
 
76
- def _Show(self) -> None:
81
+ def _BackendShow(self, modal: bool, /) -> None:
77
82
  """"""
78
- self.raw.show().close()
83
+ if modal:
84
+ self.raw.show().close()
85
+ self.AcknowledgeClosure()
86
+ else:
87
+ process = process_t(target=lambda: self.raw.show().close())
88
+ process.start()
89
+ self.showing_process = process
79
90
 
80
91
 
81
92
  def _IsoSurface(volume: array_t, iso_value: float, *_, **kwargs) -> backend_plot_t:
@@ -83,11 +94,31 @@ def _IsoSurface(volume: array_t, iso_value: float, *_, **kwargs) -> backend_plot
83
94
  return volume_t(volume).isosurface(value=[iso_value], **kwargs)
84
95
 
85
96
 
97
+ def _Mesh(triangles: array_t, vertices: array_t, *_, **kwargs) -> backend_plot_t:
98
+ """"""
99
+ output = backend_plot_t((vertices, triangles))
100
+
101
+ if "width_edge" in kwargs:
102
+ output.linewidth(kwargs["width_edge"])
103
+ if "color_edge" in kwargs:
104
+ output.linecolor(kwargs["color_edge"])
105
+ if "color_face" in kwargs:
106
+ output.color(c=kwargs["color_face"])
107
+
108
+ return output
109
+
110
+
86
111
  PLOTS = plot_e.NewPlotsTemplate()
87
112
  PLOTS[plot_e.ISOSET][2] = _IsoSurface
113
+ PLOTS[plot_e.MESH][2] = _Mesh
88
114
 
89
115
 
90
- TRANSLATIONS = {}
116
+ TRANSLATIONS = {
117
+ (_IsoSurface, "color_edge"): None,
118
+ (_IsoSurface, "color_face"): None,
119
+ (_IsoSurface, "step_size"): None,
120
+ (_IsoSurface, "width_edge"): None,
121
+ }
91
122
 
92
123
 
93
124
  """
@@ -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
  """
@@ -47,14 +47,17 @@ class backend_t:
47
47
  ) -> plot_function_h:
48
48
  """"""
49
49
  if (output := self.plot_functions.get(type_)) is None:
50
- raise ValueError(f"Unknown plot {type_}.")
50
+ L.error(f"Unknown plot {type_}.")
51
+ return _FakePlotFunction(type_, frame_dim)
51
52
 
52
53
  if output.__len__() <= frame_dim - 1:
53
- raise ValueError(f"Unknown plot {type_} for dimension {frame_dim}.")
54
+ L.error(f"Unknown plot {type_} for dimension {frame_dim}.")
55
+ return _FakePlotFunction(type_, frame_dim)
54
56
 
55
57
  output = output[frame_dim - 1]
56
58
  if output is None:
57
- raise ValueError(f"Unknown plot {type_} for dimension {frame_dim}.")
59
+ L.error(f"Unknown plot {type_} for dimension {frame_dim}.")
60
+ return _FakePlotFunction(type_, frame_dim)
58
61
 
59
62
  return output
60
63
 
@@ -73,26 +76,46 @@ class backend_t:
73
76
  out_args = []
74
77
  out_kwargs = {}
75
78
 
79
+ # TODO: Add the possibility to transform the arguments by letting "translation"
80
+ # being either a translation command or a tuple of a translation command and
81
+ # a conversion function. Useful for color format adaptation.
76
82
  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.
83
+ if (translation := translations.get((who_s_asking, idx), -1)) == -1:
84
+ # No translation needed; Keeping the passed arg.
79
85
  out_args.append(value)
80
- else:
86
+ elif translation is None:
87
+ # The argument should be discarded.
88
+ pass
89
+ else: # isinstance(translation, str)
81
90
  # The passed arg should actually be a kwarg with name "translation".
82
91
  out_kwargs[translation] = value
83
92
 
84
93
  from_kwargs_to_args = []
85
94
  for key, value in kwargs.items():
86
95
  # 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:
96
+ if (translation := translations.get((who_s_asking, key), -1)) == -1:
97
+ if (translation := translations.get(key, -1)) == -1:
98
+ # No translation needed; Keeping the passed arg.
99
+ out_kwargs[key] = value
100
+ elif translation is None:
101
+ # The argument should be discarded.
102
+ pass
103
+ else: # isinstance(translation, str)
104
+ out_kwargs[translation] = value
105
+ elif translation is None:
106
+ # The argument should be discarded.
107
+ pass
108
+ elif isinstance(translation, str):
92
109
  out_kwargs[translation] = value
93
- for value, idx in sorted(from_kwargs_to_args, key=ItemAt(0)):
110
+ else: # isinstance(translation, int)
111
+ from_kwargs_to_args.append((translation, value))
112
+
113
+ for idx, value in sorted(from_kwargs_to_args, key=ItemAt(0)):
94
114
  out_args.insert(idx, value)
95
115
 
116
+ if (ApplyGlobalTranslation := translations.get(None)) is not None:
117
+ ApplyGlobalTranslation(out_kwargs, who_s_asking)
118
+
96
119
  return out_args, out_kwargs
97
120
 
98
121
  def __str__(self) -> str:
@@ -232,6 +255,20 @@ class backends_t(dict[str, backend_t]):
232
255
  return "\n".join(output)
233
256
 
234
257
 
258
+ def _FakePlotFunction(type_: plot_e, frame_dim: int, /) -> h.Callable[..., None]:
259
+ """"""
260
+
261
+ def Actual(*args, **kwargs) -> None:
262
+ #
263
+ args = ", ".join(map(lambda _: type(_).__name__, args))
264
+ kwargs = ", ".join(
265
+ f"{_key}={type(_vle).__name__}" for _key, _vle in kwargs.items()
266
+ )
267
+ L.warning(f"Unhandled plot request: {type_.name}.{frame_dim}: {args}, {kwargs}")
268
+
269
+ return Actual
270
+
271
+
235
272
  """
236
273
  COPYRIGHT NOTICE
237
274
 
@@ -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.
@@ -204,23 +200,27 @@ class figure_t(base_t):
204
200
  raise RuntimeError(f"Figure {self.title} already shown.")
205
201
 
206
202
  if self.frames.__len__() == 0:
207
- L.warning(f"Figure {self.title} empty; Not shown.")
203
+ L.warning(f"Figure {self.title} empty; Not showing.")
208
204
  else:
209
205
  self.AdjustLayout()
210
206
 
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.8"
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.8
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