apmtools 0.2.52__tar.gz → 0.2.53__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.52 → apmtools-0.2.53}/PKG-INFO +1 -1
- {apmtools-0.2.52 → apmtools-0.2.53}/pyproject.toml +1 -1
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/matplotlib_renderer.py +56 -14
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/ui.py +73 -8
- {apmtools-0.2.52 → apmtools-0.2.53}/.gitignore +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/LICENSE +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/README.md +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/plot_app.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/plot_configs.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/appearance.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/color_editor.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/columns.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/config.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/data.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/elements.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/export.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/filter_config.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/histogram_multi.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/multi_axis.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/multi_y.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/plotly_renderer.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/src/apmtools/shiny/summary.py +0 -0
- {apmtools-0.2.52 → apmtools-0.2.53}/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.53
|
|
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>
|
|
@@ -47,6 +47,17 @@ def _axis_bounds_from_spec(spec, *, bins=30, density=False, stacked=False, cumul
|
|
|
47
47
|
if "error" in spec:
|
|
48
48
|
return {key: "" for key in ("x_min", "x_max", "y_min", "y_max")}
|
|
49
49
|
df, kind, x, ys = spec["data"], spec["kind"], spec["x"], spec["ys"]
|
|
50
|
+
# A long-form Y grouping must render its value column, not a source
|
|
51
|
+
# column removed by melt(). Normalize the local spec before faceting.
|
|
52
|
+
if kind == "scatter" and "__axis_y_value" in df.columns and any(y not in df.columns for y in ys):
|
|
53
|
+
ys = ["__axis_y_value"]
|
|
54
|
+
spec = {**spec, "ys": ys}
|
|
55
|
+
|
|
56
|
+
# Reactive selection changes can briefly expose a specification whose
|
|
57
|
+
# original Y names are no longer present after a melt/reshape.
|
|
58
|
+
ys = [y for y in ys if y in df.columns]
|
|
59
|
+
if x not in df.columns or (kind != "histogram" and not ys):
|
|
60
|
+
return {key: "" for key in ("x_min", "x_max", "y_min", "y_max")}
|
|
50
61
|
if df.empty:
|
|
51
62
|
return {key: "" for key in ("x_min", "x_max", "y_min", "y_max")}
|
|
52
63
|
bounds = {key: "" for key in ("x_min", "x_max", "y_min", "y_max")}
|
|
@@ -364,6 +375,12 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
364
375
|
return fig
|
|
365
376
|
|
|
366
377
|
df, kind, x, ys = spec["data"], spec["kind"], spec["x"], spec["ys"]
|
|
378
|
+
# A long-form Y grouping must render its value column, not a source
|
|
379
|
+
# column removed by melt(). Normalize the local spec before faceting.
|
|
380
|
+
if kind == "scatter" and "__axis_y_value" in df.columns and any(y not in df.columns for y in ys):
|
|
381
|
+
ys = ["__axis_y_value"]
|
|
382
|
+
spec = {**spec, "ys": ys}
|
|
383
|
+
|
|
367
384
|
# Facet only the native Matplotlib renderer. Each panel receives the
|
|
368
385
|
# same filtered/aggregated data subset and the same appearance settings.
|
|
369
386
|
if panel_by and _axes is None:
|
|
@@ -404,19 +421,23 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
404
421
|
stacked=stacked, histtype=histtype, cumulative=cumulative, _axes=panel_ax,
|
|
405
422
|
x_major_ticks=x_major_ticks, y_major_ticks=y_major_ticks,
|
|
406
423
|
x_minor_ticks=x_minor_ticks, y_minor_ticks=y_minor_ticks)
|
|
407
|
-
|
|
408
|
-
|
|
424
|
+
# Resolve the axes once; reuse the same ordered panel collection for
|
|
425
|
+
# legend layout rather than repeatedly materializing axes.flat.
|
|
426
|
+
flat_axes = axes.flat
|
|
427
|
+
active_axes = [flat_axes[i] for i in range(len(partitions))]
|
|
428
|
+
for i in range(len(partitions), axes.size):
|
|
429
|
+
flat_axes[i].set_visible(False)
|
|
409
430
|
if legend_inside:
|
|
410
|
-
for panel_ax in
|
|
431
|
+
for panel_ax in active_axes:
|
|
411
432
|
if panel_ax.get_legend() is not None:
|
|
412
433
|
panel_ax.get_legend().set_in_layout(False)
|
|
413
434
|
fig.tight_layout()
|
|
414
435
|
# V77 used tight_layout for facets; reserve generous gutters
|
|
415
436
|
# without changing V83's legend behaviour or column control.
|
|
416
437
|
fig.subplots_adjust(wspace=panel_wspace, hspace=panel_hspace)
|
|
417
|
-
_fit_inside_legends(fig,
|
|
438
|
+
_fit_inside_legends(fig, active_axes, legend_font_size, legend_columns)
|
|
418
439
|
else:
|
|
419
|
-
_place_outside_legend(fig,
|
|
440
|
+
_place_outside_legend(fig, active_axes,
|
|
420
441
|
legend_position, legend_font_size, legend_columns,
|
|
421
442
|
panel_hspace=panel_hspace, panel_wspace=panel_wspace)
|
|
422
443
|
return fig
|
|
@@ -457,27 +478,45 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
457
478
|
variable_names = list(ys)
|
|
458
479
|
x_names = list(spec.get("x_variables", [x]))
|
|
459
480
|
group_names = style_groups if group_col else []
|
|
481
|
+
# Reuse stable positions for per-variable markers and hatches.
|
|
482
|
+
x_position = {name: i for i, name in enumerate(x_names)}
|
|
483
|
+
y_position = {name: i for i, name in enumerate(ys)}
|
|
484
|
+
group_position = {name: i for i, name in enumerate(group_names)}
|
|
460
485
|
active = set()
|
|
461
486
|
if kind == "scatter":
|
|
462
487
|
scatter_markers = ['o', 's', 'D', 'x', '^', 'v', 'P', '*']
|
|
463
488
|
for key, group, y, xv, category, rows in entries:
|
|
464
489
|
g = group if group_col else None
|
|
465
490
|
active.add((y, g))
|
|
491
|
+
# Use the same editable series keys as the appearance controls.
|
|
492
|
+
# Previously this multi-axis branch ignored all three overrides.
|
|
493
|
+
candidates = (key, f"{g}, {y}" if g is not None else str(y),
|
|
494
|
+
str(g) if g is not None else str(y), str(y))
|
|
495
|
+
def override(mapping, fallback):
|
|
496
|
+
return next((mapping[name] for name in candidates if name in mapping), fallback)
|
|
497
|
+
size = _bounded_float({"value": override(sizes, marker_size)}, "value", marker_size, 0.1, 100)
|
|
498
|
+
alpha = _bounded_float({"value": override(marker_alphas, 1)}, "value", 1, 0, 1)
|
|
499
|
+
default_marker = scatter_markers[x_position[xv] % len(scatter_markers)]
|
|
500
|
+
marker = (_scatter_marker({"value": override(markers, "dot")}, "value")
|
|
501
|
+
if any(name in markers for name in candidates) else default_marker)
|
|
466
502
|
ax.scatter(rows[xv], rows[y], color=color_map[(y, g)],
|
|
467
|
-
marker=
|
|
468
|
-
s=marker_size**2, alpha=.8)
|
|
503
|
+
marker=marker, s=size**2, alpha=alpha)
|
|
469
504
|
else:
|
|
470
505
|
cats = list(pd.unique(df[x].astype(str)))
|
|
471
506
|
combinations = [(g, y) for g in (group_names or ['All observations']) for y in ys]
|
|
472
507
|
n = max(1, len(combinations))
|
|
473
508
|
width = .82 / n
|
|
509
|
+
# Resolve categorical positions once rather than scanning both
|
|
510
|
+
# lists for every multi-axis trace. Keep the original ordering.
|
|
511
|
+
combination_index = {item: i for i, item in enumerate(combinations)}
|
|
512
|
+
category_index = {item: i for i, item in enumerate(cats)}
|
|
474
513
|
from collections import defaultdict
|
|
475
514
|
buckets = defaultdict(list)
|
|
476
515
|
for key, group, y, xv, category, rows in entries:
|
|
477
516
|
g = group if group_col else None
|
|
478
517
|
active.add((y, g))
|
|
479
|
-
idx =
|
|
480
|
-
pos =
|
|
518
|
+
idx = combination_index[(group, y)]
|
|
519
|
+
pos = category_index[category] + (idx - (n - 1)/2)*width
|
|
481
520
|
values = pd.to_numeric(rows[y], errors='coerce').dropna().to_numpy(dtype=float)
|
|
482
521
|
buckets[(pos, y, g)].extend(values)
|
|
483
522
|
for (pos, y, g), values in buckets.items():
|
|
@@ -485,7 +524,7 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
485
524
|
continue
|
|
486
525
|
color = color_map[(y, g)]
|
|
487
526
|
hatch_options = ['', '/', '\\', 'x', '.', '+', '-']
|
|
488
|
-
hatch_index =
|
|
527
|
+
hatch_index = y_position[y] if multi_y_color_by == 'group' else group_position.get(g, 0)
|
|
489
528
|
hatch = '' if multi_y_color_by in ('none', 'both') else hatch_options[hatch_index % len(hatch_options)]
|
|
490
529
|
if kind == 'bar':
|
|
491
530
|
ax.bar(pos, sum(values), width=width*.92*bar_box_width, color=color, hatch=hatch)
|
|
@@ -584,11 +623,14 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
584
623
|
elif spec["frequency"] is not None and id_col is None:
|
|
585
624
|
line_df = _add_gap_sections(df, x_column=x, group_columns=spec["groups"], frequency=spec["frequency"])
|
|
586
625
|
section_col = "__line_section"
|
|
626
|
+
# Partition the line data once, not once per Y variable. This
|
|
627
|
+
# preserves group order and the existing equality semantics while
|
|
628
|
+
# avoiding repeated full-column masks for multi-Y plots.
|
|
629
|
+
grouped_line_rows = ([(n, line_df.loc[line_df[group_col] == n]) for n in labels]
|
|
630
|
+
if group_col else None)
|
|
587
631
|
for y in ys:
|
|
588
|
-
if
|
|
589
|
-
|
|
590
|
-
else:
|
|
591
|
-
sets = [(str(y) if len(ys) > 1 else "All observations", line_df)]
|
|
632
|
+
sets = (grouped_line_rows if grouped_line_rows is not None else
|
|
633
|
+
[(str(y) if len(ys) > 1 else "All observations", line_df)])
|
|
592
634
|
for name, rows in sets:
|
|
593
635
|
rows = rows.loc[rows[y].notna()]
|
|
594
636
|
if rows.empty:
|
|
@@ -963,6 +963,18 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
963
963
|
return
|
|
964
964
|
_last_axis_group_choices.set(signature)
|
|
965
965
|
selected = [g for g in current if g in choices]
|
|
966
|
+
# The virtual axis groups are not part of the static selectize
|
|
967
|
+
# choices at mount time. On restoring a saved plot, Shiny may
|
|
968
|
+
# discard them before this effect populates the choices.
|
|
969
|
+
# Restore only eligible saved virtual groups; do not override
|
|
970
|
+
# physical groups or subsequent user changes.
|
|
971
|
+
if previous is None:
|
|
972
|
+
restored = initial_filters.get("group_by", []) or []
|
|
973
|
+
if isinstance(restored, str):
|
|
974
|
+
restored = [restored]
|
|
975
|
+
for name in restored:
|
|
976
|
+
if name in ("variables_x", "variables_y") and name in choices and name not in selected:
|
|
977
|
+
selected.append(name)
|
|
966
978
|
ui.update_selectize("group_by", choices={c: c for c in choices}, selected=selected)
|
|
967
979
|
|
|
968
980
|
@reactive.calc
|
|
@@ -1136,9 +1148,24 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1136
1148
|
|
|
1137
1149
|
if (selected_plot == "scatter" and "variables_x" in selected_groups
|
|
1138
1150
|
and len(x_variables) > 1):
|
|
1139
|
-
|
|
1140
|
-
|
|
1151
|
+
# Melt copies rather than the original X columns. A source
|
|
1152
|
+
# variable can simultaneously be an X and a Y selection;
|
|
1153
|
+
# removing it here leaves the subsequent Y melt and the
|
|
1154
|
+
# renderer with a stale reference to a missing column.
|
|
1155
|
+
plot_df = plot_df.copy()
|
|
1156
|
+
x_copies = []
|
|
1157
|
+
x_names = {}
|
|
1158
|
+
for index, column in enumerate(x_variables):
|
|
1159
|
+
temporary = f"__plot_x_melt_{index}"
|
|
1160
|
+
while temporary in plot_df.columns:
|
|
1161
|
+
temporary += "_"
|
|
1162
|
+
plot_df[temporary] = plot_df[column]
|
|
1163
|
+
x_copies.append(temporary)
|
|
1164
|
+
x_names[temporary] = column
|
|
1165
|
+
id_columns = [c for c in plot_df.columns if c not in x_copies]
|
|
1166
|
+
plot_df = plot_df.melt(id_vars=id_columns, value_vars=x_copies,
|
|
1141
1167
|
var_name="variables_x", value_name="__axis_x_value")
|
|
1168
|
+
plot_df["variables_x"] = plot_df["variables_x"].map(x_names)
|
|
1142
1169
|
x_column = "__axis_x_value"
|
|
1143
1170
|
x_variables = [x_column]
|
|
1144
1171
|
|
|
@@ -1150,9 +1177,30 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1150
1177
|
if "variables_y" in selected_groups and len(y_columns) > 1:
|
|
1151
1178
|
# Make the selected Y variable an actual categorical dimension,
|
|
1152
1179
|
# so it works with ordinary grouping and panel partitioning.
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1180
|
+
# A variable may be selected as both scatter X and Y.
|
|
1181
|
+
# Melting the original Y columns would remove it from the
|
|
1182
|
+
# frame, leaving the X axis pointing at a missing column.
|
|
1183
|
+
# Melt temporary Y copies instead, retaining X columns.
|
|
1184
|
+
overlapping = set(y_columns).intersection(x_variables)
|
|
1185
|
+
if overlapping:
|
|
1186
|
+
plot_df = plot_df.copy()
|
|
1187
|
+
copies = []
|
|
1188
|
+
original_names = {}
|
|
1189
|
+
for index, column in enumerate(y_columns):
|
|
1190
|
+
temporary = f"__plot_y_melt_{index}"
|
|
1191
|
+
while temporary in plot_df.columns:
|
|
1192
|
+
temporary += "_"
|
|
1193
|
+
plot_df[temporary] = plot_df[column]
|
|
1194
|
+
copies.append(temporary)
|
|
1195
|
+
original_names[temporary] = column
|
|
1196
|
+
id_columns = [c for c in plot_df.columns if c not in copies]
|
|
1197
|
+
plot_df = plot_df.melt(id_vars=id_columns, value_vars=copies,
|
|
1198
|
+
var_name="variables_y", value_name="__axis_y_value")
|
|
1199
|
+
plot_df["variables_y"] = plot_df["variables_y"].map(original_names)
|
|
1200
|
+
else:
|
|
1201
|
+
id_columns = [c for c in plot_df.columns if c not in y_columns]
|
|
1202
|
+
plot_df = plot_df.melt(id_vars=id_columns, value_vars=y_columns,
|
|
1203
|
+
var_name="variables_y", value_name="__axis_y_value")
|
|
1156
1204
|
y_columns = ["__axis_y_value"]
|
|
1157
1205
|
# The categorical X labels have already been computed above.
|
|
1158
1206
|
# Scatter and bar/box now use the standard single-Y renderer.
|
|
@@ -1185,6 +1233,12 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1185
1233
|
_composite_labels(plot_df, group_columns)
|
|
1186
1234
|
)
|
|
1187
1235
|
|
|
1236
|
+
# The scatter Y-variable grouping uses a long-form value column.
|
|
1237
|
+
# Keep the spec synchronized with that column even during
|
|
1238
|
+
# reactive transitions between single- and multi-Y selections.
|
|
1239
|
+
if selected_plot == "scatter" and "__axis_y_value" in plot_df.columns:
|
|
1240
|
+
y_columns = ["__axis_y_value"]
|
|
1241
|
+
|
|
1188
1242
|
return {"data": plot_df, "kind": selected_plot, "x": x_column,
|
|
1189
1243
|
"ys": y_columns, "groups": group_columns,
|
|
1190
1244
|
"categories": x_columns if selected_plot in ("box", "bar") else None,
|
|
@@ -1365,6 +1419,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1365
1419
|
# prepared. The effect depends on the plot specification, not on
|
|
1366
1420
|
# appearance inputs, so editing a label or colour cannot reset it.
|
|
1367
1421
|
_last_line_style_keys = [None]
|
|
1422
|
+
_last_scatter_style_keys = [None]
|
|
1368
1423
|
|
|
1369
1424
|
@reactive.effect
|
|
1370
1425
|
@reactive.event(prepared_plot, lambda: _optional_input("multi_panel", False) if input.group_by() else False,
|
|
@@ -1480,9 +1535,19 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1480
1535
|
elif kind == "histogram":
|
|
1481
1536
|
ui.update_text_area("hist_alpha", value="\n".join(f"{name}=0.65" for name in names))
|
|
1482
1537
|
elif kind == "scatter":
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1538
|
+
# Preserve user edits across preparation updates; only
|
|
1539
|
+
# synchronize when the actual trace identities change.
|
|
1540
|
+
current_keys = tuple(names)
|
|
1541
|
+
previous_keys = _last_scatter_style_keys[0]
|
|
1542
|
+
_last_scatter_style_keys[0] = current_keys
|
|
1543
|
+
if previous_keys != current_keys:
|
|
1544
|
+
for field, default in (("scatter_size", "7"), ("scatter_marker", "dot"), ("scatter_alpha", "1")):
|
|
1545
|
+
if previous_keys is None and same_restored_data and field in initial_filters:
|
|
1546
|
+
continue
|
|
1547
|
+
with reactive.isolate():
|
|
1548
|
+
existing = _parse_label_map(_optional_input(field, ""))
|
|
1549
|
+
ui.update_text_area(field, value="\n".join(
|
|
1550
|
+
f"{name}={existing.get(name, default)}" for name in names))
|
|
1486
1551
|
if backend == "matplotlib":
|
|
1487
1552
|
panel_variables = ((list(_optional_input("panel_by", []) or []) or selected_groups) if len(selected_groups) > 1
|
|
1488
1553
|
else selected_groups)
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|