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,428 @@
1
+ """Scatter plot controls — collapsible per-collection sections with compact color UI."""
2
+ from __future__ import annotations
3
+
4
+ import ipywidgets as widgets
5
+ import numpy as np
6
+ from matplotlib.colors import to_hex
7
+ from matplotlib.markers import MarkerStyle
8
+
9
+ from .._commands import Command
10
+ from .._types import ArtistGroup
11
+ from ._base import ArtistPanel
12
+ from ._color_utils import (
13
+ _DW, _NW, _SN,
14
+ _get_palette_colors, _make_color_dot, _refresh_legend, _slider_num,
15
+ )
16
+
17
+
18
+ class ScatterPanel(ArtistPanel):
19
+ _plot_number: int = 0 # set by _api.py before build()
20
+
21
+ def build(self) -> widgets.Widget:
22
+ coll = self._group.artists[0]
23
+ current_label = coll.get_label() or self._group.label
24
+
25
+ fc = coll.get_facecolor()
26
+ try:
27
+ current_color = to_hex(fc[0]) if len(fc) > 0 else "#1f77b4"
28
+ except Exception:
29
+ current_color = "#1f77b4"
30
+
31
+ # --- Collapsible header ---
32
+ num = self._plot_number or ""
33
+ header_prefix = f"Scatter {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(coll, 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, c=coll):
59
+ self._is_expanded = not self._is_expanded
60
+ lbl = c.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
+ _on_label_changed = None # callback set by _api.py for legend label sync
78
+
79
+ def _build_controls(self, coll, current_label, current_color):
80
+ """Build all controls for this scatter collection."""
81
+ controls = []
82
+ self._edge_manual = False # track if user manually changed edge color
83
+
84
+ # Set initial edge color to match face color
85
+ coll.set_edgecolor(current_color)
86
+
87
+ # --- Name ---
88
+ name_field = widgets.Text(
89
+ value=current_label, description="Name:",
90
+ style={"description_width": _DW},
91
+ layout=widgets.Layout(width="95%"))
92
+
93
+ def _on_name(change, c=coll):
94
+ old_label = c.get_label()
95
+ new_label = change["new"]
96
+ self._stack.execute(
97
+ Command(c, "label", old_label, new_label,
98
+ description=f"{self._group.label} label"))
99
+ pfx = self._header_prefix
100
+ icon = "chevron-down" if self._is_expanded else "chevron-right"
101
+ self._toggle_btn.icon = icon
102
+ self._toggle_btn.description = f" {pfx}{new_label}"
103
+ _refresh_legend(c.axes)
104
+ if self._on_label_changed is not None:
105
+ self._on_label_changed()
106
+ self._canvas.force_redraw()
107
+ name_field.observe(_on_name, names="value")
108
+ controls.append(name_field)
109
+
110
+ # --- Color (facecolor) ---
111
+ controls.append(self._build_color_section(
112
+ coll, current_color, "Color:", "facecolor",
113
+ getter=lambda c: to_hex(c.get_facecolor()[0]) if len(c.get_facecolor()) > 0 else "#1f77b4",
114
+ setter_name="set_facecolor",
115
+ is_primary=True))
116
+
117
+ # --- Edge color ---
118
+ controls.append(self._build_color_section(
119
+ coll, current_color, "Edge:", "edgecolor",
120
+ getter=lambda c: to_hex(c.get_edgecolor()[0]) if len(c.get_edgecolor()) > 0 else "#000000",
121
+ setter_name="set_edgecolor",
122
+ is_primary=False,
123
+ is_edge=True))
124
+
125
+ # --- Size ---
126
+ sizes = coll.get_sizes()
127
+ avg_size = float(np.mean(sizes)) if len(sizes) > 0 else 20.0
128
+ size_sl = widgets.FloatSlider(
129
+ value=round(avg_size, 1), min=1, max=200, step=1,
130
+ description="Size:", style=_SN)
131
+
132
+ def _size_cb(change, c=coll):
133
+ old = c.get_sizes().copy()
134
+ new_val = change["new"]
135
+ n_pts = len(c.get_offsets())
136
+ def _apply():
137
+ c.set_sizes([new_val] * n_pts)
138
+ def _revert():
139
+ c.set_sizes(old)
140
+ self._stack.execute(
141
+ Command(c, "sizes", old, new_val,
142
+ apply_fn=_apply, revert_fn=_revert,
143
+ description=f"{self._group.label} size"))
144
+ _refresh_legend(c.axes)
145
+ self._canvas.force_redraw()
146
+ size_sl.observe(_size_cb, names="value")
147
+ controls.append(_slider_num(size_sl))
148
+
149
+ # --- Alpha ---
150
+ alpha_val = coll.get_alpha()
151
+ alpha_sl = widgets.FloatSlider(
152
+ value=round(alpha_val if alpha_val is not None else 1.0, 2),
153
+ min=0, max=1, step=0.05, description="Alpha:", style=_SN)
154
+
155
+ def _alpha_cb(change, c=coll):
156
+ self._stack.execute(
157
+ Command(c, "alpha", c.get_alpha(), change["new"],
158
+ description=f"{self._group.label} alpha"))
159
+ _refresh_legend(c.axes)
160
+ self._canvas.force_redraw()
161
+ alpha_sl.observe(_alpha_cb, names="value")
162
+ controls.append(_slider_num(alpha_sl))
163
+
164
+ # --- Marker ---
165
+ markers = [("circle", "o"), ("square", "s"), ("triangle", "^"),
166
+ ("diamond", "D"), ("plus", "+"), ("x", "x"),
167
+ ("star", "*"), ("point", ".")]
168
+ marker_dd = widgets.Dropdown(
169
+ options=markers, value="o", description="Marker:",
170
+ style=_SN, layout=widgets.Layout(width="150px"))
171
+
172
+ def _marker_cb(change, c=coll):
173
+ old_paths = c.get_paths()
174
+ new_marker = MarkerStyle(change["new"])
175
+ new_path = new_marker.get_path().transformed(
176
+ new_marker.get_transform())
177
+ def _apply():
178
+ c.set_paths([new_path])
179
+ def _revert():
180
+ c.set_paths(old_paths)
181
+ self._stack.execute(
182
+ Command(c, "paths", old_paths, change["new"],
183
+ apply_fn=_apply, revert_fn=_revert,
184
+ description=f"{self._group.label} marker"))
185
+ _refresh_legend(c.axes)
186
+ self._canvas.force_redraw()
187
+ marker_dd.observe(_marker_cb, names="value")
188
+ controls.append(marker_dd)
189
+
190
+ # --- Edge width ---
191
+ lw = coll.get_linewidths()
192
+ edge_w_sl = widgets.FloatSlider(
193
+ value=round(float(lw[0]) if len(lw) > 0 else 1.0, 2),
194
+ min=0, max=5, step=0.1, description="Edge w:", style=_SN)
195
+
196
+ def _ew_cb(change, c=coll):
197
+ old = c.get_linewidths().copy()
198
+ new_val = change["new"]
199
+ def _apply():
200
+ c.set_linewidths([new_val])
201
+ def _revert():
202
+ c.set_linewidths(old)
203
+ self._stack.execute(
204
+ Command(c, "linewidths", old, new_val,
205
+ apply_fn=_apply, revert_fn=_revert,
206
+ description=f"{self._group.label} edge width"))
207
+ _refresh_legend(c.axes)
208
+ self._canvas.force_redraw()
209
+ edge_w_sl.observe(_ew_cb, names="value")
210
+ controls.append(_slider_num(edge_w_sl))
211
+
212
+ return controls
213
+
214
+ def _build_color_section(self, coll, current_color, label, prop_name,
215
+ getter, setter_name, is_primary,
216
+ is_edge=False):
217
+ """Color section: click swatch -> palette swatches + expand + colorwheel.
218
+
219
+ Mirrors LinePanel._build_color_section. If is_primary, exposes
220
+ self._update_color and self._update_palette for ColormapPanel.
221
+ """
222
+ color_btn = widgets.Button(
223
+ layout=widgets.Layout(width='28px', height='28px',
224
+ padding='0', min_width='28px'),
225
+ tooltip="Click to choose color")
226
+ color_btn.style.button_color = current_color
227
+
228
+ color_row = widgets.HBox(
229
+ [widgets.Label(label, layout=widgets.Layout(width='42px')),
230
+ color_btn],
231
+ layout=widgets.Layout(align_items='center', gap='4px'))
232
+
233
+ # --- Palette: 10 swatches (compact row) + 10 more (expanded row) ---
234
+ _cmap_name = ["tab10"]
235
+
236
+ def _make_swatches(colors):
237
+ btns = []
238
+ for c in colors:
239
+ b = widgets.Button(
240
+ layout=widgets.Layout(width="18px", height="16px",
241
+ padding="0", margin="1px",
242
+ min_width="18px"))
243
+ b.style.button_color = c
244
+ btns.append(b)
245
+ return btns
246
+
247
+ colors_10 = _get_palette_colors("tab10", 10)
248
+ swatch_buttons = _make_swatches(colors_10)
249
+ colors_20 = _get_palette_colors("tab10", 20)
250
+ extra_buttons = _make_swatches(colors_20[10:])
251
+
252
+ _icon_css = widgets.HTML(
253
+ '<style>'
254
+ '.pb-swatch-btn button {'
255
+ ' padding:0 !important;'
256
+ ' min-width:0 !important;'
257
+ ' overflow:hidden !important;'
258
+ '}'
259
+ '.pb-swatch-btn .fa {'
260
+ ' font-size:9px !important;'
261
+ ' position:relative !important;'
262
+ ' top:-7px !important;'
263
+ '}'
264
+ '</style>')
265
+
266
+ expand_btn = widgets.Button(
267
+ icon="plus", tooltip="Show more colors",
268
+ layout=widgets.Layout(width="18px", height="16px",
269
+ padding="0", min_width="18px",
270
+ margin="1px"))
271
+ expand_btn.style.button_color = "#e0e0e0"
272
+ expand_btn.add_class("pb-swatch-btn")
273
+
274
+ palette_btn = widgets.Button(
275
+ icon="paint-brush", tooltip="Custom color...",
276
+ layout=widgets.Layout(width="18px", height="16px",
277
+ padding="0", min_width="18px",
278
+ margin="1px"))
279
+ palette_btn.style.button_color = "#e8e8e8"
280
+ palette_btn.add_class("pb-swatch-btn")
281
+
282
+ _picker_cls = f"pb-picker-{id(coll)}-{prop_name}"
283
+ picker = widgets.ColorPicker(
284
+ value=current_color, concise=True,
285
+ layout=widgets.Layout(width="1px", height="1px",
286
+ overflow="hidden", padding="0",
287
+ margin="0", border="0"))
288
+ picker.add_class(_picker_cls)
289
+
290
+ _js_out = widgets.Output(
291
+ layout=widgets.Layout(height="0px", overflow="hidden"))
292
+
293
+ def _on_palette_btn(b):
294
+ with _js_out:
295
+ _js_out.clear_output()
296
+ from IPython.display import display as ipy_display, Javascript
297
+ ipy_display(Javascript(
298
+ "setTimeout(function(){"
299
+ "var el=document.querySelector('.%s input[type=\"color\"]');"
300
+ "if(el)el.click();"
301
+ "},150);" % _picker_cls))
302
+ palette_btn.on_click(_on_palette_btn)
303
+
304
+ extra_row = widgets.HBox(
305
+ extra_buttons,
306
+ layout=widgets.Layout(display='none', padding='1px 0 0 0',
307
+ align_items='center', gap='1px'))
308
+
309
+ main_row = widgets.HBox(
310
+ swatch_buttons + [expand_btn, palette_btn, picker, _icon_css,
311
+ _js_out],
312
+ layout=widgets.Layout(align_items='center', gap='1px'))
313
+
314
+ palette_panel = widgets.VBox(
315
+ [main_row, extra_row],
316
+ layout=widgets.Layout(display='none', padding='2px 0 0 0'))
317
+
318
+ # --- Sync logic ---
319
+ _updating = [False]
320
+
321
+ def _sync_controls(hex_val):
322
+ """Update all visual controls without triggering callbacks."""
323
+ _updating[0] = True
324
+ try:
325
+ color_btn.style.button_color = hex_val
326
+ picker.value = hex_val
327
+ if is_primary:
328
+ self._color_indicator.value = _make_color_dot(hex_val)
329
+ finally:
330
+ _updating[0] = False
331
+
332
+ def _apply(hex_val, c=coll):
333
+ old = getattr(c, f"get_{prop_name}")().copy()
334
+ setter = getattr(c, setter_name)
335
+ def _do_apply():
336
+ setter(hex_val)
337
+ def _do_revert():
338
+ setter(old)
339
+ self._stack.execute(
340
+ Command(c, prop_name, old, hex_val,
341
+ apply_fn=_do_apply, revert_fn=_do_revert,
342
+ description=f"{self._group.label} {prop_name}"))
343
+ _refresh_legend(c.axes)
344
+ if is_primary and hasattr(self, '_marginals'):
345
+ self._marginals.sync_colors()
346
+ # Sync edge color to match face color (unless manually overridden)
347
+ if is_primary and not self._edge_manual:
348
+ c.set_edgecolor(hex_val)
349
+ if hasattr(self, '_sync_edge_ui'):
350
+ self._sync_edge_ui(hex_val)
351
+ if is_edge:
352
+ self._edge_manual = True
353
+ self._canvas.force_redraw()
354
+
355
+ # Wire swatch clicks
356
+ def _wire_swatch(btn):
357
+ def _on_swatch(b, _btn=btn):
358
+ c = _btn.style.button_color
359
+ _sync_controls(c)
360
+ _apply(c)
361
+ btn.on_click(_on_swatch)
362
+ for b in swatch_buttons + extra_buttons:
363
+ _wire_swatch(b)
364
+
365
+ # Expand / collapse extra row
366
+ def _on_expand(b):
367
+ cname = _cmap_name[0]
368
+ if extra_row.layout.display == 'none':
369
+ c20 = _get_palette_colors(cname, 20)
370
+ for i, btn in enumerate(swatch_buttons):
371
+ btn.style.button_color = c20[i]
372
+ for i, btn in enumerate(extra_buttons):
373
+ btn.style.button_color = c20[10 + i]
374
+ extra_row.layout.display = ''
375
+ expand_btn.icon = 'minus'
376
+ expand_btn.tooltip = 'Show fewer colors'
377
+ else:
378
+ c10 = _get_palette_colors(cname, 10)
379
+ for i, btn in enumerate(swatch_buttons):
380
+ btn.style.button_color = c10[i]
381
+ extra_row.layout.display = 'none'
382
+ expand_btn.icon = 'plus'
383
+ expand_btn.tooltip = 'Show more colors'
384
+ expand_btn.on_click(_on_expand)
385
+
386
+ # Picker changes
387
+ def _from_picker(change):
388
+ if _updating[0]:
389
+ return
390
+ _sync_controls(change["new"])
391
+ _apply(change["new"])
392
+ picker.observe(_from_picker, names="value")
393
+
394
+ # Toggle palette panel
395
+ def _toggle_palette(btn):
396
+ if palette_panel.layout.display == 'none':
397
+ palette_panel.layout.display = ''
398
+ else:
399
+ palette_panel.layout.display = 'none'
400
+ color_btn.on_click(_toggle_palette)
401
+
402
+ # Edge color UI sync — store so primary color can update it
403
+ if is_edge:
404
+ self._sync_edge_ui = _sync_controls
405
+
406
+ # External update hooks (used by ColormapPanel) — primary color only
407
+ if is_primary:
408
+ self._update_color = _sync_controls
409
+
410
+ def _ext_update_palette(cmap_name):
411
+ _cmap_name[0] = cmap_name
412
+ is_expanded = extra_row.layout.display != 'none'
413
+ if is_expanded:
414
+ c20 = _get_palette_colors(cmap_name, 20)
415
+ for i, btn in enumerate(swatch_buttons):
416
+ btn.style.button_color = c20[i]
417
+ for i, btn in enumerate(extra_buttons):
418
+ btn.style.button_color = c20[10 + i]
419
+ else:
420
+ c10 = _get_palette_colors(cmap_name, 10)
421
+ for i, btn in enumerate(swatch_buttons):
422
+ btn.style.button_color = c10[i]
423
+ c20 = _get_palette_colors(cmap_name, 20)
424
+ for i, btn in enumerate(extra_buttons):
425
+ btn.style.button_color = c20[10 + i]
426
+ self._update_palette = _ext_update_palette
427
+
428
+ return widgets.VBox([color_row, palette_panel])