apmtools 0.2.30__tar.gz → 0.2.31__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.30 → apmtools-0.2.31}/PKG-INFO +1 -1
- {apmtools-0.2.30 → apmtools-0.2.31}/pyproject.toml +1 -1
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/shiny/summary.py +387 -56
- {apmtools-0.2.30 → apmtools-0.2.31}/.gitignore +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/LICENSE +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/README.md +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.30 → apmtools-0.2.31}/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.31
|
|
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>
|
|
@@ -71,22 +71,69 @@ def _parse_label_map(value):
|
|
|
71
71
|
return result
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _numeric_axis_controls(kind, box_orientation="vertical"):
|
|
75
|
+
"""Return axes on which numeric/date tick-density controls apply."""
|
|
76
|
+
if kind in ("line", "scatter", "histogram"):
|
|
77
|
+
return True, True
|
|
78
|
+
if kind == "box" and box_orientation == "horizontal":
|
|
79
|
+
return True, False
|
|
80
|
+
return False, kind in ("box", "bar")
|
|
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
|
+
|
|
74
106
|
def _apply_plot_appearance(fig, *, title, x_label="", y_label="",
|
|
75
107
|
legend_labels="", trace_colors="", palette="Default",
|
|
76
108
|
x_min="", x_max="", y_min="", y_max="",
|
|
77
109
|
template="plotly", legend_position="right", legend_inside=False,
|
|
78
110
|
show_grid=True, font_size=12, line_width=2,
|
|
79
|
-
marker_size=7
|
|
111
|
+
marker_size=7, line_alpha="", line_style="", scatter_size="", scatter_marker="", scatter_alpha="",
|
|
112
|
+
hist_alpha="", box_showfliers=True, box_orientation="vertical",
|
|
113
|
+
title_font_size=14, axis_font_size=12, tick_font_size=10,
|
|
114
|
+
legend_font_size=12, panel_font_size=14, plot_kind=None,
|
|
115
|
+
x_major_ticks=6, y_major_ticks=6, x_minor_ticks=False, y_minor_ticks=False):
|
|
80
116
|
"""Apply per-plot appearance overrides without changing the source data."""
|
|
81
117
|
from plotly.colors import qualitative
|
|
82
118
|
|
|
83
|
-
fig.update_layout(title=title, template=template, font=dict(size=font_size)
|
|
119
|
+
fig.update_layout(title=title, template=template, font=dict(size=font_size),
|
|
120
|
+
title_font=dict(size=title_font_size),
|
|
121
|
+
legend=dict(font=dict(size=legend_font_size), title_font=dict(size=legend_font_size)))
|
|
122
|
+
fig.update_xaxes(title_font=dict(size=axis_font_size), tickfont=dict(size=tick_font_size))
|
|
123
|
+
fig.update_yaxes(title_font=dict(size=axis_font_size), tickfont=dict(size=tick_font_size))
|
|
84
124
|
if x_label.strip():
|
|
85
125
|
fig.update_xaxes(title_text=x_label.strip())
|
|
86
126
|
if y_label.strip():
|
|
87
127
|
fig.update_yaxes(title_text=y_label.strip())
|
|
88
128
|
fig.update_xaxes(showgrid=show_grid)
|
|
89
129
|
fig.update_yaxes(showgrid=show_grid)
|
|
130
|
+
use_x, use_y = _numeric_axis_controls(plot_kind, box_orientation)
|
|
131
|
+
for active, axis_name, count, minor in ((use_x, "x", x_major_ticks, x_minor_ticks),
|
|
132
|
+
(use_y, "y", y_major_ticks, y_minor_ticks)):
|
|
133
|
+
if not active:
|
|
134
|
+
continue
|
|
135
|
+
update = getattr(fig, f"update_{axis_name}axes")
|
|
136
|
+
update(nticks=max(2, min(30, int(count or 6))), minor=dict(ticks="inside" if minor else "", showgrid=bool(show_grid and minor)))
|
|
90
137
|
for axis, lower, upper in (("x", x_min, x_max), ("y", y_min, y_max)):
|
|
91
138
|
datetime_axis = axis == "x" and any(
|
|
92
139
|
getattr(trace, "type", "") == "scatter" and
|
|
@@ -112,9 +159,12 @@ def _apply_plot_appearance(fig, *, title, x_label="", y_label="",
|
|
|
112
159
|
"bottom": dict(x=0.5, y=0.01, xanchor="center", yanchor="bottom", orientation="h"),
|
|
113
160
|
}.get(legend_position, {})
|
|
114
161
|
legend_settings = {**legend_settings, "bgcolor": "rgba(255,255,255,0.7)"}
|
|
115
|
-
fig.update_layout(legend={**legend_settings, "title": {"text": "Legend"}})
|
|
162
|
+
fig.update_layout(legend={**legend_settings, "title": {"text": "Legend", "font": {"size": legend_font_size}}, "font": {"size": legend_font_size}})
|
|
116
163
|
labels = _parse_label_map(legend_labels)
|
|
117
164
|
colors = _parse_label_map(trace_colors)
|
|
165
|
+
alphas, styles, sizes, markers, marker_alphas = _series_style_maps(
|
|
166
|
+
line_alpha, line_style, scatter_size, scatter_marker, scatter_alpha)
|
|
167
|
+
histogram_alphas = _parse_label_map(hist_alpha)
|
|
118
168
|
palette_values = getattr(qualitative, palette, None) if palette != "Default" else None
|
|
119
169
|
# Color keys can refer to either original or renamed trace labels.
|
|
120
170
|
for i, trace in enumerate(fig.data):
|
|
@@ -134,13 +184,57 @@ def _apply_plot_appearance(fig, *, title, x_label="", y_label="",
|
|
|
134
184
|
trace.marker.color = color
|
|
135
185
|
if getattr(trace, "fillcolor", None) is not None and trace.type == "histogram":
|
|
136
186
|
trace.marker.color = color
|
|
137
|
-
if
|
|
187
|
+
if plot_kind == "histogram":
|
|
188
|
+
trace.opacity = _bounded_float(histogram_alphas, original, 0.65, 0, 1)
|
|
189
|
+
if plot_kind == "box":
|
|
190
|
+
trace.boxpoints = "outliers" if box_showfliers else False
|
|
191
|
+
if box_orientation == "horizontal":
|
|
192
|
+
old_x, old_y = trace.x, trace.y
|
|
193
|
+
trace.x, trace.y = old_y, old_x
|
|
194
|
+
trace.orientation = "h"
|
|
195
|
+
if getattr(trace, "line", None) is not None and plot_kind == "line":
|
|
138
196
|
trace.line.width = line_width
|
|
197
|
+
if plot_kind == "line":
|
|
198
|
+
trace.line.dash = {"-": "solid", ":": "dot", "--": "dash"}[_line_style(styles, original)]
|
|
199
|
+
trace.opacity = _bounded_float(alphas, original, 1, 0, 1)
|
|
139
200
|
if getattr(trace, "marker", None) is not None and trace.type in ("scatter", "scattergl"):
|
|
140
|
-
|
|
201
|
+
if plot_kind == "scatter":
|
|
202
|
+
trace.marker.size = _bounded_float(sizes, original, 7, 0.1, 100)
|
|
203
|
+
trace.marker.symbol = {"o": "circle", "x": "x"}[_scatter_marker(markers, original)]
|
|
204
|
+
trace.opacity = _bounded_float(marker_alphas, original, 1, 0, 1)
|
|
205
|
+
else:
|
|
206
|
+
trace.marker.size = marker_size
|
|
207
|
+
if plot_kind == "box" and box_orientation == "horizontal":
|
|
208
|
+
x_title = fig.layout.xaxis.title.text
|
|
209
|
+
y_title = fig.layout.yaxis.title.text
|
|
210
|
+
fig.update_xaxes(title_text=y_title, type="linear", autorange=True)
|
|
211
|
+
fig.update_yaxes(title_text=x_title, type="category", autorange=True)
|
|
141
212
|
return fig
|
|
142
213
|
|
|
143
214
|
|
|
215
|
+
def _series_style_maps(line_alpha="", line_style="", scatter_size="", scatter_marker="", scatter_alpha=""):
|
|
216
|
+
"""Parse per-trace styles, keyed by the original legend/series name."""
|
|
217
|
+
return tuple(_parse_label_map(v) for v in (line_alpha, line_style, scatter_size, scatter_marker, scatter_alpha))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _bounded_float(mapping, name, default, low, high):
|
|
221
|
+
try:
|
|
222
|
+
value = float(mapping.get(name, default))
|
|
223
|
+
return value if np.isfinite(value) and low <= value <= high else float(default)
|
|
224
|
+
except (ValueError, TypeError):
|
|
225
|
+
return float(default)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _line_style(mapping, name):
|
|
229
|
+
return {"solid": "-", "dotted": ":", "dashed": "--", "-": "-", ":": ":", "--": "--"}.get(
|
|
230
|
+
mapping.get(name, "solid").strip().lower(), "-")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _scatter_marker(mapping, name):
|
|
234
|
+
return {"dot": "o", "cross": "x", "o": "o", "x": "x"}.get(
|
|
235
|
+
mapping.get(name, "dot").strip().lower(), "o")
|
|
236
|
+
|
|
237
|
+
|
|
144
238
|
PLOT_TYPES = {
|
|
145
239
|
"bar": "Bar",
|
|
146
240
|
"box": "Boxplot",
|
|
@@ -533,6 +627,10 @@ def _boxplot_figure(data, x_column, y_column, *, group_column=None):
|
|
|
533
627
|
box_width = 0.82 / n_traces
|
|
534
628
|
for i, trace in enumerate(fig.data):
|
|
535
629
|
trace.width = box_width
|
|
630
|
+
# A single ungrouped box trace does not convey useful legend information.
|
|
631
|
+
if group_column is None and len(fig.data) == 1:
|
|
632
|
+
fig.update_traces(showlegend=False)
|
|
633
|
+
fig.update_layout(showlegend=False)
|
|
536
634
|
return fig
|
|
537
635
|
|
|
538
636
|
|
|
@@ -1012,9 +1110,14 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1012
1110
|
x_min="", x_max="", y_min="", y_max="",
|
|
1013
1111
|
template="plotly", legend_position="right",
|
|
1014
1112
|
legend_inside=False, show_grid=True, font_size=12,
|
|
1015
|
-
line_width=2, marker_size=7,
|
|
1113
|
+
line_width=2, marker_size=7, line_alpha="", line_style="",
|
|
1114
|
+
scatter_size="", scatter_marker="", scatter_alpha="", hist_alpha="",
|
|
1115
|
+
box_showfliers=True, box_orientation="vertical",
|
|
1116
|
+
title_font_size=14, axis_font_size=12, tick_font_size=10,
|
|
1117
|
+
legend_font_size=12, panel_font_size=14, bins=30, density=False,
|
|
1016
1118
|
stacked=False, histtype="bar", cumulative=0,
|
|
1017
|
-
panel_by=None, independent_panel_limits=True, panel_titles="", _axes=None
|
|
1119
|
+
panel_by=None, independent_panel_limits=True, panel_titles="", _axes=None,
|
|
1120
|
+
x_major_ticks=6, y_major_ticks=6, x_minor_ticks=False, y_minor_ticks=False):
|
|
1018
1121
|
"""Draw directly from the filtered/aggregated DataFrame, without Plotly."""
|
|
1019
1122
|
import matplotlib
|
|
1020
1123
|
matplotlib.use("Agg")
|
|
@@ -1051,7 +1154,8 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1051
1154
|
fig, axes = plt.subplots(nrows, ncols, figsize=(5*ncols, 3.8*nrows), dpi=100,
|
|
1052
1155
|
squeeze=False, sharex=False, sharey=False)
|
|
1053
1156
|
for idx, (panel_title, panel_df, other_groups) in enumerate(partitions):
|
|
1054
|
-
panel_spec = {**spec, "data": panel_df, "groups": other_groups
|
|
1157
|
+
panel_spec = {**spec, "data": panel_df, "groups": other_groups,
|
|
1158
|
+
"_hide_ungrouped_legend": not bool(other_groups)}
|
|
1055
1159
|
panel_ax = axes.flat[idx]
|
|
1056
1160
|
_matplotlib_direct_figure(panel_spec, title=panel_title_map.get(panel_title, panel_title), x_label=x_label,
|
|
1057
1161
|
y_label=y_label, legend_labels=legend_labels, trace_colors=trace_colors,
|
|
@@ -1060,8 +1164,16 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1060
1164
|
y_min=_panel_limit_value(y_min, panel_title), y_max=_panel_limit_value(y_max, panel_title),
|
|
1061
1165
|
template=template, legend_position=legend_position,
|
|
1062
1166
|
legend_inside=legend_inside, show_grid=show_grid, font_size=font_size,
|
|
1063
|
-
line_width=line_width, marker_size=marker_size,
|
|
1064
|
-
|
|
1167
|
+
line_width=line_width, marker_size=marker_size, line_alpha=line_alpha, line_style=line_style,
|
|
1168
|
+
scatter_size=scatter_size, scatter_marker=scatter_marker, scatter_alpha=scatter_alpha,
|
|
1169
|
+
hist_alpha=hist_alpha, box_showfliers=box_showfliers, box_orientation=box_orientation,
|
|
1170
|
+
title_font_size=panel_font_size, axis_font_size=axis_font_size,
|
|
1171
|
+
tick_font_size=tick_font_size, legend_font_size=legend_font_size,
|
|
1172
|
+
panel_font_size=panel_font_size,
|
|
1173
|
+
bins=bins, density=density,
|
|
1174
|
+
stacked=stacked, histtype=histtype, cumulative=cumulative, _axes=panel_ax,
|
|
1175
|
+
x_major_ticks=x_major_ticks, y_major_ticks=y_major_ticks,
|
|
1176
|
+
x_minor_ticks=x_minor_ticks, y_minor_ticks=y_minor_ticks)
|
|
1065
1177
|
for extra in list(axes.flat)[len(partitions):]:
|
|
1066
1178
|
extra.set_visible(False)
|
|
1067
1179
|
fig.tight_layout()
|
|
@@ -1070,6 +1182,9 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1070
1182
|
defaults = _matplotlib_defaults(spec, palette=palette)
|
|
1071
1183
|
rename = _parse_label_map(legend_labels)
|
|
1072
1184
|
overrides = _parse_label_map(trace_colors)
|
|
1185
|
+
alphas, styles, sizes, markers, marker_alphas = _series_style_maps(
|
|
1186
|
+
line_alpha, line_style, scatter_size, scatter_marker, scatter_alpha)
|
|
1187
|
+
histogram_alphas = _parse_label_map(hist_alpha)
|
|
1073
1188
|
palette_values = _MPL_PALETTES.get(palette, _MPL_PALETTES["Default"])
|
|
1074
1189
|
labels = defaults["names"] if group_col else ([str(y) for y in ys] if len(ys) > 1 else ["All observations"])
|
|
1075
1190
|
colors = {name: overrides.get(name, defaults["colors"].get(name, palette_values[i % len(palette_values)]))
|
|
@@ -1093,20 +1208,34 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1093
1208
|
all_vals = np.concatenate(arrays) if arrays else np.array([])
|
|
1094
1209
|
if len(all_vals):
|
|
1095
1210
|
edges = np.histogram_bin_edges(all_vals, bins=max(1, int(bins)))
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1211
|
+
_, _, hist_patches = ax.hist(
|
|
1212
|
+
arrays, bins=edges, density=density,
|
|
1213
|
+
stacked=stacked or histtype == "barstacked",
|
|
1214
|
+
histtype=("bar" if histtype == "barstacked" else histtype),
|
|
1215
|
+
cumulative=cumulative if cumulative else False,
|
|
1216
|
+
color=[colors[n] for n in labels],
|
|
1217
|
+
label=[rename.get(n, n) for n in labels])
|
|
1218
|
+
# Matplotlib returns one patch container per group for bar and
|
|
1219
|
+
# one polygon per group for step/stepfilled.
|
|
1220
|
+
for name, group_patches in zip(labels, hist_patches):
|
|
1221
|
+
alpha = _bounded_float(histogram_alphas, name,
|
|
1222
|
+
1 if histtype == "step" else .65, 0, 1)
|
|
1223
|
+
if hasattr(group_patches, "set_alpha"):
|
|
1224
|
+
group_patches.set_alpha(alpha)
|
|
1225
|
+
else:
|
|
1226
|
+
for patch in group_patches:
|
|
1227
|
+
patch.set_alpha(alpha)
|
|
1102
1228
|
defaults["y_label"] = ("Cumulative probability" if cumulative and density else
|
|
1103
1229
|
"Cumulative count" if cumulative else
|
|
1104
1230
|
"Probability density" if density else "Count")
|
|
1105
1231
|
elif kind == "scatter":
|
|
1106
1232
|
for name, rows in series():
|
|
1107
1233
|
color = colors.get(name, palette_values[0])
|
|
1108
|
-
ax.scatter(rows[x], rows[ys[0]],
|
|
1109
|
-
|
|
1234
|
+
ax.scatter(rows[x], rows[ys[0]],
|
|
1235
|
+
s=_bounded_float(sizes, name, marker_size, 0.1, 100)**2,
|
|
1236
|
+
marker=_scatter_marker(markers, name),
|
|
1237
|
+
alpha=_bounded_float(marker_alphas, name, 1, 0, 1),
|
|
1238
|
+
color=color, label=rename.get(name, name))
|
|
1110
1239
|
elif kind == "line":
|
|
1111
1240
|
line_df = df
|
|
1112
1241
|
id_col = "identifier" if "identifier" in df.columns else None
|
|
@@ -1125,7 +1254,10 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1125
1254
|
for _, part in parts:
|
|
1126
1255
|
part = part.sort_values(x)
|
|
1127
1256
|
ax.plot(part[x], part[y], color=colors.get(name, palette_values[0]),
|
|
1128
|
-
linewidth=float(line_width),
|
|
1257
|
+
linewidth=float(line_width),
|
|
1258
|
+
linestyle=_line_style(styles, name),
|
|
1259
|
+
alpha=_bounded_float(alphas, name, 1, 0, 1),
|
|
1260
|
+
label=rename.get(name, name))
|
|
1129
1261
|
elif kind in ("box", "bar"):
|
|
1130
1262
|
# Build categorical groups in a single pass. The former implementation
|
|
1131
1263
|
# repeatedly scanned the full DataFrame for every category × group,
|
|
@@ -1189,13 +1321,25 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1189
1321
|
box_positions = sorted(buckets)
|
|
1190
1322
|
bp = ax.boxplot([buckets[pos] for pos in box_positions],
|
|
1191
1323
|
positions=box_positions, widths=width * .92,
|
|
1192
|
-
manage_ticks=False, patch_artist=True
|
|
1324
|
+
manage_ticks=False, patch_artist=True,
|
|
1325
|
+
showfliers=bool(box_showfliers),
|
|
1326
|
+
vert=(box_orientation != "horizontal"))
|
|
1193
1327
|
for patch, position in zip(bp["boxes"], box_positions):
|
|
1194
1328
|
patch.set_facecolor(bucket_colors[position])
|
|
1195
1329
|
patch.set_alpha(.7)
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1330
|
+
if kind == "box" and box_orientation == "horizontal":
|
|
1331
|
+
ax.set_yticks(range(len(cats)), cats)
|
|
1332
|
+
ax.set_ylim(-.5, max(.5, len(cats) - .5))
|
|
1333
|
+
else:
|
|
1334
|
+
ax.set_xticks(range(len(cats)), cats)
|
|
1335
|
+
ax.set_xlim(-.5, max(.5, len(cats) - .5))
|
|
1336
|
+
# A boxplot with a single ungrouped series does not need a legend.
|
|
1337
|
+
# Keep legends for genuinely grouped boxplots and for bar plots.
|
|
1338
|
+
show_category_legend = not (
|
|
1339
|
+
kind == "box" and len(group_names) == 1
|
|
1340
|
+
and group_names[0] == "All observations"
|
|
1341
|
+
)
|
|
1342
|
+
if len(group_names) <= 40 and show_category_legend and not spec.get("_hide_ungrouped_legend", not bool(spec["groups"])):
|
|
1199
1343
|
ax.legend(handles=[Patch(facecolor=colors[name], label=rename.get(name, name))
|
|
1200
1344
|
for name in group_names], title="Legend")
|
|
1201
1345
|
else:
|
|
@@ -1204,7 +1348,7 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1204
1348
|
if kind not in ("box", "bar"):
|
|
1205
1349
|
handles, texts = ax.get_legend_handles_labels()
|
|
1206
1350
|
unique = dict(zip(texts, handles))
|
|
1207
|
-
if unique:
|
|
1351
|
+
if unique and not spec.get("_hide_ungrouped_legend", not bool(spec["groups"])):
|
|
1208
1352
|
ax.legend(unique.values(), unique.keys(), title="Legend")
|
|
1209
1353
|
legend = ax.get_legend()
|
|
1210
1354
|
if legend is not None:
|
|
@@ -1227,11 +1371,23 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
1227
1371
|
ax.set_facecolor("#EBEBEB")
|
|
1228
1372
|
elif template in ("plotly_white", "simple_white"):
|
|
1229
1373
|
ax.set_facecolor("white")
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1374
|
+
xlabel = x_label.strip() or defaults["x_label"]
|
|
1375
|
+
ylabel = y_label.strip() or defaults["y_label"]
|
|
1376
|
+
if kind == "box" and box_orientation == "horizontal":
|
|
1377
|
+
xlabel, ylabel = ylabel, xlabel
|
|
1378
|
+
ax.set_xlabel(xlabel, fontsize=axis_font_size)
|
|
1379
|
+
ax.set_ylabel(ylabel, fontsize=axis_font_size)
|
|
1380
|
+
ax.set_title(title, fontsize=title_font_size)
|
|
1381
|
+
ax.tick_params(labelsize=tick_font_size)
|
|
1382
|
+
if legend is not None:
|
|
1383
|
+
for text in legend.get_texts():
|
|
1384
|
+
text.set_fontsize(legend_font_size)
|
|
1385
|
+
legend.get_title().set_fontsize(legend_font_size)
|
|
1234
1386
|
ax.grid(bool(show_grid))
|
|
1387
|
+
_matplotlib_tick_controls(ax, kind=kind, box_orientation=box_orientation,
|
|
1388
|
+
show_grid=show_grid, x_major_ticks=x_major_ticks,
|
|
1389
|
+
y_major_ticks=y_major_ticks, x_minor_ticks=x_minor_ticks,
|
|
1390
|
+
y_minor_ticks=y_minor_ticks)
|
|
1235
1391
|
for lo, hi, getter, setter in ((x_min, x_max, ax.get_xlim, ax.set_xlim),
|
|
1236
1392
|
(y_min, y_max, ax.get_ylim, ax.set_ylim)):
|
|
1237
1393
|
try:
|
|
@@ -1381,6 +1537,51 @@ def _matplotlib_figure(plotly_figure):
|
|
|
1381
1537
|
fig.tight_layout()
|
|
1382
1538
|
return fig
|
|
1383
1539
|
|
|
1540
|
+
def _standalone_matplotlib_script(spec, options):
|
|
1541
|
+
"""Create a self-contained plotting script using this renderer's source.
|
|
1542
|
+
|
|
1543
|
+
The CSV holds the *prepared* data, after subsetting and aggregation.
|
|
1544
|
+
Only pandas, numpy and matplotlib are required to rerun the plot.
|
|
1545
|
+
"""
|
|
1546
|
+
import inspect
|
|
1547
|
+
import pprint
|
|
1548
|
+
|
|
1549
|
+
dependencies = (
|
|
1550
|
+
_axis_label, _parse_label_map, _series_style_maps, _bounded_float,
|
|
1551
|
+
_line_style, _scatter_marker, _composite_labels,
|
|
1552
|
+
_frequency_timedelta, _add_gap_sections, _matplotlib_defaults,
|
|
1553
|
+
_panel_columns, _panel_partitions, _panel_limit_value,
|
|
1554
|
+
_parse_axis_limit, _numeric_axis_controls, _matplotlib_tick_controls,
|
|
1555
|
+
_matplotlib_direct_figure,
|
|
1556
|
+
)
|
|
1557
|
+
source = "\n\n".join(inspect.getsource(fn) for fn in dependencies)
|
|
1558
|
+
metadata = {key: spec.get(key) for key in
|
|
1559
|
+
("kind", "x", "ys", "groups", "categories", "frequency")}
|
|
1560
|
+
date_columns = [str(c) for c in spec["data"].columns
|
|
1561
|
+
if pd.api.types.is_datetime64_any_dtype(spec["data"][c])]
|
|
1562
|
+
return (
|
|
1563
|
+
'# Generated by apmtools Summary.plot_app (Matplotlib backend).\n'
|
|
1564
|
+
'# Run: python plot_script.py\n'
|
|
1565
|
+
'# Dependencies: pip install pandas numpy matplotlib\n'
|
|
1566
|
+
'from pathlib import Path\n'
|
|
1567
|
+
'import pandas as pd\nimport numpy as np\n'
|
|
1568
|
+
'from datetime import datetime, date, timedelta\n\n'
|
|
1569
|
+
'VARIABLE_UNITS = ' + pprint.pformat(VARIABLE_UNITS, sort_dicts=True) + '\n'
|
|
1570
|
+
'_MPL_PALETTES = ' + pprint.pformat(_MPL_PALETTES, sort_dicts=True) + '\n\n'
|
|
1571
|
+
+ source + '\n\n'
|
|
1572
|
+
+ 'def main():\n'
|
|
1573
|
+
+ ' csv_path = Path(__file__).resolve().parent / "plot_data.csv"\n'
|
|
1574
|
+
+ ' frame = pd.read_csv(csv_path, parse_dates=' + repr(date_columns) + ')\n'
|
|
1575
|
+
+ ' spec = ' + repr(metadata) + '\n'
|
|
1576
|
+
+ ' spec["data"] = frame\n'
|
|
1577
|
+
+ ' options = ' + repr(options) + '\n'
|
|
1578
|
+
+ ' figure = _matplotlib_direct_figure(spec, **options)\n'
|
|
1579
|
+
+ ' import matplotlib.pyplot as plt\n'
|
|
1580
|
+
+ ' plt.savefig("plot.png")\n\n'
|
|
1581
|
+
+ 'if __name__ == "__main__":\n main()\n'
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
|
|
1384
1585
|
def create_summary_app(
|
|
1385
1586
|
data: pd.DataFrame,
|
|
1386
1587
|
*,
|
|
@@ -1448,8 +1649,8 @@ def create_summary_app(
|
|
|
1448
1649
|
def plot_tab_ui(initial_filters=None):
|
|
1449
1650
|
initial_filters = initial_filters or {}
|
|
1450
1651
|
return ui.div(
|
|
1451
|
-
ui.
|
|
1452
|
-
ui.
|
|
1652
|
+
ui.layout_sidebar(
|
|
1653
|
+
ui.sidebar(
|
|
1453
1654
|
ui.card(ui.card_header("Plot filters"),
|
|
1454
1655
|
ui.input_selectize("subset_columns", "Subset columns",
|
|
1455
1656
|
choices={c: c for c in subset_columns},
|
|
@@ -1485,8 +1686,8 @@ def create_summary_app(
|
|
|
1485
1686
|
ui.input_select("hist_cumulative", "Cumulative", choices={"0":"False","1":"True (ascending)","-1":"True (descending)"}, selected="0"),
|
|
1486
1687
|
),
|
|
1487
1688
|
),
|
|
1689
|
+
title="Plot filters / settings / aggregation", position="left", open="open", width="320px",
|
|
1488
1690
|
),
|
|
1489
|
-
ui.div(
|
|
1490
1691
|
ui.layout_sidebar(
|
|
1491
1692
|
ui.sidebar(
|
|
1492
1693
|
ui.input_text("title", "Plot title", value=""),
|
|
@@ -1512,18 +1713,42 @@ def create_summary_app(
|
|
|
1512
1713
|
ui.input_select("legend_position", "Legend position", choices={k:k.title() for k in ("right", "left", "top", "bottom")}, selected="right"),
|
|
1513
1714
|
ui.input_checkbox("legend_inside", "Place legend inside plot", value=False),
|
|
1514
1715
|
ui.input_checkbox("show_grid", "Show grid", value=True),
|
|
1515
|
-
ui.
|
|
1516
|
-
|
|
1517
|
-
|
|
1716
|
+
ui.panel_conditional("input.plot_type === 'line' || input.plot_type === 'scatter' || input.plot_type === 'histogram' || (input.plot_type === 'box' && input.box_orientation === 'horizontal')",
|
|
1717
|
+
ui.input_numeric("x_major_ticks", "X-axis major ticks (approximate)", value=6, min=2, max=30, step=1),
|
|
1718
|
+
ui.input_checkbox("x_minor_ticks", "X-axis minor ticks / gridlines", value=False)),
|
|
1719
|
+
ui.panel_conditional("input.plot_type === 'line' || input.plot_type === 'scatter' || input.plot_type === 'histogram' || input.plot_type === 'bar' || (input.plot_type === 'box' && input.box_orientation !== 'horizontal')",
|
|
1720
|
+
ui.input_numeric("y_major_ticks", "Y-axis major ticks (approximate)", value=6, min=2, max=30, step=1),
|
|
1721
|
+
ui.input_checkbox("y_minor_ticks", "Y-axis minor ticks / gridlines", value=False)),
|
|
1722
|
+
ui.input_numeric("title_font_size", "Plot title font size", value=14, min=6, max=72),
|
|
1723
|
+
ui.input_numeric("axis_font_size", "Axis title font size", value=12, min=6, max=72),
|
|
1724
|
+
ui.input_numeric("tick_font_size", "Axis tick font size", value=10, min=6, max=72),
|
|
1725
|
+
ui.input_numeric("legend_font_size", "Legend font size", value=12, min=6, max=72),
|
|
1726
|
+
ui.panel_conditional("input.plot_type === 'line'",
|
|
1727
|
+
ui.input_numeric("line_width", "Line width", value=2, min=0.5, max=15, step=0.5)),
|
|
1728
|
+
(ui.input_numeric("panel_font_size", "Panel title font size", value=14, min=6, max=72) if backend == "matplotlib" else ui.div()),
|
|
1729
|
+
ui.panel_conditional("input.plot_type === 'histogram'",
|
|
1730
|
+
ui.input_text_area("hist_alpha", "Histogram alpha (name=0..1; ...)", value="", rows=4)),
|
|
1731
|
+
ui.panel_conditional("input.plot_type === 'box'",
|
|
1732
|
+
ui.input_checkbox("box_showfliers", "Show outliers (fliers)", value=True),
|
|
1733
|
+
ui.input_select("box_orientation", "Box orientation", choices={"vertical":"Vertical", "horizontal":"Horizontal"}, selected="vertical")),
|
|
1734
|
+
ui.panel_conditional("input.plot_type === 'line'",
|
|
1735
|
+
ui.input_text_area("line_alpha", "Line alpha (name=0..1; ...)", value="", rows=4),
|
|
1736
|
+
ui.input_text_area("line_style", "Line style (name=solid/dotted/dashed; ...)", value="", rows=4)),
|
|
1737
|
+
ui.panel_conditional("input.plot_type === 'scatter'",
|
|
1738
|
+
ui.input_text_area("scatter_size", "Marker size (name=size; ...)", value="", rows=4),
|
|
1739
|
+
ui.input_text_area("scatter_marker", "Marker type (name=dot/cross; ...)", value="", rows=4),
|
|
1740
|
+
ui.input_text_area("scatter_alpha", "Marker alpha (name=0..1; ...)", value="", rows=4)),
|
|
1518
1741
|
title="Plot appearance", position="right", open="closed", width="320px",
|
|
1519
1742
|
),
|
|
1520
1743
|
ui.card(ui.card_header("Plot"),
|
|
1521
1744
|
ui.div((output_widget("plot", height="100%", width="100%") if backend == "plotly" else ui.output_plot("plot", height="100%", width="100%")),
|
|
1522
1745
|
class_="summary-plot-container"),
|
|
1523
|
-
ui.input_action_button("export_csv", "Export plotted data as CSV", class_="btn-primary mt-3")
|
|
1746
|
+
ui.input_action_button("export_csv", "Export plotted data as CSV", class_="btn-primary mt-3"),
|
|
1747
|
+
ui.input_action_button("export_plot", "Export plot", class_="btn-secondary mt-3"),
|
|
1748
|
+
(ui.download_button("export_python", "Export as a Python file and associated data",
|
|
1749
|
+
class_="btn-secondary mt-3")
|
|
1750
|
+
if backend == "matplotlib" else ui.div())),
|
|
1524
1751
|
),
|
|
1525
|
-
),
|
|
1526
|
-
col_widths=(3, 9),
|
|
1527
1752
|
),
|
|
1528
1753
|
)
|
|
1529
1754
|
|
|
@@ -1577,6 +1802,50 @@ def create_summary_app(
|
|
|
1577
1802
|
footer=ui.modal_button("Cancel"),
|
|
1578
1803
|
))
|
|
1579
1804
|
|
|
1805
|
+
@reactive.effect
|
|
1806
|
+
@reactive.event(input.export_plot)
|
|
1807
|
+
def show_plot_export_dialog():
|
|
1808
|
+
ui.modal_show(ui.modal(
|
|
1809
|
+
ui.input_numeric("plot_export_width", "Width (inches)", value=10, min=1, max=100, step=0.5),
|
|
1810
|
+
ui.input_numeric("plot_export_height", "Height (inches)", value=5, min=1, max=100, step=0.5),
|
|
1811
|
+
ui.input_numeric("plot_export_dpi", "Resolution (DPI)", value=150, min=30, max=1200, step=25),
|
|
1812
|
+
ui.download_button("download_plot_png", "Download plot.png", class_="btn-primary"),
|
|
1813
|
+
title="Export plot", easy_close=True,
|
|
1814
|
+
footer=ui.modal_button("Cancel"),
|
|
1815
|
+
))
|
|
1816
|
+
|
|
1817
|
+
@render.download(filename="plot.png", media_type="image/png")
|
|
1818
|
+
def download_plot_png():
|
|
1819
|
+
import io
|
|
1820
|
+
width = float(input.plot_export_width())
|
|
1821
|
+
height = float(input.plot_export_height())
|
|
1822
|
+
dpi = int(input.plot_export_dpi())
|
|
1823
|
+
if not (1 <= width <= 100 and 1 <= height <= 100 and 30 <= dpi <= 1200):
|
|
1824
|
+
raise ValueError("Invalid plot export dimensions or resolution")
|
|
1825
|
+
if width * dpi * height * dpi > 100_000_000:
|
|
1826
|
+
raise ValueError("Export exceeds 100 megapixels; reduce size or DPI")
|
|
1827
|
+
if backend == "matplotlib":
|
|
1828
|
+
import matplotlib.pyplot as plt
|
|
1829
|
+
fig = _matplotlib_direct_figure(prepared_plot(), **matplotlib_options())
|
|
1830
|
+
try:
|
|
1831
|
+
fig.set_size_inches(width, height, forward=True)
|
|
1832
|
+
buffer = io.BytesIO()
|
|
1833
|
+
fig.savefig(buffer, format="png", dpi=dpi)
|
|
1834
|
+
yield buffer.getvalue()
|
|
1835
|
+
finally:
|
|
1836
|
+
plt.close(fig)
|
|
1837
|
+
else:
|
|
1838
|
+
# Plotly static image export requires kaleido.
|
|
1839
|
+
# Use the same styled figure as the interactive plot.
|
|
1840
|
+
fig = styled_plotly_figure()
|
|
1841
|
+
try:
|
|
1842
|
+
yield fig.to_image(format="png", width=round(width * dpi),
|
|
1843
|
+
height=round(height * dpi), scale=1)
|
|
1844
|
+
except Exception as exc:
|
|
1845
|
+
raise RuntimeError(
|
|
1846
|
+
'Plotly PNG export requires kaleido: pip install kaleido'
|
|
1847
|
+
) from exc
|
|
1848
|
+
|
|
1580
1849
|
@render.download(filename=lambda: _safe_csv_filename(input.export_filename()))
|
|
1581
1850
|
def download_csv():
|
|
1582
1851
|
# Use the same post-filter/post-aggregation dataframe as the plot.
|
|
@@ -1659,7 +1928,7 @@ def create_summary_app(
|
|
|
1659
1928
|
"input.multi_panel === true",
|
|
1660
1929
|
ui.input_selectize("panel_by", "Panel grouping variables",
|
|
1661
1930
|
choices={name: name for name in selected},
|
|
1662
|
-
selected=
|
|
1931
|
+
selected=selected, multiple=True)))
|
|
1663
1932
|
return ui.div(*controls)
|
|
1664
1933
|
|
|
1665
1934
|
@render.ui
|
|
@@ -1810,6 +2079,12 @@ def create_summary_app(
|
|
|
1810
2079
|
else:
|
|
1811
2080
|
fig = _lineplot_figure(plot_df, x_column, y_columns, group_columns=group_columns, frequency=frequency, column_map=column_map)
|
|
1812
2081
|
|
|
2082
|
+
if not group_columns:
|
|
2083
|
+
# Retain trace names/colours for appearance editing, but
|
|
2084
|
+
# suppress the redundant ungrouped legend.
|
|
2085
|
+
fig.update_traces(showlegend=False)
|
|
2086
|
+
fig.update_layout(showlegend=False)
|
|
2087
|
+
|
|
1813
2088
|
_label_plot_axes(
|
|
1814
2089
|
fig, selected_plot, x_column, y_columns,
|
|
1815
2090
|
category_columns=x_columns if selected_plot in ("box", "bar") else None,
|
|
@@ -1838,7 +2113,7 @@ def create_summary_app(
|
|
|
1838
2113
|
if (backend == "matplotlib" and selected_groups_for_legend
|
|
1839
2114
|
and bool(_optional_input("multi_panel", False))):
|
|
1840
2115
|
panel_variables_for_legend = (
|
|
1841
|
-
list(_optional_input("panel_by", []) or [])
|
|
2116
|
+
(list(_optional_input("panel_by", []) or []) or selected_groups_for_legend)
|
|
1842
2117
|
if len(selected_groups_for_legend) > 1
|
|
1843
2118
|
else selected_groups_for_legend
|
|
1844
2119
|
)
|
|
@@ -1862,7 +2137,7 @@ def create_summary_app(
|
|
|
1862
2137
|
selected_groups = list(input.group_by() or [])
|
|
1863
2138
|
if (backend == "matplotlib" and selected_groups and bool(_optional_input("multi_panel", False))
|
|
1864
2139
|
and bool(_optional_input("independent_panel_limits", True))):
|
|
1865
|
-
panel_variable = (list(_optional_input("panel_by", []) or []) if len(selected_groups) > 1
|
|
2140
|
+
panel_variable = ((list(_optional_input("panel_by", []) or []) or selected_groups) if len(selected_groups) > 1
|
|
1866
2141
|
else selected_groups)
|
|
1867
2142
|
panel_bounds = _panel_axis_bounds(
|
|
1868
2143
|
spec, panel_variable,
|
|
@@ -1883,8 +2158,18 @@ def create_summary_app(
|
|
|
1883
2158
|
f"{name}={name}" for name in names))
|
|
1884
2159
|
ui.update_text_area("trace_colors", value="; ".join(
|
|
1885
2160
|
f"{name}={defaults['colors'][name]}" for name in names))
|
|
2161
|
+
kind = spec["kind"]
|
|
2162
|
+
if kind == "line":
|
|
2163
|
+
ui.update_text_area("line_alpha", value="; ".join(f"{name}=1" for name in names))
|
|
2164
|
+
ui.update_text_area("line_style", value="; ".join(f"{name}=solid" for name in names))
|
|
2165
|
+
elif kind == "histogram":
|
|
2166
|
+
ui.update_text_area("hist_alpha", value="; ".join(f"{name}=0.65" for name in names))
|
|
2167
|
+
elif kind == "scatter":
|
|
2168
|
+
ui.update_text_area("scatter_size", value="; ".join(f"{name}=7" for name in names))
|
|
2169
|
+
ui.update_text_area("scatter_marker", value="; ".join(f"{name}=dot" for name in names))
|
|
2170
|
+
ui.update_text_area("scatter_alpha", value="; ".join(f"{name}=1" for name in names))
|
|
1886
2171
|
if backend == "matplotlib":
|
|
1887
|
-
panel_variables = (list(_optional_input("panel_by", []) or []) if len(selected_groups) > 1
|
|
2172
|
+
panel_variables = ((list(_optional_input("panel_by", []) or []) or selected_groups) if len(selected_groups) > 1
|
|
1888
2173
|
else selected_groups)
|
|
1889
2174
|
panel_names = ([name for name, _, _ in _panel_partitions(spec, panel_variables)]
|
|
1890
2175
|
if selected_groups and bool(_optional_input("multi_panel", False)) else [])
|
|
@@ -1918,9 +2203,16 @@ def create_summary_app(
|
|
|
1918
2203
|
ui.update_select("legend_position", selected="right")
|
|
1919
2204
|
ui.update_checkbox("legend_inside", value=False)
|
|
1920
2205
|
ui.update_checkbox("show_grid", value=True)
|
|
1921
|
-
|
|
2206
|
+
for field, value in (("title_font_size", 14), ("axis_font_size", 12),
|
|
2207
|
+
("tick_font_size", 10), ("legend_font_size", 12),
|
|
2208
|
+
("panel_font_size", 14)):
|
|
2209
|
+
ui.update_numeric(field, value=value)
|
|
1922
2210
|
ui.update_numeric("line_width", value=2)
|
|
1923
|
-
ui.
|
|
2211
|
+
ui.update_checkbox("box_showfliers", value=True)
|
|
2212
|
+
ui.update_select("box_orientation", selected="vertical")
|
|
2213
|
+
ui.update_text_area("hist_alpha", value="")
|
|
2214
|
+
for field in ("line_alpha", "line_style", "scatter_size", "scatter_marker", "scatter_alpha"):
|
|
2215
|
+
ui.update_text_area(field, value="")
|
|
1924
2216
|
ui.update_numeric("bins", value=30)
|
|
1925
2217
|
ui.update_select("hist_density", selected="False")
|
|
1926
2218
|
ui.update_select("hist_stacked", selected="False")
|
|
@@ -1930,8 +2222,8 @@ def create_summary_app(
|
|
|
1930
2222
|
ui.update_checkbox("flooring_timestamp", value=True)
|
|
1931
2223
|
|
|
1932
2224
|
if backend == "plotly":
|
|
1933
|
-
@
|
|
1934
|
-
def
|
|
2225
|
+
@reactive.calc
|
|
2226
|
+
def styled_plotly_figure():
|
|
1935
2227
|
import plotly.graph_objects as go
|
|
1936
2228
|
fig = go.Figure(base_figure())
|
|
1937
2229
|
return _apply_plot_appearance(
|
|
@@ -1942,35 +2234,74 @@ def create_summary_app(
|
|
|
1942
2234
|
y_min=input.y_min(), y_max=input.y_max(),
|
|
1943
2235
|
template=input.template(), legend_position=input.legend_position(),
|
|
1944
2236
|
legend_inside=input.legend_inside(), show_grid=input.show_grid(),
|
|
1945
|
-
|
|
1946
|
-
|
|
2237
|
+
x_major_ticks=input.x_major_ticks(), y_major_ticks=input.y_major_ticks(),
|
|
2238
|
+
x_minor_ticks=input.x_minor_ticks(), y_minor_ticks=input.y_minor_ticks(),
|
|
2239
|
+
line_width=(input.line_width() if input.plot_type() == "line" else 2),
|
|
2240
|
+
hist_alpha=input.hist_alpha(), box_showfliers=input.box_showfliers(),
|
|
2241
|
+
box_orientation=input.box_orientation(),
|
|
2242
|
+
title_font_size=input.title_font_size(), axis_font_size=input.axis_font_size(),
|
|
2243
|
+
tick_font_size=input.tick_font_size(), legend_font_size=input.legend_font_size(),
|
|
2244
|
+
line_alpha=input.line_alpha(), line_style=input.line_style(),
|
|
2245
|
+
scatter_size=input.scatter_size(), scatter_marker=input.scatter_marker(),
|
|
2246
|
+
scatter_alpha=input.scatter_alpha(), plot_kind=input.plot_type(),
|
|
1947
2247
|
)
|
|
1948
|
-
|
|
1949
|
-
@
|
|
2248
|
+
|
|
2249
|
+
@render_plotly
|
|
1950
2250
|
def plot():
|
|
1951
|
-
return
|
|
1952
|
-
|
|
2251
|
+
return styled_plotly_figure()
|
|
2252
|
+
else:
|
|
2253
|
+
@reactive.calc
|
|
2254
|
+
def matplotlib_options():
|
|
2255
|
+
# Shared with the export so that all appearance options,
|
|
2256
|
+
# including per-panel overrides, are reproduced exactly.
|
|
2257
|
+
return dict(title=input.title(),
|
|
1953
2258
|
x_label=input.x_axis_label(), y_label=input.y_axis_label(),
|
|
1954
2259
|
legend_labels=input.legend_labels(), trace_colors=input.trace_colors(),
|
|
1955
2260
|
palette=input.palette(), x_min=input.x_min(), x_max=input.x_max(),
|
|
1956
2261
|
y_min=input.y_min(), y_max=input.y_max(), template=input.template(),
|
|
1957
2262
|
legend_position=input.legend_position(), legend_inside=input.legend_inside(),
|
|
1958
|
-
show_grid=input.show_grid(),
|
|
1959
|
-
|
|
2263
|
+
show_grid=input.show_grid(),
|
|
2264
|
+
x_major_ticks=input.x_major_ticks(), y_major_ticks=input.y_major_ticks(),
|
|
2265
|
+
x_minor_ticks=input.x_minor_ticks(), y_minor_ticks=input.y_minor_ticks(),
|
|
2266
|
+
title_font_size=input.title_font_size(), axis_font_size=input.axis_font_size(),
|
|
2267
|
+
tick_font_size=input.tick_font_size(), legend_font_size=input.legend_font_size(),
|
|
2268
|
+
panel_font_size=input.panel_font_size(), hist_alpha=input.hist_alpha(),
|
|
2269
|
+
box_showfliers=input.box_showfliers(), box_orientation=input.box_orientation(),
|
|
2270
|
+
line_width=(input.line_width() if input.plot_type() == "line" else 2), line_alpha=input.line_alpha(),
|
|
2271
|
+
line_style=input.line_style(), scatter_size=input.scatter_size(),
|
|
2272
|
+
scatter_marker=input.scatter_marker(), scatter_alpha=input.scatter_alpha(),
|
|
1960
2273
|
bins=int(input.bins()) if input.plot_type() == "histogram" else 30,
|
|
1961
2274
|
density=input.hist_density() == "True" if input.plot_type() == "histogram" else False,
|
|
1962
2275
|
stacked=input.hist_stacked() == "True" if input.plot_type() == "histogram" else False,
|
|
1963
2276
|
histtype=input.hist_type() if input.plot_type() == "histogram" else "bar",
|
|
1964
2277
|
cumulative=int(input.hist_cumulative()) if input.plot_type() == "histogram" else 0,
|
|
1965
2278
|
panel_by=(
|
|
1966
|
-
(list(_optional_input("panel_by", []) or []) if len(list(input.group_by() or [])) > 1
|
|
2279
|
+
((list(_optional_input("panel_by", []) or []) or list(input.group_by() or [])) if len(list(input.group_by() or [])) > 1
|
|
1967
2280
|
else list(input.group_by() or []))
|
|
1968
2281
|
if list(input.group_by() or []) and bool(_optional_input("multi_panel", False)) else None
|
|
1969
2282
|
),
|
|
1970
2283
|
independent_panel_limits=(bool(_optional_input("independent_panel_limits", True))
|
|
1971
2284
|
if input.group_by() and _optional_input("multi_panel", False) else True),
|
|
1972
|
-
panel_titles=(_optional_input("panel_titles", "") if input.group_by() and _optional_input("multi_panel", False) else ""),
|
|
1973
|
-
|
|
2285
|
+
panel_titles=(_optional_input("panel_titles", "") if input.group_by() and _optional_input("multi_panel", False) else ""), )
|
|
2286
|
+
|
|
2287
|
+
@render.download(filename="plot_export.zip", media_type="application/zip")
|
|
2288
|
+
def export_python():
|
|
2289
|
+
import io
|
|
2290
|
+
import zipfile
|
|
2291
|
+
spec = prepared_plot()
|
|
2292
|
+
if "error" in spec:
|
|
2293
|
+
raise ValueError(spec["error"])
|
|
2294
|
+
frame = pd.DataFrame(spec["data"])
|
|
2295
|
+
script = _standalone_matplotlib_script(spec, matplotlib_options())
|
|
2296
|
+
buffer = io.BytesIO()
|
|
2297
|
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
2298
|
+
archive.writestr("plot_data.csv", frame.to_csv(index=False))
|
|
2299
|
+
archive.writestr("plot_script.py", script)
|
|
2300
|
+
yield buffer.getvalue()
|
|
2301
|
+
|
|
2302
|
+
@render.plot
|
|
2303
|
+
def plot():
|
|
2304
|
+
return _matplotlib_direct_figure(prepared_plot(), **matplotlib_options())
|
|
1974
2305
|
|
|
1975
2306
|
return filter_state
|
|
1976
2307
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|