Skip to content

Commit bdb935c

Browse files
committedSep 17, 2024·
added data-table error handling, added plot height options, added color controls to clear and apply color scaling on global/local levels, refactoring, styling
1 parent e55fe41 commit bdb935c

File tree

2 files changed

+388
-155
lines changed

2 files changed

+388
-155
lines changed
 

‎src/ontoloviz/assets/style.css

+50-4
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,41 @@
8181
margin-top: 0.5rem;
8282
}
8383

84+
#colorpicker-rm, #colorpicker-add {
85+
width: 30px;
86+
}
87+
88+
.colorpicker-col1 {
89+
flex: 1 0;
90+
}
91+
92+
.colorpicker-col2 {
93+
flex: 0 0 10%;
94+
}
95+
96+
.colorpicker-col3 {
97+
flex: 0 0 10%;
98+
}
99+
100+
/*#rc-slider-track {*/
101+
/* background-color: unset;*/
102+
/*}*/
103+
104+
/*#rc-slider-rail {*/
105+
/* background-image: linear-gradient(to right, transparent 0%, rgb(0, 0, 0) 0%, rgb(195, 61, 53) 33%, rgb(255, 120, 0) 66%, rgb(184, 109, 18) 100%, transparent 100%);*/
106+
/* height: 6px;*/
107+
/*}*/
108+
84109
/* DataTable
85110
–––––––––––––––––––––––––––––––––––––––––––––––––– */
86111
#datatable-upload {
87-
width: 98.8%;
88112
height: 60px;
89113
line-height: 60px;
90-
border-width: 1px;
91-
border-style: dashed;
114+
border: 1px dashed grey;
92115
border-radius: 5px;
93116
text-align: center;
94-
margin: 10px;
117+
margin-top: 10px;
118+
margin-bottom: 10px;
95119
}
96120

97121
/* hide download button */
@@ -327,6 +351,8 @@ input[type="button"].button-primary:focus {
327351
margin-left: 0.5rem;
328352
cursor: help;
329353
border: none;
354+
padding: unset;
355+
height: unset;
330356
}
331357

332358
.popover-info-badge:hover,
@@ -497,6 +523,26 @@ hr {
497523
border-width: 0;
498524
border-top: 1px solid #E1E1E1; }
499525

526+
.error {
527+
border: 2px solid #C33D35;
528+
border-radius: 5px;
529+
}
530+
531+
.plus-minus-btn {
532+
border: 0.05rem solid black;
533+
border-radius: 10px;
534+
color: black;
535+
}
536+
537+
.config-inactive {
538+
opacity: 50%;
539+
pointer-events: none;
540+
}
541+
542+
.config-active {
543+
opacity: unset;
544+
pointer-events: unset;
545+
}
500546

501547
/* Clearing
502548
–––––––––––––––––––––––––––––––––––––––––––––––––– */

‎src/ontoloviz/web.py

+338-151
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import uuid
12
from dataclasses import dataclass
23
import datetime
34
from collections import defaultdict
@@ -18,21 +19,29 @@
1819
from dash_bootstrap_components import Popover
1920

2021
from src.ontoloviz.core import SunburstBase
22+
from src.ontoloviz.core_utils import generate_color_range, generate_composite_color_range
2123

2224

2325
""" ########################## Tree Components ############################### """
2426
FAKE_ONE: float = 1 + 1337e-9
2527
ZERO: float = 1337e-9
26-
WHITE: str = "#FFFFFF"
28+
WHITE: str = "#FFFFFFFF"
29+
TRANSPARENT: str = "#FFFFFF00"
2730
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"
2837

2938

3039
@dataclass
3140
class Leaf:
3241
id: str = ""
3342
parent: str = ""
3443
color: str = WHITE
35-
count: int = FAKE_ONE
44+
count: [float, int] = FAKE_ONE
3645
label: str = UNDEFINED
3746
description: str = UNDEFINED
3847
level: int = 0
@@ -42,6 +51,9 @@ class Leaf:
4251
class Branch:
4352
def __init__(self):
4453
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()
4557

4658
def count_levels_and_children(self):
4759
for leaf_id, leaf in self.leaves.items():
@@ -52,6 +64,13 @@ def count_levels_and_children(self):
5264
current_leaf.children += 1
5365
level += 1
5466
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)
5574

