apmtools 0.2.25__tar.gz → 0.2.26__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.025
3
+ Version: 0.2.026
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.025"
7
+ version = "0.2.026"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -449,9 +449,26 @@ def create_summary_app(
449
449
  subset_columns = columns
450
450
 
451
451
  @module.ui
452
- def plot_tab_ui():
452
+ def plot_tab_ui(initial_filters=None):
453
+ initial_filters = initial_filters or {}
453
454
  return ui.div(
454
455
  ui.layout_columns(
456
+ ui.card(ui.card_header("Plot filters"),
457
+ ui.input_selectize("subset_columns", "Subset columns",
458
+ choices={
459
+ c: c for c in subset_columns},
460
+ selected=initial_filters.get("columns", []), multiple=True),
461
+ ui.output_ui("subset_filters"),
462
+ ui.hr(),
463
+ ui.input_text("datetime_start", "Start datetime",
464
+ value=initial_filters.get("start", ""),
465
+ placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS"),
466
+ ui.input_text("datetime_end", "End datetime",
467
+ value=initial_filters.get("end", ""),
468
+ placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS"),
469
+ ui.help_text(
470
+ "Filters affect this plot only. A new plot inherits these selections."),
471
+ ),
455
472
  ui.card(ui.card_header("Plot settings"),
456
473
  ui.input_select("plot_type", "Plot type",
457
474
  choices=PLOT_TYPES, selected=plot_type),
@@ -478,14 +495,64 @@ def create_summary_app(
478
495
  ),
479
496
  ui.input_text("title", "Plot title", value=title),
480
497
  ),
481
- ui.card(ui.card_header("Plot"), ui.div(output_widget(
482
- "plot", height="100%", width="100%"), class_="summary-plot-container")),
483
- col_widths=(3, 9),
498
+ ui.div(
499
+ ui.card(ui.card_header("Plot"),
500
+ ui.div(output_widget("plot", height="100%", width="100%"),
501
+ class_="summary-plot-container")),
502
+ ui.card(ui.card_header("Dataset used for plotting"),
503
+ ui.output_data_frame("plot_table")),
504
+ ),
505
+ col_widths=(2, 2, 8),
484
506
  ),
485
507
  )
486
508
 
487
509
  @module.server
488
- def plot_tab_server(input, output, session, filtered_summary):
510
+ def plot_tab_server(input, output, session, initial_filters=None):
511
+ initial_filters = initial_filters or {}
512
+
513
+ @render.ui
514
+ def subset_filters():
515
+ controls = []
516
+ for i, name in enumerate(list(input.subset_columns())):
517
+ if name not in column_map:
518
+ continue
519
+ choices, _ = _subset_choices(summary, column_map[name])
520
+ controls.append(ui.input_selectize(
521
+ f"subset_values_{i}", f"{name} values", choices=choices,
522
+ selected=initial_filters.get(
523
+ "values", {}).get(name, []),
524
+ multiple=True,
525
+ ))
526
+ return ui.div(*controls) if controls else ui.help_text("Select columns to filter this plot.")
527
+
528
+ @reactive.calc
529
+ def filter_state():
530
+ names = list(input.subset_columns())
531
+ values = {}
532
+ for i, name in enumerate(names):
533
+ if name in column_map:
534
+ values[name] = list(input[f"subset_values_{i}"]())
535
+ return {"columns": names, "values": values,
536
+ "start": input.datetime_start().strip(),
537
+ "end": input.datetime_end().strip()}
538
+
539
+ @reactive.calc
540
+ def filtered_summary():
541
+ state = filter_state()
542
+ filters = {}
543
+ for name, keys in state["values"].items():
544
+ _, mapping = _subset_choices(summary, column_map[name])
545
+ selected = [mapping[key] for key in keys if key in mapping]
546
+ if selected:
547
+ filters[column_map[name]] = selected
548
+ return _apply_filters(summary, filters, state["start"], state["end"])
549
+
550
+ @render.data_frame
551
+ def plot_table():
552
+ df = selected_data()
553
+ return render.DataGrid(pd.DataFrame(df).reset_index(drop=True),
554
+ filters=True, height="420px")
555
+
489
556
  @render.ui
490
557
  def x_control():
491
558
  kind = input.plot_type()
@@ -788,21 +855,9 @@ def create_summary_app(
788
855
  )
789
856
  return fig
790
857
 
791
- app_ui = ui.page_sidebar(
792
- ui.sidebar(
793
- ui.h4("Global filters"),
794
- ui.input_selectize("subset_columns", "Subset columns", choices={
795
- c: c for c in subset_columns}, selected=[], multiple=True),
796
- ui.output_ui("subset_filters"),
797
- ui.hr(),
798
- ui.h5("Datetime filter"),
799
- ui.input_text("datetime_start", "Start datetime", value="",
800
- placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS"),
801
- ui.input_text("datetime_end", "End datetime", value="",
802
- placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS"),
803
- ui.help_text(
804
- "Global filters update all plots. Plot controls are independent for each tab."),
805
- ),
858
+ return filter_state
859
+
860
+ app_ui = ui.page_fluid(
806
861
  ui.tags.style("""
807
862
  .container, .container-fluid {max-width: none !important;}
808
863
  .summary-plot-container {width:100%; height:auto; aspect-ratio: 2 / 1; min-width:0; position:relative;}
@@ -850,69 +905,15 @@ def create_summary_app(
850
905
  class_="d-flex gap-2 mb-3",
851
906
  ),
852
907
  ui.navset_tab(ui.nav_panel("Plot 1", plot_tab_ui(
853
- "plot_1"), value="plot_1"), id="plot_tabs"),
908
+ "plot_1", {}), value="plot_1"), id="plot_tabs"),
854
909
  ),
855
910
  title="Summary plots",
856
911
  )
857
912
 
858
913
  def server(input, output, session):
859
- @render.ui
860
- def subset_filters():
861
- """Render one value selector for every selected subset column."""
862
- selected_columns = list(input.subset_columns())
863
- if not selected_columns:
864
- return ui.help_text(
865
- "Select one or more columns above to choose their values."
866
- )
867
-
868
- controls = []
869
- for i, selected_column in enumerate(selected_columns):
870
- if selected_column not in column_map:
871
- continue
872
- choices, _ = _subset_choices(
873
- summary, column_map[selected_column])
874
- input_id = f"subset_values_{i}"
875
- controls.append(
876
- ui.input_selectize(
877
- input_id,
878
- f"{selected_column} values",
879
- choices=choices,
880
- selected=[],
881
- multiple=True,
882
- options={
883
- "placeholder": f"Select {selected_column} values"},
884
- )
885
- )
886
- return ui.div(*controls)
887
-
888
- @reactive.calc
889
- def filtered_summary():
890
- selected_columns = list(input.subset_columns())
891
- subset_filters = {}
892
-
893
- for i, selected_column in enumerate(selected_columns):
894
- if selected_column not in column_map:
895
- continue
896
- selected_keys = list(input[f"subset_values_{i}"]())
897
- if not selected_keys:
898
- # An explicitly selected column with no selected values
899
- # should not remove all data; it simply contributes no
900
- # filtering condition until values are chosen.
901
- continue
902
- _, value_map = _subset_choices(
903
- summary, column_map[selected_column])
904
- values = [value_map[key]
905
- for key in selected_keys if key in value_map]
906
- if values:
907
- subset_filters[column_map[selected_column]] = values
908
-
909
- start = input.datetime_start().strip()
910
- end = input.datetime_end().strip()
911
- return _apply_filters(summary, subset_filters, start, end)
912
-
913
914
  next_tab = reactive.value(1)
914
915
  active_tabs = reactive.value(("plot_1",))
915
- plot_tab_server("plot_1", filtered_summary)
916
+ tab_filters = {"plot_1": plot_tab_server("plot_1", {})}
916
917
 
917
918
  @reactive.effect
918
919
  @reactive.event(input.add_tab)
@@ -921,10 +922,16 @@ def create_summary_app(
921
922
  next_tab.set(number)
922
923
  tab_id = f"plot_{number}"
923
924
  # Register module before inserting its UI.
924
- plot_tab_server(tab_id, filtered_summary)
925
+ selected = input.plot_tabs()
926
+ # Snapshot the selected plot's filters when the new plot is created.
927
+ inherited = dict(tab_filters[selected]
928
+ ()) if selected in tab_filters else {}
929
+ inherited["values"] = {
930
+ k: list(v) for k, v in inherited.get("values", {}).items()}
931
+ tab_filters[tab_id] = plot_tab_server(tab_id, inherited)
925
932
  active_tabs.set((*active_tabs.get(), tab_id))
926
933
  ui.insert_nav_panel("plot_tabs", ui.nav_panel(
927
- f"Plot {number}", plot_tab_ui(tab_id), value=tab_id), select=True)
934
+ f"Plot {number}", plot_tab_ui(tab_id, inherited), value=tab_id), select=True)
928
935
 
929
936
  @reactive.effect
930
937
  @reactive.event(input.close_tab)
File without changes
File without changes
File without changes