-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.py
356 lines (319 loc) · 13.3 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# python packages
import tempfile
import os
import polars as pl
from plotnine import ggplot, theme_void
# shiny packages
from shiny import Inputs, Outputs, Session, reactive, render, ui
from shiny.types import FileInfo
# icons
from icons import question_circle_fill
# class
from pico import PICO
# own functions
from helpers import round_up
def server(input: Inputs, output: Outputs, session: Session):
###############################################
# Reactive variables and effects
###############################################
# central reactive variable for PICO instance
pico_instance = reactive.Value(None)
@reactive.Effect
@reactive.event(input.file1)
def _():
# file can either be a list of FileInfo or None
# in this specific case only one file can be uploaded so that file[0] contains the FileInfo for the uploaded file
# TODO: this might be adjusted when working with multiple uploads
# a FileInfo object contains "name", "size", "type" and "datapath" of the uploaded file
file: list[FileInfo] | None = input.file1()
if file is None:
# if no file is uploaded, no pico_instance is set
pico_instance.set(None)
else:
# create an object of the class PICO with the information from file[0]
# use the slider_lambda to set min and max values of lambda and filter the dataframe accordingly
pico_instance.set(
PICO(file_info=file[0]),
)
# this effect is watching for changes in the lambda control elements (box and slider) and for changes in the checkboxes
# and updates the property df_couplexes_filtered of the pico_instance using pico.filtering()
@reactive.Effect
@reactive.event(
input.lambda_filter,
input.slider_lambda,
input.filter_group,
input.filter_sample,
input.filter_antibodies,
)
def _():
pico = pico_instance.get()
# obivously, this is only relevant if there is actually a file uploaded
if pico is not None:
# if any of these have a value it shall perform the filtering
if (
input.lambda_filter()
or input.filter_group()
or input.filter_sample()
or input.filter_antibodies()
):
pico.filtering(
lambda_filter=input.lambda_filter(),
filter_values_lambda=input.slider_lambda(),
groups=input.filter_group(),
samples=input.filter_sample(),
antibodies=input.filter_antibodies(),
)
else:
pico.df_couplexes_filtered = pico.df_couplexes
# extrac the file name of the original file to make it available for the download
@reactive.Calc
def extract_filename():
pico = pico_instance.get()
if pico is None:
# if no file is uploaded, the empty download csv will be called "nothing_processed.csv"
return "nothing"
return pico.file_name
# function for the action button to reset the lambda range to its initial status
@reactive.Effect
@reactive.event(input.reset_lambda)
def _():
return ui.update_slider(
"slider_lambda",
value=[0.01, 0.25],
)
# this function is watching the lambda filter control elements and checkboxes to update the message with the number of values displayed
@reactive.Calc
@reactive.event(
input.lambda_filter,
input.slider_lambda,
input.filter_group,
input.filter_sample,
input.filter_antibodies,
)
def filter_message():
pico = pico_instance.get()
if pico is not None:
if (
input.lambda_filter()
or input.filter_group()
or input.filter_sample()
or input.filter_antibodies()
):
return ui.div(ui.HTML(pico.filter_msg))
else:
return ui.HTML("")
# this is the function to display the message in the ui.
@output
@render.ui
def render_filter_message():
return filter_message()
###############################################
# UI elements shown upon upload of a file
###############################################
# dynamically render the lambda filter and the checkboxes for filtering
@output
@render.ui
def dynamic_filters():
pico = pico_instance.get()
if pico is None:
# if nothing is uploaded, it returns an empty HTML element
return ui.HTML("")
else:
# if an object of the PICO class was generated it returns the checkboxes for groups, samples and antibodies and lambda filters
return (
ui.card(
ui.card_header(
ui.tooltip(
ui.span(
"Filter for a valid \u03bb-range: ",
question_circle_fill,
),
"The suggested range is from 0.01 to 0.25.",
),
),
ui.card(
# control elements for the lambda filter
ui.layout_columns(
ui.input_checkbox("lambda_filter", "Apply filter", False),
# this resets the filter values to the defaults
ui.input_action_button(
"reset_lambda", "Reset filter range"
),
),
),
ui.input_slider(
"slider_lambda",
"Define valid \u03bb-range.",
min=0,
# maximal lambda, which is also used for the histogram of the lambda range
max=round_up(pico.max_lambda, 1),
value=[0.01, 0.25],
),
ui.output_plot("render_lambda_hist", height="100px"),
),
# the default is that all groups, samples and antibodies are selected
ui.card(
ui.card_header(
ui.tooltip(
ui.span(
"Select displayed items: ",
question_circle_fill,
),
"You defined these items in the QIAcuity Software Suite.",
),
),
ui.layout_columns(
ui.input_checkbox_group(
"filter_group",
"Reaction mixes:",
choices=pico.groups,
selected=pico.groups,
),
ui.input_checkbox_group(
"filter_sample",
"Samples:",
choices=pico.samples,
selected=pico.samples,
),
ui.input_checkbox_group(
"filter_antibodies",
ui.tooltip(
ui.span(
"Antibodies: ",
question_circle_fill,
),
"The antibodies names only appear, if you specified them as targets of the reaction mix in the QIAcuity Software Suite.",
),
choices=pico.antibodies,
selected=pico.antibodies,
),
),
),
)
###############################################
# Histogram of lambda range in sidebar
###############################################
# the plotting function needs to watch the inputs lambda_filter and slider_lambda
# otherwise the plot is not updated when the values are changed
@reactive.Calc
@reactive.event(input.lambda_filter, input.slider_lambda)
def plot_lambda_hist():
pico = pico_instance.get()
if pico is None:
# this will just display an empty plot, when no file is uploaded
return ggplot() + theme_void()
else:
# this will generate the plot of the histogram
# if input.lambda_filter() is False, which is the default, there is no color formatting
# otherwise, this will color the bins of the histograms that are used in the violin plot of the couplexes green
return pico.get_lambda_hist(
lambda_filter=input.lambda_filter(),
filter_values_lambda=input.slider_lambda(),
)
# calls plot_couplexes to plot the data
@output
@render.plot
def render_lambda_hist():
return plot_lambda_hist()
###############################################
# Violin plots of couplexes
###############################################
# the plotting function needs to watch the inputs lambda_filter and slider_lambda as well as the checkboxes to be updated when something changed
@reactive.Calc
@reactive.event(
input.lambda_filter,
input.slider_lambda,
input.filter_group,
input.filter_sample,
input.filter_antibodies,
)
def plot_couplexes_violin():
pico = pico_instance.get()
if pico is None:
# this will just display an empty plot, when no file is uploaded
# so when downloaded, it'll be a white piece of paper
return ggplot() + theme_void()
else:
return pico.get_couplex_plot(
lambda_filter=input.lambda_filter(),
groups=input.filter_group(),
samples=input.filter_sample(),
antibodies=input.filter_antibodies(),
)
# calls plot_couplexes to plot the data
@output
@render.plot
def render_plot_couplexes_violin():
return plot_couplexes_violin()
###############################################
# Range plots of lambda from experimental groups
###############################################
# the plotting function needs to watch the inputs lambda_filter and slider_lambda as well as the checkboxes to be updated when something changed
@reactive.Calc
@reactive.event(
input.lambda_filter,
input.slider_lambda,
input.filter_group,
input.filter_sample,
input.filter_antibodies,
)
def plot_lambda_ranges():
pico = pico_instance.get()
if pico is None:
return ggplot() + theme_void()
else:
return pico.get_lambda_ranges(
lambda_filter=input.lambda_filter(),
groups=input.filter_group(),
samples=input.filter_sample(),
antibodies=input.filter_antibodies(),
)
@output
@render.plot
def render_plot_lambda_ranges():
return plot_lambda_ranges()
###############################################
# Downloads
###############################################
# lambda is necessary to use the reactive function for the generation of the filename
@render.download(filename=lambda: f"{extract_filename()}_processed.csv")
def download_data():
pico = pico_instance.get()
if pico is None:
# if no file is uploaded, the empty download csv will be called "nothing_processed.csv"
yield pl.DataFrame().write_csv()
else:
# this dataframe is almost unfiltered
# the only filters applied are in the function pico._general_filtering
yield pico.get_processed_data().write_csv()
# same as download above but with the filtered dataframe
@render.download(filename=lambda: f"{extract_filename()}_processed_filtered.csv")
def download_data_filtered():
pico = pico_instance.get()
if pico is None:
yield pl.DataFrame().write_csv()
else:
yield pico.get_processed_filtered_data().write_csv()
@render.download(filename=lambda: f"{extract_filename()}_plot_couplexes.pdf")
def download_plot_couplexes():
plt = plot_couplexes_violin()
# create temporary file on local machine
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmpfile:
plt.save(tmpfile.name, format="pdf")
# open the file to ensure it is saved and can be read
with open(tmpfile.name, "rb") as f:
yield f.read()
# remove the temporary file after saving
os.remove(tmpfile.name)
@render.download(filename=lambda: f"{extract_filename()}_plot_lambda.pdf")
def download_plot_lambda():
plt = plot_lambda_ranges()
# create temporary file on local machine
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmpfile:
plt.save(tmpfile.name, format="pdf")
# open the file to ensure it is saved and can be read
with open(tmpfile.name, "rb") as f:
yield f.read()
# remove the temporary file after saving
os.remove(tmpfile.name)
# Download handler for the CSV file