apmtools 0.2.41__tar.gz → 0.2.42__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.41 → apmtools-0.2.42}/PKG-INFO +2 -1
- {apmtools-0.2.41 → apmtools-0.2.42}/pyproject.toml +2 -1
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/classes.py +0 -3
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/shiny/__init__.py +1 -1
- apmtools-0.2.42/src/apmtools/shiny/appearance.py +260 -0
- apmtools-0.2.42/src/apmtools/shiny/columns.py +98 -0
- apmtools-0.2.42/src/apmtools/shiny/config.py +55 -0
- apmtools-0.2.42/src/apmtools/shiny/data.py +333 -0
- apmtools-0.2.42/src/apmtools/shiny/export.py +71 -0
- apmtools-0.2.42/src/apmtools/shiny/matplotlib_renderer.py +789 -0
- apmtools-0.2.42/src/apmtools/shiny/plotly_renderer.py +294 -0
- apmtools-0.2.42/src/apmtools/shiny/summary.py +14 -0
- apmtools-0.2.42/src/apmtools/shiny/ui.py +1149 -0
- apmtools-0.2.41/src/apmtools/shiny/summary.py +0 -2655
- {apmtools-0.2.41 → apmtools-0.2.42}/.gitignore +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/LICENSE +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/README.md +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.41 → apmtools-0.2.42}/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.42
|
|
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>
|
|
@@ -12,6 +12,7 @@ Requires-Python: >=3.8
|
|
|
12
12
|
Provides-Extra: shiny
|
|
13
13
|
Requires-Dist: plotly>=6.0; extra == 'shiny'
|
|
14
14
|
Requires-Dist: shiny>=1.10; extra == 'shiny'
|
|
15
|
+
Requires-Dist: shinyswatch>=0.12; extra == 'shiny'
|
|
15
16
|
Requires-Dist: shinywidgets>=0.6; extra == 'shiny'
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apmtools"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.42"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
|
|
10
10
|
]
|
|
@@ -24,5 +24,6 @@ Homepage = "https://github.com/federlorenz/apmtools"
|
|
|
24
24
|
shiny = [
|
|
25
25
|
"shiny>=1.10",
|
|
26
26
|
"shinywidgets>=0.6",
|
|
27
|
+
"shinyswatch>=0.12",
|
|
27
28
|
"plotly>=6.0",
|
|
28
29
|
]
|
|
@@ -1574,7 +1574,6 @@ class Summary(pd.DataFrame):
|
|
|
1574
1574
|
|
|
1575
1575
|
return output
|
|
1576
1576
|
|
|
1577
|
-
|
|
1578
1577
|
def get_monitor(self, value):
|
|
1579
1578
|
|
|
1580
1579
|
out = self.subset({"monitor": [value]})
|
|
@@ -2066,8 +2065,6 @@ class LcmSeries(ApmSeries):
|
|
|
2066
2065
|
def _constructor(self):
|
|
2067
2066
|
return LcmSeries
|
|
2068
2067
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
2068
|
class PolarH10(dict):
|
|
2072
2069
|
monitor = "polarh10"
|
|
2073
2070
|
def __init__(self, *args, **kwargs):
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Figure labels and appearance controls."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import date, datetime, time, timedelta
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from ..units import upas_units, lascar_units, purple_units
|
|
9
|
+
|
|
10
|
+
from .config import VARIABLE_UNITS, PLOT_TYPES, FREQUENCIES, AGGREGATES, _MPL_PALETTES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _axis_label(variable: str) -> str:
|
|
14
|
+
"""Return a variable label with units, including aggregated columns.
|
|
15
|
+
|
|
16
|
+
Aggregation produces names such as ``pm25_mean`` or ``CO(ppm)_max``.
|
|
17
|
+
Strip only a *recognized* aggregation suffix, and only when the full
|
|
18
|
+
column name does not already have its own units entry.
|
|
19
|
+
"""
|
|
20
|
+
name = str(variable)
|
|
21
|
+
unit = VARIABLE_UNITS.get(name)
|
|
22
|
+
if unit is None:
|
|
23
|
+
for suffix in ("count", "max", "mean", "median", "min", "std", "sum"):
|
|
24
|
+
marker = f"_{suffix}"
|
|
25
|
+
if name.endswith(marker):
|
|
26
|
+
unit = VARIABLE_UNITS.get(name[:-len(marker)])
|
|
27
|
+
if unit is not None:
|
|
28
|
+
break
|
|
29
|
+
return f"{name} [{unit}]" if unit else name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
|
|
34
|
+
category_columns=None, x_label_override=None):
|
|
35
|
+
"""Label plot axes while retaining special histogram and facet labels."""
|
|
36
|
+
if plot_type == "histogram":
|
|
37
|
+
fig.update_xaxes(title_text=_axis_label(x_column))
|
|
38
|
+
elif plot_type in {"box", "bar"}:
|
|
39
|
+
categories = category_columns or [x_column]
|
|
40
|
+
fig.update_xaxes(title_text=" | ".join(_axis_label(c) for c in categories))
|
|
41
|
+
fig.update_yaxes(title_text=_axis_label(y_columns[0]))
|
|
42
|
+
elif plot_type == "line" and len(y_columns) > 1:
|
|
43
|
+
fig.update_xaxes(title_text=_axis_label(x_column))
|
|
44
|
+
# The values axis is shared by multiple series; facet headings carry
|
|
45
|
+
# the variable-specific unit rather than misleadingly naming one unit.
|
|
46
|
+
fig.update_yaxes(title_text="Value")
|
|
47
|
+
for annotation in fig.layout.annotations:
|
|
48
|
+
label = str(annotation.text)
|
|
49
|
+
if label.startswith("variable="):
|
|
50
|
+
annotation.text = _axis_label(label.split("=", 1)[1])
|
|
51
|
+
else:
|
|
52
|
+
fig.update_xaxes(title_text=_axis_label(x_column))
|
|
53
|
+
if y_columns:
|
|
54
|
+
fig.update_yaxes(title_text=_axis_label(y_columns[0]))
|
|
55
|
+
if x_label_override is not None:
|
|
56
|
+
fig.update_xaxes(title_text=x_label_override)
|
|
57
|
+
return fig
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_label_map(value):
|
|
62
|
+
"""Parse semicolon-separated original=replacement entries."""
|
|
63
|
+
result = {}
|
|
64
|
+
for entry in (value or "").split(";"):
|
|
65
|
+
if "=" in entry:
|
|
66
|
+
key, replacement = entry.split("=", 1)
|
|
67
|
+
if key.strip() and replacement.strip():
|
|
68
|
+
result[key.strip()] = replacement.strip()
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _numeric_axis_controls(kind, box_orientation="vertical"):
|
|
74
|
+
"""Return axes on which numeric/date tick-density controls apply."""
|
|
75
|
+
if kind in ("line", "scatter", "histogram"):
|
|
76
|
+
return True, True
|
|
77
|
+
if kind == "box" and box_orientation == "horizontal":
|
|
78
|
+
return True, False
|
|
79
|
+
return False, kind in ("box", "bar")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _matplotlib_tick_controls(ax, *, kind, box_orientation="vertical", show_grid=True,
|
|
84
|
+
x_major_ticks=6, y_major_ticks=6,
|
|
85
|
+
x_minor_ticks=False, y_minor_ticks=False):
|
|
86
|
+
from matplotlib.ticker import MaxNLocator, AutoMinorLocator
|
|
87
|
+
from matplotlib.dates import AutoDateLocator
|
|
88
|
+
use_x, use_y = _numeric_axis_controls(kind, box_orientation)
|
|
89
|
+
for active, axis, count, minor in ((use_x, ax.xaxis, x_major_ticks, x_minor_ticks),
|
|
90
|
+
(use_y, ax.yaxis, y_major_ticks, y_minor_ticks)):
|
|
91
|
+
if not active:
|
|
92
|
+
continue
|
|
93
|
+
count = max(2, min(30, int(count or 6)))
|
|
94
|
+
if axis.get_scale() == "linear":
|
|
95
|
+
if isinstance(axis.get_major_locator(), AutoDateLocator):
|
|
96
|
+
axis.set_major_locator(AutoDateLocator(minticks=max(2, count - 2), maxticks=count))
|
|
97
|
+
else:
|
|
98
|
+
axis.set_major_locator(MaxNLocator(nbins=count))
|
|
99
|
+
if minor:
|
|
100
|
+
axis.set_minor_locator(AutoMinorLocator())
|
|
101
|
+
else:
|
|
102
|
+
axis.set_minor_locator(__import__("matplotlib.ticker", fromlist=["NullLocator"]).NullLocator())
|
|
103
|
+
axis.grid(bool(show_grid and minor), which="minor")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _apply_plot_appearance(fig, *, title, x_label="", y_label="",
|
|
108
|
+
legend_labels="", trace_colors="", palette="Default",
|
|
109
|
+
x_min="", x_max="", y_min="", y_max="",
|
|
110
|
+
template="plotly", legend_position="right", legend_inside=False,
|
|
111
|
+
show_grid=True, font_size=12, line_width=2,
|
|
112
|
+
marker_size=7, line_alpha="", line_style="", scatter_size="", scatter_marker="", scatter_alpha="",
|
|
113
|
+
hist_alpha="", box_showfliers=True, box_orientation="vertical",
|
|
114
|
+
title_font_size=14, axis_font_size=12, tick_font_size=10,
|
|
115
|
+
legend_font_size=12, panel_font_size=14, plot_kind=None,
|
|
116
|
+
x_major_ticks=6, y_major_ticks=6, x_minor_ticks=False, y_minor_ticks=False):
|
|
117
|
+
"""Apply per-plot appearance overrides without changing the source data."""
|
|
118
|
+
from plotly.colors import qualitative
|
|
119
|
+
|
|
120
|
+
fig.update_layout(title=title, template=template, font=dict(size=font_size),
|
|
121
|
+
title_font=dict(size=title_font_size),
|
|
122
|
+
legend=dict(font=dict(size=legend_font_size), title_font=dict(size=legend_font_size)))
|
|
123
|
+
fig.update_xaxes(title_font=dict(size=axis_font_size), tickfont=dict(size=tick_font_size))
|
|
124
|
+
fig.update_yaxes(title_font=dict(size=axis_font_size), tickfont=dict(size=tick_font_size))
|
|
125
|
+
if x_label.strip():
|
|
126
|
+
fig.update_xaxes(title_text=x_label.strip())
|
|
127
|
+
if y_label.strip():
|
|
128
|
+
fig.update_yaxes(title_text=y_label.strip())
|
|
129
|
+
fig.update_xaxes(showgrid=show_grid)
|
|
130
|
+
fig.update_yaxes(showgrid=show_grid)
|
|
131
|
+
use_x, use_y = _numeric_axis_controls(plot_kind, box_orientation)
|
|
132
|
+
for active, axis_name, count, minor in ((use_x, "x", x_major_ticks, x_minor_ticks),
|
|
133
|
+
(use_y, "y", y_major_ticks, y_minor_ticks)):
|
|
134
|
+
if not active:
|
|
135
|
+
continue
|
|
136
|
+
update = getattr(fig, f"update_{axis_name}axes")
|
|
137
|
+
update(nticks=max(2, min(30, int(count or 6))), minor=dict(ticks="inside" if minor else "", showgrid=bool(show_grid and minor)))
|
|
138
|
+
for axis, lower, upper in (("x", x_min, x_max), ("y", y_min, y_max)):
|
|
139
|
+
datetime_axis = axis == "x" and any(
|
|
140
|
+
getattr(trace, "type", "") == "scatter" and
|
|
141
|
+
any(isinstance(v, (pd.Timestamp, datetime, np.datetime64)) for v in (trace.x[:3] if trace.x is not None else []))
|
|
142
|
+
for trace in fig.data
|
|
143
|
+
)
|
|
144
|
+
low = _parse_axis_limit(lower, datetime_axis=datetime_axis)
|
|
145
|
+
high = _parse_axis_limit(upper, datetime_axis=datetime_axis)
|
|
146
|
+
if low is not None or high is not None:
|
|
147
|
+
getattr(fig, f"update_{axis}axes")(range=[low, high], autorange=False)
|
|
148
|
+
positions = {
|
|
149
|
+
"right": dict(x=1.02, y=1, xanchor="left", yanchor="top"),
|
|
150
|
+
"left": dict(x=-0.1, y=1, xanchor="right", yanchor="top"),
|
|
151
|
+
"top": dict(x=0.5, y=1.13, xanchor="center", yanchor="bottom", orientation="h"),
|
|
152
|
+
"bottom": dict(x=0.5, y=-0.2, xanchor="center", yanchor="top", orientation="h"),
|
|
153
|
+
}
|
|
154
|
+
legend_settings = positions.get(legend_position, positions["right"]).copy()
|
|
155
|
+
if legend_inside:
|
|
156
|
+
legend_settings = {
|
|
157
|
+
"right": dict(x=0.99, y=0.99, xanchor="right", yanchor="top"),
|
|
158
|
+
"left": dict(x=0.01, y=0.99, xanchor="left", yanchor="top"),
|
|
159
|
+
"top": dict(x=0.5, y=0.99, xanchor="center", yanchor="top", orientation="h"),
|
|
160
|
+
"bottom": dict(x=0.5, y=0.01, xanchor="center", yanchor="bottom", orientation="h"),
|
|
161
|
+
}.get(legend_position, {})
|
|
162
|
+
legend_settings = {**legend_settings, "bgcolor": "rgba(255,255,255,0.7)"}
|
|
163
|
+
fig.update_layout(legend={**legend_settings, "title": {"text": "Legend", "font": {"size": legend_font_size}}, "font": {"size": legend_font_size}})
|
|
164
|
+
labels = _parse_label_map(legend_labels)
|
|
165
|
+
colors = _parse_label_map(trace_colors)
|
|
166
|
+
alphas, styles, sizes, markers, marker_alphas = _series_style_maps(
|
|
167
|
+
line_alpha, line_style, scatter_size, scatter_marker, scatter_alpha)
|
|
168
|
+
histogram_alphas = _parse_label_map(hist_alpha)
|
|
169
|
+
palette_values = getattr(qualitative, palette, None) if palette != "Default" else None
|
|
170
|
+
# Color keys can refer to either original or renamed trace labels.
|
|
171
|
+
for i, trace in enumerate(fig.data):
|
|
172
|
+
original = str(trace.name or "")
|
|
173
|
+
renamed = labels.get(original, original)
|
|
174
|
+
if renamed != original:
|
|
175
|
+
trace.name = renamed
|
|
176
|
+
if getattr(trace, "legendgroup", None) == original:
|
|
177
|
+
trace.legendgroup = renamed
|
|
178
|
+
color = colors.get(original, colors.get(renamed))
|
|
179
|
+
if color is None and palette_values:
|
|
180
|
+
color = palette_values[i % len(palette_values)]
|
|
181
|
+
if color:
|
|
182
|
+
if getattr(trace, "line", None) is not None:
|
|
183
|
+
trace.line.color = color
|
|
184
|
+
if getattr(trace, "marker", None) is not None:
|
|
185
|
+
trace.marker.color = color
|
|
186
|
+
if getattr(trace, "fillcolor", None) is not None and trace.type == "histogram":
|
|
187
|
+
trace.marker.color = color
|
|
188
|
+
if plot_kind == "histogram":
|
|
189
|
+
trace.opacity = _bounded_float(histogram_alphas, original, 0.65, 0, 1)
|
|
190
|
+
if plot_kind == "box":
|
|
191
|
+
trace.boxpoints = "outliers" if box_showfliers else False
|
|
192
|
+
if box_orientation == "horizontal":
|
|
193
|
+
old_x, old_y = trace.x, trace.y
|
|
194
|
+
trace.x, trace.y = old_y, old_x
|
|
195
|
+
trace.orientation = "h"
|
|
196
|
+
if getattr(trace, "line", None) is not None and plot_kind == "line":
|
|
197
|
+
trace.line.width = line_width
|
|
198
|
+
if plot_kind == "line":
|
|
199
|
+
trace.line.dash = {"-": "solid", ":": "dot", "--": "dash"}[_line_style(styles, original)]
|
|
200
|
+
trace.opacity = _bounded_float(alphas, original, 1, 0, 1)
|
|
201
|
+
if getattr(trace, "marker", None) is not None and trace.type in ("scatter", "scattergl"):
|
|
202
|
+
if plot_kind == "scatter":
|
|
203
|
+
trace.marker.size = _bounded_float(sizes, original, 7, 0.1, 100)
|
|
204
|
+
trace.marker.symbol = {"o": "circle", "x": "x"}[_scatter_marker(markers, original)]
|
|
205
|
+
trace.opacity = _bounded_float(marker_alphas, original, 1, 0, 1)
|
|
206
|
+
else:
|
|
207
|
+
trace.marker.size = marker_size
|
|
208
|
+
if plot_kind == "box" and box_orientation == "horizontal":
|
|
209
|
+
x_title = fig.layout.xaxis.title.text
|
|
210
|
+
y_title = fig.layout.yaxis.title.text
|
|
211
|
+
fig.update_xaxes(title_text=y_title, type="linear", autorange=True)
|
|
212
|
+
fig.update_yaxes(title_text=x_title, type="category", autorange=True)
|
|
213
|
+
return fig
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _series_style_maps(line_alpha="", line_style="", scatter_size="", scatter_marker="", scatter_alpha=""):
|
|
218
|
+
"""Parse per-trace styles, keyed by the original legend/series name."""
|
|
219
|
+
return tuple(_parse_label_map(v) for v in (line_alpha, line_style, scatter_size, scatter_marker, scatter_alpha))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _bounded_float(mapping, name, default, low, high):
|
|
224
|
+
try:
|
|
225
|
+
value = float(mapping.get(name, default))
|
|
226
|
+
return value if np.isfinite(value) and low <= value <= high else float(default)
|
|
227
|
+
except (ValueError, TypeError):
|
|
228
|
+
return float(default)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _line_style(mapping, name):
|
|
233
|
+
return {"solid": "-", "dotted": ":", "dashed": "--", "-": "-", ":": ":", "--": "--"}.get(
|
|
234
|
+
mapping.get(name, "solid").strip().lower(), "-")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _scatter_marker(mapping, name):
|
|
239
|
+
return {"dot": "o", "cross": "x", "o": "o", "x": "x"}.get(
|
|
240
|
+
mapping.get(name, "dot").strip().lower(), "o")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_axis_limit(value, *, datetime_axis=False):
|
|
245
|
+
"""Parse numeric bounds or an ISO timestamp for a datetime X axis."""
|
|
246
|
+
if value is None or not str(value).strip():
|
|
247
|
+
return None
|
|
248
|
+
if datetime_axis:
|
|
249
|
+
try:
|
|
250
|
+
parsed = pd.to_datetime(value, errors="raise")
|
|
251
|
+
if not pd.isna(parsed):
|
|
252
|
+
return parsed
|
|
253
|
+
except (TypeError, ValueError, OverflowError):
|
|
254
|
+
return None
|
|
255
|
+
try:
|
|
256
|
+
number = float(value)
|
|
257
|
+
return number if np.isfinite(number) else None
|
|
258
|
+
except (TypeError, ValueError):
|
|
259
|
+
return None
|
|
260
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Column classification and default selections."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import date, datetime, time, timedelta
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from ..units import upas_units, lascar_units, purple_units
|
|
9
|
+
|
|
10
|
+
from .config import VARIABLE_UNITS, PLOT_TYPES, FREQUENCIES, AGGREGATES, _MPL_PALETTES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _default_x(data: pd.DataFrame) -> str:
|
|
14
|
+
"""Return ``time`` when available, otherwise the first column."""
|
|
15
|
+
if "datetime" in data.columns:
|
|
16
|
+
return "datetime"
|
|
17
|
+
datetime_columns = [
|
|
18
|
+
column
|
|
19
|
+
for column in data.columns
|
|
20
|
+
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
21
|
+
]
|
|
22
|
+
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_group(data: pd.DataFrame) -> str | None:
|
|
27
|
+
"""Return ``identifier`` when available."""
|
|
28
|
+
return "identifier" if "identifier" in data.columns else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _numeric_columns(data: pd.DataFrame) -> list[str]:
|
|
33
|
+
"""Return numeric column names as strings."""
|
|
34
|
+
return sorted([
|
|
35
|
+
str(column)
|
|
36
|
+
for column in data.columns
|
|
37
|
+
if pd.api.types.is_numeric_dtype(data[column])
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _categorical_columns(data: pd.DataFrame) -> list[str]:
|
|
43
|
+
"""Return categorical column names as strings."""
|
|
44
|
+
return sorted(
|
|
45
|
+
[
|
|
46
|
+
str(column)
|
|
47
|
+
for column in data.columns
|
|
48
|
+
if not pd.api.types.is_numeric_dtype(data[column])
|
|
49
|
+
and not pd.api.types.is_datetime64_any_dtype(data[column])
|
|
50
|
+
],
|
|
51
|
+
key=str.casefold,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _datetime_columns(data: pd.DataFrame) -> list[str]:
|
|
57
|
+
"""Return datetime column names as strings."""
|
|
58
|
+
return sorted(
|
|
59
|
+
[
|
|
60
|
+
str(column)
|
|
61
|
+
for column in data.columns
|
|
62
|
+
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
63
|
+
],
|
|
64
|
+
key=str.casefold,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _timedelta_columns(data: pd.DataFrame) -> list[str]:
|
|
70
|
+
"""Return timedelta column names as strings."""
|
|
71
|
+
return sorted(
|
|
72
|
+
[
|
|
73
|
+
str(column)
|
|
74
|
+
for column in data.columns
|
|
75
|
+
if pd.api.types.is_timedelta64_dtype(data[column])
|
|
76
|
+
],
|
|
77
|
+
key=str.casefold,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _alphabetical(values) -> list[str]:
|
|
83
|
+
"""Return string values sorted alphabetically, case-insensitively."""
|
|
84
|
+
return sorted((str(value) for value in values), key=str.casefold)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
|
|
89
|
+
"""Validate and normalise an initial Y selection."""
|
|
90
|
+
numeric = _numeric_columns(data)
|
|
91
|
+
if y is None:
|
|
92
|
+
return [column for column in numeric if column != x][:1]
|
|
93
|
+
values = [y] if isinstance(y, str) else list(y)
|
|
94
|
+
missing = [column for column in values if column not in data.columns]
|
|
95
|
+
if missing:
|
|
96
|
+
raise ValueError(f"Unknown y column(s): {missing}")
|
|
97
|
+
return [str(column) for column in values]
|
|
98
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import threading
|
|
3
|
+
from datetime import date, datetime, time, timedelta
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from ..units import upas_units, lascar_units, purple_units
|
|
8
|
+
|
|
9
|
+
VARIABLE_UNITS = {**upas_units, **lascar_units, **purple_units}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PLOT_TYPES = {
|
|
13
|
+
"bar": "Bar",
|
|
14
|
+
"box": "Boxplot",
|
|
15
|
+
"histogram": "Histogram",
|
|
16
|
+
"line": "Line",
|
|
17
|
+
"scatter": "Scatter",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FREQUENCIES = {
|
|
22
|
+
"None": None,
|
|
23
|
+
"Day": "day",
|
|
24
|
+
"Hour": "hour",
|
|
25
|
+
"Minute": "minute",
|
|
26
|
+
"Second": "second",
|
|
27
|
+
"Week": "week",
|
|
28
|
+
"Weekday": "weekday",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
AGGREGATES = {
|
|
33
|
+
"None": None,
|
|
34
|
+
"Count": "count",
|
|
35
|
+
"Maximum": "max",
|
|
36
|
+
"Mean": "mean",
|
|
37
|
+
"Median": "median",
|
|
38
|
+
"Minimum": "min",
|
|
39
|
+
"Standard deviation": "std",
|
|
40
|
+
"Sum": "sum",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_MPL_PALETTES = {
|
|
45
|
+
"Default": ["#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A", "#19D3F3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52"],
|
|
46
|
+
"Plotly": ["#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A", "#19D3F3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52"],
|
|
47
|
+
"D3": ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"],
|
|
48
|
+
"G10": ["#3366CC", "#DC3912", "#FF9900", "#109618", "#990099", "#0099C6", "#DD4477", "#66AA00", "#B82E2E", "#316395"],
|
|
49
|
+
"T10": ["#4C78A8", "#F58518", "#E45756", "#72B7B2", "#54A24B", "#EECA3B", "#B279A2", "#FF9DA6", "#9D755D", "#BAB0AC"],
|
|
50
|
+
"Set1": ["#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999"],
|
|
51
|
+
"Set2": ["#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3"],
|
|
52
|
+
"Pastel": ["#AEC6CF", "#FFB347", "#77DD77", "#CFCFC4", "#B39EB5", "#FF6961", "#FDFD96", "#CB99C9"],
|
|
53
|
+
"Dark24": ["#2E91E5", "#E15F99", "#1CA71C", "#FB0D0D", "#DA16FF", "#222A2A", "#B68100", "#750D86", "#EB663B", "#511CFB", "#00A08B", "#FB00D1", "#FC0080", "#B2828D", "#6C7C32", "#778AAE", "#862A16", "#A777F1", "#620042", "#1616A7", "#DA60CA", "#6C4516", "#0D2A63", "#AF0038"],
|
|
54
|
+
}
|
|
55
|
+
|