5675
def get_sunburst_object(self) -> go.Sunburst:
5776
return go.Sunburst(
@@ -77,13 +96,13 @@ def get_sunburst_object(self) -> go.Sunburst:
7796
_.count if _.count != FAKE_ONE else 0
7897
] for _ in self.leaves.values()
7998
],
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+
),
87106
)
88107

89108

@@ -102,12 +121,45 @@ def __init__(self, id_separator: str = "|", level_separator: str = ".", id_col:
102121

103122
self.branches: defaultdict[str, Branch] = defaultdict(Branch)
104123
self.branch_title_lookup = []
124+
self.id_to_leaf = dict()
105125
self.traces = None
106126

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]]):
108160
test_row = rows[0]
109161
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.")
111163
if self.id_col not in test_row.keys():
112164
raise KeyError(f"ID column '{self.id_col}' not found in table.")
113165

@@ -128,12 +180,18 @@ def add_parent_based_rows(self, rows: list[dict[str, Any]]):
128180
label=row.get(self.label_col) or UNDEFINED,
129181
description=row.get(self.description_col) or UNDEFINED,
130182
)
183+
self.id_to_leaf[leaf_id] = leaf
131184
if not _parent:
132185
self.branches[leaf_id].leaves[leaf_id] = leaf
133186
self.branch_title_lookup.append(leaf_id)
134187
else:
135188
post_process.append(leaf)
136189

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
137195
while len(post_process) != len(processed):
138196
for leaf in post_process:
139197
if leaf.id in processed:
@@ -143,11 +201,11 @@ def add_parent_based_rows(self, rows: list[dict[str, Any]]):
143201
branch.leaves[leaf.id] = leaf
144202
processed.append(leaf.id)
145203
break
204+
iterations += 1
205+
if iterations == 100:
206+
raise ValueError("Could not build tree. Make sure the Columns are set properly.")
146207

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):
151209
table_data = dict()
152210

153211
for row in rows:
@@ -167,6 +225,7 @@ def add_id_based_rows(self, rows):
167225
id=uncertain_leaf_id,
168226
parent=parent,
169227
)
228+
self.id_to_leaf[uncertain_leaf_id] = leaf
170229

171230
first_level_id = uncertain_leaf_id.split(self.level_separator)[0]
172231
if first_level_id not in self.branches:
@@ -198,15 +257,18 @@ def get_individual_plots(self) -> go.Figure:
198257

199258
menu = [{
200259
"active": 0,
260+
"type": "buttons",
261+
"direction": "right",
201262
"buttons": buttons,
202263
"yanchor": "bottom",
203264
"pad": {"t": 0, "b": 10},
204265
"x": 0.5,
266+
"y": 1.2,
205267
"xanchor": "center"
206268
}]
207269
layout = {
208270
"showlegend": False,
209-
"updatemenus": menu
271+
"updatemenus": menu,
210272
}
211273

212274
# create figure, hide initial data
@@ -216,6 +278,7 @@ def get_individual_plots(self) -> go.Figure:
216278
fig.data[0].update(visible=True)
217279
else:
218280
fig = go.Figure(data=self.traces[0])
281+
219282
return fig
220283

221284
def get_summary_plot(self, cols: int):
@@ -367,36 +430,60 @@ def get_layout_navbar() -> dbc.Navbar:
367430
def get_layout_data_table() -> dbc.Collapse:
368431
return dbc.Collapse(
369432
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"),
400487
]), id="collapse-load", is_open=True,
401488
)
402489

