maxplotlibx 0.1.8__tar.gz → 0.1.9__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.
- {maxplotlibx-0.1.8/src/maxplotlibx.egg-info → maxplotlibx-0.1.9}/PKG-INFO +1 -1
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/pyproject.toml +1 -1
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/canvas/canvas.py +203 -5
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/subfigure/line_plot.py +620 -74
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_canvas.py +52 -0
- maxplotlibx-0.1.9/src/maxplotlib/tests/test_plotly_backend.py +684 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9/src/maxplotlibx.egg-info}/PKG-INFO +1 -1
- maxplotlibx-0.1.8/src/maxplotlib/tests/test_plotly_backend.py +0 -338
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/LICENSE +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/README.md +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/setup.cfg +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/backends/matplotlib/utils.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/backends/plotext/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/backends/plotext/figure.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/backends/plotly/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/backends/plotly/utils.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/canvas/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/colors/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/colors/colors.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/linestyle/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/linestyle/linestyle.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/objects/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/objects/node.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/objects/path.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/subfigure/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/subfigure/subfigure.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_flame_chart.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_gantt_chart.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_imports.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_plot.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_plotext.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/tests/test_styles_and_colors.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/utils/__init__.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlib/utils/options.py +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlibx.egg-info/SOURCES.txt +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlibx.egg-info/dependency_links.txt +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlibx.egg-info/requires.txt +0 -0
- {maxplotlibx-0.1.8 → maxplotlibx-0.1.9}/src/maxplotlibx.egg-info/top_level.txt +0 -0
|
@@ -71,6 +71,44 @@ def _running_in_jupyter() -> bool:
|
|
|
71
71
|
return shell is not None and "IPKernelApp" in getattr(shell, "config", {})
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
_MATPLOTLIB_LOC_TO_PLOTLY_ANCHOR = {
|
|
75
|
+
"upper right": dict(x=1.0, y=1.0, xanchor="right", yanchor="top"),
|
|
76
|
+
"upper left": dict(x=0.0, y=1.0, xanchor="left", yanchor="top"),
|
|
77
|
+
"lower right": dict(x=1.0, y=0.0, xanchor="right", yanchor="bottom"),
|
|
78
|
+
"lower left": dict(x=0.0, y=0.0, xanchor="left", yanchor="bottom"),
|
|
79
|
+
"upper center": dict(x=0.5, y=1.0, xanchor="center", yanchor="top"),
|
|
80
|
+
"lower center": dict(x=0.5, y=0.0, xanchor="center", yanchor="bottom"),
|
|
81
|
+
"center left": dict(x=0.0, y=0.5, xanchor="left", yanchor="middle"),
|
|
82
|
+
"center right": dict(x=1.0, y=0.5, xanchor="right", yanchor="middle"),
|
|
83
|
+
"center": dict(x=0.5, y=0.5, xanchor="center", yanchor="middle"),
|
|
84
|
+
"right": dict(x=1.0, y=0.5, xanchor="right", yanchor="middle"),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _plotly_legend_kwargs(legend_kwargs):
|
|
89
|
+
"""Translate Matplotlib ``ax.legend()`` keywords into Plotly's legend.
|
|
90
|
+
|
|
91
|
+
Only the settings that map cleanly are translated (position via ``loc``,
|
|
92
|
+
``title``, ``fontsize``, and a two-or-more-column ``ncol`` as horizontal
|
|
93
|
+
orientation); "best"/unrecognized locations and other Matplotlib-only
|
|
94
|
+
legend options (``handlelength``, ``framealpha``, ...) are left at
|
|
95
|
+
Plotly's defaults rather than guessed at.
|
|
96
|
+
"""
|
|
97
|
+
if not legend_kwargs:
|
|
98
|
+
return {}
|
|
99
|
+
layout = {}
|
|
100
|
+
loc = legend_kwargs.get("loc")
|
|
101
|
+
if isinstance(loc, str):
|
|
102
|
+
layout.update(_MATPLOTLIB_LOC_TO_PLOTLY_ANCHOR.get(loc, {}))
|
|
103
|
+
if legend_kwargs.get("title") is not None:
|
|
104
|
+
layout["title"] = dict(text=legend_kwargs["title"])
|
|
105
|
+
if legend_kwargs.get("fontsize") is not None:
|
|
106
|
+
layout["font"] = dict(size=legend_kwargs["fontsize"])
|
|
107
|
+
if legend_kwargs.get("ncol", legend_kwargs.get("ncols", 1)) not in (None, 1):
|
|
108
|
+
layout["orientation"] = "h"
|
|
109
|
+
return layout
|
|
110
|
+
|
|
111
|
+
|
|
74
112
|
def _apply_matplotlib_customizations(fig, axes, customizations) -> None:
|
|
75
113
|
"""Apply declarative method calls to a Matplotlib figure and its axes."""
|
|
76
114
|
if callable(customizations):
|
|
@@ -247,6 +285,17 @@ def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None):
|
|
|
247
285
|
|
|
248
286
|
|
|
249
287
|
class Canvas:
|
|
288
|
+
"""A figure made of one or more subplots, rendered by any backend.
|
|
289
|
+
|
|
290
|
+
The drawing methods mirror Matplotlib's ``Axes`` API and forward their
|
|
291
|
+
keyword arguments to the selected backend. Two keyword arguments are
|
|
292
|
+
handled by maxplotlib itself and accepted by every drawing method:
|
|
293
|
+
``hover=`` (hover text, used by the Plotly backend, ignored elsewhere) and
|
|
294
|
+
``meta=`` (an opaque tag set as ``meta`` on Plotly traces and as ``gid``
|
|
295
|
+
on Matplotlib artists, so the artists can be found by identity rather than
|
|
296
|
+
by drawing order). See :class:`~maxplotlib.subfigure.line_plot.LinePlot`.
|
|
297
|
+
"""
|
|
298
|
+
|
|
250
299
|
def __init__(
|
|
251
300
|
self,
|
|
252
301
|
nrows: int = 1,
|
|
@@ -312,6 +361,7 @@ class Canvas:
|
|
|
312
361
|
self._matplotlib_fig = None
|
|
313
362
|
self._matplotlib_axes = None
|
|
314
363
|
self._plotext_figure = None
|
|
364
|
+
self._plotly_fig = None
|
|
315
365
|
self._suptitle: str | None = None
|
|
316
366
|
self._suptitle_kwargs: dict = {}
|
|
317
367
|
self._supxlabel: str | None = None
|
|
@@ -326,6 +376,7 @@ class Canvas:
|
|
|
326
376
|
self._align_xlabels = False
|
|
327
377
|
self._align_ylabels = False
|
|
328
378
|
self._autofmt_xdate_kwargs: dict | None = None
|
|
379
|
+
self._barmode: str | None = None
|
|
329
380
|
|
|
330
381
|
# Dictionary to store lines for each subplot
|
|
331
382
|
# Key: (row, col), Value: list of lines with their data and kwargs
|
|
@@ -560,7 +611,9 @@ class Canvas:
|
|
|
560
611
|
height (array-like): Heights of the bars.
|
|
561
612
|
layer (int): Layer index (default 0).
|
|
562
613
|
row, col (int): Subplot position (default top-left).
|
|
563
|
-
**kwargs: Forwarded to the backend (e.g., color, width,
|
|
614
|
+
**kwargs: Forwarded to the backend (e.g., color, width, bottom,
|
|
615
|
+
alpha, edgecolor, label). ``bottom`` stacks the bars on every
|
|
616
|
+
backend.
|
|
564
617
|
"""
|
|
565
618
|
sp = self._get_or_create_subplot(row, col)
|
|
566
619
|
sp.bar(x, height, layer=layer, **kwargs)
|
|
@@ -995,7 +1048,10 @@ class Canvas:
|
|
|
995
1048
|
start_times (array-like, optional): Start times for each frame. If None, computed from hierarchy.
|
|
996
1049
|
layer (int): Layer index (default 0).
|
|
997
1050
|
row, col (int): Subplot position (default top-left).
|
|
998
|
-
**kwargs: Forwarded to the backend (e.g., colormap,
|
|
1051
|
+
**kwargs: Forwarded to the backend (e.g., colormap, colors,
|
|
1052
|
+
edgecolor, label). ``colormap`` accepts a Matplotlib colormap name
|
|
1053
|
+
or a Plotly colorscale name on either backend; ``colors`` sets an
|
|
1054
|
+
explicit color per frame.
|
|
999
1055
|
"""
|
|
1000
1056
|
sp = self._get_or_create_subplot(row, col)
|
|
1001
1057
|
sp.flame_chart(
|
|
@@ -1086,6 +1142,24 @@ class Canvas:
|
|
|
1086
1142
|
self.tight_layout()
|
|
1087
1143
|
return self
|
|
1088
1144
|
|
|
1145
|
+
def set_barmode(self, mode: str | None):
|
|
1146
|
+
"""Set how overlapping bars are arranged in the Plotly backend.
|
|
1147
|
+
|
|
1148
|
+
``mode`` is one of Plotly's ``"group"``, ``"overlay"``, ``"stack"``
|
|
1149
|
+
or ``"relative"``, or ``None`` to let maxplotlib decide. By default
|
|
1150
|
+
the canvas uses ``"overlay"`` whenever any bar positions itself with
|
|
1151
|
+
an explicit base (``bottom=``/``left=``, Gantt rows, flame frames),
|
|
1152
|
+
because Plotly's default grouping would re-offset those bars away
|
|
1153
|
+
from the positions the caller asked for. Ignored by the other
|
|
1154
|
+
backends, which have no equivalent layout-level setting.
|
|
1155
|
+
"""
|
|
1156
|
+
self._barmode = mode
|
|
1157
|
+
return self
|
|
1158
|
+
|
|
1159
|
+
@property
|
|
1160
|
+
def barmode(self):
|
|
1161
|
+
return self._barmode
|
|
1162
|
+
|
|
1089
1163
|
def set_xlim(
|
|
1090
1164
|
self, left=None, right=None, row: int | None = None, col: int | None = None
|
|
1091
1165
|
):
|
|
@@ -1105,10 +1179,22 @@ class Canvas:
|
|
|
1105
1179
|
self._get_or_create_subplot(row, col).set_grid(visible)
|
|
1106
1180
|
|
|
1107
1181
|
def set_legend(
|
|
1108
|
-
self,
|
|
1182
|
+
self,
|
|
1183
|
+
visible: bool = True,
|
|
1184
|
+
row: int | None = None,
|
|
1185
|
+
col: int | None = None,
|
|
1186
|
+
**kwargs,
|
|
1109
1187
|
):
|
|
1110
|
-
"""Show or hide the legend for a subplot (default top-left).
|
|
1111
|
-
|
|
1188
|
+
"""Show or hide the legend for a subplot (default top-left).
|
|
1189
|
+
|
|
1190
|
+
``**kwargs`` are forwarded to ``ax.legend()`` on Matplotlib; ``loc``,
|
|
1191
|
+
``title``, ``fontsize`` and ``ncol``/``ncols`` are also translated for
|
|
1192
|
+
the Plotly backend's legend.
|
|
1193
|
+
"""
|
|
1194
|
+
self._get_or_create_subplot(row, col).set_legend(visible, **kwargs)
|
|
1195
|
+
|
|
1196
|
+
def legend(self, row=None, col=None, **kwargs):
|
|
1197
|
+
self._get_or_create_subplot(row, col).set_legend(**kwargs)
|
|
1112
1198
|
|
|
1113
1199
|
def legend(self, row=None, col=None, **kwargs):
|
|
1114
1200
|
self._get_or_create_subplot(row, col).set_legend(**kwargs)
|
|
@@ -1992,6 +2078,63 @@ class Canvas:
|
|
|
1992
2078
|
"""
|
|
1993
2079
|
return self._render(*args, **kwargs)
|
|
1994
2080
|
|
|
2081
|
+
# ------------------------------------------------------------------
|
|
2082
|
+
# Escape hatches: get the native object a backend builds, to edit with
|
|
2083
|
+
# that backend's own (more mature) API for anything maxplotlib doesn't
|
|
2084
|
+
# wrap. Each is a thin, discoverable alias for the corresponding
|
|
2085
|
+
# ``plot_<backend>()`` method; none of them display the result.
|
|
2086
|
+
# ------------------------------------------------------------------
|
|
2087
|
+
|
|
2088
|
+
def get_matplotlib_figaxs(
|
|
2089
|
+
self,
|
|
2090
|
+
layers: list | None = None,
|
|
2091
|
+
usetex: bool | None = None,
|
|
2092
|
+
verbose: bool = False,
|
|
2093
|
+
matplotlib_postprocess=None,
|
|
2094
|
+
matplotlib_customizations=None,
|
|
2095
|
+
):
|
|
2096
|
+
"""Render with Matplotlib and return ``(figure, axes)`` to edit directly.
|
|
2097
|
+
|
|
2098
|
+
Use this to reach any Matplotlib API maxplotlib doesn't wrap natively
|
|
2099
|
+
(custom artists, fine-grained styling, ``savefig`` options, ...).
|
|
2100
|
+
Further calls on the canvas after this one will not retroactively
|
|
2101
|
+
affect the returned objects; re-call to pick up later changes.
|
|
2102
|
+
"""
|
|
2103
|
+
return self.plot_matplotlib(
|
|
2104
|
+
layers=layers,
|
|
2105
|
+
usetex=usetex,
|
|
2106
|
+
verbose=verbose,
|
|
2107
|
+
matplotlib_postprocess=matplotlib_postprocess,
|
|
2108
|
+
matplotlib_customizations=matplotlib_customizations,
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
def get_plotly_fig(
|
|
2112
|
+
self,
|
|
2113
|
+
layers: list | None = None,
|
|
2114
|
+
usetex: bool | None = None,
|
|
2115
|
+
verbose: bool = False,
|
|
2116
|
+
allow_unsupported: bool = False,
|
|
2117
|
+
):
|
|
2118
|
+
"""Render with Plotly and return the ``go.Figure`` to edit directly.
|
|
2119
|
+
|
|
2120
|
+
Use this to reach any Plotly API maxplotlib doesn't wrap natively
|
|
2121
|
+
(``fig.update_traces()``, custom layout, ``fig.write_html()``, ...).
|
|
2122
|
+
"""
|
|
2123
|
+
return self.plot_plotly(
|
|
2124
|
+
layers=layers,
|
|
2125
|
+
usetex=usetex,
|
|
2126
|
+
verbose=verbose,
|
|
2127
|
+
allow_unsupported=allow_unsupported,
|
|
2128
|
+
)
|
|
2129
|
+
|
|
2130
|
+
def get_plotext_fig(self, layers: list | None = None, verbose: bool = False):
|
|
2131
|
+
"""Render with plotext and return the ``PlotextFigure`` to edit directly."""
|
|
2132
|
+
return self.plot_plotext(layers=layers, verbose=verbose)
|
|
2133
|
+
|
|
2134
|
+
def get_tikz_figure(self, verbose: bool = False):
|
|
2135
|
+
"""Render with TikZ and return the ``TikzFigure`` to edit directly."""
|
|
2136
|
+
return self.plot_tikzfigure(verbose=verbose)
|
|
2137
|
+
|
|
1995
2138
|
def show(
|
|
1996
2139
|
self,
|
|
1997
2140
|
backend: Backends = "matplotlib",
|
|
@@ -2776,6 +2919,7 @@ class Canvas:
|
|
|
2776
2919
|
fig.update_xaxes(
|
|
2777
2920
|
tickmode="array",
|
|
2778
2921
|
ticktext=line_plot._xticklabels,
|
|
2922
|
+
tickangle=line_plot._xticklabel_kwargs.get("rotation"),
|
|
2779
2923
|
tickfont={
|
|
2780
2924
|
key: line_plot._xticklabel_kwargs[key]
|
|
2781
2925
|
for key in ("size", "color", "family")
|
|
@@ -2788,6 +2932,7 @@ class Canvas:
|
|
|
2788
2932
|
fig.update_yaxes(
|
|
2789
2933
|
tickmode="array",
|
|
2790
2934
|
ticktext=line_plot._yticklabels,
|
|
2935
|
+
tickangle=line_plot._yticklabel_kwargs.get("rotation"),
|
|
2791
2936
|
tickfont={
|
|
2792
2937
|
key: line_plot._yticklabel_kwargs[key]
|
|
2793
2938
|
for key in ("size", "color", "family")
|
|
@@ -2797,6 +2942,35 @@ class Canvas:
|
|
|
2797
2942
|
col=col + 1,
|
|
2798
2943
|
)
|
|
2799
2944
|
|
|
2945
|
+
# tick_params() is the neutral place to ask for rotated or
|
|
2946
|
+
# restyled tick labels; translate the Matplotlib spelling into
|
|
2947
|
+
# the Plotly axis properties.
|
|
2948
|
+
tick_params = line_plot._tick_params
|
|
2949
|
+
if tick_params:
|
|
2950
|
+
axis = tick_params.get("axis", "both")
|
|
2951
|
+
rotation = tick_params.get("labelrotation", tick_params.get("rotation"))
|
|
2952
|
+
tickfont = {}
|
|
2953
|
+
if tick_params.get("labelsize") is not None:
|
|
2954
|
+
tickfont["size"] = tick_params["labelsize"]
|
|
2955
|
+
if tick_params.get("labelcolor") is not None:
|
|
2956
|
+
tickfont["color"] = tick_params["labelcolor"]
|
|
2957
|
+
tick_kwargs = {}
|
|
2958
|
+
if rotation is not None:
|
|
2959
|
+
tick_kwargs["tickangle"] = rotation
|
|
2960
|
+
if tickfont:
|
|
2961
|
+
tick_kwargs["tickfont"] = tickfont
|
|
2962
|
+
if tick_params.get("color") is not None:
|
|
2963
|
+
tick_kwargs["tickcolor"] = tick_params["color"]
|
|
2964
|
+
if tick_params.get("length") is not None:
|
|
2965
|
+
tick_kwargs["ticklen"] = tick_params["length"]
|
|
2966
|
+
if tick_params.get("width") is not None:
|
|
2967
|
+
tick_kwargs["tickwidth"] = tick_params["width"]
|
|
2968
|
+
if tick_kwargs:
|
|
2969
|
+
if axis in ("x", "both"):
|
|
2970
|
+
fig.update_xaxes(row=row + 1, col=col + 1, **tick_kwargs)
|
|
2971
|
+
if axis in ("y", "both"):
|
|
2972
|
+
fig.update_yaxes(row=row + 1, col=col + 1, **tick_kwargs)
|
|
2973
|
+
|
|
2800
2974
|
if line_plot._axis_settings:
|
|
2801
2975
|
axis_args = line_plot._axis_settings.get("args", ())
|
|
2802
2976
|
if axis_args and axis_args[0] == "off":
|
|
@@ -2825,6 +2999,29 @@ class Canvas:
|
|
|
2825
2999
|
font=dict(size=self.fontsize),
|
|
2826
3000
|
margin=dict(l=10, r=10, t=40, b=10),
|
|
2827
3001
|
)
|
|
3002
|
+
barmode = self._barmode
|
|
3003
|
+
if barmode is None:
|
|
3004
|
+
barmode = next(
|
|
3005
|
+
(
|
|
3006
|
+
subplot._plotly_barmode_hint
|
|
3007
|
+
for subplot in self._subplot_dict.values()
|
|
3008
|
+
if getattr(subplot, "_plotly_barmode_hint", None) is not None
|
|
3009
|
+
),
|
|
3010
|
+
None,
|
|
3011
|
+
)
|
|
3012
|
+
if barmode is not None:
|
|
3013
|
+
fig.update_layout(barmode=barmode)
|
|
3014
|
+
legend_kwargs = next(
|
|
3015
|
+
(
|
|
3016
|
+
subplot._legend_kwargs
|
|
3017
|
+
for subplot in self._subplot_dict.values()
|
|
3018
|
+
if subplot._legend and subplot._legend_kwargs
|
|
3019
|
+
),
|
|
3020
|
+
None,
|
|
3021
|
+
)
|
|
3022
|
+
legend_layout = _plotly_legend_kwargs(legend_kwargs)
|
|
3023
|
+
if legend_layout:
|
|
3024
|
+
fig.update_layout(legend=legend_layout)
|
|
2828
3025
|
if self._suptitle:
|
|
2829
3026
|
fig.update_layout(title=dict(text=self._suptitle, x=0.5))
|
|
2830
3027
|
|
|
@@ -2837,6 +3034,7 @@ class Canvas:
|
|
|
2837
3034
|
"install kaleido (e.g., `pip install -U kaleido`)."
|
|
2838
3035
|
) from exc
|
|
2839
3036
|
|
|
3037
|
+
self._plotly_fig = fig
|
|
2840
3038
|
return fig
|
|
2841
3039
|
|
|
2842
3040
|
def _save_plotly(self, fig, filename: str) -> None:
|