apmtools 0.2.21__tar.gz → 0.2.22__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.021
3
+ Version: 0.2.022
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.021"
7
+ version = "0.2.022"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -1,13 +1,13 @@
1
- """Interactive Plotly/Shiny application for apmtools Summary objects."""
1
+ """Interactive Plotly/Shiny application for :class:`apmtools.Summary`."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import threading
6
6
  from collections.abc import Sequence
7
+
7
8
  import numpy as np
8
9
  import pandas as pd
9
10
 
10
-
11
11
  PLOT_TYPES = {
12
12
  "bar": "Bar",
13
13
  "box": "Boxplot",
@@ -19,11 +19,11 @@ PLOT_TYPES = {
19
19
  # ``None`` means row-level plotting. The other values are passed directly to
20
20
  # Summary.group_stat(frequency=...).
21
21
  FREQUENCIES = {
22
+ "Day": "day",
23
+ "Hour": "hour",
24
+ "Minute": "minute",
22
25
  "None (raw data)": None,
23
26
  "Second": "second",
24
- "Minute": "minute",
25
- "Hour": "hour",
26
- "Day": "day",
27
27
  "Week": "week",
28
28
  "Weekday": "weekday",
29
29
  }
@@ -38,22 +38,24 @@ AGGREGATES = {
38
38
  "Sum": "sum",
39
39
  }
40
40
 
41
+
41
42
  def _default_x(data: pd.DataFrame) -> str:
42
- """Return the first datetime column, or otherwise the first column."""
43
+ """Return ``time`` when available, otherwise the first column."""
44
+ if "time" in data.columns:
45
+ return "time"
43
46
  datetime_columns = [
44
47
  column
45
48
  for column in data.columns
46
49
  if pd.api.types.is_datetime64_any_dtype(data[column])
47
50
  ]
48
- if "time" in datetime_columns:
49
- return "time"
50
- else:
51
- return str(datetime_columns[0] if datetime_columns else data.columns[0])
51
+ return str(datetime_columns[0] if datetime_columns else data.columns[0])
52
+
52
53
 
53
54
  def _default_group(data: pd.DataFrame) -> str | None:
54
55
  """Return ``identifier`` when available."""
55
56
  return "identifier" if "identifier" in data.columns else None
56
57
 
58
+
57
59
  def _numeric_columns(data: pd.DataFrame) -> list[str]:
58
60
  """Return numeric column names as strings."""
59
61
  return sorted([
@@ -100,6 +102,11 @@ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
100
102
  )
101
103
 
102
104
 
105
+ def _alphabetical(values) -> list[str]:
106
+ """Return string values sorted alphabetically, case-insensitively."""
107
+ return sorted((str(value) for value in values), key=str.casefold)
108
+
109
+
103
110
  def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
104
111
  """Validate and normalise an initial Y selection."""
105
112
  numeric = _numeric_columns(data)
@@ -112,15 +119,10 @@ def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> l
112
119
  return [str(column) for column in values]
113
120
 
114
121
 
115
- def _alphabetical(values) -> list[str]:
116
- """Return string values sorted alphabetically, case-insensitively."""
117
- return sorted((str(value) for value in values), key=str.casefold)
118
-
119
-
120
-
121
122
  def _subset_choices(data: pd.DataFrame, column: str) -> tuple[dict[str, str], dict[str, object]]:
122
123
  """Build stable Shiny choices and their corresponding original values."""
123
- values = pd.unique(data[column].dropna())
124
+ values = sorted(pd.unique(data[column].dropna()),
125
+ key=lambda value: str(value).casefold())
124
126
  choices: dict[str, str] = {}
125
127
  value_map: dict[str, object] = {}
126
128
  for i, value in enumerate(values):
@@ -179,6 +181,16 @@ def _aggregate(data, value_columns, frequency, grouping_by, aggregate):
179
181
  return result
180
182
 
181
183
 
184
+ def _aggregated_value_columns(data, value_columns, aggregate):
185
+ """Return the columns produced by Summary.group_stat()."""
186
+ columns = []
187
+ for column in value_columns:
188
+ aggregated = f"{column}_{aggregate}"
189
+ if aggregated in data.columns:
190
+ columns.append(aggregated)
191
+ return columns
192
+
193
+
182
194
  def _histogram_figure(
183
195
  data: pd.DataFrame,
184
196
  x_column: str,
@@ -294,6 +306,7 @@ def _histogram_figure(
294
306
  )
295
307
  return fig
296
308
 
309
+
297
310
  def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
298
311
  """Convert a timestamp aggregation frequency to its expected spacing."""
299
312
  if not frequency or frequency == "weekday":
@@ -436,7 +449,8 @@ def create_summary_app(
436
449
  subset_columns = columns
437
450
 
438
451
  app_ui = ui.page_sidebar(
439
- ui.tags.style("""
452
+ ui.div(
453
+ ui.tags.style("""
440
454
  /* Allow the main pane to consume all width remaining after the sidebar. */
441
455
  .bslib-sidebar-layout > .main,
442
456
  .bslib-sidebar-layout > main,
@@ -465,7 +479,7 @@ def create_summary_app(
465
479
  max-width: none !important;
466
480
  }
467
481
  """),
468
- ui.tags.script("""
482
+ ui.tags.script("""
469
483
  // Plotly sometimes retains the pixel width measured before the
470
484
  // Shiny sidebar and widget have finished laying out.
471
485
  (() => {
@@ -495,7 +509,12 @@ def create_summary_app(
495
509
  }
496
510
  })();
497
511
  """),
498
- ui.sidebar(
512
+ ui.div(
513
+ output_widget("plot", height="100%", width="100%"),
514
+ class_="summary-plot-container",
515
+ ),
516
+ ),
517
+ sidebar=ui.sidebar(
499
518
  ui.h4("Plot controls"),
500
519
  ui.input_select("plot_type", "Plot type",
501
520
  choices=PLOT_TYPES, selected=plot_type),
@@ -559,10 +578,6 @@ def create_summary_app(
559
578
  "Summary.group_stat()."
560
579
  ),
561
580
  ),
562
- ui.div(
563
- output_widget("plot", height="100%", width="100%"),
564
- class_="summary-plot-container",
565
- ),
566
581
  )
567
582
 
568
583
  def server(input, output, session):
File without changes
File without changes
File without changes