@@ -406,51 +493,48 @@ def get_layout_config() -> dbc.Collapse:
406493
dbc.Card([
407494
dbc.CardBody([
408495
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-
]),
421496
dbc.Row([
422497
dbc.Col(html.Span("Data Mapping"), className="collapse-card-header"),
423498
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"),
454538
]),
455539
]), id="collapse-config", className="ms-2 me-2 mt-2 mb-2", is_open=True,
456540
)
@@ -459,8 +543,9 @@ def get_layout_config() -> dbc.Collapse:
459543
def get_layout_config_data_elements() -> list[html.Div]:
460544
return [
461545
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"),
464549
dcc.Dropdown(
465550
id="separator-character",
466551
options=[
@@ -474,40 +559,46 @@ def get_layout_config_data_elements() -> list[html.Div]:
474559
),
475560
], id="separator-character-row", className="me-5"),
476561
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"),
478563
dcc.Dropdown(id="id-column"),
479564
], className="me-5"),
480565
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"),
482568
dcc.Dropdown(id="parent-column"),
483569
], id="parent-column-row", className="me-5"),
484570
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"),
486573
dcc.Dropdown(id="label-column"),
487574
], className="me-5"),
488575
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"),
491578
dcc.Dropdown(id="description-column"),
492579
], className="me-5"),
493580
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)"),
495583
dcc.Dropdown(id="count-column"),
496584
], className="me-5"),
497585
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)"),
499588
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+
]
501592

502593

