maxplotlibx 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl

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.
@@ -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, label).
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, edgecolor, label).
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, visible: bool = True, row: int | None = None, col: int | None = None
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
- self._get_or_create_subplot(row, col).set_legend(visible)
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: