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.
- matplotly/__init__.py +124 -0
- matplotly/_api.py +984 -0
- matplotly/_code_gen.py +1793 -0
- matplotly/_commands.py +109 -0
- matplotly/_introspect.py +1197 -0
- matplotly/_profiles.py +241 -0
- matplotly/_renderer.py +79 -0
- matplotly/_style_import.py +155 -0
- matplotly/_types.py +31 -0
- matplotly/panels/__init__.py +37 -0
- matplotly/panels/_bar.py +788 -0
- matplotly/panels/_base.py +38 -0
- matplotly/panels/_color_utils.py +221 -0
- matplotly/panels/_distribution.py +1605 -0
- matplotly/panels/_errorbar.py +652 -0
- matplotly/panels/_fill.py +90 -0
- matplotly/panels/_global.py +1507 -0
- matplotly/panels/_heatmap.py +898 -0
- matplotly/panels/_histogram.py +938 -0
- matplotly/panels/_line.py +709 -0
- matplotly/panels/_marginal.py +944 -0
- matplotly/panels/_scatter.py +428 -0
- matplotly/panels/_subplot.py +846 -0
- matplotly-0.1.0.dist-info/METADATA +120 -0
- matplotly-0.1.0.dist-info/RECORD +27 -0
- matplotly-0.1.0.dist-info/WHEEL +4 -0
- matplotly-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""Line plot controls — collapsible per-curve sections with compact color UI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ipywidgets as widgets
|
|
5
|
+
import matplotlib
|
|
6
|
+
from matplotlib.colors import to_hex
|
|
7
|
+
|
|
8
|
+
from .._commands import BatchCommand, Command
|
|
9
|
+
from .._types import ArtistGroup, PlotType
|
|
10
|
+
from ._base import ArtistPanel
|
|
11
|
+
from ._color_utils import (
|
|
12
|
+
_COLORMAPS, _DW, _NW, _SN,
|
|
13
|
+
_cmap_color, _get_palette_colors, _make_color_dot,
|
|
14
|
+
_refresh_legend, _slider_num,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LinePanel(ArtistPanel):
|
|
19
|
+
_plot_number: int = 0 # set by _api.py before build()
|
|
20
|
+
_on_label_changed = None # callback set by _api.py for legend label sync
|
|
21
|
+
|
|
22
|
+
def build(self) -> widgets.Widget:
|
|
23
|
+
line = self._group.artists[0]
|
|
24
|
+
current_label = line.get_label() or self._group.label
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
current_color = to_hex(line.get_color())
|
|
28
|
+
except Exception:
|
|
29
|
+
current_color = "#1f77b4"
|
|
30
|
+
|
|
31
|
+
# --- Collapsible header ---
|
|
32
|
+
num = self._plot_number or ""
|
|
33
|
+
header_prefix = f"Line {num}: " if num else ""
|
|
34
|
+
self._header_prefix = header_prefix
|
|
35
|
+
|
|
36
|
+
self._color_indicator = widgets.HTML(_make_color_dot(current_color))
|
|
37
|
+
|
|
38
|
+
toggle_btn = widgets.Button(
|
|
39
|
+
description=f" {header_prefix}{current_label}",
|
|
40
|
+
icon="chevron-right",
|
|
41
|
+
layout=widgets.Layout(width='100%', height='28px'))
|
|
42
|
+
toggle_btn.style.button_color = '#f0f0f0'
|
|
43
|
+
toggle_btn.style.font_weight = 'bold'
|
|
44
|
+
self._toggle_btn = toggle_btn
|
|
45
|
+
|
|
46
|
+
header_row = widgets.HBox(
|
|
47
|
+
[self._color_indicator, toggle_btn],
|
|
48
|
+
layout=widgets.Layout(align_items='center', gap='4px'))
|
|
49
|
+
|
|
50
|
+
# --- Controls (collapsed by default) ---
|
|
51
|
+
controls = self._build_controls(line, current_label, current_color)
|
|
52
|
+
controls_box = widgets.VBox(
|
|
53
|
+
controls,
|
|
54
|
+
layout=widgets.Layout(display='none', padding='2px 0 4px 12px'))
|
|
55
|
+
self._controls_box = controls_box
|
|
56
|
+
self._is_expanded = False
|
|
57
|
+
|
|
58
|
+
def _toggle(btn, l=line):
|
|
59
|
+
self._is_expanded = not self._is_expanded
|
|
60
|
+
lbl = l.get_label() or self._group.label
|
|
61
|
+
if self._is_expanded:
|
|
62
|
+
controls_box.layout.display = ''
|
|
63
|
+
toggle_btn.icon = "chevron-down"
|
|
64
|
+
toggle_btn.description = f" {self._header_prefix}{lbl}"
|
|
65
|
+
else:
|
|
66
|
+
controls_box.layout.display = 'none'
|
|
67
|
+
toggle_btn.icon = "chevron-right"
|
|
68
|
+
toggle_btn.description = f" {self._header_prefix}{lbl}"
|
|
69
|
+
toggle_btn.on_click(_toggle)
|
|
70
|
+
|
|
71
|
+
return widgets.VBox(
|
|
72
|
+
[header_row, controls_box],
|
|
73
|
+
layout=widgets.Layout(
|
|
74
|
+
border='1px solid #ddd', border_radius='4px',
|
|
75
|
+
margin='2px 0', padding='2px'))
|
|
76
|
+
|
|
77
|
+
def _build_controls(self, line, current_label, current_color):
|
|
78
|
+
"""Build all controls for this line."""
|
|
79
|
+
controls = []
|
|
80
|
+
|
|
81
|
+
# --- Name ---
|
|
82
|
+
name_field = widgets.Text(
|
|
83
|
+
value=current_label, description="Name:",
|
|
84
|
+
style={"description_width": _DW},
|
|
85
|
+
layout=widgets.Layout(width="95%"))
|
|
86
|
+
|
|
87
|
+
def _on_name(change, l=line):
|
|
88
|
+
old_label = l.get_label()
|
|
89
|
+
new_label = change["new"]
|
|
90
|
+
self._stack.execute(
|
|
91
|
+
Command(l, "label", old_label, new_label,
|
|
92
|
+
description=f"{self._group.label} label"))
|
|
93
|
+
pfx = self._header_prefix
|
|
94
|
+
icon = "chevron-down" if self._is_expanded else "chevron-right"
|
|
95
|
+
self._toggle_btn.icon = icon
|
|
96
|
+
self._toggle_btn.description = f" {pfx}{new_label}"
|
|
97
|
+
_refresh_legend(l.axes)
|
|
98
|
+
if self._on_label_changed is not None:
|
|
99
|
+
self._on_label_changed()
|
|
100
|
+
self._canvas.force_redraw()
|
|
101
|
+
name_field.observe(_on_name, names="value")
|
|
102
|
+
controls.append(name_field)
|
|
103
|
+
|
|
104
|
+
# --- Color ---
|
|
105
|
+
controls.append(self._build_color_section(line, current_color))
|
|
106
|
+
|
|
107
|
+
# --- Width ---
|
|
108
|
+
width = widgets.FloatSlider(
|
|
109
|
+
value=round(line.get_linewidth(), 2),
|
|
110
|
+
min=0.1, max=10, step=0.1, description="Width:", style=_SN)
|
|
111
|
+
|
|
112
|
+
def _width_cb(change, l=line):
|
|
113
|
+
self._stack.execute(
|
|
114
|
+
Command(l, "linewidth", l.get_linewidth(), change["new"],
|
|
115
|
+
description=f"{self._group.label} width"))
|
|
116
|
+
_refresh_legend(l.axes)
|
|
117
|
+
self._canvas.force_redraw()
|
|
118
|
+
width.observe(_width_cb, names="value")
|
|
119
|
+
controls.append(_slider_num(width))
|
|
120
|
+
|
|
121
|
+
# --- Style ---
|
|
122
|
+
styles = [("solid", "-"), ("dashed", "--"),
|
|
123
|
+
("dotted", ":"), ("dashdot", "-.")]
|
|
124
|
+
_ls_map = {"solid": "-", "dashed": "--", "dotted": ":", "dashdot": "-."}
|
|
125
|
+
current_ls = _ls_map.get(line.get_linestyle(), line.get_linestyle())
|
|
126
|
+
if current_ls not in [v for _, v in styles]:
|
|
127
|
+
current_ls = "-"
|
|
128
|
+
style = widgets.Dropdown(
|
|
129
|
+
options=styles, value=current_ls, description="Style:",
|
|
130
|
+
style=_SN, layout=widgets.Layout(width="150px"))
|
|
131
|
+
|
|
132
|
+
def _style_cb(change, l=line):
|
|
133
|
+
self._stack.execute(
|
|
134
|
+
Command(l, "linestyle", l.get_linestyle(), change["new"],
|
|
135
|
+
description=f"{self._group.label} style"))
|
|
136
|
+
_refresh_legend(l.axes)
|
|
137
|
+
self._canvas.force_redraw()
|
|
138
|
+
style.observe(_style_cb, names="value")
|
|
139
|
+
controls.append(style)
|
|
140
|
+
|
|
141
|
+
# --- Alpha ---
|
|
142
|
+
alpha_val = line.get_alpha()
|
|
143
|
+
alpha_sl = widgets.FloatSlider(
|
|
144
|
+
value=round(alpha_val if alpha_val is not None else 1.0, 2),
|
|
145
|
+
min=0, max=1, step=0.05, description="Alpha:", style=_SN)
|
|
146
|
+
|
|
147
|
+
def _alpha_cb(change, l=line):
|
|
148
|
+
self._stack.execute(
|
|
149
|
+
Command(l, "alpha", l.get_alpha(), change["new"],
|
|
150
|
+
description=f"{self._group.label} alpha"))
|
|
151
|
+
_refresh_legend(l.axes)
|
|
152
|
+
self._canvas.force_redraw()
|
|
153
|
+
alpha_sl.observe(_alpha_cb, names="value")
|
|
154
|
+
controls.append(_slider_num(alpha_sl))
|
|
155
|
+
|
|
156
|
+
# --- Marker ---
|
|
157
|
+
markers = [("none", ""), ("circle", "o"), ("square", "s"),
|
|
158
|
+
("triangle", "^"), ("diamond", "D"), ("plus", "+"),
|
|
159
|
+
("x", "x"), ("star", "*")]
|
|
160
|
+
cur_m = line.get_marker()
|
|
161
|
+
if cur_m not in [v for _, v in markers]:
|
|
162
|
+
cur_m = ""
|
|
163
|
+
marker = widgets.Dropdown(
|
|
164
|
+
options=markers, value=cur_m, description="Marker:",
|
|
165
|
+
style=_SN, layout=widgets.Layout(width="150px"))
|
|
166
|
+
|
|
167
|
+
def _marker_cb(change, l=line):
|
|
168
|
+
self._stack.execute(
|
|
169
|
+
Command(l, "marker", l.get_marker(), change["new"],
|
|
170
|
+
description=f"{self._group.label} marker"))
|
|
171
|
+
_refresh_legend(l.axes)
|
|
172
|
+
self._canvas.force_redraw()
|
|
173
|
+
marker.observe(_marker_cb, names="value")
|
|
174
|
+
controls.append(marker)
|
|
175
|
+
|
|
176
|
+
# --- Marker size ---
|
|
177
|
+
marker_size = widgets.FloatSlider(
|
|
178
|
+
value=round(line.get_markersize(), 2), min=0, max=20,
|
|
179
|
+
step=0.5, description="Mkr sz:", style=_SN)
|
|
180
|
+
|
|
181
|
+
def _ms_cb(change, l=line):
|
|
182
|
+
self._stack.execute(
|
|
183
|
+
Command(l, "markersize", l.get_markersize(), change["new"],
|
|
184
|
+
description=f"{self._group.label} marker size"))
|
|
185
|
+
_refresh_legend(l.axes)
|
|
186
|
+
self._canvas.force_redraw()
|
|
187
|
+
marker_size.observe(_ms_cb, names="value")
|
|
188
|
+
controls.append(_slider_num(marker_size))
|
|
189
|
+
|
|
190
|
+
return controls
|
|
191
|
+
|
|
192
|
+
def _build_color_section(self, line, current_color):
|
|
193
|
+
"""Color: click swatch → palette swatches + expand + colorwheel."""
|
|
194
|
+
|
|
195
|
+
# --- Main color button (shows current color) ---
|
|
196
|
+
color_btn = widgets.Button(
|
|
197
|
+
layout=widgets.Layout(width='28px', height='28px',
|
|
198
|
+
padding='0', min_width='28px'),
|
|
199
|
+
tooltip="Click to choose color")
|
|
200
|
+
color_btn.style.button_color = current_color
|
|
201
|
+
|
|
202
|
+
color_row = widgets.HBox(
|
|
203
|
+
[widgets.Label("Color:", layout=widgets.Layout(width='42px')),
|
|
204
|
+
color_btn],
|
|
205
|
+
layout=widgets.Layout(align_items='center', gap='4px'))
|
|
206
|
+
|
|
207
|
+
# --- Palette: 10 swatches (compact row) + 10 more (expanded row) ---
|
|
208
|
+
_cmap_name = ["tab10"] # mutable, updated by ColormapPanel
|
|
209
|
+
|
|
210
|
+
def _make_swatches(colors):
|
|
211
|
+
btns = []
|
|
212
|
+
for c in colors:
|
|
213
|
+
b = widgets.Button(
|
|
214
|
+
layout=widgets.Layout(width="18px", height="16px",
|
|
215
|
+
padding="0", margin="1px",
|
|
216
|
+
min_width="18px"))
|
|
217
|
+
b.style.button_color = c
|
|
218
|
+
btns.append(b)
|
|
219
|
+
return btns
|
|
220
|
+
|
|
221
|
+
colors_10 = _get_palette_colors("tab10", 10)
|
|
222
|
+
swatch_buttons = _make_swatches(colors_10)
|
|
223
|
+
colors_20 = _get_palette_colors("tab10", 20)
|
|
224
|
+
extra_buttons = _make_swatches(colors_20[10:])
|
|
225
|
+
|
|
226
|
+
# CSS: match swatch size, nudge FA icon up to center it
|
|
227
|
+
_icon_css = widgets.HTML(
|
|
228
|
+
'<style>'
|
|
229
|
+
'.pb-swatch-btn button {'
|
|
230
|
+
' padding:0 !important;'
|
|
231
|
+
' min-width:0 !important;'
|
|
232
|
+
' overflow:hidden !important;'
|
|
233
|
+
'}'
|
|
234
|
+
'.pb-swatch-btn .fa {'
|
|
235
|
+
' font-size:9px !important;'
|
|
236
|
+
' position:relative !important;'
|
|
237
|
+
' top:-7px !important;'
|
|
238
|
+
'}'
|
|
239
|
+
'</style>')
|
|
240
|
+
|
|
241
|
+
# Expand / collapse button (same size as swatch: 18x16)
|
|
242
|
+
expand_btn = widgets.Button(
|
|
243
|
+
icon="plus", tooltip="Show more colors",
|
|
244
|
+
layout=widgets.Layout(width="18px", height="16px",
|
|
245
|
+
padding="0", min_width="18px",
|
|
246
|
+
margin="1px"))
|
|
247
|
+
expand_btn.style.button_color = "#e0e0e0"
|
|
248
|
+
expand_btn.add_class("pb-swatch-btn")
|
|
249
|
+
|
|
250
|
+
# Palette icon button (same size as swatch: 18x16)
|
|
251
|
+
palette_btn = widgets.Button(
|
|
252
|
+
icon="paint-brush", tooltip="Custom color...",
|
|
253
|
+
layout=widgets.Layout(width="18px", height="16px",
|
|
254
|
+
padding="0", min_width="18px",
|
|
255
|
+
margin="1px"))
|
|
256
|
+
palette_btn.style.button_color = "#e8e8e8"
|
|
257
|
+
palette_btn.add_class("pb-swatch-btn")
|
|
258
|
+
|
|
259
|
+
# Hidden color picker (programmatically opened by JS click)
|
|
260
|
+
_picker_cls = f"pb-picker-{id(line)}"
|
|
261
|
+
picker = widgets.ColorPicker(
|
|
262
|
+
value=current_color, concise=True,
|
|
263
|
+
layout=widgets.Layout(width="1px", height="1px",
|
|
264
|
+
overflow="hidden", padding="0",
|
|
265
|
+
margin="0", border="0"))
|
|
266
|
+
picker.add_class(_picker_cls)
|
|
267
|
+
|
|
268
|
+
_js_out = widgets.Output(
|
|
269
|
+
layout=widgets.Layout(height="0px", overflow="hidden"))
|
|
270
|
+
|
|
271
|
+
def _on_palette_btn(b):
|
|
272
|
+
with _js_out:
|
|
273
|
+
_js_out.clear_output()
|
|
274
|
+
from IPython.display import display as ipy_display, Javascript
|
|
275
|
+
ipy_display(Javascript(
|
|
276
|
+
"setTimeout(function(){"
|
|
277
|
+
"var el=document.querySelector('.%s input[type=\"color\"]');"
|
|
278
|
+
"if(el)el.click();"
|
|
279
|
+
"},150);" % _picker_cls))
|
|
280
|
+
palette_btn.on_click(_on_palette_btn)
|
|
281
|
+
|
|
282
|
+
extra_row = widgets.HBox(
|
|
283
|
+
extra_buttons,
|
|
284
|
+
layout=widgets.Layout(display='none', padding='1px 0 0 0',
|
|
285
|
+
align_items='center', gap='1px'))
|
|
286
|
+
|
|
287
|
+
main_row = widgets.HBox(
|
|
288
|
+
swatch_buttons + [expand_btn, palette_btn, picker, _icon_css,
|
|
289
|
+
_js_out],
|
|
290
|
+
layout=widgets.Layout(align_items='center', gap='1px'))
|
|
291
|
+
|
|
292
|
+
palette_panel = widgets.VBox(
|
|
293
|
+
[main_row, extra_row],
|
|
294
|
+
layout=widgets.Layout(display='none', padding='2px 0 0 0'))
|
|
295
|
+
|
|
296
|
+
# --- Sync logic ---
|
|
297
|
+
_updating = [False]
|
|
298
|
+
|
|
299
|
+
def _sync_controls(hex_val):
|
|
300
|
+
"""Update all visual controls without triggering callbacks."""
|
|
301
|
+
_updating[0] = True
|
|
302
|
+
try:
|
|
303
|
+
color_btn.style.button_color = hex_val
|
|
304
|
+
picker.value = hex_val
|
|
305
|
+
self._color_indicator.value = _make_color_dot(hex_val)
|
|
306
|
+
finally:
|
|
307
|
+
_updating[0] = False
|
|
308
|
+
|
|
309
|
+
def _apply(hex_val, l=line):
|
|
310
|
+
self._stack.execute(
|
|
311
|
+
Command(l, "color", to_hex(l.get_color()), hex_val,
|
|
312
|
+
description=f"{self._group.label} color"))
|
|
313
|
+
_refresh_legend(l.axes)
|
|
314
|
+
self._canvas.force_redraw()
|
|
315
|
+
|
|
316
|
+
# Wire swatch clicks (both rows)
|
|
317
|
+
def _wire_swatch(btn):
|
|
318
|
+
def _on_swatch(b, _btn=btn):
|
|
319
|
+
c = _btn.style.button_color
|
|
320
|
+
_sync_controls(c)
|
|
321
|
+
_apply(c)
|
|
322
|
+
btn.on_click(_on_swatch)
|
|
323
|
+
for b in swatch_buttons + extra_buttons:
|
|
324
|
+
_wire_swatch(b)
|
|
325
|
+
|
|
326
|
+
# Expand / collapse extra row
|
|
327
|
+
def _on_expand(b):
|
|
328
|
+
cname = _cmap_name[0]
|
|
329
|
+
if extra_row.layout.display == 'none':
|
|
330
|
+
# Expanding: switch row 1 to first 10 of 20-point sampling
|
|
331
|
+
c20 = _get_palette_colors(cname, 20)
|
|
332
|
+
for i, btn in enumerate(swatch_buttons):
|
|
333
|
+
btn.style.button_color = c20[i]
|
|
334
|
+
for i, btn in enumerate(extra_buttons):
|
|
335
|
+
btn.style.button_color = c20[10 + i]
|
|
336
|
+
extra_row.layout.display = ''
|
|
337
|
+
expand_btn.icon = 'minus'
|
|
338
|
+
expand_btn.tooltip = 'Show fewer colors'
|
|
339
|
+
else:
|
|
340
|
+
# Collapsing: switch row 1 back to 10-point full-range sampling
|
|
341
|
+
c10 = _get_palette_colors(cname, 10)
|
|
342
|
+
for i, btn in enumerate(swatch_buttons):
|
|
343
|
+
btn.style.button_color = c10[i]
|
|
344
|
+
extra_row.layout.display = 'none'
|
|
345
|
+
expand_btn.icon = 'plus'
|
|
346
|
+
expand_btn.tooltip = 'Show more colors'
|
|
347
|
+
expand_btn.on_click(_on_expand)
|
|
348
|
+
|
|
349
|
+
# Picker changes
|
|
350
|
+
def _from_picker(change):
|
|
351
|
+
if _updating[0]:
|
|
352
|
+
return
|
|
353
|
+
_sync_controls(change["new"])
|
|
354
|
+
_apply(change["new"])
|
|
355
|
+
picker.observe(_from_picker, names="value")
|
|
356
|
+
|
|
357
|
+
# Toggle palette panel
|
|
358
|
+
def _toggle_palette(btn):
|
|
359
|
+
if palette_panel.layout.display == 'none':
|
|
360
|
+
palette_panel.layout.display = ''
|
|
361
|
+
else:
|
|
362
|
+
palette_panel.layout.display = 'none'
|
|
363
|
+
color_btn.on_click(_toggle_palette)
|
|
364
|
+
|
|
365
|
+
# External update hooks (used by ColormapPanel)
|
|
366
|
+
self._update_color = _sync_controls
|
|
367
|
+
|
|
368
|
+
def _ext_update_palette(cmap_name):
|
|
369
|
+
_cmap_name[0] = cmap_name
|
|
370
|
+
is_expanded = extra_row.layout.display != 'none'
|
|
371
|
+
if is_expanded:
|
|
372
|
+
c20 = _get_palette_colors(cmap_name, 20)
|
|
373
|
+
for i, btn in enumerate(swatch_buttons):
|
|
374
|
+
btn.style.button_color = c20[i]
|
|
375
|
+
for i, btn in enumerate(extra_buttons):
|
|
376
|
+
btn.style.button_color = c20[10 + i]
|
|
377
|
+
else:
|
|
378
|
+
c10 = _get_palette_colors(cmap_name, 10)
|
|
379
|
+
for i, btn in enumerate(swatch_buttons):
|
|
380
|
+
btn.style.button_color = c10[i]
|
|
381
|
+
# Pre-compute row 2 for when it gets expanded
|
|
382
|
+
c20 = _get_palette_colors(cmap_name, 20)
|
|
383
|
+
for i, btn in enumerate(extra_buttons):
|
|
384
|
+
btn.style.button_color = c20[10 + i]
|
|
385
|
+
self._update_palette = _ext_update_palette
|
|
386
|
+
|
|
387
|
+
return widgets.VBox([color_row, palette_panel])
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class ColormapPanel:
|
|
391
|
+
"""Global colormap selector — compact header with expandable picker.
|
|
392
|
+
|
|
393
|
+
Shows current colormap name + swatches. Click to expand full list.
|
|
394
|
+
Selecting a colormap auto-applies it and collapses the list.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def __init__(self, groups: list[ArtistGroup], stack, canvas,
|
|
398
|
+
line_panels: list | None = None):
|
|
399
|
+
self._color_groups = [g for g in groups
|
|
400
|
+
if g.plot_type in (PlotType.LINE, PlotType.SCATTER,
|
|
401
|
+
PlotType.HISTOGRAM,
|
|
402
|
+
PlotType.BAR,
|
|
403
|
+
PlotType.GROUPED_BAR,
|
|
404
|
+
PlotType.BOXPLOT,
|
|
405
|
+
PlotType.VIOLIN,
|
|
406
|
+
PlotType.ERRORBAR)]
|
|
407
|
+
self._stack = stack
|
|
408
|
+
self._canvas = canvas
|
|
409
|
+
self._line_panels = line_panels or []
|
|
410
|
+
self._selected = "tab10"
|
|
411
|
+
|
|
412
|
+
def apply(self, cmap_name: str) -> None:
|
|
413
|
+
"""Apply a colormap by name (public API for Preferred Defaults)."""
|
|
414
|
+
self._do_apply(cmap_name)
|
|
415
|
+
|
|
416
|
+
def build(self) -> widgets.Widget:
|
|
417
|
+
if not self._color_groups:
|
|
418
|
+
return widgets.HTML("<i>No series to apply colormap to.</i>")
|
|
419
|
+
|
|
420
|
+
# --- Compact header: name + large swatches on one row, Change below ---
|
|
421
|
+
self._name_html = widgets.HTML(
|
|
422
|
+
f"<b style='font-size:12px'>{self._selected}</b>")
|
|
423
|
+
self._swatch_display = widgets.HTML(
|
|
424
|
+
self._row_swatch(self._selected, n=10, size=20))
|
|
425
|
+
|
|
426
|
+
header_row = widgets.HBox(
|
|
427
|
+
[self._name_html, self._swatch_display],
|
|
428
|
+
layout=widgets.Layout(align_items='center', gap='6px'))
|
|
429
|
+
|
|
430
|
+
self._change_btn = widgets.Button(
|
|
431
|
+
description="Change Colormap", icon="chevron-right",
|
|
432
|
+
layout=widgets.Layout(width="100%", height="26px"))
|
|
433
|
+
self._change_btn.style.button_color = '#f5f5f5'
|
|
434
|
+
|
|
435
|
+
header = widgets.VBox([header_row, self._change_btn])
|
|
436
|
+
|
|
437
|
+
# --- Expandable list of all colormaps ---
|
|
438
|
+
self._row_btns: list[widgets.Button] = []
|
|
439
|
+
self._swatch_btns: list[widgets.Button] = []
|
|
440
|
+
rows = []
|
|
441
|
+
for name in _COLORMAPS:
|
|
442
|
+
# Small clickable name button
|
|
443
|
+
btn = widgets.Button(
|
|
444
|
+
description=name,
|
|
445
|
+
layout=widgets.Layout(width='70px', height='22px',
|
|
446
|
+
padding='0 2px', min_width='70px'),
|
|
447
|
+
tooltip=name)
|
|
448
|
+
btn.style.button_color = 'transparent'
|
|
449
|
+
btn.style.font_weight = 'normal'
|
|
450
|
+
btn.style.font_size = '10px'
|
|
451
|
+
self._row_btns.append(btn)
|
|
452
|
+
|
|
453
|
+
# Swatch gradient bar (HTML) + transparent overlay button
|
|
454
|
+
swatch_html = self._row_swatch(name, n=10, size=20, stretch=True)
|
|
455
|
+
swatch_w = widgets.HTML(
|
|
456
|
+
swatch_html,
|
|
457
|
+
layout=widgets.Layout(flex='1 1 auto', min_width='0'))
|
|
458
|
+
# Invisible button overlaying the swatch for click detection
|
|
459
|
+
swatch_click = widgets.Button(
|
|
460
|
+
description='',
|
|
461
|
+
layout=widgets.Layout(width='100%', height='20px',
|
|
462
|
+
padding='0', margin='-20px 0 0 0'),
|
|
463
|
+
tooltip=f"Apply {name}")
|
|
464
|
+
swatch_click.style.button_color = 'transparent'
|
|
465
|
+
self._swatch_btns.append(swatch_click)
|
|
466
|
+
|
|
467
|
+
swatch_stack = widgets.VBox(
|
|
468
|
+
[swatch_w, swatch_click],
|
|
469
|
+
layout=widgets.Layout(flex='1 1 auto', min_width='0'))
|
|
470
|
+
|
|
471
|
+
row = widgets.HBox(
|
|
472
|
+
[btn, swatch_stack],
|
|
473
|
+
layout=widgets.Layout(align_items='center', gap='6px',
|
|
474
|
+
padding='4px 2px'))
|
|
475
|
+
|
|
476
|
+
def _on_click(b, cmap_name=name):
|
|
477
|
+
self._do_apply(cmap_name)
|
|
478
|
+
self._cmap_list.layout.display = 'none'
|
|
479
|
+
self._change_btn.icon = 'chevron-right'
|
|
480
|
+
btn.on_click(_on_click)
|
|
481
|
+
swatch_click.on_click(_on_click)
|
|
482
|
+
rows.append(row)
|
|
483
|
+
|
|
484
|
+
# Highlight initial selection
|
|
485
|
+
for rb in self._row_btns:
|
|
486
|
+
if rb.description == self._selected:
|
|
487
|
+
rb.style.button_color = '#d4e6f1'
|
|
488
|
+
rb.style.font_weight = 'bold'
|
|
489
|
+
|
|
490
|
+
self._cmap_list = widgets.VBox(
|
|
491
|
+
rows,
|
|
492
|
+
layout=widgets.Layout(
|
|
493
|
+
border='1px solid #ddd', padding='4px',
|
|
494
|
+
display='none')) # collapsed by default
|
|
495
|
+
|
|
496
|
+
def _toggle_list(btn):
|
|
497
|
+
if self._cmap_list.layout.display == 'none':
|
|
498
|
+
self._cmap_list.layout.display = ''
|
|
499
|
+
self._change_btn.icon = 'chevron-down'
|
|
500
|
+
else:
|
|
501
|
+
self._cmap_list.layout.display = 'none'
|
|
502
|
+
self._change_btn.icon = 'chevron-right'
|
|
503
|
+
self._change_btn.on_click(_toggle_list)
|
|
504
|
+
|
|
505
|
+
return widgets.VBox([header, self._cmap_list])
|
|
506
|
+
|
|
507
|
+
def _do_apply(self, cmap_name: str) -> None:
|
|
508
|
+
"""Apply colormap to all lines/scatter and update all UI elements."""
|
|
509
|
+
self._selected = cmap_name
|
|
510
|
+
cmap = matplotlib.colormaps.get_cmap(cmap_name)
|
|
511
|
+
n = len(self._color_groups)
|
|
512
|
+
cmds = []
|
|
513
|
+
for i, group in enumerate(self._color_groups):
|
|
514
|
+
new_color = to_hex(_cmap_color(cmap, i, n))
|
|
515
|
+
if group.plot_type == PlotType.LINE:
|
|
516
|
+
artist = group.artists[0] if group.artists else None
|
|
517
|
+
if artist is None:
|
|
518
|
+
continue
|
|
519
|
+
try:
|
|
520
|
+
old_color = to_hex(artist.get_color())
|
|
521
|
+
except Exception:
|
|
522
|
+
old_color = "#000000"
|
|
523
|
+
cmds.append(Command(artist, "color", old_color, new_color,
|
|
524
|
+
description=f"{group.label} color"))
|
|
525
|
+
elif group.plot_type == PlotType.SCATTER:
|
|
526
|
+
artist = group.artists[0] if group.artists else None
|
|
527
|
+
if artist is None:
|
|
528
|
+
continue
|
|
529
|
+
old_fc = artist.get_facecolor().copy()
|
|
530
|
+
_new = new_color # capture for closures
|
|
531
|
+
_old = old_fc
|
|
532
|
+
_art = artist
|
|
533
|
+
# Check if edge color should follow face color
|
|
534
|
+
_panel = (self._line_panels[i]
|
|
535
|
+
if i < len(self._line_panels) else None)
|
|
536
|
+
_sync_edge = (_panel is not None
|
|
537
|
+
and hasattr(_panel, '_edge_manual')
|
|
538
|
+
and not _panel._edge_manual)
|
|
539
|
+
old_ec = artist.get_edgecolor().copy() if _sync_edge else None
|
|
540
|
+
cmds.append(Command(
|
|
541
|
+
artist, "facecolor", old_fc, new_color,
|
|
542
|
+
apply_fn=lambda _a=_art, _n=_new, _se=_sync_edge: (
|
|
543
|
+
_a.set_facecolor(_n),
|
|
544
|
+
_a.set_edgecolor(_n) if _se else None),
|
|
545
|
+
revert_fn=lambda _a=_art, _o=_old, _se=_sync_edge, _oe=old_ec: (
|
|
546
|
+
_a.set_facecolor(_o),
|
|
547
|
+
_a.set_edgecolor(_oe) if _se else None),
|
|
548
|
+
description=f"{group.label} color"))
|
|
549
|
+
elif group.plot_type == PlotType.HISTOGRAM:
|
|
550
|
+
# Apply color to all patches in the histogram
|
|
551
|
+
if not group.artists:
|
|
552
|
+
continue
|
|
553
|
+
artist = group.artists[0]
|
|
554
|
+
_patches = group.artists
|
|
555
|
+
_new = new_color
|
|
556
|
+
_old_colors = [to_hex(p.get_facecolor()) for p in _patches]
|
|
557
|
+
def _apply_hist(_ps=_patches, _c=_new):
|
|
558
|
+
for p in _ps:
|
|
559
|
+
p.set_facecolor(_c)
|
|
560
|
+
def _revert_hist(_ps=_patches, _oc=_old_colors):
|
|
561
|
+
for p, c in zip(_ps, _oc):
|
|
562
|
+
p.set_facecolor(c)
|
|
563
|
+
cmds.append(Command(
|
|
564
|
+
artist, "facecolor", _old_colors, new_color,
|
|
565
|
+
apply_fn=_apply_hist, revert_fn=_revert_hist,
|
|
566
|
+
description=f"{group.label} color"))
|
|
567
|
+
# Also update the panel's internal color state
|
|
568
|
+
_panel = (self._line_panels[i]
|
|
569
|
+
if i < len(self._line_panels) else None)
|
|
570
|
+
if _panel is not None and hasattr(_panel, '_color'):
|
|
571
|
+
_panel._color = new_color
|
|
572
|
+
elif group.plot_type in (PlotType.BAR, PlotType.GROUPED_BAR):
|
|
573
|
+
# Apply color to all patches in the bar group
|
|
574
|
+
if not group.artists:
|
|
575
|
+
continue
|
|
576
|
+
artist = group.artists[0]
|
|
577
|
+
_patches = group.artists
|
|
578
|
+
_new = new_color
|
|
579
|
+
_old_colors = [to_hex(p.get_facecolor()) for p in _patches]
|
|
580
|
+
def _apply_bar(_ps=_patches, _c=_new):
|
|
581
|
+
for p in _ps:
|
|
582
|
+
p.set_facecolor(_c)
|
|
583
|
+
def _revert_bar(_ps=_patches, _oc=_old_colors):
|
|
584
|
+
for p, c in zip(_ps, _oc):
|
|
585
|
+
p.set_facecolor(c)
|
|
586
|
+
cmds.append(Command(
|
|
587
|
+
artist, "facecolor", _old_colors, new_color,
|
|
588
|
+
apply_fn=_apply_bar, revert_fn=_revert_bar,
|
|
589
|
+
description=f"{group.label} color"))
|
|
590
|
+
_panel = (self._line_panels[i]
|
|
591
|
+
if i < len(self._line_panels) else None)
|
|
592
|
+
if _panel is not None and hasattr(_panel, '_color'):
|
|
593
|
+
_panel._color = new_color
|
|
594
|
+
elif group.plot_type in (PlotType.BOXPLOT, PlotType.VIOLIN):
|
|
595
|
+
# Apply color to distribution panel (no artist needed —
|
|
596
|
+
# originals are cleared by _redraw, colours live on panel)
|
|
597
|
+
_panel = (self._line_panels[i]
|
|
598
|
+
if i < len(self._line_panels) else None)
|
|
599
|
+
if _panel is not None:
|
|
600
|
+
_old_bc = getattr(_panel, '_box_color', '#1f77b4')
|
|
601
|
+
_new_c = new_color
|
|
602
|
+
_p = _panel
|
|
603
|
+
def _apply_dist(_p=_p, _c=_new_c):
|
|
604
|
+
_p._box_color = _c
|
|
605
|
+
_p._violin_color = _c
|
|
606
|
+
_p._jitter_color = _c
|
|
607
|
+
if hasattr(_p, '_shared_panel') and _p._shared_panel:
|
|
608
|
+
_p._shared_panel._redraw()
|
|
609
|
+
def _revert_dist(_p=_p, _c=_old_bc):
|
|
610
|
+
_p._box_color = _c
|
|
611
|
+
_p._violin_color = _c
|
|
612
|
+
_p._jitter_color = _c
|
|
613
|
+
if hasattr(_p, '_shared_panel') and _p._shared_panel:
|
|
614
|
+
_p._shared_panel._redraw()
|
|
615
|
+
# Use group.axes as artist placeholder (original
|
|
616
|
+
# artists are removed by _redraw).
|
|
617
|
+
cmds.append(Command(
|
|
618
|
+
group.axes, "color", _old_bc, new_color,
|
|
619
|
+
apply_fn=_apply_dist, revert_fn=_revert_dist,
|
|
620
|
+
description=f"{group.label} color"))
|
|
621
|
+
elif group.plot_type == PlotType.ERRORBAR:
|
|
622
|
+
_panel = (self._line_panels[i]
|
|
623
|
+
if i < len(self._line_panels) else None)
|
|
624
|
+
if _panel is not None:
|
|
625
|
+
_old_ec = getattr(_panel, '_bar_color', '#1f77b4')
|
|
626
|
+
_new_c = new_color
|
|
627
|
+
_p = _panel
|
|
628
|
+
def _apply_eb(_p=_p, _c=_new_c):
|
|
629
|
+
_p._bar_color = _c
|
|
630
|
+
_p._marker_color = _c
|
|
631
|
+
_p._line_color = _c
|
|
632
|
+
_p._shade_color = _c
|
|
633
|
+
_p._redraw()
|
|
634
|
+
def _revert_eb(_p=_p, _c=_old_ec):
|
|
635
|
+
_p._bar_color = _c
|
|
636
|
+
_p._marker_color = _c
|
|
637
|
+
_p._line_color = _c
|
|
638
|
+
_p._shade_color = _c
|
|
639
|
+
_p._redraw()
|
|
640
|
+
cmds.append(Command(
|
|
641
|
+
group.axes, "color", _old_ec, new_color,
|
|
642
|
+
apply_fn=_apply_eb, revert_fn=_revert_eb,
|
|
643
|
+
description=f"{group.label} color"))
|
|
644
|
+
if cmds:
|
|
645
|
+
self._stack.execute(BatchCommand(cmds, "Apply colormap"))
|
|
646
|
+
for ax_set in {g.axes for g in self._color_groups}:
|
|
647
|
+
_refresh_legend(ax_set)
|
|
648
|
+
self._canvas.redraw()
|
|
649
|
+
for i, panel in enumerate(self._line_panels):
|
|
650
|
+
if hasattr(panel, "_update_color") and i < n:
|
|
651
|
+
color = to_hex(_cmap_color(cmap, i, n))
|
|
652
|
+
panel._update_color(color)
|
|
653
|
+
# Sync edge color UI if not manually overridden
|
|
654
|
+
if (hasattr(panel, '_edge_manual')
|
|
655
|
+
and not panel._edge_manual
|
|
656
|
+
and hasattr(panel, '_sync_edge_ui')):
|
|
657
|
+
panel._sync_edge_ui(color)
|
|
658
|
+
if hasattr(panel, "_update_palette"):
|
|
659
|
+
panel._update_palette(cmap_name)
|
|
660
|
+
|
|
661
|
+
# Update header display
|
|
662
|
+
if hasattr(self, '_name_html'):
|
|
663
|
+
self._name_html.value = (
|
|
664
|
+
f"<b style='font-size:12px'>{cmap_name}</b>")
|
|
665
|
+
self._swatch_display.value = self._row_swatch(cmap_name, n=10, size=20)
|
|
666
|
+
# Highlight selection in list
|
|
667
|
+
if hasattr(self, '_row_btns'):
|
|
668
|
+
for rb in self._row_btns:
|
|
669
|
+
if rb.description == cmap_name:
|
|
670
|
+
rb.style.button_color = '#d4e6f1'
|
|
671
|
+
rb.style.font_weight = 'bold'
|
|
672
|
+
else:
|
|
673
|
+
rb.style.button_color = 'transparent'
|
|
674
|
+
rb.style.font_weight = 'normal'
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def _row_swatch(cmap_name: str, n: int = 10, size: int = 18,
|
|
678
|
+
stretch: bool = False) -> str:
|
|
679
|
+
"""Generate inline HTML swatches for a colormap.
|
|
680
|
+
|
|
681
|
+
If *stretch* is True, renders as a CSS-gradient bar that fills its
|
|
682
|
+
container width. Otherwise, renders n fixed-width squares of *size*
|
|
683
|
+
pixels each (used for the compact header).
|
|
684
|
+
"""
|
|
685
|
+
try:
|
|
686
|
+
cmap = matplotlib.colormaps.get_cmap(cmap_name)
|
|
687
|
+
except Exception:
|
|
688
|
+
return ""
|
|
689
|
+
if stretch:
|
|
690
|
+
# Build a banded CSS gradient that fills full width
|
|
691
|
+
stops = []
|
|
692
|
+
for i in range(n):
|
|
693
|
+
c = to_hex(_cmap_color(cmap, i, n))
|
|
694
|
+
pct0 = round(i / n * 100, 2)
|
|
695
|
+
pct1 = round((i + 1) / n * 100, 2)
|
|
696
|
+
stops.append(f"{c} {pct0}%, {c} {pct1}%")
|
|
697
|
+
grad = ", ".join(stops)
|
|
698
|
+
return (f'<div style="width:100%;height:{size}px;'
|
|
699
|
+
f'border-radius:3px;background:linear-gradient('
|
|
700
|
+
f'to right, {grad})"></div>')
|
|
701
|
+
# Fixed-width squares
|
|
702
|
+
spans = []
|
|
703
|
+
for i in range(n):
|
|
704
|
+
c = to_hex(_cmap_color(cmap, i, n))
|
|
705
|
+
spans.append(
|
|
706
|
+
f'<span style="display:inline-block;width:{size}px;'
|
|
707
|
+
f'height:{size}px;background:{c};margin:0;border-right:'
|
|
708
|
+
f'1px solid rgba(0,0,0,0.05)"></span>')
|
|
709
|
+
return ''.join(spans)
|