matplotly 0.1.0__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.
@@ -0,0 +1,38 @@
1
+ """Base class for artist-specific control panels."""
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+
6
+ import ipywidgets as widgets
7
+
8
+ from .._commands import CommandStack
9
+ from .._renderer import CanvasManager
10
+ from .._types import ArtistGroup
11
+
12
+
13
+ class ArtistPanel(abc.ABC):
14
+ """Abstract base for a panel that controls one ArtistGroup."""
15
+
16
+ def __init__(self, group: ArtistGroup, stack: CommandStack,
17
+ canvas: CanvasManager):
18
+ self._group = group
19
+ self._stack = stack
20
+ self._canvas = canvas
21
+ self._widget: widgets.Widget | None = None
22
+
23
+ @abc.abstractmethod
24
+ def build(self) -> widgets.Widget:
25
+ """Construct and return the ipywidgets control panel."""
26
+
27
+ @property
28
+ def widget(self) -> widgets.Widget:
29
+ if self._widget is None:
30
+ self._widget = self.build()
31
+ return self._widget
32
+
33
+ # -- helpers -----------------------------------------------------------
34
+
35
+ def _execute_and_redraw(self, cmd) -> None:
36
+ """Execute a command and refresh the canvas."""
37
+ self._stack.execute(cmd)
38
+ self._canvas.redraw()
@@ -0,0 +1,221 @@
1
+ """Shared color utilities used by LinePanel, ScatterPanel, and GlobalPanel."""
2
+ from __future__ import annotations
3
+
4
+ import ipywidgets as widgets
5
+ import matplotlib
6
+ from matplotlib.colors import ListedColormap, to_hex
7
+
8
+ _COLORMAPS = [
9
+ # Qualitative
10
+ "tab10", "tab20", "Set1", "Set2", "Set3", "Paired",
11
+ "Dark2", "Accent", "Pastel1", "Pastel2",
12
+ # Diverging
13
+ "bwr", "coolwarm", "RdBu", "RdYlBu", "RdYlGn",
14
+ "PiYG", "PRGn", "seismic",
15
+ # Sequential
16
+ "viridis", "plasma", "inferno", "magma", "cividis",
17
+ "Blues", "Reds", "Greens", "Oranges", "Purples",
18
+ "YlOrRd", "YlGnBu", "BuGn",
19
+ # Cyclic
20
+ "twilight", "hsv",
21
+ ]
22
+
23
+ _DW = "48px" # uniform description_width
24
+ _NW = "50px" # uniform number-edit width
25
+ _SN = {"description_width": _DW}
26
+
27
+
28
+ def _slider_num(slider, desc_width=None):
29
+ """Slider (no readout) + linked number edit box (2 dp)."""
30
+ slider.readout = False
31
+ slider.style = {"description_width": desc_width or _DW}
32
+ slider.layout.flex = "1 1 auto"
33
+ if isinstance(slider, widgets.IntSlider):
34
+ num = widgets.IntText(value=slider.value,
35
+ layout=widgets.Layout(width=_NW))
36
+ else:
37
+ num = widgets.BoundedFloatText(
38
+ value=round(slider.value, 2), step=slider.step,
39
+ min=slider.min, max=slider.max,
40
+ layout=widgets.Layout(width=_NW))
41
+ widgets.link((slider, "value"), (num, "value"))
42
+ return widgets.HBox([slider, num],
43
+ layout=widgets.Layout(width="80%"))
44
+
45
+
46
+ def _cmap_color(cmap, i, n):
47
+ """Sample color i of n from a colormap.
48
+
49
+ For small qualitative colormaps (tab10, Set1, etc. with N <= 20), returns
50
+ sequential discrete colors (1st, 2nd, 3rd...). For everything else
51
+ (continuous or large-N listed colormaps like viridis with N=256),
52
+ interpolates evenly across the full [0,1] range.
53
+ """
54
+ if isinstance(cmap, ListedColormap) and cmap.N <= 20:
55
+ return cmap(i % cmap.N)
56
+ return cmap(i / max(n - 1, 1))
57
+
58
+
59
+ def _get_palette_colors(cmap_name, n=10):
60
+ """Get n hex colors from a colormap."""
61
+ try:
62
+ cmap = matplotlib.colormaps.get_cmap(cmap_name)
63
+ except Exception:
64
+ cmap = matplotlib.colormaps.get_cmap("tab10")
65
+ return [to_hex(_cmap_color(cmap, i, n)) for i in range(n)]
66
+
67
+
68
+ def _make_color_dot(hex_color):
69
+ return (f'<div style="width:14px;height:14px;background:{hex_color};'
70
+ f'border:1px solid #666;border-radius:2px;flex-shrink:0"></div>')
71
+
72
+
73
+ def cmap_color_btn(initial_color, on_change_fn, cmap_panel=None):
74
+ """Compact color button; click toggles colormap swatch row + palette.
75
+
76
+ *on_change_fn(hex_val)* is called when a new color is picked.
77
+ Returns ``(color_btn, swatch_row)`` widgets for flexible layout.
78
+ """
79
+ color_btn = widgets.Button(
80
+ layout=widgets.Layout(width='28px', height='24px',
81
+ padding='0', min_width='28px'),
82
+ tooltip="Click to choose color")
83
+ color_btn.style.button_color = initial_color
84
+
85
+ swatch_btns = []
86
+ for _ in range(10):
87
+ b = widgets.Button(
88
+ layout=widgets.Layout(width="18px", height="16px",
89
+ padding="0", margin="1px",
90
+ min_width="18px"))
91
+ swatch_btns.append(b)
92
+
93
+ palette_btn = widgets.Button(
94
+ icon="paint-brush", tooltip="Custom color...",
95
+ layout=widgets.Layout(width="18px", height="16px",
96
+ padding="0", min_width="18px",
97
+ margin="1px"))
98
+ palette_btn.style.button_color = "#e8e8e8"
99
+ palette_btn.add_class("pb-swatch-btn")
100
+
101
+ _pk_cls = f"pb-txtpk-{id(color_btn)}"
102
+ picker = widgets.ColorPicker(
103
+ value=initial_color, concise=True,
104
+ layout=widgets.Layout(width="1px", height="1px",
105
+ overflow="hidden", padding="0",
106
+ margin="0", border="0"))
107
+ picker.add_class(_pk_cls)
108
+
109
+ _js = widgets.Output(
110
+ layout=widgets.Layout(height="0px", overflow="hidden"))
111
+
112
+ swatch_row = widgets.HBox(
113
+ swatch_btns + [palette_btn, picker, _js],
114
+ layout=widgets.Layout(display='none', align_items='center',
115
+ gap='1px'))
116
+
117
+ _upd = [False]
118
+
119
+ def _refresh():
120
+ cname = 'tab10'
121
+ if cmap_panel and hasattr(cmap_panel, '_selected'):
122
+ cname = cmap_panel._selected
123
+ colors = _get_palette_colors(cname, 10)
124
+ for i, btn in enumerate(swatch_btns):
125
+ btn.style.button_color = colors[i]
126
+
127
+ def _apply(hex_val):
128
+ _upd[0] = True
129
+ color_btn.style.button_color = hex_val
130
+ picker.value = hex_val
131
+ _upd[0] = False
132
+ on_change_fn(hex_val)
133
+
134
+ for b in swatch_btns:
135
+ def _on_sw(btn, _b=b):
136
+ _apply(_b.style.button_color)
137
+ b.on_click(_on_sw)
138
+
139
+ def _toggle(btn):
140
+ if swatch_row.layout.display == 'none':
141
+ _refresh()
142
+ swatch_row.layout.display = ''
143
+ else:
144
+ swatch_row.layout.display = 'none'
145
+ color_btn.on_click(_toggle)
146
+
147
+ def _on_pal(b):
148
+ with _js:
149
+ _js.clear_output()
150
+ from IPython.display import display as ipy_display, Javascript
151
+ ipy_display(Javascript(
152
+ "setTimeout(function(){"
153
+ "var el=document.querySelector('.%s input[type=\"color\"]');"
154
+ "if(el)el.click();"
155
+ "},150);" % _pk_cls))
156
+ palette_btn.on_click(_on_pal)
157
+
158
+ def _on_pk(change):
159
+ if _upd[0]:
160
+ return
161
+ _apply(change["new"])
162
+ picker.observe(_on_pk, names="value")
163
+
164
+ return color_btn, swatch_row
165
+
166
+
167
+ def _refresh_legend(ax):
168
+ """Recreate the legend so it reflects current artist properties."""
169
+ if ax is None:
170
+ return
171
+ leg = ax.get_legend()
172
+ if leg is None:
173
+ return
174
+ handles, labels = ax.get_legend_handles_labels()
175
+ if not handles:
176
+ return
177
+ props = {}
178
+ try:
179
+ props['frameon'] = leg.get_frame().get_visible()
180
+ except Exception:
181
+ pass
182
+ try:
183
+ props['fontsize'] = leg._fontsize
184
+ except Exception:
185
+ pass
186
+ try:
187
+ props['ncol'] = getattr(leg, '_ncols', 1)
188
+ except Exception:
189
+ pass
190
+ try:
191
+ props['markerfirst'] = leg._markerfirst
192
+ except Exception:
193
+ pass
194
+ try:
195
+ props['handletextpad'] = leg.handletextpad
196
+ except Exception:
197
+ pass
198
+ try:
199
+ props['handleheight'] = leg.handleheight
200
+ except Exception:
201
+ pass
202
+ try:
203
+ props['loc'] = leg._loc
204
+ except Exception:
205
+ pass
206
+ try:
207
+ if hasattr(leg, '_bbox_to_anchor') and leg._bbox_to_anchor is not None:
208
+ inv = ax.transAxes.inverted()
209
+ bx, by = inv.transform(
210
+ (leg._bbox_to_anchor.x0, leg._bbox_to_anchor.y0))
211
+ props['bbox_to_anchor'] = (round(bx, 3), round(by, 3))
212
+ except Exception:
213
+ pass
214
+ ax.legend(handles, labels, **props)
215
+ # Preserve custom legend text colors
216
+ if hasattr(ax, '_matplotly_leg_text_colors'):
217
+ new_leg = ax.get_legend()
218
+ if new_leg:
219
+ for _i, _c in enumerate(ax._matplotly_leg_text_colors):
220
+ if _i < len(new_leg.get_texts()):
221
+ new_leg.get_texts()[_i].set_color(_c)