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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apmtools
3
- Version: 0.2.008
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>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apmtools"
7
- version = "0.2.008"
7
+ version = "0.2.009"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -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, subset_column, subset_values, datetime_start, datetime_end):
87
- """Apply Summary.subset() and Summary.dtfilter() in that order."""
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 subset_column != "None" and subset_values:
91
- choices, value_map = _subset_choices(filtered, subset_column)
92
- values = [value_map[key] for key in subset_values if key in value_map]
93
- if values:
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 = ["None", *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
- "subset_values", "Values", choices={}, selected=[], multiple=True,
221
- options={"placeholder": "Select values"},
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
- @reactive.effect
250
- def _update_subset_values():
251
- selected_column = input.subset_column()
252
- if selected_column == "None":
253
- choices = {}
254
- else:
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
- ui.update_selectize("subset_values", choices=choices, selected=[])
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
- subset_column = input.subset_column()
262
- subset_values = list(input.subset_values())
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
- if not group_columns:
419
- fig = px.line(plot_df, x=x_column, y=y_columns)
420
- else:
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
- long_df = plot_df[[x_column, group_column, *y_columns]].melt(
423
- id_vars=[x_column, group_column], var_name="variable", value_name="value",
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, x=x_column, y="value", color=group_column,
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