503-
def get_layout_config_color_elements() -> list[html.Div]:
594+
def get_layout_config_color_elements() -> list[dbc.Col]:
504595
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")),
507596
dbc.Col([
508597
dbc.Row([dcc.RangeSlider(
509598
id="colorpicker-slider",
510-
min=0, max=100, value=[0, 100],
599+
min=0,
600+
max=100,
601+
value=[0, 100],
511602
pushable=2,
512603
className="color-picker-scale",
513604
marks={0: "#000000", 100: "#C33D35"},
@@ -519,10 +610,31 @@ def get_layout_config_color_elements() -> list[html.Div]:
519610
ColorPicker.get_row(idx=0, color="#000000"),
520611
ColorPicker.get_row(idx=1, color="#C33D35"),
521612
], 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")
526638
]
527639

528640

@@ -532,6 +644,8 @@ def get_layout_config_color_elements() -> list[html.Div]:
532644
Output("colorpicker-slider", "marks"),
533645
Output("colorpicker-slider", "value"),
534646
Output("colorpicker-sample", "style"),
647+
Output("colorpicker-add", "disabled"),
648+
Output("colorpicker-rm", "disabled"),
535649
],
536650
[
537651
Input("colorpicker-add", "n_clicks"),
@@ -548,7 +662,7 @@ def update_color_picker(n_clicks_add, n_clicks_rm, slider_values, colorpicker_va
548662
if not callback_context.triggered:
549663
if n_clicks_add == 1:
550664
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
552666
raise PreventUpdate
553667

554668
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
566680
elif "colorpicker_input" in button_id:
567681
cp.picker_event(picker_obj=json.loads(button_id), colors=colorpicker_values)
568682

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
570687

571688

572689
def get_layout_config_label_elements() -> list[html.Div]:
573690
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),
575693
dcc.Dropdown(
576694
id="show-labels",
577695
options=[
@@ -582,7 +700,7 @@ def get_layout_config_label_elements() -> list[html.Div]:
582700
{"label": "first + last", "value": "first + last"},
583701
],
584702
value="all",
585-
className="fixed-width"
703+
className="ms-2 fixed-width"
586704
),
587705
]
588706

@@ -594,7 +712,7 @@ def get_layout_config_propagate_elements() -> list[html.Div]:
594712
]),
595713
html.Div([
596714
html.Div([
597-
*_get_label_badge_combo(description="Scale",
715+
*_get_label_badge_combo(label="Scale",
598716
tooltip="This option controls whether the propagation should "
599717
"be limited to each tree (Individual), or to consider "
600718
"the entire ontology (Global)"),
@@ -603,7 +721,7 @@ def get_layout_config_propagate_elements() -> list[html.Div]:
603721
id="propagate-individual-global")
604722
], className="d-flex flex-row align-items-center ms-2 me-5"),
605723
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 "
607725
"should be up-propagated"),
608726
dbc.Input(type="number", min=1, max=15, step=1, value=1, id="propagate-level")
609727
], className="d-flex flex-row align-items-center ms-5")
@@ -646,23 +764,27 @@ def get_layout_config_legend_elements() -> list[html.Div]:
646764
def get_layout_plot_type_elements() -> list[html.Div]:
647765
return [
648766
html.Div([
649-
dcc.RadioItems(["Individual Plots & Menu", "Summary Plot"], "Individual Plots & Menu",
767+
dcc.RadioItems([INDIVIDUAL_PLOTS, SUMMARY_PLOT], INDIVIDUAL_PLOTS,
650768
inline=True, labelStyle={"padding-right": "20px"}, inputStyle={"margin-right": "4px"},
651769
id="plot-type"),
652770
], className="me-2"),
653771
html.Div([
654772
dbc.Input(type="number", min=1, max=15, step=1, value=3, id="plot-type-cols", placeholder="Columns",
655773
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%"}),
657779
]
658780

659781

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())
662784
return (
663785
html.Div(
664786
[
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,
666788
dbc.Button(
667789
dbc.Badge("i", color="info", pill=True),
668790
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
671793
className="popover-info-container"
672794
),
673795
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"})
678799
)
679800

680801

@@ -766,7 +887,7 @@ def toggle_collapse_load(n, is_open):
766887
Input("plot-type", "value"),
767888
)
768889
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
770891

771892

772893
@callback(
@@ -790,7 +911,7 @@ def toggle_legend_elements(value):
790911

791912

792913
"""
793-
############################## Plot brain callback ###########################
914+
############################## Table brain callback ###########################
794915
"""
795916

796917

@@ -815,29 +936,43 @@ def toggle_legend_elements(value):
815936
], [
816937
Input('datatable-upload', 'contents'),
817938
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"),
819942
], [
820943
State('datatable-upload', 'filename'),
821944
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"),
823956
])
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):
825960
triggered = [t['prop_id'] for t in callback_context.triggered]
826961

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"}
829964

830965
# vars below must match number of output parameters defined in callback above and must be returned
831966
datatable_data = no_update
832967
datatable_columns = datatable_columns if "datatable-add-row-button.n_clicks" in triggered else no_update
833968
datatable_tooltip_data = no_update
834969
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"}
837972

838973
# initial load of a template
839974
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"
841976
df = pd.read_csv(f"../../templates/{template}", delimiter="\t")
842977
datatable_data, datatable_columns, datatable_tooltip_data, column_options = get_table_objects(df=df)
843978

@@ -850,6 +985,37 @@ def update_output(contents, add_row_n_clicks, value, filename, datatable_rows, d
850985
df = parse_contents(contents, filename)
851986
datatable_data, datatable_columns, datatable_tooltip_data, column_options = get_table_objects(df=df)
852987

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+
8531019
return [
8541020
datatable_data,
8551021
datatable_columns,
@@ -893,7 +1059,12 @@ def toggle_collapse_load(n, is_open):
8931059

8941060

8951061
@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+
], [
8971068
Input("datatable", "data"),
8981069
Input("datatable", "columns"),
8991070
Input("ontology-type", "value"),
@@ -905,10 +1076,11 @@ def toggle_collapse_load(n, is_open):
9051076
Input("count-column", "value"),
9061077
Input("color-column", "value"),
9071078
Input("plot-type", "value"),
908-
Input("plot-type-cols", "value")
1079+
Input("plot-type-cols", "value"),
1080+
Input("plot-height", "value"),
9091081
])
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):
9121084
tree = Tree(
9131085
id_separator="|",
9141086
level_separator=level_separator,
@@ -919,16 +1091,31 @@ def update_color_picker(rows, columns, ontology_type, level_separator, id_col, p
9191091
count_col=count_col,
9201092
color_col=color_col
9211093
)
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")
9321119

9331120
"""
9341121
############################## Export ###################
@@ -951,14 +1138,14 @@ def update_color_picker(rows, columns, ontology_type, level_separator, id_col, p
9511138
State("description-column", "value"),
9521139
State("count-column", "value"),
9531140
State("color-column", "value"),
1141+
State("plot-type", "value"),
1142+
State("plot-type-cols", "value")
9541143
)
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):
9581146
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)
9621149
buffer = io.StringIO()
9631150
fig.write_html(buffer)
9641151
html_string = buffer.getvalue()

0 commit comments

Comments
 (0)
Please sign in to comment.