apmtools 0.2.48__tar.gz → 0.2.49__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.48 → apmtools-0.2.49}/PKG-INFO +1 -1
- {apmtools-0.2.48 → apmtools-0.2.49}/pyproject.toml +1 -1
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/matplotlib_renderer.py +5 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/ui.py +112 -8
- {apmtools-0.2.48 → apmtools-0.2.49}/.gitignore +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/LICENSE +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/README.md +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/appearance.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/color_editor.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/columns.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/config.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/data.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/elements.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/export.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/histogram_multi.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/multi_axis.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/multi_y.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/plotly_renderer.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/src/apmtools/shiny/summary.py +0 -0
- {apmtools-0.2.48 → apmtools-0.2.49}/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.49
|
|
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>
|
|
@@ -738,6 +738,11 @@ def _matplotlib_direct_figure(spec, *, title="", x_label="", y_label="",
|
|
|
738
738
|
for group in (group_names or [None]):
|
|
739
739
|
for variable in variable_names:
|
|
740
740
|
key = (variable, group)
|
|
741
|
+
# A visible element may be only one of several Y variables
|
|
742
|
+
# within a group. Never create proxy legend entries for
|
|
743
|
+
# combinations with no plotted observations.
|
|
744
|
+
if key not in active:
|
|
745
|
+
continue
|
|
741
746
|
label = f"{group}, {variable}" if group is not None else variable
|
|
742
747
|
legend_handles.append(Line2D([], [], color=multi_colors[key], linestyle=multi_dashes[key], linewidth=line_width))
|
|
743
748
|
legend_labels_out.append(rename.get(label, label))
|
|
@@ -499,7 +499,11 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
499
499
|
|
|
500
500
|
def _label_string(field):
|
|
501
501
|
# Internal adapter for the existing renderer/export APIs.
|
|
502
|
-
|
|
502
|
+
# Label discovery is downstream of figure rendering. Do not
|
|
503
|
+
# subscribe the figure to its own discovered key set: this
|
|
504
|
+
# otherwise forms plot -> legend keys -> plot feedback cycles.
|
|
505
|
+
with reactive.isolate():
|
|
506
|
+
keys = _label_keys[field].get()
|
|
503
507
|
values = _label_values[field].get()
|
|
504
508
|
return "; ".join(f"{key}={values.get(key, key)}" for key in keys
|
|
505
509
|
if values.get(key, key) != key)
|
|
@@ -532,6 +536,12 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
532
536
|
@reactive.effect
|
|
533
537
|
def _watch():
|
|
534
538
|
keys = _label_keys[field].get()
|
|
539
|
+
# A plot-type transition unmounts the old inputs asynchronously.
|
|
540
|
+
# Do not consume values from the previous plot's editors while
|
|
541
|
+
# Shiny is replacing their DOM nodes.
|
|
542
|
+
with reactive.isolate():
|
|
543
|
+
if not keys:
|
|
544
|
+
return
|
|
535
545
|
# Read current mappings without subscribing this watcher to
|
|
536
546
|
# its own writes: otherwise each keystroke can retrigger it.
|
|
537
547
|
with reactive.isolate():
|
|
@@ -742,6 +752,13 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
742
752
|
if not choices:
|
|
743
753
|
return ui.help_text(f"No compatible X variables for {kind}.")
|
|
744
754
|
selected = initial_filters.get("x", [])
|
|
755
|
+
# This control is remounted on plot-type changes. Never feed
|
|
756
|
+
# a datetime line X into a categorical box/bar selectize:
|
|
757
|
+
# Shiny otherwise has to reconcile a selection absent from
|
|
758
|
+
# the new choice set while dependent reactive plots run.
|
|
759
|
+
candidate = [selected] if isinstance(selected, str) else list(selected or [])
|
|
760
|
+
candidate = [value for value in candidate if value in choices]
|
|
761
|
+
selected = candidate if multiple else (candidate[0] if candidate else "")
|
|
745
762
|
if multiple:
|
|
746
763
|
return ui.input_selectize(
|
|
747
764
|
"x", "X-axis / variables" if kind in ("scatter", "histogram") else "X-axis / categories",
|
|
@@ -788,8 +805,11 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
788
805
|
@reactive.event(input.group_by, ignore_none=False)
|
|
789
806
|
def sync_panel_group_choices():
|
|
790
807
|
selected = list(input.group_by() or [])
|
|
808
|
+
with reactive.isolate():
|
|
809
|
+
current_panels = list(_optional_input("panel_by", []) or [])
|
|
810
|
+
valid_panels = [name for name in current_panels if name in selected]
|
|
791
811
|
ui.update_selectize("panel_by", choices={name: name for name in selected},
|
|
792
|
-
selected=
|
|
812
|
+
selected=valid_panels)
|
|
793
813
|
|
|
794
814
|
@render.ui
|
|
795
815
|
def frequency_options():
|
|
@@ -800,6 +820,34 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
800
820
|
ui.input_checkbox("flooring_timestamp", "Floor timestamps", value=initial_filters.get("flooring_timestamp", True)),
|
|
801
821
|
)
|
|
802
822
|
|
|
823
|
+
# The virtual group choices depend on plot type and selected axes,
|
|
824
|
+
# not on the Group by value. Updating a selectize control while
|
|
825
|
+
# subscribing to that same control can create an invalidation loop,
|
|
826
|
+
# especially during a line -> box/bar transition with multiple Y's.
|
|
827
|
+
_last_axis_group_choices = reactive.value(None)
|
|
828
|
+
|
|
829
|
+
@reactive.effect
|
|
830
|
+
def sync_axis_group_choices():
|
|
831
|
+
kind = input.plot_type()
|
|
832
|
+
x_raw = _optional_input("x", [])
|
|
833
|
+
y_raw = _optional_input("y", [])
|
|
834
|
+
xs = [x_raw] if isinstance(x_raw, str) else list(x_raw or [])
|
|
835
|
+
ys = [y_raw] if isinstance(y_raw, str) else list(y_raw or [])
|
|
836
|
+
choices = list(group_columns)
|
|
837
|
+
if kind in ("histogram", "scatter") and len(xs) > 1:
|
|
838
|
+
choices.append("variables_x")
|
|
839
|
+
if kind != "histogram" and len(ys) > 1:
|
|
840
|
+
choices.append("variables_y")
|
|
841
|
+
signature = tuple(choices)
|
|
842
|
+
with reactive.isolate():
|
|
843
|
+
previous = _last_axis_group_choices.get()
|
|
844
|
+
current = list(_optional_input("group_by", []) or [])
|
|
845
|
+
if signature == previous:
|
|
846
|
+
return
|
|
847
|
+
_last_axis_group_choices.set(signature)
|
|
848
|
+
selected = [g for g in current if g in choices]
|
|
849
|
+
ui.update_selectize("group_by", choices={c: c for c in choices}, selected=selected)
|
|
850
|
+
|
|
803
851
|
@reactive.calc
|
|
804
852
|
def selected_data():
|
|
805
853
|
current = filtered_summary()
|
|
@@ -830,7 +878,8 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
830
878
|
):
|
|
831
879
|
return pd.DataFrame()
|
|
832
880
|
|
|
833
|
-
selected_groups =
|
|
881
|
+
selected_groups = [g for g in (input.group_by() or [])
|
|
882
|
+
if g not in ("variables_x", "variables_y")]
|
|
834
883
|
# Derive hour/day/etc. through group_stat without aggregation.
|
|
835
884
|
# The returned index column identifies source rows.
|
|
836
885
|
for component in selected_groups:
|
|
@@ -908,6 +957,7 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
908
957
|
y_input = None if selected_plot == "histogram" else input.y()
|
|
909
958
|
selected_y = ([y_input] if isinstance(y_input, str) else list(y_input or []))
|
|
910
959
|
selected_groups = list(input.group_by() or [])
|
|
960
|
+
physical_groups = [g for g in selected_groups if g not in ("variables_x", "variables_y")]
|
|
911
961
|
frequency = FREQUENCIES.get(input.frequency())
|
|
912
962
|
should_aggregate = AGGREGATES.get(input.aggregate()) is not None
|
|
913
963
|
|
|
@@ -919,6 +969,13 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
919
969
|
selected_plot != "histogram" and not selected_y
|
|
920
970
|
):
|
|
921
971
|
return {"error": "No data for the selected filters"}
|
|
972
|
+
# The X widget is replaced asynchronously by render.ui. Until
|
|
973
|
+
# the browser mounts it, the old line X can remain in input.x.
|
|
974
|
+
# Treat that intermediate state as unready, not as box data.
|
|
975
|
+
if selected_plot in ("box", "bar") and any(
|
|
976
|
+
x not in categorical_columns for x in selected_x
|
|
977
|
+
):
|
|
978
|
+
return {"error": "Waiting for categorical X-axis selection"}
|
|
922
979
|
|
|
923
980
|
aggregate = AGGREGATES.get(input.aggregate())
|
|
924
981
|
def mapped(column):
|
|
@@ -954,16 +1011,35 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
954
1011
|
if selected_plot == "histogram" and len(x_variables) > 1:
|
|
955
1012
|
from .histogram_multi import histogram_long_data
|
|
956
1013
|
plot_df = histogram_long_data(plot_df, x_variables, group_columns=[
|
|
957
|
-
column_map[c] for c in
|
|
1014
|
+
column_map[c] for c in physical_groups if column_map[c] in plot_df.columns
|
|
958
1015
|
])
|
|
1016
|
+
plot_df["variables_x"] = plot_df["__hist_variable"]
|
|
959
1017
|
x_column = "__hist_value"
|
|
960
1018
|
x_label_override = " | ".join(x_variables)
|
|
961
1019
|
|
|
1020
|
+
if (selected_plot == "scatter" and "variables_x" in selected_groups
|
|
1021
|
+
and len(x_variables) > 1):
|
|
1022
|
+
id_columns = [c for c in plot_df.columns if c not in x_variables]
|
|
1023
|
+
plot_df = plot_df.melt(id_vars=id_columns, value_vars=x_variables,
|
|
1024
|
+
var_name="variables_x", value_name="__axis_x_value")
|
|
1025
|
+
x_column = "__axis_x_value"
|
|
1026
|
+
x_variables = [x_column]
|
|
1027
|
+
|
|
962
1028
|
if selected_plot in ("box", "bar"):
|
|
963
1029
|
plot_df = plot_df.copy(deep=False)
|
|
964
1030
|
plot_df["__plot_category"] = _composite_labels(plot_df, x_columns)
|
|
965
1031
|
x_column = "__plot_category"
|
|
966
1032
|
|
|
1033
|
+
if "variables_y" in selected_groups and len(y_columns) > 1:
|
|
1034
|
+
# Make the selected Y variable an actual categorical dimension,
|
|
1035
|
+
# so it works with ordinary grouping and panel partitioning.
|
|
1036
|
+
id_columns = [c for c in plot_df.columns if c not in y_columns]
|
|
1037
|
+
plot_df = plot_df.melt(id_vars=id_columns, value_vars=y_columns,
|
|
1038
|
+
var_name="variables_y", value_name="__axis_y_value")
|
|
1039
|
+
y_columns = ["__axis_y_value"]
|
|
1040
|
+
# The categorical X labels have already been computed above.
|
|
1041
|
+
# Scatter and bar/box now use the standard single-Y renderer.
|
|
1042
|
+
|
|
967
1043
|
# Convert durations once for Plotly, Matplotlib, editable
|
|
968
1044
|
# numeric bounds, faceting and exported plots. Retain the
|
|
969
1045
|
# original variable name and label its displayed unit.
|
|
@@ -980,8 +1056,12 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
980
1056
|
# Multiple grouping columns are supported by group_stat(). For
|
|
981
1057
|
# plotting, Plotly needs one color/category column, so combine the
|
|
982
1058
|
# selected grouping columns into a readable composite label.
|
|
983
|
-
group_columns = [column_map[c] for c in
|
|
1059
|
+
group_columns = [column_map[c] for c in physical_groups
|
|
984
1060
|
if column_map[c] in plot_df.columns]
|
|
1061
|
+
if "variables_x" in selected_groups and "variables_x" in plot_df:
|
|
1062
|
+
group_columns.append("variables_x")
|
|
1063
|
+
if "variables_y" in selected_groups and "variables_y" in plot_df:
|
|
1064
|
+
group_columns.append("variables_y")
|
|
985
1065
|
if group_columns:
|
|
986
1066
|
plot_df = plot_df.copy(deep=False)
|
|
987
1067
|
plot_df["__plot_group"] = (
|
|
@@ -1317,6 +1397,11 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1317
1397
|
ui.update_text(field, value="")
|
|
1318
1398
|
for field in ("legend_labels", "x_tick_labels", "y_tick_labels", "panel_titles"):
|
|
1319
1399
|
_set_label_keys(field, ())
|
|
1400
|
+
# Invalidate figure-derived editor identities immediately. The
|
|
1401
|
+
# previous lineplot legend/ticks must never be treated as boxplot
|
|
1402
|
+
# identities during the asynchronous input transition.
|
|
1403
|
+
rendered_legend_keys.set(())
|
|
1404
|
+
rendered_tick_keys.set(((), ()))
|
|
1320
1405
|
_color_keys.set(())
|
|
1321
1406
|
ui.update_select("palette", selected="Default")
|
|
1322
1407
|
ui.update_select("template", selected="plotly")
|
|
@@ -1397,7 +1482,12 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1397
1482
|
from .appearance import _parse_label_map
|
|
1398
1483
|
with reactive.isolate():
|
|
1399
1484
|
mapping = _parse_label_map(_label_string("legend_labels"))
|
|
1400
|
-
|
|
1485
|
+
# Resolve displayed labels against stable, original keys. A
|
|
1486
|
+
# reverse dictionary is unsafe: two different keys may have
|
|
1487
|
+
# the same edited label, and mapping order then makes keys
|
|
1488
|
+
# alternate as the figure is rendered repeatedly.
|
|
1489
|
+
with reactive.isolate():
|
|
1490
|
+
known_keys = _label_keys["legend_labels"].get()
|
|
1401
1491
|
if is_plotly:
|
|
1402
1492
|
if getattr(fig.layout, "showlegend", None) is False:
|
|
1403
1493
|
labels = []
|
|
@@ -1408,8 +1498,22 @@ if (!window.__apmColorPickerInstalled) {
|
|
|
1408
1498
|
legends = list(getattr(fig, "legends", []))
|
|
1409
1499
|
legends += [ax.get_legend() for ax in fig.axes if ax.get_legend() is not None]
|
|
1410
1500
|
labels = [txt.get_text() for leg in legends for txt in leg.get_texts()]
|
|
1411
|
-
|
|
1412
|
-
|
|
1501
|
+
excluded = ("Y variable", "Grouping", "Legend", "All observations")
|
|
1502
|
+
labels = [label for label in labels if label.strip() and label not in excluded]
|
|
1503
|
+
# If this is merely a relabelled rendering of the existing
|
|
1504
|
+
# traces, do not rediscover/rename their identities.
|
|
1505
|
+
expected = [mapping.get(key, key) for key in known_keys]
|
|
1506
|
+
if known_keys and set(labels) == set(expected):
|
|
1507
|
+
keys = tuple(known_keys)
|
|
1508
|
+
else:
|
|
1509
|
+
reverse = {}
|
|
1510
|
+
for key in known_keys:
|
|
1511
|
+
display = mapping.get(key, key)
|
|
1512
|
+
reverse.setdefault(display, []).append(key)
|
|
1513
|
+
keys = tuple(dict.fromkeys(
|
|
1514
|
+
reverse[label][0] if len(reverse.get(label, ())) == 1
|
|
1515
|
+
else label for label in labels
|
|
1516
|
+
))
|
|
1413
1517
|
with reactive.isolate():
|
|
1414
1518
|
old = rendered_legend_keys.get()
|
|
1415
1519
|
if keys != old:
|
|
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
|