apmtools 0.1.63__tar.gz → 0.2.2__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.1.63 → apmtools-0.2.2}/PKG-INFO +5 -1
- {apmtools-0.1.63 → apmtools-0.2.2}/pyproject.toml +8 -1
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/classes.py +84 -1
- apmtools-0.2.2/src/apmtools/shiny/__init__.py +5 -0
- apmtools-0.2.2/src/apmtools/shiny/summary.py +353 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/.gitignore +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/LICENSE +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/README.md +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/__init__.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/functions.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/legacy.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.2}/src/apmtools/units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apmtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.002
|
|
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>
|
|
@@ -9,6 +9,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.8
|
|
12
|
+
Provides-Extra: shiny
|
|
13
|
+
Requires-Dist: plotly>=6.0; extra == 'shiny'
|
|
14
|
+
Requires-Dist: shiny>=1.10; extra == 'shiny'
|
|
15
|
+
Requires-Dist: shinywidgets>=0.6; extra == 'shiny'
|
|
12
16
|
Description-Content-Type: text/markdown
|
|
13
17
|
|
|
14
18
|
# apmtools
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apmtools"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.002"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
|
|
10
10
|
]
|
|
@@ -19,3 +19,10 @@ classifiers = [
|
|
|
19
19
|
|
|
20
20
|
[project.urls]
|
|
21
21
|
Homepage = "https://github.com/federlorenz/apmtools"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
shiny = [
|
|
25
|
+
"shiny>=1.10",
|
|
26
|
+
"shinywidgets>=0.6",
|
|
27
|
+
"plotly>=6.0",
|
|
28
|
+
]
|
|
@@ -837,7 +837,7 @@ class Dataset(DictionaryPlus):
|
|
|
837
837
|
if (k in value.m.keys()) and (value.m[k] == v):
|
|
838
838
|
value.m[k] = None
|
|
839
839
|
|
|
840
|
-
def
|
|
840
|
+
def get_monitor(self,value):
|
|
841
841
|
return self.subset({"monitor":[value]})
|
|
842
842
|
|
|
843
843
|
class Group(pd.DataFrame):
|
|
@@ -1397,6 +1397,77 @@ class Summary(pd.DataFrame):
|
|
|
1397
1397
|
|
|
1398
1398
|
return output
|
|
1399
1399
|
|
|
1400
|
+
def get_monitor(self, value):
|
|
1401
|
+
return self.subset({"monitor": [value]})
|
|
1402
|
+
|
|
1403
|
+
def plot_app(
|
|
1404
|
+
self,
|
|
1405
|
+
x=None,
|
|
1406
|
+
y=None,
|
|
1407
|
+
group_by=None,
|
|
1408
|
+
plot_type="line",
|
|
1409
|
+
title="Summary plot",
|
|
1410
|
+
host="127.0.0.1",
|
|
1411
|
+
port=0,
|
|
1412
|
+
launch_browser=True,
|
|
1413
|
+
daemon=True,
|
|
1414
|
+
):
|
|
1415
|
+
"""
|
|
1416
|
+
Launch an interactive Shiny plotting application for this Summary.
|
|
1417
|
+
|
|
1418
|
+
Parameters
|
|
1419
|
+
----------
|
|
1420
|
+
x : str, optional
|
|
1421
|
+
Initial X-axis or categorical column.
|
|
1422
|
+
y : str or sequence of str, optional
|
|
1423
|
+
Initial numeric variable or variables.
|
|
1424
|
+
group_by : str, optional
|
|
1425
|
+
Initial grouping column.
|
|
1426
|
+
plot_type : {"line", "bar", "histogram", "box"}, default="line"
|
|
1427
|
+
Initial plot type. The user can change the plot type in the app.
|
|
1428
|
+
title : str, default="Summary plot"
|
|
1429
|
+
Initial plot title.
|
|
1430
|
+
host : str, default="127.0.0.1"
|
|
1431
|
+
Server host.
|
|
1432
|
+
port : int, default=0
|
|
1433
|
+
Server port. Zero selects an available port.
|
|
1434
|
+
launch_browser : bool, default=True
|
|
1435
|
+
Open the app in the default browser.
|
|
1436
|
+
daemon : bool, default=True
|
|
1437
|
+
Run the server thread as a daemon.
|
|
1438
|
+
|
|
1439
|
+
Returns
|
|
1440
|
+
-------
|
|
1441
|
+
threading.Thread
|
|
1442
|
+
Background thread running the Shiny server.
|
|
1443
|
+
|
|
1444
|
+
Notes
|
|
1445
|
+
-----
|
|
1446
|
+
Install the optional dependencies with:
|
|
1447
|
+
|
|
1448
|
+
pip install "apmtools[shiny]"
|
|
1449
|
+
"""
|
|
1450
|
+
try:
|
|
1451
|
+
from .shiny import run_summary_app
|
|
1452
|
+
except ImportError as exc:
|
|
1453
|
+
raise ImportError(
|
|
1454
|
+
"Summary.plot_app() requires the optional Shiny dependencies. "
|
|
1455
|
+
'Install them with: pip install "apmtools[shiny]"'
|
|
1456
|
+
) from exc
|
|
1457
|
+
|
|
1458
|
+
return run_summary_app(
|
|
1459
|
+
self,
|
|
1460
|
+
x=x,
|
|
1461
|
+
y=y,
|
|
1462
|
+
group_by=group_by,
|
|
1463
|
+
plot_type=plot_type,
|
|
1464
|
+
title=title,
|
|
1465
|
+
host=host,
|
|
1466
|
+
port=port,
|
|
1467
|
+
launch_browser=launch_browser,
|
|
1468
|
+
daemon=daemon,
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1400
1471
|
class Apm(pd.DataFrame):
|
|
1401
1472
|
monitor = "apm"
|
|
1402
1473
|
def __init__(self, *args, **kwargs):
|
|
@@ -1707,6 +1778,10 @@ class Upas(Apm):
|
|
|
1707
1778
|
def _constructor_sliced(self):
|
|
1708
1779
|
return UpasSeries
|
|
1709
1780
|
|
|
1781
|
+
@property
|
|
1782
|
+
def units(self):
|
|
1783
|
+
return upas_units
|
|
1784
|
+
|
|
1710
1785
|
class UpasSeries(ApmSeries):
|
|
1711
1786
|
def __init__(self, *args, **kwargs):
|
|
1712
1787
|
ApmSeries.__init__(self, *args, **kwargs)
|
|
@@ -1733,6 +1808,10 @@ class Lascar(Apm):
|
|
|
1733
1808
|
def _constructor_sliced(self):
|
|
1734
1809
|
return LascarSeries
|
|
1735
1810
|
|
|
1811
|
+
@property
|
|
1812
|
+
def units(self):
|
|
1813
|
+
return lascar_units
|
|
1814
|
+
|
|
1736
1815
|
class LascarSeries(ApmSeries):
|
|
1737
1816
|
def __init__(self, *args, **kwargs):
|
|
1738
1817
|
ApmSeries.__init__(self, *args, **kwargs)
|
|
@@ -1758,6 +1837,10 @@ class Purple(Apm):
|
|
|
1758
1837
|
def _constructor_sliced(self):
|
|
1759
1838
|
return PurpleSeries
|
|
1760
1839
|
|
|
1840
|
+
@property
|
|
1841
|
+
def units(self):
|
|
1842
|
+
return purple_units
|
|
1843
|
+
|
|
1761
1844
|
class PurpleSeries(ApmSeries):
|
|
1762
1845
|
def __init__(self, *args, **kwargs):
|
|
1763
1846
|
ApmSeries.__init__(self, *args, **kwargs)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Interactive Plotly/Shiny application for apmtools Summary objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PLOT_TYPES = {
|
|
12
|
+
"line": "Line",
|
|
13
|
+
"bar": "Bar",
|
|
14
|
+
"histogram": "Histogram",
|
|
15
|
+
"box": "Boxplot",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_x(data: pd.DataFrame) -> str:
|
|
20
|
+
"""Return the first datetime column, or otherwise the first column."""
|
|
21
|
+
datetime_columns = [
|
|
22
|
+
column
|
|
23
|
+
for column in data.columns
|
|
24
|
+
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
25
|
+
]
|
|
26
|
+
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _numeric_columns(data: pd.DataFrame) -> list[str]:
|
|
30
|
+
"""Return numeric column names as strings."""
|
|
31
|
+
return [
|
|
32
|
+
str(column)
|
|
33
|
+
for column in data.columns
|
|
34
|
+
if pd.api.types.is_numeric_dtype(data[column])
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalise_y(
|
|
39
|
+
data: pd.DataFrame,
|
|
40
|
+
x: str,
|
|
41
|
+
y: str | Sequence[str] | None,
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
"""Validate and normalise an initial Y selection."""
|
|
44
|
+
if y is None:
|
|
45
|
+
return [column for column in _numeric_columns(data) if column != x][:1]
|
|
46
|
+
|
|
47
|
+
values = [y] if isinstance(y, str) else list(y)
|
|
48
|
+
missing = [column for column in values if column not in data.columns]
|
|
49
|
+
if missing:
|
|
50
|
+
raise ValueError(f"Unknown y column(s): {missing}")
|
|
51
|
+
return [str(column) for column in values]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_summary_app(
|
|
55
|
+
data: pd.DataFrame,
|
|
56
|
+
*,
|
|
57
|
+
x: str | None = None,
|
|
58
|
+
y: str | Sequence[str] | None = None,
|
|
59
|
+
group_by: str | None = None,
|
|
60
|
+
plot_type: str = "line",
|
|
61
|
+
title: str = "Summary plot",
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Create, but do not start, a Shiny application for a Summary/DataFrame.
|
|
65
|
+
|
|
66
|
+
Supported plot types are ``line``, ``bar``, ``histogram`` and ``box``.
|
|
67
|
+
"""
|
|
68
|
+
from shiny import App, reactive, render, ui
|
|
69
|
+
from shinywidgets import output_widget, render_plotly
|
|
70
|
+
import plotly.express as px
|
|
71
|
+
|
|
72
|
+
df = pd.DataFrame(data).copy()
|
|
73
|
+
if df.empty:
|
|
74
|
+
raise ValueError("Cannot create a Shiny application from an empty Summary.")
|
|
75
|
+
|
|
76
|
+
original_columns = list(df.columns)
|
|
77
|
+
column_map = {str(column): column for column in original_columns}
|
|
78
|
+
columns = list(column_map)
|
|
79
|
+
numeric_columns = _numeric_columns(df)
|
|
80
|
+
|
|
81
|
+
if x is None:
|
|
82
|
+
x = _default_x(df)
|
|
83
|
+
else:
|
|
84
|
+
x = str(x)
|
|
85
|
+
if x not in column_map:
|
|
86
|
+
raise ValueError(f"Unknown x column: {x!r}")
|
|
87
|
+
|
|
88
|
+
initial_y = _normalise_y(df, column_map[x], y)
|
|
89
|
+
|
|
90
|
+
if group_by is not None:
|
|
91
|
+
group_by = str(group_by)
|
|
92
|
+
if group_by not in column_map:
|
|
93
|
+
raise ValueError(f"Unknown group_by column: {group_by!r}")
|
|
94
|
+
|
|
95
|
+
if plot_type not in PLOT_TYPES:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Unknown plot_type {plot_type!r}. "
|
|
98
|
+
f"Choose one of: {', '.join(PLOT_TYPES)}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
app_ui = ui.page_sidebar(
|
|
102
|
+
ui.sidebar(
|
|
103
|
+
ui.h4("Plot controls"),
|
|
104
|
+
|
|
105
|
+
ui.input_select(
|
|
106
|
+
"plot_type",
|
|
107
|
+
"Plot type",
|
|
108
|
+
choices=PLOT_TYPES,
|
|
109
|
+
selected=plot_type,
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
ui.input_select(
|
|
113
|
+
"x",
|
|
114
|
+
"X-axis / category",
|
|
115
|
+
choices=columns,
|
|
116
|
+
selected=x,
|
|
117
|
+
),
|
|
118
|
+
|
|
119
|
+
ui.input_selectize(
|
|
120
|
+
"y",
|
|
121
|
+
"Y-axis / variable",
|
|
122
|
+
choices=numeric_columns,
|
|
123
|
+
selected=initial_y,
|
|
124
|
+
multiple=True,
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
ui.input_select(
|
|
128
|
+
"group_by",
|
|
129
|
+
"Group by",
|
|
130
|
+
choices={"None": "None", **{c: c for c in columns}},
|
|
131
|
+
selected=group_by if group_by is not None else "None",
|
|
132
|
+
),
|
|
133
|
+
|
|
134
|
+
ui.input_numeric(
|
|
135
|
+
"bins",
|
|
136
|
+
"Histogram bins",
|
|
137
|
+
value=30,
|
|
138
|
+
min=1,
|
|
139
|
+
max=200,
|
|
140
|
+
step=1,
|
|
141
|
+
),
|
|
142
|
+
|
|
143
|
+
ui.input_text(
|
|
144
|
+
"title",
|
|
145
|
+
"Plot title",
|
|
146
|
+
value=title,
|
|
147
|
+
),
|
|
148
|
+
|
|
149
|
+
ui.help_text(
|
|
150
|
+
"Line: ordered X values and one or more Y variables. "
|
|
151
|
+
"Bar: categorical X and numeric Y. "
|
|
152
|
+
"Histogram: distribution of numeric variables. "
|
|
153
|
+
"Boxplot: distribution of numeric variables, optionally grouped."
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
ui.card(
|
|
158
|
+
ui.card_header("Interactive plot"),
|
|
159
|
+
output_widget("plot", height="75vh"),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def server(input, output, session):
|
|
164
|
+
@reactive.calc
|
|
165
|
+
def selected_data() -> pd.DataFrame:
|
|
166
|
+
selected_plot = input.plot_type()
|
|
167
|
+
selected_x = input.x()
|
|
168
|
+
selected_y = list(input.y())
|
|
169
|
+
selected_group = input.group_by()
|
|
170
|
+
|
|
171
|
+
if selected_plot == "histogram":
|
|
172
|
+
if not selected_y:
|
|
173
|
+
return pd.DataFrame()
|
|
174
|
+
use_columns = [column_map[c] for c in selected_y]
|
|
175
|
+
else:
|
|
176
|
+
if not selected_y:
|
|
177
|
+
return pd.DataFrame()
|
|
178
|
+
use_columns = [
|
|
179
|
+
column_map[selected_x],
|
|
180
|
+
*(column_map[c] for c in selected_y),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
if selected_group != "None":
|
|
184
|
+
group_column = column_map[selected_group]
|
|
185
|
+
if group_column not in use_columns:
|
|
186
|
+
use_columns.append(group_column)
|
|
187
|
+
|
|
188
|
+
return df.loc[:, use_columns].copy()
|
|
189
|
+
|
|
190
|
+
@render_plotly
|
|
191
|
+
def plot():
|
|
192
|
+
plot_df = selected_data()
|
|
193
|
+
selected_plot = input.plot_type()
|
|
194
|
+
selected_x = input.x()
|
|
195
|
+
selected_y = list(input.y())
|
|
196
|
+
selected_group = input.group_by()
|
|
197
|
+
|
|
198
|
+
if plot_df.empty or not selected_y:
|
|
199
|
+
return px.scatter(title="Select at least one numeric variable")
|
|
200
|
+
|
|
201
|
+
y_columns = [column_map[c] for c in selected_y]
|
|
202
|
+
|
|
203
|
+
if selected_plot == "histogram":
|
|
204
|
+
# Plotly accepts multiple columns through y with a long-form
|
|
205
|
+
# transformation; melt gives consistent legends.
|
|
206
|
+
long_df = plot_df[y_columns].melt(
|
|
207
|
+
var_name="variable",
|
|
208
|
+
value_name="value",
|
|
209
|
+
)
|
|
210
|
+
fig = px.histogram(
|
|
211
|
+
long_df,
|
|
212
|
+
x="value",
|
|
213
|
+
color="variable",
|
|
214
|
+
nbins=int(input.bins()),
|
|
215
|
+
barmode="overlay" if len(y_columns) > 1 else "relative",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
elif selected_plot == "box":
|
|
219
|
+
if selected_group == "None":
|
|
220
|
+
long_df = plot_df[y_columns].melt(
|
|
221
|
+
var_name="variable",
|
|
222
|
+
value_name="value",
|
|
223
|
+
)
|
|
224
|
+
fig = px.box(
|
|
225
|
+
long_df,
|
|
226
|
+
x="variable",
|
|
227
|
+
y="value",
|
|
228
|
+
points="outliers",
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
group_column = column_map[selected_group]
|
|
232
|
+
long_df = plot_df[
|
|
233
|
+
[group_column, *y_columns]
|
|
234
|
+
].melt(
|
|
235
|
+
id_vars=[group_column],
|
|
236
|
+
var_name="variable",
|
|
237
|
+
value_name="value",
|
|
238
|
+
)
|
|
239
|
+
fig = px.box(
|
|
240
|
+
long_df,
|
|
241
|
+
x="variable",
|
|
242
|
+
y="value",
|
|
243
|
+
color=group_column,
|
|
244
|
+
points="outliers",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
elif selected_plot == "bar":
|
|
248
|
+
x_column = column_map[selected_x]
|
|
249
|
+
if selected_group == "None":
|
|
250
|
+
fig = px.bar(
|
|
251
|
+
plot_df,
|
|
252
|
+
x=x_column,
|
|
253
|
+
y=y_columns,
|
|
254
|
+
barmode="group",
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
group_column = column_map[selected_group]
|
|
258
|
+
long_df = plot_df.melt(
|
|
259
|
+
id_vars=[x_column, group_column],
|
|
260
|
+
value_vars=y_columns,
|
|
261
|
+
var_name="variable",
|
|
262
|
+
value_name="value",
|
|
263
|
+
)
|
|
264
|
+
fig = px.bar(
|
|
265
|
+
long_df,
|
|
266
|
+
x=x_column,
|
|
267
|
+
y="value",
|
|
268
|
+
color=group_column,
|
|
269
|
+
facet_row="variable" if len(y_columns) > 1 else None,
|
|
270
|
+
barmode="group",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
else: # line
|
|
274
|
+
x_column = column_map[selected_x]
|
|
275
|
+
if selected_group == "None":
|
|
276
|
+
fig = px.line(
|
|
277
|
+
plot_df,
|
|
278
|
+
x=x_column,
|
|
279
|
+
y=y_columns,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
group_column = column_map[selected_group]
|
|
283
|
+
long_df = plot_df.melt(
|
|
284
|
+
id_vars=[x_column, group_column],
|
|
285
|
+
value_vars=y_columns,
|
|
286
|
+
var_name="variable",
|
|
287
|
+
value_name="value",
|
|
288
|
+
)
|
|
289
|
+
fig = px.line(
|
|
290
|
+
long_df,
|
|
291
|
+
x=x_column,
|
|
292
|
+
y="value",
|
|
293
|
+
color=group_column,
|
|
294
|
+
facet_row="variable" if len(y_columns) > 1 else None,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
fig.update_layout(
|
|
298
|
+
title=input.title(),
|
|
299
|
+
hovermode="x unified"
|
|
300
|
+
if selected_plot == "line"
|
|
301
|
+
else "closest",
|
|
302
|
+
margin=dict(l=60, r=30, t=70, b=60),
|
|
303
|
+
)
|
|
304
|
+
return fig
|
|
305
|
+
|
|
306
|
+
return App(app_ui, server)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def run_summary_app(
|
|
310
|
+
data: pd.DataFrame,
|
|
311
|
+
*,
|
|
312
|
+
x: str | None = None,
|
|
313
|
+
y: str | Sequence[str] | None = None,
|
|
314
|
+
group_by: str | None = None,
|
|
315
|
+
plot_type: str = "line",
|
|
316
|
+
title: str = "Summary plot",
|
|
317
|
+
host: str = "127.0.0.1",
|
|
318
|
+
port: int = 0,
|
|
319
|
+
launch_browser: bool = True,
|
|
320
|
+
daemon: bool = True,
|
|
321
|
+
):
|
|
322
|
+
"""
|
|
323
|
+
Create and start the Shiny app in a background thread.
|
|
324
|
+
|
|
325
|
+
``port=0`` asks the operating system to select an available port.
|
|
326
|
+
"""
|
|
327
|
+
from shiny import run_app
|
|
328
|
+
|
|
329
|
+
app = create_summary_app(
|
|
330
|
+
data,
|
|
331
|
+
x=x,
|
|
332
|
+
y=y,
|
|
333
|
+
group_by=group_by,
|
|
334
|
+
plot_type=plot_type,
|
|
335
|
+
title=title,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _run() -> None:
|
|
339
|
+
run_app(
|
|
340
|
+
app,
|
|
341
|
+
host=host,
|
|
342
|
+
port=port,
|
|
343
|
+
launch_browser=launch_browser,
|
|
344
|
+
dev_mode=False,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
thread = threading.Thread(
|
|
348
|
+
target=_run,
|
|
349
|
+
name="apmtools-shiny-summary",
|
|
350
|
+
daemon=daemon,
|
|
351
|
+
)
|
|
352
|
+
thread.start()
|
|
353
|
+
return thread
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|