sclab 0.1.7__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,594 @@
1
+ import pandas as pd
2
+ from ipywidgets import (
3
+ Accordion,
4
+ Button,
5
+ Checkbox,
6
+ Dropdown,
7
+ FloatSlider,
8
+ HBox,
9
+ IntSlider,
10
+ Layout,
11
+ Tab,
12
+ Textarea,
13
+ ToggleButtons,
14
+ ValueWidget,
15
+ VBox,
16
+ )
17
+
18
+ from ...event import EventBroker, EventClient
19
+ from .._dataset import SCLabDataset
20
+
21
+
22
+ class PlotterControls(EventClient, VBox):
23
+ data_key: Dropdown
24
+ n_dimensions: ToggleButtons
25
+ aspect_equal: Checkbox
26
+ enable_hover_info: Checkbox
27
+ marker_size: FloatSlider
28
+ marker_opacity: FloatSlider
29
+ plot_width: IntSlider
30
+ plot_height: IntSlider
31
+ selected_axes: VBox
32
+ log_axes: VBox
33
+ histogram_nbins: IntSlider
34
+ color: Dropdown
35
+ show_density: Checkbox
36
+ density_grid_resolution: IntSlider
37
+ density_line_smoothing: FloatSlider
38
+ density_bandwidth_factor: FloatSlider
39
+ density_contours: IntSlider
40
+ refresh_button: Button
41
+ save_button: Button
42
+
43
+ preemptions: dict[str, list[str]] = {
44
+ "ctrl_data_key_change": [
45
+ "ctrl_n_dimensions_change",
46
+ ]
47
+ }
48
+
49
+ def __init__(self, broker: EventBroker):
50
+ self._make_controls()
51
+ self._make_controls_layout()
52
+ VBox.__init__(self, self.controls_layout, layout=Layout(width="100%"))
53
+
54
+ self.dict: dict[str, ValueWidget | Button] = {
55
+ "data_key": self.data_key,
56
+ "color": self.color,
57
+ "sizeby": self.sizeby,
58
+ "n_dimensions": self.n_dimensions,
59
+ "selected_axes_1": self.selected_axes.children[0],
60
+ "selected_axes_2": self.selected_axes.children[1],
61
+ "selected_axes_3": self.selected_axes.children[2],
62
+ "log_axes_1": self.log_axes.children[0],
63
+ "log_axes_2": self.log_axes.children[1],
64
+ "log_axes_3": self.log_axes.children[2],
65
+ "rotate_steps": self.rotate_steps,
66
+ "marker_size": self.marker_size,
67
+ "marker_opacity": self.marker_opacity,
68
+ "plot_width": self.plot_width,
69
+ "plot_height": self.plot_height,
70
+ "histogram_nbins": self.histogram_nbins,
71
+ "aspect_equal": self.aspect_equal,
72
+ "enable_hover_info": self.enable_hover_info,
73
+ "show_density": self.show_density,
74
+ "density_grid_resolution": self.density_grid_resolution,
75
+ "density_line_smoothing": self.density_line_smoothing,
76
+ "density_bandwidth_factor": self.density_bandwidth_factor,
77
+ "density_contours": self.density_contours,
78
+ "refresh_button": self.refresh_button,
79
+ "save_button": self.save_button,
80
+ }
81
+
82
+ self.events = (
83
+ [
84
+ f"ctrl_{control_name}_change"
85
+ for control_name, widget in self.dict.items()
86
+ if not isinstance(widget, Button)
87
+ ]
88
+ + [
89
+ f"ctrl_{control_name}_click"
90
+ for control_name, widget in self.dict.items()
91
+ if isinstance(widget, Button)
92
+ ]
93
+ + [
94
+ f"ctrl_{control_name}_value_change_request"
95
+ for control_name, widget in self.dict.items()
96
+ if isinstance(widget, ValueWidget)
97
+ ]
98
+ + [
99
+ "ctrl_value_change_request",
100
+ ]
101
+ )
102
+ EventClient.__init__(self, broker)
103
+
104
+ for control_name, widget in self.dict.items():
105
+ if isinstance(widget, Button):
106
+ widget.on_click(self.button_click_event_publisher("ctrl", control_name))
107
+ else:
108
+ widget.observe(
109
+ self.control_value_change_event_publisher("ctrl", control_name),
110
+ names="value",
111
+ type="change",
112
+ )
113
+ for control_name, widget in self.dict.items():
114
+ if isinstance(widget, ValueWidget):
115
+ callback = self.control_value_change_request_callback_generator(widget)
116
+ self.broker.subscribe(
117
+ f"ctrl_{control_name}_value_change_request", callback
118
+ )
119
+
120
+ def get_callback(checkbox: Checkbox):
121
+ def callback(_):
122
+ checkbox.value = False
123
+
124
+ return callback
125
+
126
+ for i in range(3):
127
+ dropdown: Dropdown = self.selected_axes.children[i]
128
+ checkbox: Checkbox = self.log_axes.children[i]
129
+ dropdown.observe(get_callback(checkbox), names="value", type="change")
130
+
131
+ self.broker.subscribe("dplt_dataset_change", self.populate)
132
+
133
+ self.broker.subscribe("dset_data_dict_change", self.set_data_key_dd_options)
134
+ self.broker.subscribe(
135
+ "dset_data_key_selection_change", self.set_selected_axes_dd_options
136
+ )
137
+ self.broker.subscribe("dset_metadata_change", self.set_color_dd_options)
138
+ self.broker.subscribe("dset_metadata_change", self.set_sizeby_dd_options)
139
+
140
+ def control_value_change_request_callback_generator(self, widget: ValueWidget):
141
+ def callback(new_value):
142
+ widget.value = new_value
143
+
144
+ return callback
145
+
146
+ def _make_controls(self):
147
+ self.data_key: Dropdown = Dropdown(
148
+ description="Data",
149
+ options={"": None},
150
+ value=None,
151
+ layout=dict(margin="10px 0px 0px 0px"),
152
+ )
153
+ self.n_dimensions: ToggleButtons = ToggleButtons(
154
+ options=[
155
+ "NA", # Will be initialized later and populated based on the data
156
+ # Options should be ["1D", "2D", "3D"]
157
+ ],
158
+ value="NA",
159
+ button_style="primary",
160
+ layout=dict(margin="10px 0px 0px 0px"),
161
+ disabled=True,
162
+ )
163
+ self.aspect_equal: Checkbox = Checkbox(
164
+ description="Aspect equal",
165
+ value=False,
166
+ )
167
+ self.enable_hover_info: Checkbox = Checkbox(
168
+ description="Enable hover info",
169
+ value=True,
170
+ )
171
+ self.marker_size: FloatSlider = FloatSlider(
172
+ min=0.01,
173
+ max=20.0,
174
+ description="Marker size",
175
+ value=8.0,
176
+ step=0.01,
177
+ continuous_update=True,
178
+ readout=False,
179
+ )
180
+ self.marker_opacity: FloatSlider = FloatSlider(
181
+ min=0,
182
+ max=1,
183
+ description="Opacity",
184
+ value=1,
185
+ step=0.01,
186
+ continuous_update=True,
187
+ readout=False,
188
+ )
189
+ self.plot_width: IntSlider = IntSlider(
190
+ min=0,
191
+ max=2000,
192
+ description="Plot width",
193
+ value=0,
194
+ step=10,
195
+ continuous_update=True,
196
+ readout=False,
197
+ )
198
+ self.plot_height: IntSlider = IntSlider(
199
+ min=100,
200
+ max=2000,
201
+ description="Plot height",
202
+ value=600,
203
+ step=10,
204
+ continuous_update=True,
205
+ readout=False,
206
+ )
207
+ self.selected_axes = VBox(
208
+ [
209
+ Dropdown(description=f"{ax} axis", options={"": None}, value=None)
210
+ for ax in ["X", "Y", "Z"]
211
+ ]
212
+ )
213
+ self.log_axes = VBox(
214
+ [
215
+ Checkbox(description=f"{ax} axis log scale", value=False)
216
+ for ax in ["X", "Y", "Z"]
217
+ ]
218
+ )
219
+ self.rotate_steps: Textarea = Textarea(
220
+ description="Rotation steps",
221
+ value="",
222
+ rows=5,
223
+ )
224
+ self.histogram_nbins: IntSlider = IntSlider(
225
+ min=5,
226
+ max=200,
227
+ description="Histo. bins",
228
+ value=100,
229
+ step=5,
230
+ continuous_update=True,
231
+ readout=False,
232
+ )
233
+ self.color: Dropdown = Dropdown(
234
+ description="Color",
235
+ options={"": None},
236
+ value=None,
237
+ layout=dict(margin="10px 0px 0px 0px"),
238
+ )
239
+ self.sizeby: Dropdown = Dropdown(
240
+ description="Size by",
241
+ options={"": None},
242
+ value=None,
243
+ layout=Layout(margin="10px 0px 0px 0px"),
244
+ )
245
+ self.show_density: Checkbox = Checkbox(
246
+ description="Show density",
247
+ value=False,
248
+ )
249
+ self.density_grid_resolution: IntSlider = IntSlider(
250
+ min=4,
251
+ max=8,
252
+ description="Resolution",
253
+ value=6,
254
+ step=1,
255
+ continuous_update=False,
256
+ readout=False,
257
+ )
258
+ self.density_line_smoothing: FloatSlider = FloatSlider(
259
+ min=0,
260
+ max=1.3,
261
+ description="Smoothing",
262
+ value=1.3,
263
+ step=0.05,
264
+ continuous_update=True,
265
+ readout=False,
266
+ )
267
+ self.density_bandwidth_factor: FloatSlider = FloatSlider(
268
+ min=0.002,
269
+ max=0.02,
270
+ description="Bandwidth",
271
+ value=0.017,
272
+ step=0.003,
273
+ continuous_update=False,
274
+ readout=False,
275
+ )
276
+ self.density_contours: IntSlider = IntSlider(
277
+ min=5,
278
+ max=100,
279
+ description="Contours",
280
+ value=20,
281
+ step=1,
282
+ continuous_update=True,
283
+ readout=False,
284
+ )
285
+ self.refresh_button: Button = Button(
286
+ description="Refresh", button_style="primary"
287
+ )
288
+ self.save_button: Button = Button(description="Save", button_style="primary")
289
+
290
+ def _make_controls_layout(self):
291
+ plot_settings_tab = Tab(
292
+ children=[
293
+ self.selected_axes,
294
+ self.log_axes,
295
+ self.rotate_steps,
296
+ ],
297
+ titles=["Axes", "Log axes", "Rotate"],
298
+ layout=Layout(margin="10px 0px 0px 0px"),
299
+ )
300
+
301
+ plot_settings_grid = VBox(
302
+ [
303
+ self.data_key,
304
+ self.color,
305
+ self.sizeby,
306
+ self.n_dimensions,
307
+ plot_settings_tab,
308
+ ]
309
+ )
310
+
311
+ general_visual_settings = VBox(
312
+ [
313
+ self.marker_size,
314
+ self.marker_opacity,
315
+ self.plot_width,
316
+ self.plot_height,
317
+ self.aspect_equal,
318
+ self.enable_hover_info,
319
+ ]
320
+ )
321
+
322
+ histogram_visual_settings = VBox(
323
+ [
324
+ self.histogram_nbins,
325
+ ]
326
+ )
327
+
328
+ density_visual_settings = VBox(
329
+ [
330
+ self.show_density,
331
+ self.density_bandwidth_factor,
332
+ self.density_grid_resolution,
333
+ self.density_line_smoothing,
334
+ self.density_contours,
335
+ ]
336
+ )
337
+
338
+ visual_settings = Tab(
339
+ [
340
+ general_visual_settings,
341
+ histogram_visual_settings,
342
+ density_visual_settings,
343
+ ],
344
+ titles=["General", "Histogram", "Density"],
345
+ )
346
+
347
+ sliders_grid = Accordion(
348
+ [
349
+ plot_settings_grid,
350
+ visual_settings,
351
+ ],
352
+ titles=["Plot Settings", "Visual Settings"],
353
+ selected_index=0,
354
+ layout=dict(width="auto"),
355
+ )
356
+
357
+ self.plot_width.layout.width = "95%"
358
+ self.plot_height.layout.width = "95%"
359
+ self.marker_size.layout.width = "95%"
360
+ self.marker_opacity.layout.width = "95%"
361
+ self.histogram_nbins.layout.width = "95%"
362
+ self.aspect_equal.layout.width = "95%"
363
+ self.enable_hover_info.layout.width = "95%"
364
+ self.show_density.layout.width = "95%"
365
+ self.density_grid_resolution.layout.width = "95%"
366
+ self.density_line_smoothing.layout.width = "95%"
367
+ self.density_bandwidth_factor.layout.width = "95%"
368
+ self.density_contours.layout.width = "95%"
369
+
370
+ self.data_key.layout.width = "95%"
371
+ self.data_key.style.description_width = "15%"
372
+ self.color.layout.width = "95%"
373
+ self.color.style.description_width = "15%"
374
+ self.sizeby.layout.width = "95%"
375
+ self.sizeby.style.description_width = "15%"
376
+
377
+ self.n_dimensions.layout.width = "auto"
378
+ self.n_dimensions.style.button_width = "30%"
379
+ self.n_dimensions.layout.padding = "0px 0px 0px 17%"
380
+
381
+ self.selected_axes.layout.width = "95%"
382
+ self.log_axes.layout.width = "95%"
383
+ self.rotate_steps.layout.width = "95%"
384
+ for i in range(3):
385
+ self.selected_axes.children[i].layout.width = "95%"
386
+ self.log_axes.children[i].layout.width = "95%"
387
+
388
+ self.controls_layout = [
389
+ sliders_grid,
390
+ HBox([self.refresh_button, self.save_button]),
391
+ ]
392
+
393
+ def clear(self):
394
+ self.data_key.options = {"": None}
395
+ self.data_key.value = None
396
+
397
+ self.color.options = {"": None}
398
+ self.color.value = None
399
+
400
+ self.sizeby.options = {"": None}
401
+ self.sizeby.value = None
402
+
403
+ self.n_dimensions.value = None
404
+
405
+ dropdown_list: list[Dropdown] = self.selected_axes.children
406
+ for dropdown in dropdown_list:
407
+ dropdown.options = {"": None}
408
+ dropdown.value = None
409
+ dropdown.disabled = True
410
+ dropdown.layout.visibility = "hidden"
411
+
412
+ checkbox_list: list[Checkbox] = self.log_axes.children
413
+ for checkbox in checkbox_list:
414
+ checkbox.value = False
415
+ checkbox.disabled = True
416
+ checkbox.layout.visibility = "hidden"
417
+
418
+ self.marker_size.disabled = True
419
+ self.marker_opacity.disabled = True
420
+ self.histogram_nbins.disabled = True
421
+
422
+ def dset_metadata_change_callback(
423
+ self, metadata: pd.DataFrame, value: str | None = None
424
+ ):
425
+ if self.data_key.value == "metadata":
426
+ data = metadata.select_dtypes(include=["number"])
427
+ self.set_selected_axes_dd_options(data)
428
+
429
+ def set_color_dd_options(self, metadata: pd.DataFrame, value: str | None = None):
430
+ options = metadata.select_dtypes(include=["bool", "category", "number"]).columns
431
+
432
+ if value is not None:
433
+ pass
434
+ elif self.color.value not in options:
435
+ value = None
436
+ else:
437
+ value = self.color.value
438
+
439
+ self.color.options = {"": None, **{c: c for c in options}}
440
+ self.color.value = value
441
+
442
+ def set_sizeby_dd_options(self, metadata: pd.DataFrame, _: str | None = None):
443
+ df = metadata.select_dtypes(include=["number"])
444
+
445
+ # only show columns that have non-negative values for all rows
446
+ options = df.columns[(df >= 0).sum() == df.shape[0]]
447
+
448
+ if self.sizeby.value not in options:
449
+ value = None
450
+ else:
451
+ value = self.sizeby.value
452
+
453
+ self.sizeby.options = {"": None, **{c: c for c in options}}
454
+ self.sizeby.value = value
455
+
456
+ def set_data_key_dd_options(
457
+ self, data_dict: dict[str, pd.DataFrame], value: str | None = None
458
+ ):
459
+ options = list(data_dict.keys())
460
+
461
+ if value is not None:
462
+ pass
463
+ elif self.data_key.value not in options:
464
+ value = None
465
+ else:
466
+ value = self.data_key.value
467
+
468
+ # self.data_key.disabled = True
469
+ self.data_key.options = {"": None, **{c: c for c in options}}
470
+ # self.data_key.disabled = False
471
+
472
+ self.data_key.value = value
473
+
474
+ def set_selected_axes_dd_options(self, data: pd.DataFrame):
475
+ columns = data.columns
476
+ data_ncols = len(columns)
477
+
478
+ dropdown_list: list[Dropdown] = self.selected_axes.children
479
+ checkbox_list: list[Checkbox] = self.log_axes.children
480
+
481
+ options = {c: c for c in columns}
482
+ for i, (column, dropdown, checkbox) in enumerate(
483
+ zip(columns, dropdown_list, checkbox_list)
484
+ ):
485
+ current_value = dropdown.value
486
+
487
+ if i == 0:
488
+ dropdown.options = {**options}
489
+ else:
490
+ dropdown.options = options
491
+
492
+ if current_value is not None and current_value in dropdown.options:
493
+ dropdown.value = current_value
494
+ else:
495
+ dropdown.value = column
496
+ checkbox.value = False
497
+
498
+ n = data_ncols
499
+ for dropdown, checkbox in zip(dropdown_list[:n], checkbox_list[:n]):
500
+ dropdown.disabled = False
501
+ dropdown.layout.visibility = "visible"
502
+
503
+ checkbox.disabled = False
504
+ checkbox.layout.visibility = "visible"
505
+
506
+ for dropdown, checkbox in zip(dropdown_list[n:], checkbox_list[n:]):
507
+ dropdown.disabled = True
508
+ dropdown.layout.visibility = "hidden"
509
+
510
+ checkbox.disabled = True
511
+ checkbox.layout.visibility = "hidden"
512
+
513
+ for dropdown, checkbox in zip(
514
+ dropdown_list[data_ncols:], checkbox_list[data_ncols:]
515
+ ):
516
+ dropdown.options = None
517
+ dropdown.value = None
518
+ checkbox.value = False
519
+
520
+ def ctrl_n_dimensions_change_callback(self, new_value: str | None):
521
+ dropdown_list: list[Dropdown] = self.selected_axes.children
522
+ checkbox_list: list[Checkbox] = self.log_axes.children
523
+ if new_value is None:
524
+ for dropdown, checkbox in zip(dropdown_list, checkbox_list):
525
+ dropdown.disabled = True
526
+ dropdown.layout.visibility = "hidden"
527
+ checkbox.disabled = True
528
+ checkbox.value = False
529
+ checkbox.layout.visibility = "hidden"
530
+ else:
531
+ # ndim = int(new_value[0])
532
+ for i, (dropdown, checkbox) in enumerate(zip(dropdown_list, checkbox_list)):
533
+ dropdown.disabled = False
534
+ dropdown.layout.visibility = "visible"
535
+ checkbox.disabled = False
536
+ checkbox.value = False
537
+ checkbox.layout.visibility = "visible"
538
+
539
+ if new_value is None:
540
+ self.marker_size.disabled = True
541
+ self.marker_opacity.disabled = True
542
+ self.histogram_nbins.disabled = False
543
+ elif new_value == "1D":
544
+ self.marker_size.disabled = True
545
+ self.marker_opacity.disabled = True
546
+ self.histogram_nbins.disabled = False
547
+ else:
548
+ self.marker_size.disabled = False
549
+ self.marker_opacity.disabled = False
550
+ self.histogram_nbins.disabled = True
551
+
552
+ def ctrl_value_change_request_callback(self, **kwargs):
553
+ for control_name, new_value in kwargs.items():
554
+ control = self.dict[control_name]
555
+ assert isinstance(control, ValueWidget)
556
+
557
+ control.value = new_value
558
+
559
+ def dset_data_key_selection_change_callback(self, data: pd.DataFrame):
560
+ data_ncols = len(data.columns)
561
+
562
+ if data_ncols == 1:
563
+ options = ["1D"]
564
+ value = "1D"
565
+ elif data_ncols == 2:
566
+ options = ["1D", "2D"]
567
+ value = "2D"
568
+ elif data_ncols >= 3:
569
+ options = ["1D", "2D", "3D"]
570
+ value = "2D"
571
+ else:
572
+ options = ["NA"]
573
+ value = "NA"
574
+
575
+ self.n_dimensions.options = options
576
+ self.n_dimensions.value = value
577
+ self.n_dimensions.disabled = data_ncols == 0
578
+
579
+ def populate(self, dataset: SCLabDataset):
580
+ if not isinstance(dataset, SCLabDataset):
581
+ raise TypeError("dataset must be an instance of SCLabDataset")
582
+
583
+ data_dict = dataset.data_dict
584
+ data = dataset.data
585
+ metadata = dataset.metadata
586
+
587
+ # if not data_dict:
588
+ # self.clear()
589
+ # return
590
+
591
+ self.set_data_key_dd_options(data_dict)
592
+ self.set_selected_axes_dd_options(data)
593
+ self.set_color_dd_options(metadata)
594
+ self.set_sizeby_dd_options(metadata)