1
+ import uuid
1
2
from dataclasses import dataclass
2
3
import datetime
3
4
from collections import defaultdict
18
19
from dash_bootstrap_components import Popover
19
20
20
21
from src .ontoloviz .core import SunburstBase
22
+ from src .ontoloviz .core_utils import generate_color_range , generate_composite_color_range
21
23
22
24
23
25
""" ########################## Tree Components ############################### """
24
26
FAKE_ONE : float = 1 + 1337e-9
25
27
ZERO : float = 1337e-9
26
- WHITE : str = "#FFFFFF"
28
+ WHITE : str = "#FFFFFFFF"
29
+ TRANSPARENT : str = "#FFFFFF00"
27
30
UNDEFINED : str = "Undefined"
31
+ INDIVIDUAL_PLOTS : str = "Individual Plots & Menu"
32
+ SUMMARY_PLOT : str = "Summary Plot"
33
+ PARENT_BASED_ONTOLOGY : str = "Parent-based"
34
+ SEPARATOR_BASED_ONTOLOGY : str = "Separator-based"
35
+ GLOBAL : str = "global"
36
+ LOCAL : str = "local"
28
37
29
38
30
39
@dataclass
31
40
class Leaf :
32
41
id : str = ""
33
42
parent : str = ""
34
43
color : str = WHITE
35
- count : int = FAKE_ONE
44
+ count : [ float , int ] = FAKE_ONE
36
45
label : str = UNDEFINED
37
46
description : str = UNDEFINED
38
47
level : int = 0
@@ -42,6 +51,9 @@ class Leaf:
42
51
class Branch :
43
52
def __init__ (self ):
44
53
self .leaves : defaultdict [str , Leaf ] = defaultdict (Leaf )
54
+ self .max_val : float = 0.0
55
+ self .min_val : float = 0.0
56
+ self .unique_vals : set = set ()
45
57
46
58
def count_levels_and_children (self ):
47
59
for leaf_id , leaf in self .leaves .items ():
@@ -52,6 +64,13 @@ def count_levels_and_children(self):
52
64
current_leaf .children += 1
53
65
level += 1
54
66
leaf .level = level
67
+ if leaf .count not in [FAKE_ONE , ZERO ]:
68
+ count = float (leaf .count )
69
+ if count >= self .max_val :
70
+ self .max_val = count
71
+ if count <= self .min_val :
72
+ self .min_val = count
73
+ self .unique_vals .add (count )
55
74
56
75
def get_sunburst_object (self ) -> go .Sunburst :
57
76
return go .Sunburst (
@@ -77,13 +96,13 @@ def get_sunburst_object(self) -> go.Sunburst:
77
96
_ .count if _ .count != FAKE_ONE else 0
78
97
] for _ in self .leaves .values ()
79
98
],
80
- marker = {
81
- " colors" : [_ .color for _ in self .leaves .values ()],
82
- " line" : {
83
- " color" : "black" ,
84
- " width" : 2
85
- }
86
- }
99
+ marker = dict (
100
+ colors = [_ .color for _ in self .leaves .values ()],
101
+ line = dict (
102
+ color = "black" ,
103
+ width = 2
104
+ ),
105
+ ),
87
106
)
88
107
89
108
@@ -102,12 +121,45 @@ def __init__(self, id_separator: str = "|", level_separator: str = ".", id_col:
102
121
103
122
self .branches : defaultdict [str , Branch ] = defaultdict (Branch )
104
123
self .branch_title_lookup = []
124
+ self .id_to_leaf = dict ()
105
125
self .traces = None
106
126
107
- def add_parent_based_rows (self , rows : list [dict [str , Any ]]):
127
+ def apply_color (self , color_scale : dict [str , str ], global_scale : bool ):
128
+ global_max_val = None
129
+ global_min_val = None
130
+ global_unique_vals = None
131
+ global_color_range = None
132
+ cs = {float (k ): v for k , v in sorted (color_scale .items (), key = lambda x : int (x [0 ]))}
133
+ if global_scale :
134
+ global_max_val = max (_ .max_val for _ in self .branches .values ())
135
+ global_min_val = min (_ .min_val for _ in self .branches .values ())
136
+ global_unique_vals = set (v for branch in self .branches .values () for v in branch .unique_vals )
137
+ global_color_range = {k : v for k , v in zip (global_unique_vals , generate_composite_color_range (color_scale = cs , total_colors = len (global_unique_vals )))}
138
+
139
+ for branch in self .branches .values ():
140
+ _max = global_max_val if global_max_val else branch .max_val
141
+ _min = global_min_val if global_min_val else branch .min_val
142
+ _vals = global_unique_vals if global_unique_vals else branch .unique_vals
143
+ _cr = global_color_range if global_color_range else {k : v for k , v in zip (
144
+ _vals , generate_composite_color_range (color_scale = cs , total_colors = len (_vals )))}
145
+ for leaf in branch .leaves .values ():
146
+ if leaf .count not in [FAKE_ONE , ZERO ] and leaf .color == WHITE :
147
+ leaf .color = _cr [float (leaf .count )]
148
+ # print(f"Apply color to {leaf} based on min: {_min} and max: {_max}")
149
+
150
+
151
+ def add_rows (self , rows : list [dict [str , Any ]], ontology_type : str ):
152
+ if ontology_type == PARENT_BASED_ONTOLOGY :
153
+ self ._add_parent_based_rows (rows = rows )
154
+ else :
155
+ self ._add_id_based_rows (rows = rows )
156
+ for branch in self .branches .values ():
157
+ branch .count_levels_and_children ()
158
+
159
+ def _add_parent_based_rows (self , rows : list [dict [str , Any ]]):
108
160
test_row = rows [0 ]
109
161
if self .parent_col not in test_row .keys ():
110
- raise KeyError (f"Parent-based ontology expected, but '{ self .parent_col } ' not found in table." )
162
+ raise KeyError (f"{ PARENT_BASED_ONTOLOGY } ontology expected, but '{ self .parent_col } ' not found in table." )
111
163
if self .id_col not in test_row .keys ():
112
164
raise KeyError (f"ID column '{ self .id_col } ' not found in table." )
113
165
@@ -128,12 +180,18 @@ def add_parent_based_rows(self, rows: list[dict[str, Any]]):
128
180
label = row .get (self .label_col ) or UNDEFINED ,
129
181
description = row .get (self .description_col ) or UNDEFINED ,
130
182
)
183
+ self .id_to_leaf [leaf_id ] = leaf
131
184
if not _parent :
132
185
self .branches [leaf_id ].leaves [leaf_id ] = leaf
133
186
self .branch_title_lookup .append (leaf_id )
134
187
else :
135
188
post_process .append (leaf )
136
189
190
+ if len (self .branches ) == 0 :
191
+ raise ValueError ("Could not identify any branch! Make sure the Parent Column is set correctly, and that at "
192
+ "least one ID without a parent exists to create a new branch." )
193
+
194
+ iterations = 0
137
195
while len (post_process ) != len (processed ):
138
196
for leaf in post_process :
139
197
if leaf .id in processed :
@@ -143,11 +201,11 @@ def add_parent_based_rows(self, rows: list[dict[str, Any]]):
143
201
branch .leaves [leaf .id ] = leaf
144
202
processed .append (leaf .id )
145
203
break
204
+ iterations += 1
205
+ if iterations == 100 :
206
+ raise ValueError ("Could not build tree. Make sure the Columns are set properly." )
146
207
147
- for branch in self .branches .values ():
148
- branch .count_levels_and_children ()
149
-
150
- def add_id_based_rows (self , rows ):
208
+ def _add_id_based_rows (self , rows ):
151
209
table_data = dict ()
152
210
153
211
for row in rows :
@@ -167,6 +225,7 @@ def add_id_based_rows(self, rows):
167
225
id = uncertain_leaf_id ,
168
226
parent = parent ,
169
227
)
228
+ self .id_to_leaf [uncertain_leaf_id ] = leaf
170
229
171
230
first_level_id = uncertain_leaf_id .split (self .level_separator )[0 ]
172
231
if first_level_id not in self .branches :
@@ -198,15 +257,18 @@ def get_individual_plots(self) -> go.Figure:
198
257
199
258
menu = [{
200
259
"active" : 0 ,
260
+ "type" : "buttons" ,
261
+ "direction" : "right" ,
201
262
"buttons" : buttons ,
202
263
"yanchor" : "bottom" ,
203
264
"pad" : {"t" : 0 , "b" : 10 },
204
265
"x" : 0.5 ,
266
+ "y" : 1.2 ,
205
267
"xanchor" : "center"
206
268
}]
207
269
layout = {
208
270
"showlegend" : False ,
209
- "updatemenus" : menu
271
+ "updatemenus" : menu ,
210
272
}
211
273
212
274
# create figure, hide initial data
@@ -216,6 +278,7 @@ def get_individual_plots(self) -> go.Figure:
216
278
fig .data [0 ].update (visible = True )
217
279
else :
218
280
fig = go .Figure (data = self .traces [0 ])
281
+
219
282
return fig
220
283
221
284
def get_summary_plot (self , cols : int ):
@@ -367,36 +430,60 @@ def get_layout_navbar() -> dbc.Navbar:
367
430
def get_layout_data_table () -> dbc .Collapse :
368
431
return dbc .Collapse (
369
432
html .Div ([
370
- dcc .Upload (
371
- id = "datatable-upload" ,
372
- children = html .Div (["Drag and Drop or " , html .A ("Click to Upload" )]),
373
- ),
374
- dash_table .DataTable (
375
- id = "datatable" ,
376
- page_current = 0 ,
377
- page_size = 10 ,
378
- export_format = "csv" ,
379
- editable = True ,
380
- row_deletable = True ,
381
- sort_action = "native" ,
382
- style_table = {
383
- "padding" : "6px" ,
384
- },
385
- style_cell = {
386
- "overflow" : "hidden" ,
387
- "textOverflow" : "ellipsis" ,
388
- "maxWidth" : 0 ,
389
- "padding" : "2px" ,
390
- },
391
- style_data = {},
392
- tooltip_duration = None ,
393
- tooltip_delay = 1000 ,
394
- css = [{
395
- "selector" : ".dash-table-tooltip" ,
396
- "rule" : "background-color: #C33D35; color: white;"
397
- }],
398
- ),
399
- dbc .Button ('Add Row' , id = 'datatable-add-row-button' , n_clicks = 0 , className = "ms-2 me-2" ),
433
+ dbc .Card ([
434
+ dbc .CardBody ([
435
+ dbc .Row ([
436
+ dbc .Col ([
437
+ * _get_label_badge_combo (label = "Ontology Type" ,
438
+ tooltip = (
439
+ "You can load two types of ontologies: parent-based and "
440
+ "separator-based. Parent-based ontologies include columns for "
441
+ "IDs and their respective parents. Separator-based ontologies "
442
+ "use an ID with tree-syntax, such as the MeSH ontology with "
443
+ "IDs like 'C01.001'. When loading custom files, please ensure "
444
+ "you set the appropriate ontology type beforehand."
445
+ ),
446
+ bold = False , italic = False )],
447
+ className = "collapse-card-header" ),
448
+ dbc .Col ([html .Div ([
449
+ dcc .RadioItems (
450
+ options = [PARENT_BASED_ONTOLOGY , SEPARATOR_BASED_ONTOLOGY ],
451
+ value = PARENT_BASED_ONTOLOGY ,
452
+ inline = True ,
453
+ labelStyle = {"padding-right" : "20px" },
454
+ inputStyle = {"margin-right" : "4px" },
455
+ id = "ontology-type"
456
+ ),
457
+ ]), ])
458
+ ]),
459
+ dcc .Upload (
460
+ id = "datatable-upload" ,
461
+ children = html .Div (["Drag and Drop or " , html .A ("Click to Upload" )]),
462
+ ),
463
+ dash_table .DataTable (
464
+ id = "datatable" ,
465
+ page_current = 0 ,
466
+ page_size = 10 ,
467
+ export_format = "csv" ,
468
+ editable = True ,
469
+ row_deletable = True ,
470
+ sort_action = "native" ,
471
+ style_cell = {
472
+ "overflow" : "hidden" ,
473
+ "textOverflow" : "ellipsis" ,
474
+ "maxWidth" : 0 ,
475
+ "padding" : "2px" ,
476
+ },
477
+ style_data = {},
478
+ tooltip_duration = None ,
479
+ tooltip_delay = 1000 ,
480
+ css = [{
481
+ "selector" : ".dash-table-tooltip" ,
482
+ "rule" : "background-color: #C33D35; color: white;"
483
+ }],
484
+ ),
485
+ dbc .Button ("Add Row" , id = 'datatable-add-row-button' , n_clicks = 0 , className = "mt-2 mb-2" ),
486
+ ])], className = "ms-2 me-2 mt-2 mb-2" ),
400
487
]), id = "collapse-load" , is_open = True ,
401
488
)
402
489
@@ -406,51 +493,48 @@ def get_layout_config() -> dbc.Collapse:
406
493
dbc .Card ([
407
494
dbc .CardBody ([
408
495
html .Div (id = "dummy-div" , style = {"display" : "none" }),
409
- dbc .Row ([
410
- dbc .Col ([
411
- * _get_label_badge_combo (description = "Ontology Type" ,
412
- tooltip = "EDIT ME Ontology type description" ,
413
- bold_italic = False )],
414
- className = "collapse-card-header" ),
415
- dbc .Col ([html .Div ([
416
- dcc .RadioItems (["Parent-based" , "Separator-based" ], "Parent-based" , inline = True ,
417
- labelStyle = {"padding-right" : "20px" }, inputStyle = {"margin-right" : "4px" },
418
- id = "ontology-type" ),
419
- ], className = "me-5" ), ], className = "collapse-card" )
420
- ]),
421
496
dbc .Row ([
422
497
dbc .Col (html .Span ("Data Mapping" ), className = "collapse-card-header" ),
423
498
dbc .Col (get_layout_config_data_elements (), className = "collapse-card" )
424
- ], className = "border-top mt-4 pt-2" ),
425
- dbc .Row ([
426
- dbc .Col (html .Span ("Colors" ), className = "collapse-card-header" ),
427
- dbc .Col (get_layout_config_color_elements (), className = "collapse-card" ),
428
- ], className = "border-top mt-4 pt-2" ),
429
- dbc .Row ([
430
- dbc .Col (html .Span ("Labels" ), className = "collapse-card-header" ),
431
- dbc .Col (get_layout_config_label_elements (), className = "collapse-card" ),
432
- ], className = "border-top mt-4 pt-2" ),
433
- dbc .Row ([
434
- dbc .Col ([
435
- * _get_label_badge_combo (description = "Propagation" ,
436
- tooltip = "By enabling propagation, counts and colors can be "
437
- "up-propagated up to the central node of the tree" ,
438
- bold_italic = False )],
439
- className = "collapse-card-header" ),
440
- dbc .Col (get_layout_config_propagate_elements (), className = "collapse-card" ),
441
- ], className = "border-top mt-4 pt-2" ),
442
- dbc .Row ([
443
- dbc .Col (html .Span ("Border" ), className = "collapse-card-header" ),
444
- dbc .Col (get_layout_config_border_elements (), className = "collapse-card" ),
445
- ], className = "border-top mt-4 pt-2" ),
446
- dbc .Row ([
447
- dbc .Col (html .Span ("Legend" ), className = "collapse-card-header" ),
448
- dbc .Col (get_layout_config_legend_elements (), className = "collapse-card" ),
449
- ], className = "border-top mt-4 pt-2" ),
450
- dbc .Row ([
451
- dbc .Col (html .Span ("Plot Type" ), className = "collapse-card-header" ),
452
- dbc .Col (get_layout_plot_type_elements (), className = "collapse-card" ),
453
- ], className = "border-top mt-4 pt-2" ),
499
+ ], className = "border-top mt-4 pt-2" , id = "data-columns-config" ),
500
+ html .Div ([
501
+ dbc .Row ([
502
+ dbc .Col ([
503
+ * _get_label_badge_combo (label = "Colors" ,
504
+ tooltip = "Applies the defined color scale to rows with a count value. "
505
+ "Rows with a manually defined color are not overwritten. "
506
+ "Values outside of the defined thresholds will remain "
507
+ "transparent. 0% represents the row with the lowest count, "
508
+ "100% the row with the highest count." ,
509
+ bold = False , italic = False )], className = "collapse-card-header" ),
510
+ dbc .Col (get_layout_config_color_elements (), className = "collapse-card" ),
511
+ ], className = "border-top mt-4 pt-2" ),
512
+ dbc .Row ([
513
+ dbc .Col (html .Span ("Labels" ), className = "collapse-card-header" ),
514
+ dbc .Col (get_layout_config_label_elements (), className = "collapse-card" ),
515
+ ], className = "border-top mt-4 pt-2" ),
516
+ dbc .Row ([
517
+ dbc .Col ([
518
+ * _get_label_badge_combo (label = "Propagation" ,
519
+ tooltip = "By enabling propagation, counts and colors can be "
520
+ "up-propagated up to the central node of the tree" ,
521
+ bold = False , italic = False )],
522
+ className = "collapse-card-header" ),
523
+ dbc .Col (get_layout_config_propagate_elements (), className = "collapse-card" ),
524
+ ], className = "border-top mt-4 pt-2" ),
525
+ dbc .Row ([
526
+ dbc .Col (html .Span ("Border" ), className = "collapse-card-header" ),
527
+ dbc .Col (get_layout_config_border_elements (), className = "collapse-card" ),
528
+ ], className = "border-top mt-4 pt-2" ),
529
+ dbc .Row ([
530
+ dbc .Col (html .Span ("Legend" ), className = "collapse-card-header" ),
531
+ dbc .Col (get_layout_config_legend_elements (), className = "collapse-card" ),
532
+ ], className = "border-top mt-4 pt-2" ),
533
+ dbc .Row ([
534
+ dbc .Col (html .Span ("Plot Style" ), className = "collapse-card-header" ),
535
+ dbc .Col (get_layout_plot_type_elements (), className = "collapse-card" ),
536
+ ], className = "border-top mt-4 pt-2" ),
537
+ ], id = "config-inactive-controller" ),
454
538
]),
455
539
]), id = "collapse-config" , className = "ms-2 me-2 mt-2 mb-2" , is_open = True ,
456
540
)
@@ -459,8 +543,9 @@ def get_layout_config() -> dbc.Collapse:
459
543
def get_layout_config_data_elements () -> list [html .Div ]:
460
544
return [
461
545
html .Div ([
462
- * _get_label_badge_combo (description = "Level Separator" ,
463
- tooltip = "EDIT ME Separator Character description" ),
546
+ * _get_label_badge_combo (label = "Level Separator" ,
547
+ tooltip = "Select the character that is used to distinguish hierarchical levels in "
548
+ "the ID values of your data" ),
464
549
dcc .Dropdown (
465
550
id = "separator-character" ,
466
551
options = [
@@ -474,40 +559,46 @@ def get_layout_config_data_elements() -> list[html.Div]:
474
559
),
475
560
], id = "separator-character-row" , className = "me-5" ),
476
561
html .Div ([
477
- * _get_label_badge_combo (description = "ID Column" , tooltip = "EDIT ME ID Column description " ),
562
+ * _get_label_badge_combo (label = "ID Column" , tooltip = "Select the column in your data that contains IDs " ),
478
563
dcc .Dropdown (id = "id-column" ),
479
564
], className = "me-5" ),
480
565
html .Div ([
481
- * _get_label_badge_combo (description = "Parent Column" , tooltip = "EDIT ME Parent Column description" ),
566
+ * _get_label_badge_combo (label = "Parent Column" , tooltip = "Select the column in your data that contains "
567
+ "parent IDs" ),
482
568
dcc .Dropdown (id = "parent-column" ),
483
569
], id = "parent-column-row" , className = "me-5" ),
484
570
html .Div ([
485
- * _get_label_badge_combo (description = "Label Column" , tooltip = "EDIT ME Label Column description" ),
571
+ * _get_label_badge_combo (label = "Label Column" , tooltip = "Select the column in your data that contains "
572
+ "labels" ),
486
573
dcc .Dropdown (id = "label-column" ),
487
574
], className = "me-5" ),
488
575
html .Div ([
489
- * _get_label_badge_combo (description = "Description Column" ,
490
- tooltip = "EDIT ME Description Column description " ),
576
+ * _get_label_badge_combo (label = "Description Column" , tooltip = "Select the column in your data that contains "
577
+ "descriptions (shown as interactive tooltips " ),
491
578
dcc .Dropdown (id = "description-column" ),
492
579
], className = "me-5" ),
493
580
html .Div ([
494
- * _get_label_badge_combo (description = "Count Column" , tooltip = "EDIT ME Count Column description" ),
581
+ * _get_label_badge_combo (label = "Count Column" , tooltip = "Select the column in your data that contains "
582
+ "counts (int or float values greater than 0)" ),
495
583
dcc .Dropdown (id = "count-column" ),
496
584
], className = "me-5" ),
497
585
html .Div ([
498
- * _get_label_badge_combo (description = "Color Column" , tooltip = "EDIT ME Color Column description" ),
586
+ * _get_label_badge_combo (label = "Color Column" , tooltip = "Select the column in your data that contains "
587
+ "colors (hex codes with preceding #, e.g. #FF0000)" ),
499
588
dcc .Dropdown (id = "color-column" ),
500
- ], className = "me-5" ), ]
589
+ ], className = "me-5" ),
590
+ html .Span (id = "data-columns-config-status" , className = "mt-2 text-danger" )
591
+ ]
501
592
502
593
503
- def get_layout_config_color_elements () -> list [html . Div ]:
594
+ def get_layout_config_color_elements () -> list [dbc . Col ]:
504
595
return [
505
- html .Div (dbc .Button ("Add" , id = "colorpicker-add" , n_clicks = 1 )),
506
- html .Div (dbc .Button ("Remove" , id = "colorpicker-rm" , n_clicks = 0 , className = "ms-2 me-2" )),
507
596
dbc .Col ([
508
597
dbc .Row ([dcc .RangeSlider (
509
598
id = "colorpicker-slider" ,
510
- min = 0 , max = 100 , value = [0 , 100 ],
599
+ min = 0 ,
600
+ max = 100 ,
601
+ value = [0 , 100 ],
511
602
pushable = 2 ,
512
603
className = "color-picker-scale" ,
513
604
marks = {0 : "#000000" , 100 : "#C33D35" },
@@ -519,10 +610,31 @@ def get_layout_config_color_elements() -> list[html.Div]:
519
610
ColorPicker .get_row (idx = 0 , color = "#000000" ),
520
611
ColorPicker .get_row (idx = 1 , color = "#C33D35" ),
521
612
], id = "colorpicker-container" ),
522
- ], className = "ms-4" ),
523
- html .Div (
524
- dbc .Button ("Apply to Table" , id = "colorpicker-apply" , n_clicks = 0 , className = "ms-4" )
525
- ),
613
+ ], className = "ms-4 colorpicker-col1" ),
614
+ dbc .Col ([
615
+ * _get_label_badge_combo (label = "Thresholds" , tooltip = "Add/remove threshold levels to the color scale" ,
616
+ bold = True , italic = False ),
617
+ dbc .ButtonGroup ([
618
+ dbc .Button (" - " , id = "colorpicker-rm" , n_clicks = 0 , disabled = True ,
619
+ className = "plus-minus-btn btn-danger" ),
620
+ dbc .Button (" + " , id = "colorpicker-add" , n_clicks = 1 , disabled = False ,
621
+ className = "plus-minus-btn btn-success" ),
622
+ ], className = "d-flex justify-content-center" )
623
+ ], className = "colorpicker-col2" ),
624
+ dbc .Col ([
625
+ html .Div ([
626
+ dcc .RadioItems ([GLOBAL , LOCAL ], GLOBAL , inline = True ,
627
+ inputStyle = {"margin-right" : "4px" , "margin-left" : "4px" },
628
+ id = "colorpicker-global-local" ),
629
+ * _get_label_badge_combo (label = None ,
630
+ tooltip = "apply the color scale based on the maximum values of the entire tree "
631
+ "(global) or each sub-tree individually (local)" )
632
+ ], className = "d-flex justify-content-center mb-2" ),
633
+ html .Div ([
634
+ dbc .Button ("Clear" , id = "colorpicker-reset" , n_clicks = 0 , className = "ms-2 me-2 btn-warning" ),
635
+ dbc .Button ("Apply" , id = "colorpicker-apply" , n_clicks = 0 )
636
+ ], className = "d-flex justify-content-center" ),
637
+ ], className = "colorpicker-col3" )
526
638
]
527
639
528
640
@@ -532,6 +644,8 @@ def get_layout_config_color_elements() -> list[html.Div]:
532
644
Output ("colorpicker-slider" , "marks" ),
533
645
Output ("colorpicker-slider" , "value" ),
534
646
Output ("colorpicker-sample" , "style" ),
647
+ Output ("colorpicker-add" , "disabled" ),
648
+ Output ("colorpicker-rm" , "disabled" ),
535
649
],
536
650
[
537
651
Input ("colorpicker-add" , "n_clicks" ),
@@ -548,7 +662,7 @@ def update_color_picker(n_clicks_add, n_clicks_rm, slider_values, colorpicker_va
548
662
if not callback_context .triggered :
549
663
if n_clicks_add == 1 :
550
664
cp = ColorPicker (children = container_children , marks = slider_marks , values = slider_values )
551
- return cp .children , cp .marks , cp .values , cp .sample_scale_style
665
+ return cp .children , cp .marks , cp .values , cp .sample_scale_style , no_update , no_update
552
666
raise PreventUpdate
553
667
554
668
button_id = callback_context .triggered [0 ]['prop_id' ].split ('.' )[0 ]
@@ -566,12 +680,16 @@ def update_color_picker(n_clicks_add, n_clicks_rm, slider_values, colorpicker_va
566
680
elif "colorpicker_input" in button_id :
567
681
cp .picker_event (picker_obj = json .loads (button_id ), colors = colorpicker_values )
568
682
569
- return cp .children , cp .marks , cp .values , cp .sample_scale_style
683
+ add_btn_disabled = True if len (cp .values ) >= 20 else False
684
+ rm_btn_disabled = True if len (cp .values ) <= 2 else False
685
+
686
+ return cp .children , cp .marks , cp .values , cp .sample_scale_style , add_btn_disabled , rm_btn_disabled
570
687
571
688
572
689
def get_layout_config_label_elements () -> list [html .Div ]:
573
690
return [
574
- * _get_label_badge_combo (description = "Show Labels" , tooltip = "EDIT ME Show Labels description" ),
691
+ * _get_label_badge_combo (label = "Show Labels" , tooltip = "EDIT ME Show Labels description" ,
692
+ bold = True , italic = False ),
575
693
dcc .Dropdown (
576
694
id = "show-labels" ,
577
695
options = [
@@ -582,7 +700,7 @@ def get_layout_config_label_elements() -> list[html.Div]:
582
700
{"label" : "first + last" , "value" : "first + last" },
583
701
],
584
702
value = "all" ,
585
- className = "fixed-width"
703
+ className = "ms-2 fixed-width"
586
704
),
587
705
]
588
706
@@ -594,7 +712,7 @@ def get_layout_config_propagate_elements() -> list[html.Div]:
594
712
]),
595
713
html .Div ([
596
714
html .Div ([
597
- * _get_label_badge_combo (description = "Scale" ,
715
+ * _get_label_badge_combo (label = "Scale" ,
598
716
tooltip = "This option controls whether the propagation should "
599
717
"be limited to each tree (Individual), or to consider "
600
718
"the entire ontology (Global)" ),
@@ -603,7 +721,7 @@ def get_layout_config_propagate_elements() -> list[html.Div]:
603
721
id = "propagate-individual-global" )
604
722
], className = "d-flex flex-row align-items-center ms-2 me-5" ),
605
723
html .Div ([
606
- * _get_label_badge_combo (description = "Level" , tooltip = "Determine to which level in the tree the counts "
724
+ * _get_label_badge_combo (label = "Level" , tooltip = "Determine to which level in the tree the counts "
607
725
"should be up-propagated" ),
608
726
dbc .Input (type = "number" , min = 1 , max = 15 , step = 1 , value = 1 , id = "propagate-level" )
609
727
], className = "d-flex flex-row align-items-center ms-5" )
@@ -646,23 +764,27 @@ def get_layout_config_legend_elements() -> list[html.Div]:
646
764
def get_layout_plot_type_elements () -> list [html .Div ]:
647
765
return [
648
766
html .Div ([
649
- dcc .RadioItems (["Individual Plots & Menu" , "Summary Plot" ], "Individual Plots & Menu" ,
767
+ dcc .RadioItems ([INDIVIDUAL_PLOTS , SUMMARY_PLOT ], INDIVIDUAL_PLOTS ,
650
768
inline = True , labelStyle = {"padding-right" : "20px" }, inputStyle = {"margin-right" : "4px" },
651
769
id = "plot-type" ),
652
770
], className = "me-2" ),
653
771
html .Div ([
654
772
dbc .Input (type = "number" , min = 1 , max = 15 , step = 1 , value = 3 , id = "plot-type-cols" , placeholder = "Columns" ,
655
773
disabled = True )
656
- ])
774
+ ], className = "me-2" ),
775
+ html .Div ([dcc .Slider (min = 10 , max = 3000 , step = 10 , value = 800 , marks = None ,
776
+ tooltip = {"placement" : "right" , "always_visible" : True ,
777
+ "template" : "Plot Height: {value}px" },
778
+ className = "me-5 mt-4" , id = "plot-height" )], style = {"width" : "33%" }),
657
779
]
658
780
659
781
660
- def _get_label_badge_combo (description : str , tooltip : str , bold_italic : bool = True ) -> tuple [Div , Popover ]:
661
- _id = description .lower ().replace (" " , "-" )
782
+ def _get_label_badge_combo (label : str | None , tooltip : str , bold : bool = True , italic : bool = True ) -> tuple [Div , Popover ]:
783
+ _id = label .lower ().replace (" " , "-" ) if label else str ( uuid . uuid4 () )
662
784
return (
663
785
html .Div (
664
786
[
665
- dbc .Label (description , className = " fw-bold fst-italic" if bold_italic else "" ) ,
787
+ dbc .Label (label , className = f" { [ '' , ' fw-bold' ][ bold ] } { [ '' , ' fst-italic' ][ italic ] } " ) if label else None ,
666
788
dbc .Button (
667
789
dbc .Badge ("i" , color = "info" , pill = True ),
668
790
id = f"{ _id } -target" , className = "btn-link popover-info-badge"
@@ -671,10 +793,9 @@ def _get_label_badge_combo(description: str, tooltip: str, bold_italic: bool = T
671
793
className = "popover-info-container"
672
794
),
673
795
dbc .Popover ([
674
- dbc .PopoverHeader (description ),
675
- dbc .PopoverBody (tooltip )
676
- ], target = f"{ _id } -target" , trigger = "focus"
677
- )
796
+ dbc .PopoverHeader (label , style = {"font-weight" : "bold" , "font-size" : "16px" }),
797
+ dbc .PopoverBody (tooltip , style = {"font-size" : "14px" })
798
+ ], target = f"{ _id } -target" , trigger = "hover" , style = {"max-width" : "50%" , "width" : "auto" })
678
799
)
679
800
680
801
@@ -766,7 +887,7 @@ def toggle_collapse_load(n, is_open):
766
887
Input ("plot-type" , "value" ),
767
888
)
768
889
def toggle_plot_type_columns (value ):
769
- return True if value == "Individual Plots & Menu" else False
890
+ return True if value == INDIVIDUAL_PLOTS else False
770
891
771
892
772
893
@callback (
@@ -790,7 +911,7 @@ def toggle_legend_elements(value):
790
911
791
912
792
913
"""
793
- ############################## Plot brain callback ###########################
914
+ ############################## Table brain callback ###########################
794
915
"""
795
916
796
917
@@ -815,29 +936,43 @@ def toggle_legend_elements(value):
815
936
], [
816
937
Input ('datatable-upload' , 'contents' ),
817
938
Input ('datatable-add-row-button' , 'n_clicks' ),
818
- Input ("ontology-type" , "value" )
939
+ Input ("ontology-type" , "value" ),
940
+ Input ("colorpicker-apply" , "n_clicks" ),
941
+ Input ("colorpicker-reset" , "n_clicks" ),
819
942
], [
820
943
State ('datatable-upload' , 'filename' ),
821
944
State ('datatable' , 'data' ),
822
- State ('datatable' , 'columns' )
945
+ State ('datatable' , 'columns' ),
946
+ State ("colorpicker-slider" , "marks" ),
947
+ State ("colorpicker-global-local" , "value" ),
948
+ State ("plot-type" , "value" ),
949
+ State ("separator-character" , "value" ),
950
+ State ("id-column" , "value" ),
951
+ State ("parent-column" , "value" ),
952
+ State ("label-column" , "value" ),
953
+ State ("description-column" , "value" ),
954
+ State ("count-column" , "value" ),
955
+ State ("color-column" , "value" ),
823
956
])
824
- def update_output (contents , add_row_n_clicks , value , filename , datatable_rows , datatable_columns ):
957
+ def update_output (contents , add_row_n_clicks , ontology_type , colorpicker_apply_n_clicks , colorpicker_reset_n_clicks ,
958
+ filename , datatable_rows , datatable_columns , colorpicker_slider_marks , colorpicker_global_local ,
959
+ plot_type , level_separator , id_col , parent_col , label_col , description_col , count_col , color_col ):
825
960
triggered = [t ['prop_id' ] for t in callback_context .triggered ]
826
961
827
- # parent_column_opt = {"display": "block" if value == "Parent-based" else "none"}
828
- # separator_column_opt = {"display": "none" if value == "Parent-based" else "block"}
962
+ # parent_column_opt = {"display": "block" if value == PARENT_BASED_ONTOLOGY else "none"}
963
+ # separator_column_opt = {"display": "none" if value == PARENT_BASED_ONTOLOGY else "block"}
829
964
830
965
# vars below must match number of output parameters defined in callback above and must be returned
831
966
datatable_data = no_update
832
967
datatable_columns = datatable_columns if "datatable-add-row-button.n_clicks" in triggered else no_update
833
968
datatable_tooltip_data = no_update
834
969
column_options = no_update
835
- parent_column_row = {"display" : "block" if value == "Parent-based" else "none" }
836
- separator_character_row = {"display" : "none" if value == "Parent-based" else "block" }
970
+ parent_column_row = {"display" : "block" if ontology_type == PARENT_BASED_ONTOLOGY else "none" }
971
+ separator_character_row = {"display" : "none" if ontology_type == PARENT_BASED_ONTOLOGY else "block" }
837
972
838
973
# initial load of a template
839
974
if triggered == ["." ] or triggered == ["ontology-type.value" ]:
840
- template = "custom_template_separator_based.tsv" if value == "Separator-based" else "custom_template_parent_based.tsv"
975
+ template = "custom_template_separator_based.tsv" if ontology_type == SEPARATOR_BASED_ONTOLOGY else "custom_template_parent_based.tsv"
841
976
df = pd .read_csv (f"../../templates/{ template } " , delimiter = "\t " )
842
977
datatable_data , datatable_columns , datatable_tooltip_data , column_options = get_table_objects (df = df )
843
978
@@ -850,6 +985,37 @@ def update_output(contents, add_row_n_clicks, value, filename, datatable_rows, d
850
985
df = parse_contents (contents , filename )
851
986
datatable_data , datatable_columns , datatable_tooltip_data , column_options = get_table_objects (df = df )
852
987
988
+ elif "colorpicker-reset.n_clicks" in triggered and colorpicker_reset_n_clicks > 0 :
989
+ if color_col not in datatable_rows [0 ].keys ():
990
+ return
991
+ for row in datatable_rows :
992
+ row [color_col ] = None
993
+ datatable_data = datatable_rows
994
+
995
+ elif "colorpicker-apply.n_clicks" in triggered and colorpicker_apply_n_clicks > 0 :
996
+ tree = Tree (
997
+ id_separator = "|" ,
998
+ level_separator = level_separator ,
999
+ id_col = id_col ,
1000
+ parent_col = parent_col ,
1001
+ label_col = label_col ,
1002
+ description_col = description_col ,
1003
+ count_col = count_col ,
1004
+ color_col = color_col
1005
+ )
1006
+ tree .add_rows (rows = datatable_rows , ontology_type = ontology_type )
1007
+
1008
+ # add 0 and 100 as transparent marks if not existent
1009
+ color_scale = colorpicker_slider_marks | {k : TRANSPARENT for k in ["0" , "100" ]
1010
+ if k not in colorpicker_slider_marks .keys ()}
1011
+ tree .apply_color (color_scale = color_scale , global_scale = True if colorpicker_global_local == "global" else False )
1012
+ datatable_data = []
1013
+ for row in datatable_rows :
1014
+ if not row [color_col ] and row [count_col ]:
1015
+ first_id = row [id_col ].split ("|" )[0 ]
1016
+ row [color_col ] = tree .id_to_leaf [first_id ].color
1017
+ datatable_data .append (row )
1018
+
853
1019
return [
854
1020
datatable_data ,
855
1021
datatable_columns ,
@@ -893,7 +1059,12 @@ def toggle_collapse_load(n, is_open):
893
1059
894
1060
895
1061
@callback (
896
- Output ("table-output" , "figure" ), [
1062
+ [
1063
+ Output ("table-output" , "figure" ),
1064
+ Output ("data-columns-config" , "className" ),
1065
+ Output ("data-columns-config-status" , "children" ),
1066
+ Output ("config-inactive-controller" , "style" ),
1067
+ ], [
897
1068
Input ("datatable" , "data" ),
898
1069
Input ("datatable" , "columns" ),
899
1070
Input ("ontology-type" , "value" ),
@@ -905,10 +1076,11 @@ def toggle_collapse_load(n, is_open):
905
1076
Input ("count-column" , "value" ),
906
1077
Input ("color-column" , "value" ),
907
1078
Input ("plot-type" , "value" ),
908
- Input ("plot-type-cols" , "value" )
1079
+ Input ("plot-type-cols" , "value" ),
1080
+ Input ("plot-height" , "value" ),
909
1081
])
910
- def update_color_picker ( rows , columns , ontology_type , level_separator , id_col , parent_col , label_col , description_col ,
911
- count_col , color_col , plot_type , plot_type_cols ):
1082
+ def visualize ( datatable_data , datatable_columns , ontology_type , level_separator , id_col , parent_col , label_col ,
1083
+ description_col , count_col , color_col , plot_type , plot_type_cols , plot_height ):
912
1084
tree = Tree (
913
1085
id_separator = "|" ,
914
1086
level_separator = level_separator ,
@@ -919,16 +1091,31 @@ def update_color_picker(rows, columns, ontology_type, level_separator, id_col, p
919
1091
count_col = count_col ,
920
1092
color_col = color_col
921
1093
)
922
- if ontology_type == "Parent-based" :
923
- tree .add_parent_based_rows (rows = rows )
924
- else :
925
- tree .add_id_based_rows (rows = rows )
926
- tree .get_traces ()
927
- if plot_type == "Individual Plots & Menu" :
928
- return tree .get_individual_plots ()
929
- else :
930
- return tree .get_summary_plot (cols = plot_type_cols )
931
-
1094
+ try :
1095
+ tree .add_rows (rows = datatable_data , ontology_type = ontology_type )
1096
+ tree .get_traces ()
1097
+ if plot_type == INDIVIDUAL_PLOTS :
1098
+ figure = tree .get_individual_plots ()
1099
+ else :
1100
+ figure = tree .get_summary_plot (cols = plot_type_cols )
1101
+ figure .update_traces (leaf = dict (opacity = 1 ))
1102
+ figure .update_layout (height = plot_height )
1103
+ toggle_config_inactivity (inactive = False )
1104
+ return figure , "border-top mt-4 pt-2" , "" , {"opacity" : "unset" , "pointer-events" : "unset" }
1105
+
1106
+ except Exception as e :
1107
+ toggle_config_inactivity (inactive = True )
1108
+ return no_update , "mt-4 pt-2 error" , str (e ), {"opacity" : "50%" , "pointer-events" : "none" }
1109
+
1110
+
1111
+ def toggle_config_inactivity (inactive : bool ):
1112
+ for element in app .layout .children :
1113
+ if isinstance (element , dbc .Row ) and "config-inactive-controller" in element .className :
1114
+ if inactive :
1115
+ element .className = element .className .replace ("config-inactive-controller-active" , "config-inactive-controller-inactive" )
1116
+ else :
1117
+ element .className = element .className .replace ("config-inactive-controller-inactive" ,
1118
+ "config-inactive-controller-active" )
932
1119
933
1120
"""
934
1121
############################## Export ###################
@@ -951,14 +1138,14 @@ def update_color_picker(rows, columns, ontology_type, level_separator, id_col, p
951
1138
State ("description-column" , "value" ),
952
1139
State ("count-column" , "value" ),
953
1140
State ("color-column" , "value" ),
1141
+ State ("plot-type" , "value" ),
1142
+ State ("plot-type-cols" , "value" )
954
1143
)
955
- def export_html (export_html_n_clicks , rows , columns , ontology_type , level_separator , id_col , parent_col , label_col ,
956
- description_col ,
957
- count_col , color_col ):
1144
+ def export_html (export_html_n_clicks , datatable_data , datatable_columns , ontology_type , level_separator , id_col ,
1145
+ parent_col , label_col , description_col , count_col , color_col , plot_type , plot_type_cols ):
958
1146
if export_html_n_clicks > 0 :
959
- fig = update_color_picker (rows , columns , ontology_type , level_separator , id_col , parent_col , label_col ,
960
- description_col ,
961
- count_col , color_col )
1147
+ fig = visualize (datatable_data , datatable_columns , ontology_type , level_separator , id_col , parent_col ,
1148
+ label_col , description_col , count_col , color_col , plot_type , plot_type_cols )
962
1149
buffer = io .StringIO ()
963
1150
fig .write_html (buffer )
964
1151
html_string = buffer .getvalue ()
0 commit comments