bindmc 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.
- bindmc/main.py +67 -0
- bindmc/webgui/__init__.py +0 -0
- bindmc/webgui/app.py +54 -0
- bindmc/webgui/classes/BindingConstant.py +23 -0
- bindmc/webgui/classes/ChemicalShiftParam.py +40 -0
- bindmc/webgui/classes/Component.py +111 -0
- bindmc/webgui/classes/ExptData.py +485 -0
- bindmc/webgui/classes/ExptDataType.py +92 -0
- bindmc/webgui/classes/FitResult.py +173 -0
- bindmc/webgui/classes/MCMCSim.py +232 -0
- bindmc/webgui/classes/Model.py +86 -0
- bindmc/webgui/classes/RawData.py +36 -0
- bindmc/webgui/classes/Simulation.py +104 -0
- bindmc/webgui/classes/UIBindings.py +19 -0
- bindmc/webgui/classes/__init__.py +28 -0
- bindmc/webgui/components/__init__.py +29 -0
- bindmc/webgui/components/base.py +24 -0
- bindmc/webgui/components/bayes.py +689 -0
- bindmc/webgui/components/bayes_priors.py +351 -0
- bindmc/webgui/components/binding_model.py +330 -0
- bindmc/webgui/components/body.py +276 -0
- bindmc/webgui/components/data_gen.py +419 -0
- bindmc/webgui/components/data_import.py +450 -0
- bindmc/webgui/components/data_model.py +609 -0
- bindmc/webgui/components/fitting.py +886 -0
- bindmc/webgui/components/graph.py +649 -0
- bindmc/webgui/components/header.py +124 -0
- bindmc/webgui/components/simulation.py +385 -0
- bindmc/webgui/export/__init__.py +0 -0
- bindmc/webgui/export/notebook_exporter.py +727 -0
- bindmc/webgui/state/__init__.py +1 -0
- bindmc/webgui/state/statemanager.py +2043 -0
- bindmc/webgui/utils.py +322 -0
- bindmc-0.1.0.dist-info/METADATA +22 -0
- bindmc-0.1.0.dist-info/RECORD +37 -0
- bindmc-0.1.0.dist-info/WHEEL +5 -0
- bindmc-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
|
|
5
|
+
from .base import BaseComponent
|
|
6
|
+
from ..classes import Simulation, FitResult
|
|
7
|
+
from ..utils import safe_filename
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
GRAPH_LEGEND_TITLE_W = 15
|
|
12
|
+
X_AXIS_ROW_INDEX = "Row index"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Graph(BaseComponent):
|
|
16
|
+
def __init__(self, state_manager, mode="sim", css_classes="h-[50vh]"):
|
|
17
|
+
self.mode = mode
|
|
18
|
+
self.sm = state_manager
|
|
19
|
+
|
|
20
|
+
self.data_frames = dict() # for now, make these DataFrames with sensible colnames, and which incorporate component concs.
|
|
21
|
+
self.line_styles = dict()
|
|
22
|
+
self.curr_x = None
|
|
23
|
+
self._comp_names = set() # set of all component names in the graph
|
|
24
|
+
|
|
25
|
+
# Plotly's default color sequence for consistent coloring
|
|
26
|
+
self.plotly_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
|
|
27
|
+
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
|
|
28
|
+
self._color_mapping = {} # Maps column names to color indices
|
|
29
|
+
|
|
30
|
+
self.css_classes = css_classes
|
|
31
|
+
|
|
32
|
+
# set up sim_fig layout
|
|
33
|
+
if mode == "sim":
|
|
34
|
+
if not hasattr(self.sm, "sim_fig_data"):
|
|
35
|
+
self.sm.sim_fig_data = {
|
|
36
|
+
"data": [],
|
|
37
|
+
"layout": {
|
|
38
|
+
"title": "Simulation Results",
|
|
39
|
+
"xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
|
|
40
|
+
"yaxis": {"title": {"text": "Concentration [M]"}},
|
|
41
|
+
"showlegend": True,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
self.graph_data = self.sm.sim_fig_data
|
|
45
|
+
|
|
46
|
+
if mode == "data_preview":
|
|
47
|
+
if not hasattr(self.sm, "data_preview_fig_data"):
|
|
48
|
+
self.sm.data_preview_fig_data = {
|
|
49
|
+
"data": [],
|
|
50
|
+
"layout": {
|
|
51
|
+
"title": "Data Preview",
|
|
52
|
+
"xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
|
|
53
|
+
"yaxis": {"title": {"text": "Y-axis"}},
|
|
54
|
+
"showlegend": True,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
self.graph_data = self.sm.data_preview_fig_data
|
|
58
|
+
|
|
59
|
+
if mode == "expt_preview":
|
|
60
|
+
if not hasattr(self.sm, "expt_preview_fig_data"):
|
|
61
|
+
self.sm.expt_preview_fig_data = {
|
|
62
|
+
"data": [],
|
|
63
|
+
"layout": {
|
|
64
|
+
"title": "Experimental Data Preview",
|
|
65
|
+
"xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
|
|
66
|
+
"yaxis": {"title": {"text": "Y-axis"}},
|
|
67
|
+
"showlegend": True,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
self.graph_data = self.sm.expt_preview_fig_data
|
|
71
|
+
|
|
72
|
+
# set up fit_fig layout
|
|
73
|
+
if mode == "fit":
|
|
74
|
+
if not hasattr(self.sm, "fit_fig_data"):
|
|
75
|
+
self.sm.fit_fig_data = {
|
|
76
|
+
"data": [],
|
|
77
|
+
"layout": {
|
|
78
|
+
"title": "Fit Results",
|
|
79
|
+
"xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
|
|
80
|
+
"yaxis": {"title": {"text": "Y-axis"}},
|
|
81
|
+
"showlegend": True,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
self.graph_data = self.sm.fit_fig_data
|
|
85
|
+
|
|
86
|
+
if mode == "fit_speciation":
|
|
87
|
+
if not hasattr(self.sm, "fit_speciation_fig_data"):
|
|
88
|
+
self.sm.fit_speciation_fig_data = {
|
|
89
|
+
"data": [],
|
|
90
|
+
"layout": {
|
|
91
|
+
"title": "Fit Speciation",
|
|
92
|
+
"xaxis": {"title": {"text": X_AXIS_ROW_INDEX}},
|
|
93
|
+
"yaxis": {"title": {"text": "Y-axis"}},
|
|
94
|
+
"showlegend": True,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
self.graph_data = self.sm.fit_speciation_fig_data
|
|
98
|
+
|
|
99
|
+
# Plotly can render with a stale width when created inside hidden containers (e.g., inactive tabs).
|
|
100
|
+
# Setting autosize helps it adapt to the parent element when it is (re)drawn.
|
|
101
|
+
if isinstance(getattr(self, "graph_data", None), dict):
|
|
102
|
+
self.graph_data.setdefault("layout", {})
|
|
103
|
+
self.graph_data["layout"].setdefault("autosize", True)
|
|
104
|
+
self._default_y_axis_title = (
|
|
105
|
+
self.graph_data.get("layout", {})
|
|
106
|
+
.get("yaxis", {})
|
|
107
|
+
.get("title", {})
|
|
108
|
+
.get("text", "Y-axis")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
super().__init__(state_manager)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_trimmed_cols(self,df,trimtail=2):
|
|
115
|
+
"""Get the trimmed columns of a DataFrame."""
|
|
116
|
+
if df is None:
|
|
117
|
+
return []
|
|
118
|
+
return [col[:-trimtail] for col in df.columns] # remove trailing _x
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def comp_names(self):
|
|
123
|
+
"""Get the component names for the graph."""
|
|
124
|
+
return self._comp_names
|
|
125
|
+
|
|
126
|
+
def add_comp_name(self, name):
|
|
127
|
+
"""Add a component name to the graph."""
|
|
128
|
+
self._comp_names.add(name)
|
|
129
|
+
|
|
130
|
+
def get_color_for_column(self, col_name: str) -> str:
|
|
131
|
+
"""Get a consistent color for a column name using Plotly's default color cycle."""
|
|
132
|
+
if col_name not in self._color_mapping:
|
|
133
|
+
# Assign next available color index
|
|
134
|
+
next_index = len(self._color_mapping) % len(self.plotly_colors)
|
|
135
|
+
self._color_mapping[col_name] = next_index
|
|
136
|
+
|
|
137
|
+
color_index = self._color_mapping[col_name]
|
|
138
|
+
return self.plotly_colors[color_index]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def setup_nicegui(self):
|
|
142
|
+
self._generate_graph()
|
|
143
|
+
self._generate_options()
|
|
144
|
+
|
|
145
|
+
def setup_bindings(self):
|
|
146
|
+
super().setup_bindings()
|
|
147
|
+
# if self.mode == "sim":
|
|
148
|
+
# self.sm.add_listener("simulation_completed", self._update_graph)
|
|
149
|
+
# elif self.mode == 'fit':
|
|
150
|
+
# self.sm.add_listener('fit_results_updated', self._update_fit_results)
|
|
151
|
+
|
|
152
|
+
def load_simulations_data(self):
|
|
153
|
+
if len(self.sm.simulations) > 0:
|
|
154
|
+
for sim in self.sm.simulations.values():
|
|
155
|
+
self.add_graph_lines_xy(sim.comp_concs, sim.results[[c for c in sim.results.columns if c not in sim.comp_concs.columns]], scatter="lines", run_name=sim.name, run_id=str(sim.id))
|
|
156
|
+
#self.add_graph_lines(sim.results, sim.name, sim.id)
|
|
157
|
+
self._update_graph()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _update_graph(self, e=None):
|
|
161
|
+
self.update_x_axis_selects()
|
|
162
|
+
self.update_graph_x()
|
|
163
|
+
self.graph.update()
|
|
164
|
+
|
|
165
|
+
def update_graph(self):
|
|
166
|
+
self._update_graph()
|
|
167
|
+
|
|
168
|
+
# def add_line(self, x, y, name,mode='markers'):
|
|
169
|
+
# """Add a line to the graph."""
|
|
170
|
+
# self.graph_data["data"].append({
|
|
171
|
+
# "type": "scatter",
|
|
172
|
+
# "mode": mode,
|
|
173
|
+
# "x": x,
|
|
174
|
+
# "y": y,
|
|
175
|
+
# "name": name,
|
|
176
|
+
# "visible": True,
|
|
177
|
+
# "trace_id": name, # Unique trace ID for this line
|
|
178
|
+
# })
|
|
179
|
+
|
|
180
|
+
def add_graph_lines_xy(self, x: pd.DataFrame , y: pd.DataFrame, run_name: str ="", run_id: str | uuid.UUID ="", scatter: str ="lines", redraw: bool =False, color: str | None = None):
|
|
181
|
+
|
|
182
|
+
"""Add a set of lines from x and y to the graph.
|
|
183
|
+
x and y should be dataframes of the same length.
|
|
184
|
+
run_name and run_id are used to identify the simulation.
|
|
185
|
+
scatter is the type of line to plot, e.g. 'lines', 'markers', etc.
|
|
186
|
+
redraw is a boolean indicating whether to redraw the graph after adding the lines.
|
|
187
|
+
color is an optional color specification for the traces.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
if run_id is None:
|
|
191
|
+
raise ValueError("run_id cannot be None")
|
|
192
|
+
#run_id = str(uuid.uuid4())
|
|
193
|
+
|
|
194
|
+
self.data_frames[str(run_id)] = (x,y) # Store the original x and y data for later use
|
|
195
|
+
self.line_styles[str(run_id)] = scatter
|
|
196
|
+
|
|
197
|
+
for ii,col in enumerate(x.columns):
|
|
198
|
+
self.add_comp_name(col)
|
|
199
|
+
|
|
200
|
+
for ii,col in enumerate(y.columns):
|
|
201
|
+
trace_data = {
|
|
202
|
+
"type": "scatter",
|
|
203
|
+
"mode": scatter,
|
|
204
|
+
"x": x[x.columns[0]].tolist(), # TODO clean up?
|
|
205
|
+
"y": y[col].tolist(),
|
|
206
|
+
"name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
|
|
207
|
+
"species": col,
|
|
208
|
+
"trace_id": str(run_id) # is the uuid for the fit/etc
|
|
209
|
+
+ "-"
|
|
210
|
+
+ col, # Unique trace ID for this simulation
|
|
211
|
+
"visible": True,
|
|
212
|
+
#'legendgroup': self.sm.modelName,
|
|
213
|
+
#'legendgrouptitle': dict(text=self.sm.modelName)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Use consistent color based on column name, or provided color
|
|
217
|
+
if color is not None:
|
|
218
|
+
col_color = color
|
|
219
|
+
else:
|
|
220
|
+
# Use Plotly's default color cycle for consistency across calc/expt
|
|
221
|
+
col_color = self.get_color_for_column(col)
|
|
222
|
+
|
|
223
|
+
if scatter == "lines":
|
|
224
|
+
trace_data["line"] = {"color": col_color}
|
|
225
|
+
else:
|
|
226
|
+
trace_data["marker"] = {"color": col_color}
|
|
227
|
+
|
|
228
|
+
self.graph_data["data"].append(trace_data)
|
|
229
|
+
if (
|
|
230
|
+
col[:-5] in [d[:-4] for d in x.columns if d.endswith("_tot") ]
|
|
231
|
+
and hasattr(self, "chkNoComp")
|
|
232
|
+
and self.chkNoComp.value is False
|
|
233
|
+
):
|
|
234
|
+
self.graph_data["data"][-1]["visible"] = "legendonly"
|
|
235
|
+
# elif, plot non-concentration data... TODO
|
|
236
|
+
if redraw:
|
|
237
|
+
self.graph.update()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def add_graph_lines(self, df, run_name, run_id, scatter="lines",redraw=False):
|
|
241
|
+
"""Add a set of lines from a dataframe to the graph.
|
|
242
|
+
Dataframe should have columns labelled _tot and _free for
|
|
243
|
+
components and species respectively."""
|
|
244
|
+
|
|
245
|
+
# Split df into x (component concentrations) and y (species concentrations) DataFrames
|
|
246
|
+
# to maintain consistency with add_graph_lines_xy storage format
|
|
247
|
+
comp_cols = [col for col in df.columns if col.endswith("_tot")]
|
|
248
|
+
spec_cols = [col for col in df.columns if col.endswith("_free")]
|
|
249
|
+
other_cols = [col for col in df.columns if not (col.endswith("_tot") or col.endswith("_free"))]
|
|
250
|
+
|
|
251
|
+
# x DataFrame includes component concentrations and any other columns (like row index, time, etc.)
|
|
252
|
+
x_df = df[comp_cols + other_cols] if comp_cols + other_cols else df[[df.columns[0]]]
|
|
253
|
+
# y DataFrame includes species concentrations
|
|
254
|
+
y_df = df[spec_cols] if spec_cols else pd.DataFrame()
|
|
255
|
+
|
|
256
|
+
self.data_frames[str(run_id)] = (x_df, y_df)
|
|
257
|
+
self.line_styles[str(run_id)] = scatter
|
|
258
|
+
|
|
259
|
+
for ii, col in enumerate(df.columns):
|
|
260
|
+
if col.endswith("_tot"):
|
|
261
|
+
# this is a component concentration, so we skip it
|
|
262
|
+
self.add_comp_name(col)
|
|
263
|
+
continue
|
|
264
|
+
elif col.endswith("_free"):
|
|
265
|
+
# this is a species concentration, so we plot it
|
|
266
|
+
col_color = self.get_color_for_column(col)
|
|
267
|
+
trace_data = {
|
|
268
|
+
"type": "scatter",
|
|
269
|
+
"mode": scatter,
|
|
270
|
+
"x": df[df.columns[0]].tolist(), # TODO clean up?
|
|
271
|
+
"y": df[col].tolist(),
|
|
272
|
+
"name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
|
|
273
|
+
"species": col,
|
|
274
|
+
"trace_id": run_id
|
|
275
|
+
+ "-"
|
|
276
|
+
+ col, # Unique trace ID for this simulation
|
|
277
|
+
"visible": True,
|
|
278
|
+
#'legendgroup': self.sm.modelName,
|
|
279
|
+
#'legendgrouptitle': dict(text=self.sm.modelName)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if scatter == "lines":
|
|
283
|
+
trace_data["line"] = {"color": col_color}
|
|
284
|
+
else:
|
|
285
|
+
trace_data["marker"] = {"color": col_color}
|
|
286
|
+
|
|
287
|
+
self.graph_data["data"].append(trace_data)
|
|
288
|
+
if (
|
|
289
|
+
col[:-5] in [d[:-4] for d in df.columns if d.endswith("_tot") ]
|
|
290
|
+
and hasattr(self, "chkNoComp")
|
|
291
|
+
and self.chkNoComp.value is False
|
|
292
|
+
):
|
|
293
|
+
self.graph_data["data"][-1]["visible"] = "legendonly"
|
|
294
|
+
else:
|
|
295
|
+
# assume it's a concentration
|
|
296
|
+
col_color = self.get_color_for_column(col)
|
|
297
|
+
trace_data = {
|
|
298
|
+
"type": "scatter",
|
|
299
|
+
"mode": scatter,
|
|
300
|
+
"x": df[df.columns[0]].tolist(), # TODO clean up?
|
|
301
|
+
"y": df[col].tolist(),
|
|
302
|
+
"name": run_name[:GRAPH_LEGEND_TITLE_W] + " " + col,
|
|
303
|
+
"species": col,
|
|
304
|
+
"trace_id": run_id
|
|
305
|
+
+ "-"
|
|
306
|
+
+ col, # Unique trace ID for this simulation
|
|
307
|
+
"visible": True,
|
|
308
|
+
#'legendgroup': self.sm.modelName,
|
|
309
|
+
#'legendgrouptitle': dict(text=self.sm.modelName)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if scatter == "lines":
|
|
313
|
+
trace_data["line"] = {"color": col_color}
|
|
314
|
+
else:
|
|
315
|
+
trace_data["marker"] = {"color": col_color}
|
|
316
|
+
|
|
317
|
+
self.graph_data["data"].append(trace_data)
|
|
318
|
+
|
|
319
|
+
# elif, plot non-concentration data... TODO
|
|
320
|
+
if redraw:
|
|
321
|
+
self.graph.update()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _generate_graph(self):
|
|
325
|
+
"""Generate the graph component."""
|
|
326
|
+
|
|
327
|
+
self.graph = (
|
|
328
|
+
ui.plotly(self.graph_data)
|
|
329
|
+
.classes(f"w-full min-w-0 {self.css_classes}")
|
|
330
|
+
.style("width: 100%;")
|
|
331
|
+
)
|
|
332
|
+
self.graph.on("plotly_restyle", self.plotly_restyle_handler)
|
|
333
|
+
|
|
334
|
+
def _generate_options(self):
|
|
335
|
+
"""Generate the options for the graph."""
|
|
336
|
+
with ui.card().classes("w-full mt-4"):
|
|
337
|
+
with ui.row().classes("w-full items-center"):
|
|
338
|
+
ui.label("Plotting options")
|
|
339
|
+
ui.space()
|
|
340
|
+
ui.button("Save", icon="save", on_click=self.download_graph_png).props("dense")
|
|
341
|
+
with ui.row():
|
|
342
|
+
ui.label("x-axis:")
|
|
343
|
+
self.chkratio = ui.checkbox("Ratio?").on_value_change(
|
|
344
|
+
lambda e: self.xAxDenominatorSelect.set_value(
|
|
345
|
+
1 if not e.value else self.xAxDenominatorSelect.value
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
self.xAxNumeratorSelect = ui.select([1, 2, 3]).props("inline")
|
|
349
|
+
ui.label("/").bind_visibility_from(self.chkratio, "value")
|
|
350
|
+
self.xAxDenominatorSelect = (
|
|
351
|
+
ui.select([1, 2, 3])
|
|
352
|
+
.props("inline")
|
|
353
|
+
.bind_visibility_from(self.chkratio, "value")
|
|
354
|
+
)
|
|
355
|
+
ui.button("Apply").on_click(self.update_graph_x)
|
|
356
|
+
|
|
357
|
+
with ui.row():
|
|
358
|
+
self.chkNormalizeY = ui.checkbox(
|
|
359
|
+
"Normalize Y per trace (0-1)",
|
|
360
|
+
value=False,
|
|
361
|
+
on_change=lambda e: self.update_graph_y(),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if self.mode != "expt_preview":
|
|
365
|
+
with ui.row():
|
|
366
|
+
self.chkNoComp = ui.checkbox(
|
|
367
|
+
"Plot [Component]_free",
|
|
368
|
+
value=True,
|
|
369
|
+
on_change=(self.update_plot_compfree),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _default_export_filename(self) -> str:
|
|
373
|
+
active_sim = self.sm.active_sim_or_none
|
|
374
|
+
active_fit = self.sm.active_fit_or_none
|
|
375
|
+
if self.mode == "sim":
|
|
376
|
+
if active_sim is not None:
|
|
377
|
+
return f"simulation_{safe_filename(active_sim.name, fallback='simulation')}_results"
|
|
378
|
+
return "simulation_results"
|
|
379
|
+
if self.mode == "fit":
|
|
380
|
+
if active_fit is not None:
|
|
381
|
+
return f"fit_{safe_filename(active_fit.name, fallback='fit')}_results"
|
|
382
|
+
return "fit_results"
|
|
383
|
+
if self.mode == "fit_speciation":
|
|
384
|
+
if active_fit is not None:
|
|
385
|
+
return f"fit_{safe_filename(active_fit.name, fallback='fit')}_speciation"
|
|
386
|
+
return "fit_speciation"
|
|
387
|
+
title = self.graph_data.get("layout", {}).get("title", "graph")
|
|
388
|
+
title_text = title.get("text", "graph") if isinstance(title, dict) else str(title)
|
|
389
|
+
return safe_filename(title_text, fallback="graph")
|
|
390
|
+
|
|
391
|
+
async def download_graph_png(self) -> None:
|
|
392
|
+
filename = self._default_export_filename()
|
|
393
|
+
if not self.graph_data.get("data"):
|
|
394
|
+
ui.notify(f"No plotted data available for {filename}.", type="warning")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
js = f"""
|
|
398
|
+
(() => {{
|
|
399
|
+
const root = getElement({self.graph.id});
|
|
400
|
+
const container = root?.$el ?? root;
|
|
401
|
+
const plot = container?.querySelector('.js-plotly-plot') ?? container;
|
|
402
|
+
if (!plot || typeof Plotly === 'undefined') return false;
|
|
403
|
+
Plotly.downloadImage(plot, {{format: 'png', filename: {json.dumps(filename)}, scale: 2}});
|
|
404
|
+
return true;
|
|
405
|
+
}})()
|
|
406
|
+
"""
|
|
407
|
+
ok = await ui.run_javascript(js)
|
|
408
|
+
if not ok:
|
|
409
|
+
ui.notify(f"Unable to export {filename}.png", type="negative")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def update_plot_compfree(self, e):
|
|
413
|
+
"""Update the plot to show or hide component free concentrations."""
|
|
414
|
+
|
|
415
|
+
# comp_names = [d["species"][:-4] for d in self.graph_data["data"] if d["species"].endswith("_tot")]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
for d in self.graph_data["data"]:
|
|
419
|
+
if d["species"].endswith("_free"):
|
|
420
|
+
col = d["species"][:-5] # Remove '_free' to get the component name
|
|
421
|
+
col = col+"_tot"
|
|
422
|
+
if col in self.comp_names:
|
|
423
|
+
if self.chkNoComp.value is False:
|
|
424
|
+
d["visible"] = "legendonly"
|
|
425
|
+
elif self.chkNoComp.value is True:
|
|
426
|
+
d["visible"] = True
|
|
427
|
+
self.graph.update()
|
|
428
|
+
|
|
429
|
+
def plotly_restyle_handler(self, e):
|
|
430
|
+
# update is update item
|
|
431
|
+
# trace is array of traces
|
|
432
|
+
|
|
433
|
+
change = e.args["0"]
|
|
434
|
+
traces = e.args["1"]
|
|
435
|
+
|
|
436
|
+
# for multiples, we want to do if change.keys() == ['visible']
|
|
437
|
+
# then for each item in change['visible'] (which will be a list)
|
|
438
|
+
# change the corresponding trace
|
|
439
|
+
if list(change.keys()) == [
|
|
440
|
+
"visible"
|
|
441
|
+
]: # because if it's not just one entry, we don't want to mess things up.
|
|
442
|
+
for ii, changeitem in enumerate(change["visible"]):
|
|
443
|
+
if changeitem == "legendonly":
|
|
444
|
+
# if the trace is set to legendonly, we need to set this parameter in the simFigData
|
|
445
|
+
# so that it persists if we add more traces
|
|
446
|
+
self.graph_data["data"][traces[ii]]["visible"] = "legendonly"
|
|
447
|
+
|
|
448
|
+
def update_graph_x(self, e=None):
|
|
449
|
+
numName = self.xAxNumeratorSelect.value
|
|
450
|
+
deNomName = self.xAxDenominatorSelect.value if not None else 1
|
|
451
|
+
# self.graph_data["data"] = [] # Clear existing data for the new plot
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
throw_warning = False
|
|
456
|
+
for d in self.graph_data["data"]:
|
|
457
|
+
run_id = '-'.join(d["trace_id"].split("-",5)[0:5]) # Get the simulation ID from the trace_id
|
|
458
|
+
if run_id not in self.data_frames.keys():
|
|
459
|
+
ui.notify("Data not found for trace ID: " + run_id, type="warning")
|
|
460
|
+
continue
|
|
461
|
+
x_df = self.data_frames[run_id][0]
|
|
462
|
+
x_cols = x_df.columns
|
|
463
|
+
|
|
464
|
+
if numName == X_AXIS_ROW_INDEX:
|
|
465
|
+
d["x"] = list(range(len(x_df)))
|
|
466
|
+
continue
|
|
467
|
+
if numName not in x_cols :
|
|
468
|
+
ui.notify(
|
|
469
|
+
f"Numerator '{numName}' not found in simulation data for trace ID: {run_id}",
|
|
470
|
+
type="warning",
|
|
471
|
+
)
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
if deNomName != 1 and deNomName not in x_cols:
|
|
475
|
+
ui.notify(
|
|
476
|
+
f"Denomination '{deNomName}' not found in simulation data for trace ID: {run_id}",
|
|
477
|
+
type="warning",
|
|
478
|
+
)
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
if deNomName == 1 or deNomName is None:
|
|
482
|
+
d["x"] = self.data_frames[run_id][0][numName].tolist()
|
|
483
|
+
else:
|
|
484
|
+
d["x"] = (self.data_frames[run_id][0][numName] / self.data_frames[run_id][0][deNomName]).tolist()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
self.graph_data.setdefault("layout", {})
|
|
488
|
+
self.graph_data["layout"].setdefault("xaxis", {})
|
|
489
|
+
|
|
490
|
+
if numName == X_AXIS_ROW_INDEX:
|
|
491
|
+
x_title_text = X_AXIS_ROW_INDEX
|
|
492
|
+
elif isinstance(deNomName, int) and deNomName == 1:
|
|
493
|
+
# if denominator is 1, we just plot the numerator
|
|
494
|
+
x_title_text = f"[{numName}]"
|
|
495
|
+
else:
|
|
496
|
+
x_title_text = f"[{numName}] / [{deNomName}]"
|
|
497
|
+
|
|
498
|
+
self.graph_data["layout"]["xaxis"]["title"] = {"text": x_title_text}
|
|
499
|
+
|
|
500
|
+
if throw_warning:
|
|
501
|
+
ui.notify(
|
|
502
|
+
"Some lines could not be plotted because you have requested an x-axis which they do not support.",
|
|
503
|
+
type="warning",
|
|
504
|
+
)
|
|
505
|
+
self.update_graph_y(update=False)
|
|
506
|
+
self.graph.update()
|
|
507
|
+
# 'x': (self.sd.compConcs['G']/self.sd.compConcs['H']).tolist(),
|
|
508
|
+
|
|
509
|
+
def _normalize_trace_values(self, y_values):
|
|
510
|
+
series = pd.to_numeric(pd.Series(y_values), errors="coerce")
|
|
511
|
+
if series.notna().sum() == 0:
|
|
512
|
+
return y_values
|
|
513
|
+
|
|
514
|
+
y_min = series.min(skipna=True)
|
|
515
|
+
y_max = series.max(skipna=True)
|
|
516
|
+
if pd.isna(y_min) or pd.isna(y_max) or y_max == y_min:
|
|
517
|
+
return [0.0 if pd.notna(v) else None for v in series.tolist()]
|
|
518
|
+
|
|
519
|
+
return [
|
|
520
|
+
None if pd.isna(v) else (float(v) - float(y_min)) / (float(y_max) - float(y_min))
|
|
521
|
+
for v in series.tolist()
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
def update_graph_y(self, e=None, update=True):
|
|
525
|
+
normalize = hasattr(self, "chkNormalizeY") and bool(self.chkNormalizeY.value)
|
|
526
|
+
|
|
527
|
+
for d in self.graph_data["data"]:
|
|
528
|
+
trace_id = d.get("trace_id", "")
|
|
529
|
+
run_id = "-".join(trace_id.split("-", 5)[0:5])
|
|
530
|
+
if run_id not in self.data_frames:
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
x_df, y_df = self.data_frames[run_id]
|
|
534
|
+
species = d.get("species")
|
|
535
|
+
y_values = None
|
|
536
|
+
if species in y_df.columns:
|
|
537
|
+
y_values = y_df[species].tolist()
|
|
538
|
+
elif species in x_df.columns:
|
|
539
|
+
y_values = x_df[species].tolist()
|
|
540
|
+
|
|
541
|
+
if y_values is None:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
d["y"] = self._normalize_trace_values(y_values) if normalize else y_values
|
|
545
|
+
|
|
546
|
+
self.graph_data.setdefault("layout", {})
|
|
547
|
+
self.graph_data["layout"].setdefault("yaxis", {})
|
|
548
|
+
self.graph_data["layout"]["yaxis"]["title"] = {
|
|
549
|
+
"text": "Normalized y (0-1)" if normalize else self._default_y_axis_title
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if update:
|
|
553
|
+
self.graph.update()
|
|
554
|
+
|
|
555
|
+
def compspec_name_to_obj(self, name, sim):
|
|
556
|
+
if name is None:
|
|
557
|
+
return None
|
|
558
|
+
if name == 1:
|
|
559
|
+
return 1
|
|
560
|
+
elif name.endswith("_tot"):
|
|
561
|
+
# it's a component
|
|
562
|
+
if name[:-4] in list(sim.comp_concs.columns):
|
|
563
|
+
# i = self.sd.componentNames.index(name[:-4])
|
|
564
|
+
return sim.comp_concs[name[:-4]]
|
|
565
|
+
elif name.endswith("_free"):
|
|
566
|
+
# s = name[:-5] # remove '_free'
|
|
567
|
+
# combo = [c.name for c in self.sd.components]+ [s for s in self.sd.species]
|
|
568
|
+
if name in list(sim.results.columns):
|
|
569
|
+
# i = self.sd.species.index(s)
|
|
570
|
+
return sim.results[name] # return the results for that species
|
|
571
|
+
else:
|
|
572
|
+
return None # If the name does not match any known format, return None
|
|
573
|
+
|
|
574
|
+
def update_x_axis_selects(self, e=None):
|
|
575
|
+
# Prefer component-derived options for simulation workflows.
|
|
576
|
+
# Fall back to x-dataframe columns only when no component names exist.
|
|
577
|
+
x_axis_cols = [str(comp_name) for comp_name in self.comp_names]
|
|
578
|
+
if len(x_axis_cols) == 0:
|
|
579
|
+
for x_df, _ in self.data_frames.values():
|
|
580
|
+
for col in x_df.columns:
|
|
581
|
+
col_name = str(col)
|
|
582
|
+
if col_name not in x_axis_cols:
|
|
583
|
+
x_axis_cols.append(col_name)
|
|
584
|
+
|
|
585
|
+
uniq_opts = [X_AXIS_ROW_INDEX, *x_axis_cols]
|
|
586
|
+
self.xAxNumeratorSelect.set_options(uniq_opts)
|
|
587
|
+
self.xAxDenominatorSelect.set_options([1, *uniq_opts])
|
|
588
|
+
|
|
589
|
+
if self.xAxNumeratorSelect.value is not None and self.xAxNumeratorSelect.value not in uniq_opts:
|
|
590
|
+
self.xAxNumeratorSelect.value = X_AXIS_ROW_INDEX
|
|
591
|
+
self.xAxDenominatorSelect.value = 1
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
if self.xAxNumeratorSelect.value is None:
|
|
595
|
+
# first varying comp
|
|
596
|
+
# would prefer to, for example, set this to the first varying component concentration
|
|
597
|
+
# TODO
|
|
598
|
+
if len(uniq_opts)>1: # because row index is always present but not a helpful option
|
|
599
|
+
# look in the first dataframe and see which - if either - of these components vary
|
|
600
|
+
first_x_data = next(iter(self.data_frames.values()))[0]
|
|
601
|
+
# Find component with largest variation (max - min)
|
|
602
|
+
variations = (first_x_data.max() - first_x_data.min()).sort_values(ascending=False)
|
|
603
|
+
|
|
604
|
+
# Set to first component that has variation and is in options
|
|
605
|
+
for comp_name in variations.index:
|
|
606
|
+
if str(comp_name) in uniq_opts:
|
|
607
|
+
self.xAxNumeratorSelect.value = str(comp_name)
|
|
608
|
+
self.xAxDenominatorSelect.value = 1
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
self.xAxNumeratorSelect.value = uniq_opts[1] if len(uniq_opts) > 1 else uniq_opts[0] if len(uniq_opts) > 0 else None
|
|
612
|
+
self.xAxDenominatorSelect.value = 1
|
|
613
|
+
|
|
614
|
+
def clear_graph(self, e=None, update=True):
|
|
615
|
+
"""Clear the graph."""
|
|
616
|
+
self.graph_data["data"] = []
|
|
617
|
+
self.data_frames = {}
|
|
618
|
+
self._comp_names = set()
|
|
619
|
+
self._color_mapping = {}
|
|
620
|
+
if update:
|
|
621
|
+
self.graph.update()
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def remove_data(self, run_id: str):
|
|
625
|
+
if isinstance(run_id, Simulation):
|
|
626
|
+
run_id = str(run_id.id)
|
|
627
|
+
if isinstance(run_id, uuid.UUID):
|
|
628
|
+
run_id = str(run_id)
|
|
629
|
+
if isinstance(run_id, FitResult):
|
|
630
|
+
run_id = str(run_id.id)
|
|
631
|
+
|
|
632
|
+
"""Remove data for a specific run_id from the graph."""
|
|
633
|
+
if run_id in self.data_frames:
|
|
634
|
+
del self.data_frames[run_id]
|
|
635
|
+
if run_id in self.line_styles:
|
|
636
|
+
del self.line_styles[run_id]
|
|
637
|
+
|
|
638
|
+
# Remove traces from the graph data
|
|
639
|
+
self.graph_data["data"] = [
|
|
640
|
+
d for d in self.graph_data["data"] if not d["trace_id"].startswith(run_id)
|
|
641
|
+
]
|
|
642
|
+
|
|
643
|
+
self.regenerate_comp_names()
|
|
644
|
+
|
|
645
|
+
# Update the graph
|
|
646
|
+
self.graph.update()
|
|
647
|
+
|
|
648
|
+
def regenerate_comp_names(self):
|
|
649
|
+
self._comp_names = set(col for x in self.data_frames.values() for col in x[0].columns)
|