apmtools 0.2.38__tar.gz → 0.2.40__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.38
3
+ Version: 0.2.40
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.38"
7
+ version = "0.2.40"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -36,7 +36,7 @@ def _axis_label(variable: str) -> str:
36
36
 
37
37
 
38
38
  def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
39
- category_columns=None):
39
+ category_columns=None, x_label_override=None):
40
40
  """Label plot axes while retaining special histogram and facet labels."""
41
41
  if plot_type == "histogram":
42
42
  fig.update_xaxes(title_text=_axis_label(x_column))
@@ -58,6 +58,8 @@ def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
58
58
  fig.update_xaxes(title_text=_axis_label(x_column))
59
59
  if y_columns:
60
60
  fig.update_yaxes(title_text=_axis_label(y_columns[0]))
61
+ if x_label_override is not None:
62
+ fig.update_xaxes(title_text=x_label_override)
61
63
  return fig
62
64
 
63
65
 
@@ -947,6 +949,7 @@ def _matplotlib_defaults(spec, *, palette="Default"):
947
949
  y_label = ("Probability density" if kind == "histogram" else
948
950
  "Value" if kind == "line" and len(ys) > 1 else
949
951
  _axis_label(ys[0]) if ys else "Count")
952
+ x_label = spec.get("x_label_override") or x_label
950
953
  return dict(x_label=x_label, y_label=y_label, names=names, colors=colors)
951
954
 
952
955
 
@@ -1680,6 +1683,7 @@ def create_summary_app(
1680
1683
  numeric_columns = _alphabetical(_numeric_columns(summary))
1681
1684
  categorical_columns = _categorical_columns(summary)
1682
1685
  datetime_columns = _datetime_columns(summary)
1686
+ timedelta_columns = _timedelta_columns(summary)
1683
1687
  time_group_columns = [f for f in FREQUENCIES.values(
1684
1688
  ) if f is not None and f not in column_map]
1685
1689
  group_columns = _alphabetical(
@@ -1986,7 +1990,8 @@ def create_summary_app(
1986
1990
  choices = categorical_columns
1987
1991
  multiple = True
1988
1992
  elif kind == "line":
1989
- choices = datetime_columns
1993
+ choices = _alphabetical(
1994
+ datetime_columns + timedelta_columns)
1990
1995
  multiple = False
1991
1996
  else:
1992
1997
  choices = numeric_columns
@@ -2131,6 +2136,12 @@ def create_summary_app(
2131
2136
  value_columns = list(dict.fromkeys(
2132
2137
  [column_map[selected_x[0]], *value_columns]
2133
2138
  ))
2139
+ if (kind == "line" and selected_x[0] in timedelta_columns
2140
+ and should_aggregate):
2141
+ # group_stat aggregation drops the original timedelta X
2142
+ # unless it is explicitly aggregated. Never silently plot
2143
+ # an unrelated frequency column instead.
2144
+ return pd.DataFrame()
2134
2145
  if should_aggregate:
2135
2146
  current = _aggregate(
2136
2147
  current, value_columns, frequency, grouping_by, aggregate,
@@ -2156,6 +2167,10 @@ def create_summary_app(
2156
2167
  should_aggregate = AGGREGATES.get(
2157
2168
  input.aggregate()) is not None
2158
2169
 
2170
+ if (selected_plot == "line" and selected_x
2171
+ and selected_x[0] in timedelta_columns
2172
+ and should_aggregate):
2173
+ return {"error": "Timedelta X-axis requires Aggregation: None. Disable aggregation to plot elapsed time."}
2159
2174
  if plot_df.empty or not selected_x or (
2160
2175
  selected_plot != "histogram" and not selected_y
2161
2176
  ):
@@ -2178,7 +2193,9 @@ def create_summary_app(
2178
2193
  x_column = mapped(selected_x[0])
2179
2194
  y_columns = [mapped(selected_y[0])]
2180
2195
  else:
2181
- x_column = frequency if frequency is not None else column_map[selected_x[0]]
2196
+ x_column = (column_map[selected_x[0]] if selected_x[0] in timedelta_columns
2197
+ else frequency if frequency is not None
2198
+ else column_map[selected_x[0]])
2182
2199
  y_columns = [mapped(c) for c in selected_y]
2183
2200
 
2184
2201
  if selected_plot == "line" and should_aggregate and frequency is None:
@@ -2196,6 +2213,20 @@ def create_summary_app(
2196
2213
  plot_df, x_columns)
2197
2214
  x_column = "__plot_category"
2198
2215
 
2216
+ # Convert durations once for Plotly, Matplotlib, editable
2217
+ # numeric bounds, faceting and exported plots. Retain the
2218
+ # original variable name and label its displayed unit.
2219
+ x_label_override = None
2220
+ if (selected_plot == "line" and
2221
+ pd.api.types.is_timedelta64_dtype(plot_df[x_column])):
2222
+ plot_df = plot_df.copy()
2223
+ plot_df[x_column] = plot_df[x_column] / \
2224
+ pd.Timedelta(hours=1)
2225
+ x_label_override = f"{x_column} [hours]"
2226
+ # Frequency-based gap detection is only meaningful for
2227
+ # aggregated timestamp series, not raw durations.
2228
+ frequency = None
2229
+
2199
2230
  # Multiple grouping columns are supported by group_stat(). For
2200
2231
  # plotting, Plotly needs one color/category column, so combine the
2201
2232
  # selected grouping columns into a readable composite label.
@@ -2210,7 +2241,7 @@ def create_summary_app(
2210
2241
  return {"data": plot_df, "kind": selected_plot, "x": x_column,
2211
2242
  "ys": y_columns, "groups": group_columns,
2212
2243
  "categories": x_columns if selected_plot in ("box", "bar") else None,
2213
- "frequency": frequency}
2244
+ "frequency": frequency, "x_label_override": x_label_override}
2214
2245
 
2215
2246
  @reactive.calc
2216
2247
  def base_figure():
@@ -2258,6 +2289,7 @@ def create_summary_app(
2258
2289
  fig, selected_plot, x_column, y_columns,
2259
2290
  category_columns=x_columns if selected_plot in (
2260
2291
  "box", "bar") else None,
2292
+ x_label_override=spec.get("x_label_override"),
2261
2293
  )
2262
2294
  fig.update_layout(
2263
2295
  hovermode="x unified" if selected_plot == "line" else "closest",
File without changes
File without changes
File without changes