apmtools 0.2.8__tar.gz → 0.2.9__tar.gz
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.
- {apmtools-0.2.8 → apmtools-0.2.9}/PKG-INFO +1 -1
- {apmtools-0.2.8 → apmtools-0.2.9}/pyproject.toml +1 -1
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/shiny/summary.py +126 -34
- {apmtools-0.2.8 → apmtools-0.2.9}/.gitignore +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/LICENSE +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/README.md +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.8 → apmtools-0.2.9}/src/apmtools/units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apmtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.009
|
|
4
4
|
Summary: A collection of tools for processing air pollution monitoring data
|
|
5
5
|
Project-URL: Homepage, https://github.com/federlorenz/apmtools
|
|
6
6
|
Author-email: Federico Lorenzetti <lorenzetti.federico@gmail.com>
|
|
@@ -83,15 +83,22 @@ def _subset_choices(data: pd.DataFrame, column: str) -> tuple[dict[str, str], di
|
|
|
83
83
|
return choices, value_map
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _apply_filters(data,
|
|
87
|
-
"""Apply Summary.subset() and
|
|
86
|
+
def _apply_filters(data, subset_filters, datetime_start=None, datetime_end=None):
|
|
87
|
+
"""Apply multiple ``Summary.subset()`` filters and datetime filtering.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
subset_filters : dict
|
|
92
|
+
Mapping of column names to the original values that should be retained.
|
|
93
|
+
Conditions for different columns are combined with ``filter_style="all"``.
|
|
94
|
+
Values within one column are OR'd by :meth:`Summary.subset`.
|
|
95
|
+
"""
|
|
88
96
|
filtered = data
|
|
89
97
|
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
filtered = filtered.subset({subset_column: values})
|
|
98
|
+
if subset_filters:
|
|
99
|
+
# Summary.subset() handles non-contiguous DataFrame indices
|
|
100
|
+
# positionally, so no index manipulation is required here.
|
|
101
|
+
filtered = filtered.subset(subset_filters, filter_style="all")
|
|
95
102
|
|
|
96
103
|
if datetime_start or datetime_end:
|
|
97
104
|
filtered = filtered.dtfilter(
|
|
@@ -182,7 +189,7 @@ def create_summary_app(
|
|
|
182
189
|
raise ValueError(
|
|
183
190
|
f"Unknown plot_type {plot_type!r}. Choose one of: {', '.join(PLOT_TYPES)}")
|
|
184
191
|
|
|
185
|
-
subset_columns =
|
|
192
|
+
subset_columns = columns
|
|
186
193
|
|
|
187
194
|
app_ui = ui.page_sidebar(
|
|
188
195
|
ui.sidebar(
|
|
@@ -214,12 +221,12 @@ def create_summary_app(
|
|
|
214
221
|
),
|
|
215
222
|
ui.hr(),
|
|
216
223
|
ui.h5("Subset"),
|
|
217
|
-
ui.input_select("subset_column", "Column",
|
|
218
|
-
choices=subset_columns, selected="None"),
|
|
219
224
|
ui.input_selectize(
|
|
220
|
-
"
|
|
221
|
-
|
|
225
|
+
"subset_columns", "Columns", choices={c: c for c in subset_columns},
|
|
226
|
+
selected=[], multiple=True,
|
|
227
|
+
options={"placeholder": "Select one or more columns"},
|
|
222
228
|
),
|
|
229
|
+
ui.output_ui("subset_filters"),
|
|
223
230
|
ui.hr(),
|
|
224
231
|
ui.h5("Datetime filter"),
|
|
225
232
|
ui.input_text(
|
|
@@ -246,29 +253,59 @@ def create_summary_app(
|
|
|
246
253
|
)
|
|
247
254
|
|
|
248
255
|
def server(input, output, session):
|
|
249
|
-
@
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
@render.ui
|
|
257
|
+
def subset_filters():
|
|
258
|
+
"""Render one value selector for every selected subset column."""
|
|
259
|
+
selected_columns = list(input.subset_columns())
|
|
260
|
+
if not selected_columns:
|
|
261
|
+
return ui.help_text(
|
|
262
|
+
"Select one or more columns above to choose their values."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
controls = []
|
|
266
|
+
for i, selected_column in enumerate(selected_columns):
|
|
267
|
+
if selected_column not in column_map:
|
|
268
|
+
continue
|
|
255
269
|
choices, _ = _subset_choices(
|
|
256
270
|
summary, column_map[selected_column])
|
|
257
|
-
|
|
271
|
+
input_id = f"subset_values_{i}"
|
|
272
|
+
controls.append(
|
|
273
|
+
ui.input_selectize(
|
|
274
|
+
input_id,
|
|
275
|
+
f"{selected_column} values",
|
|
276
|
+
choices=choices,
|
|
277
|
+
selected=[],
|
|
278
|
+
multiple=True,
|
|
279
|
+
options={
|
|
280
|
+
"placeholder": f"Select {selected_column} values"},
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
return ui.div(*controls)
|
|
258
284
|
|
|
259
285
|
@reactive.calc
|
|
260
286
|
def filtered_summary():
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
selected_columns = list(input.subset_columns())
|
|
288
|
+
subset_filters = {}
|
|
289
|
+
|
|
290
|
+
for i, selected_column in enumerate(selected_columns):
|
|
291
|
+
if selected_column not in column_map:
|
|
292
|
+
continue
|
|
293
|
+
selected_keys = list(input[f"subset_values_{i}"]())
|
|
294
|
+
if not selected_keys:
|
|
295
|
+
# An explicitly selected column with no selected values
|
|
296
|
+
# should not remove all data; it simply contributes no
|
|
297
|
+
# filtering condition until values are chosen.
|
|
298
|
+
continue
|
|
299
|
+
_, value_map = _subset_choices(
|
|
300
|
+
summary, column_map[selected_column])
|
|
301
|
+
values = [value_map[key]
|
|
302
|
+
for key in selected_keys if key in value_map]
|
|
303
|
+
if values:
|
|
304
|
+
subset_filters[column_map[selected_column]] = values
|
|
305
|
+
|
|
263
306
|
start = input.datetime_start().strip()
|
|
264
307
|
end = input.datetime_end().strip()
|
|
265
|
-
return _apply_filters(
|
|
266
|
-
summary,
|
|
267
|
-
column_map[subset_column] if subset_column != "None" else "None",
|
|
268
|
-
subset_values,
|
|
269
|
-
start,
|
|
270
|
-
end,
|
|
271
|
-
)
|
|
308
|
+
return _apply_filters(summary, subset_filters, start, end)
|
|
272
309
|
|
|
273
310
|
@reactive.calc
|
|
274
311
|
def selected_data():
|
|
@@ -415,17 +452,72 @@ def create_summary_app(
|
|
|
415
452
|
facet_row="variable" if len(y_columns) > 1 else None, barmode="group",
|
|
416
453
|
)
|
|
417
454
|
else:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
455
|
+
# ``identifier`` identifies an individual input monitor. It
|
|
456
|
+
# must therefore be used as Plotly's line-group key even when
|
|
457
|
+
# it is not one of the user-selected colour/group columns.
|
|
458
|
+
# This prevents observations from two different monitors
|
|
459
|
+
# being connected by a line merely because they share the
|
|
460
|
+
# same selected group. The identifier is deliberately NOT
|
|
461
|
+
# used for colour, so monitors in the same selected group
|
|
462
|
+
# retain the same colour.
|
|
463
|
+
identifier_column = (
|
|
464
|
+
column_map["identifier"]
|
|
465
|
+
if "identifier" in column_map
|
|
466
|
+
else None
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if group_columns:
|
|
421
470
|
group_column = "__plot_group"
|
|
422
|
-
|
|
423
|
-
|
|
471
|
+
line_columns = [x_column, group_column, *y_columns]
|
|
472
|
+
if identifier_column is not None:
|
|
473
|
+
line_columns.append(identifier_column)
|
|
474
|
+
long_df = plot_df[line_columns].melt(
|
|
475
|
+
id_vars=[
|
|
476
|
+
x_column,
|
|
477
|
+
group_column,
|
|
478
|
+
*([identifier_column]
|
|
479
|
+
if identifier_column is not None else []),
|
|
480
|
+
],
|
|
481
|
+
var_name="variable",
|
|
482
|
+
value_name="value",
|
|
424
483
|
)
|
|
425
484
|
fig = px.line(
|
|
426
|
-
long_df,
|
|
485
|
+
long_df,
|
|
486
|
+
x=x_column,
|
|
487
|
+
y="value",
|
|
488
|
+
color=group_column,
|
|
489
|
+
line_group=identifier_column if identifier_column is not None else None,
|
|
427
490
|
facet_row="variable" if len(y_columns) > 1 else None,
|
|
428
491
|
)
|
|
492
|
+
elif identifier_column is not None:
|
|
493
|
+
# With no explicit grouping, retain the normal Y-variable
|
|
494
|
+
# colouring while using identifier to keep each monitor's
|
|
495
|
+
# time series separate.
|
|
496
|
+
if len(y_columns) == 1:
|
|
497
|
+
fig = px.line(
|
|
498
|
+
plot_df,
|
|
499
|
+
x=x_column,
|
|
500
|
+
y=y_columns[0],
|
|
501
|
+
line_group=identifier_column,
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
line_columns = [
|
|
505
|
+
x_column, identifier_column, *y_columns]
|
|
506
|
+
long_df = plot_df[line_columns].melt(
|
|
507
|
+
id_vars=[x_column, identifier_column],
|
|
508
|
+
var_name="variable",
|
|
509
|
+
value_name="value",
|
|
510
|
+
)
|
|
511
|
+
fig = px.line(
|
|
512
|
+
long_df,
|
|
513
|
+
x=x_column,
|
|
514
|
+
y="value",
|
|
515
|
+
color="variable",
|
|
516
|
+
line_group=identifier_column,
|
|
517
|
+
facet_row="variable",
|
|
518
|
+
)
|
|
519
|
+
else:
|
|
520
|
+
fig = px.line(plot_df, x=x_column, y=y_columns)
|
|
429
521
|
|
|
430
522
|
fig.update_layout(
|
|
431
523
|
title=input.title(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|