apmtools 0.2.41__tar.gz → 0.2.43__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.41
3
+ Version: 0.2.43
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.41"
7
+ version = "0.2.43"
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
  ]
@@ -257,6 +257,7 @@ class Dataset(DictionaryPlus):
257
257
  def __init__(self, *args, **kwargs):
258
258
  DictionaryPlus.__init__(self, *args, **kwargs)
259
259
  self.filter_key = None
260
+ self.description = ""
260
261
 
261
262
  @property
262
263
  def _constructor(self):
@@ -285,6 +286,7 @@ class Dataset(DictionaryPlus):
285
286
  if verbose:
286
287
  print(key)
287
288
  a.filter_key = self.filter_key
289
+ a.description = self.description + f"\n Applied function {func}"
288
290
  return a
289
291
 
290
292
  def subset(self, filter_dict={}, filter_style='all', condition=None):
@@ -435,6 +437,8 @@ class Dataset(DictionaryPlus):
435
437
  a[key] = value
436
438
  a = Dataset(a)
437
439
  a.filter_key = self.filter_key
440
+ a.description = self.description + \
441
+ f"\nSubset by {filter_dict} on style {filter_style} by condition {condition}"
438
442
  return a
439
443
  else:
440
444
  b = {}
@@ -443,10 +447,14 @@ class Dataset(DictionaryPlus):
443
447
  b[key] = value
444
448
  b = Dataset(b)
445
449
  b.filter_key = self.filter_key
450
+ b.description = self.description + \
451
+ f"\nSubset by {filter_dict} on style {filter_style} by condition {condition}"
446
452
  return b
447
453
  else:
448
454
  a = Dataset(a)
449
455
  a.filter_key = self.filter_key
456
+ a.description = self.description + \
457
+ f"\nSubset by {filter_dict} on style {filter_style} by condition {condition}"
450
458
  return a
451
459
 
452
460
  def scan_folder(self, directory="./", levels=[], level=0, monitor=None, levels_dict=None, utc_timezone_shift=0, interpolate=False, add_m={}):
@@ -1026,16 +1034,18 @@ class Summary(pd.DataFrame):
1026
1034
  def _constructor(self):
1027
1035
  return Summary
1028
1036
 
1029
- def __init__(self, *args, metadata=set(), variable=None, **kwargs):
1037
+ def __init__(self, *args, metadata=set(), variable=None, description="",**kwargs):
1030
1038
  pd.DataFrame.__init__(self, *args, **kwargs)
1031
1039
 
1032
1040
  self.metadata = metadata
1033
1041
  self.variable = variable
1042
+ self.description = description
1034
1043
 
1035
1044
  def __finalize__(self, other, method=None, **kwargs):
1036
1045
  if isinstance(other, Summary):
1037
1046
  self.variable = other.variable
1038
1047
  self.metadata = other.metadata.copy()
1048
+ self.description = other.description
1039
1049
 
1040
1050
  return self
1041
1051
 
@@ -1070,6 +1080,7 @@ class Summary(pd.DataFrame):
1070
1080
  if filter_style not in {'all', 'any', 'negative all', 'negative any'}:
1071
1081
  return return_dict
1072
1082
 
1083
+
1073
1084
  # Only conditions referring to existing columns can be evaluated.
1074
1085
  conditions = []
1075
1086
  for column, values in filter_dict.items():
@@ -1091,6 +1102,8 @@ class Summary(pd.DataFrame):
1091
1102
  mask = (~conditions).all(axis=0).to_numpy()
1092
1103
  else: # negative any
1093
1104
  mask = (~conditions).any(axis=0).to_numpy()
1105
+
1106
+ return_dict.description = self.description + f"\nSubset by {filter_dict} on style {filter_style}"
1094
1107
 
1095
1108
  return return_dict.loc[mask]
1096
1109
 
@@ -1152,6 +1165,9 @@ class Summary(pd.DataFrame):
1152
1165
  if day is not None:
1153
1166
  self = self.loc[[a in day for a in [self["datetime"].map(lambda x: x.date().isoweekday())
1154
1167
  for i in range(len(self["datetime"]))]]]
1168
+
1169
+ self.description = self.description + \
1170
+ f"\nFiltered by {time_start if time_start != None else ""} till {time_end if time_end != None else ""}, {date_start if date_start != None else ""} till {date_end if date_end != None else ""}, {day if day != None else ""}"
1155
1171
  return self.reset_index()
1156
1172
 
1157
1173
  def set_attrib(self, attribute):
@@ -1574,11 +1590,11 @@ class Summary(pd.DataFrame):
1574
1590
 
1575
1591
  return output
1576
1592
 
1577
-
1578
1593
  def get_monitor(self, value):
1579
1594
 
1580
1595
  out = self.subset({"monitor": [value]})
1581
1596
  out = out.dropna(axis=1,how="all")
1597
+ out.description = out.description + f"\nReturned monitor {value}"
1582
1598
  return out
1583
1599
 
1584
1600
  def plot_app(
@@ -2066,8 +2082,6 @@ class LcmSeries(ApmSeries):
2066
2082
  def _constructor(self):
2067
2083
  return LcmSeries
2068
2084
 
2069
-
2070
-
2071
2085
  class PolarH10(dict):
2072
2086
  monitor = "polarh10"
2073
2087
  def __init__(self, *args, **kwargs):
@@ -1,4 +1,4 @@
1
- """Optional Shiny integration for apmtools."""
1
+ """Optional Shiny integration for apmtools Summary objects."""
2
2
 
3
3
  from .summary import PLOT_TYPES, create_summary_app, run_summary_app
4
4
 
@@ -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
+