scistackplot 0.1.43__tar.gz → 0.1.44__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.
- {scistackplot-0.1.43 → scistackplot-0.1.44}/PKG-INFO +2 -2
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/cell.py +49 -2
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/reduce.py +7 -1
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/resolved.py +30 -25
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/ylimits.py +3 -1
- {scistackplot-0.1.43 → scistackplot-0.1.44}/.gitignore +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/README.md +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/pyproject.toml +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/__init__.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/aliases.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/autosize.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/capability.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/codegen.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/colors.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/compare.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/custom.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/dedup.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/design.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/diffbars.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/export.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/figsize.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/figure_file.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/framesize.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/groups.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/numeric.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/panels.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/paper.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/presets.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/reducer.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/render/__init__.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/render/base.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/render/mpl.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/render/plotly_.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/restore.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/roles.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/series_stats.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/shape.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/sources/__init__.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/sources/base.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/sources/csv.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/sources/frame.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/spaghetti.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/spec.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/spread.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/table.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/textsize.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/ticklabels.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/variants.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/weights.py +0 -0
- {scistackplot-0.1.43 → scistackplot-0.1.44}/src/scistackplot/xaxis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scistackplot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.44
|
|
4
4
|
Summary: Spec-driven plotting for long-format scientific data — standalone, GUI-friendly
|
|
5
5
|
Author: SciStack Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Requires-Dist: csv-stats>=0.2.1
|
|
21
21
|
Requires-Dist: numpy>=1.21
|
|
22
22
|
Requires-Dist: pandas>=1.5
|
|
23
|
-
Requires-Dist: scistacklog==0.1.
|
|
23
|
+
Requires-Dist: scistacklog==0.1.44
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: matplotlib>=3.6; extra == 'dev'
|
|
26
26
|
Requires-Dist: plotly>=5.0; extra == 'dev'
|
|
@@ -44,10 +44,11 @@ from __future__ import annotations
|
|
|
44
44
|
import time
|
|
45
45
|
from dataclasses import replace
|
|
46
46
|
|
|
47
|
+
import numpy as np
|
|
47
48
|
import pandas as pd
|
|
48
49
|
from scistacklog import Log
|
|
49
50
|
|
|
50
|
-
from .series_stats import cell_arrays, collapse_cells
|
|
51
|
+
from .series_stats import cell_array, cell_arrays, collapse_cells
|
|
51
52
|
from .shape import Shape
|
|
52
53
|
from .spec import SCALAR_KINDS, PlotSpec, Statistic
|
|
53
54
|
from .table import LongTable
|
|
@@ -124,7 +125,11 @@ def apply_cell_collapse(spec: PlotSpec, table: LongTable) -> LongTable:
|
|
|
124
125
|
# Kept as NaN rows, never dropped: dropping would change the level
|
|
125
126
|
# counts the factor summary reports, so "this trial recorded nothing"
|
|
126
127
|
# would read as "this trial does not exist".
|
|
127
|
-
|
|
128
|
+
#
|
|
129
|
+
# DEBUG: SOME empty cells are ordinary data (78 WARNs in scidb.log
|
|
130
|
+
# 2026-10-19). The case worth a WARN — nothing at all to draw — is
|
|
131
|
+
# `all_nan_warning`'s, over the post-filter rows.
|
|
132
|
+
Log.debug(
|
|
128
133
|
"%d of %d row(s) of %r collapsed to NaN (empty or all-NaN cells) — "
|
|
129
134
|
"kept as gaps",
|
|
130
135
|
empty,
|
|
@@ -194,6 +199,48 @@ def _collapse_exploded(
|
|
|
194
199
|
return out, len(out), len(frame)
|
|
195
200
|
|
|
196
201
|
|
|
202
|
+
def _cell_is_nan(value) -> bool:
|
|
203
|
+
"""One object-dtype cell holds no number: None, NaN, an empty or all-NaN
|
|
204
|
+
array. A value that is not numeric at all (a string) is NOT NaN — this
|
|
205
|
+
must never warn about data it cannot read."""
|
|
206
|
+
if value is None:
|
|
207
|
+
return True
|
|
208
|
+
if isinstance(value, (np.ndarray, list, tuple)):
|
|
209
|
+
array = cell_array(value)
|
|
210
|
+
return array.size == 0 or bool(np.isnan(array).all())
|
|
211
|
+
try:
|
|
212
|
+
return bool(np.isnan(float(value)))
|
|
213
|
+
except (TypeError, ValueError):
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def all_nan_warning(frame: pd.DataFrame, measure: str) -> str | None:
|
|
218
|
+
"""The message when every value of ``measure`` in ``frame`` is NaN, or None.
|
|
219
|
+
|
|
220
|
+
The one owner of "this plot has nothing to draw because the data is all
|
|
221
|
+
NaN" — logged as a WARN and returned for the GUI to show as a popup.
|
|
222
|
+
``frame`` is the POST-filter frame, so the answer is about what the user
|
|
223
|
+
is plotting, not columns they filtered away. A frame with no rows is a
|
|
224
|
+
different case (filtered to nothing) and returns None. Scalar cells and
|
|
225
|
+
1-D cells (arrays, empty arrays) are both handled.
|
|
226
|
+
"""
|
|
227
|
+
if measure not in frame.columns or frame.empty:
|
|
228
|
+
return None
|
|
229
|
+
column = frame[measure]
|
|
230
|
+
if column.dtype == object:
|
|
231
|
+
all_nan = all(_cell_is_nan(value) for value in column)
|
|
232
|
+
else:
|
|
233
|
+
all_nan = bool(column.isna().all())
|
|
234
|
+
if not all_nan:
|
|
235
|
+
return None
|
|
236
|
+
message = (
|
|
237
|
+
f"Every value of {measure!r} is NaN in the {len(frame)} row(s) being "
|
|
238
|
+
f"plotted — there is nothing to draw. Check the function that saved it."
|
|
239
|
+
)
|
|
240
|
+
Log.warn("all-NaN measure: %s", message, layer=LAYER)
|
|
241
|
+
return message
|
|
242
|
+
|
|
243
|
+
|
|
197
244
|
def cell_collapse_note(spec: PlotSpec, table: LongTable) -> str | None:
|
|
198
245
|
"""One line naming the collapse, for a figure's notes, or None.
|
|
199
246
|
|
|
@@ -96,7 +96,7 @@ from .compare import (
|
|
|
96
96
|
from .panels import override_for, panel_key_text, unmatched
|
|
97
97
|
from .ylimits import ExtentMode, eligible_scope, limits_for, panel_factors, spread_bounds
|
|
98
98
|
from .groups import apply_level_groups
|
|
99
|
-
from .cell import apply_cell_collapse, cell_collapse_note, cell_collapses
|
|
99
|
+
from .cell import all_nan_warning, apply_cell_collapse, cell_collapse_note, cell_collapses
|
|
100
100
|
from .variants import apply_variant_sets, strip_answered_roles
|
|
101
101
|
|
|
102
102
|
LAYER = "scistackplot"
|
|
@@ -376,6 +376,7 @@ def resolve(
|
|
|
376
376
|
)
|
|
377
377
|
for figure in figures:
|
|
378
378
|
figure.fanout_notes = plan.notes
|
|
379
|
+
figure.data_warnings = plan.data_warnings
|
|
379
380
|
for position, figure in enumerate(figures):
|
|
380
381
|
figure.difference_siblings = [
|
|
381
382
|
figures[other] for other in _difference_sibling_indices(plan, position)
|
|
@@ -434,6 +435,9 @@ class _Plan:
|
|
|
434
435
|
frame: "pd.DataFrame" = field(default_factory=pd.DataFrame)
|
|
435
436
|
#: The y-limit scope, after ineligible factors were dropped.
|
|
436
437
|
y_scope: list[str] = field(default_factory=list)
|
|
438
|
+
#: ``cell.all_nan_warning`` over ``frame``. On the plan, not computed per
|
|
439
|
+
#: figure, so a plan-cache HIT still carries it to every figure.
|
|
440
|
+
data_warnings: list[str] = field(default_factory=list)
|
|
437
441
|
#: ``{scope values: (low, high)}`` for the WHOLE fan-out, computed here
|
|
438
442
|
#: rather than per figure — a scope of ``[]`` means one range across every
|
|
439
443
|
#: figure, including the ones ``resolve_one`` never builds. Computed off the
|
|
@@ -731,6 +735,7 @@ def _build_plan_timed(spec: PlotSpec, table: LongTable, timer) -> _Plan:
|
|
|
731
735
|
explode=explode,
|
|
732
736
|
iterate=iterate,
|
|
733
737
|
notes=[collapsed_note] if collapsed_note else [],
|
|
738
|
+
data_warnings=[warning] if (warning := all_nan_warning(frame, y_measure)) else [],
|
|
734
739
|
groups=groups,
|
|
735
740
|
frame=frame,
|
|
736
741
|
y_scope=eligible_scope(spec.y_axis.scope, roles, table),
|
|
@@ -811,6 +816,7 @@ def resolve_one(
|
|
|
811
816
|
axis_frame=plan.frame,
|
|
812
817
|
)
|
|
813
818
|
figure.fanout_notes = plan.notes
|
|
819
|
+
figure.data_warnings = plan.data_warnings
|
|
814
820
|
siblings = _difference_sibling_indices(plan, position)
|
|
815
821
|
if siblings:
|
|
816
822
|
with timing.phase("difference_siblings", extra=f"{len(siblings)} figure(s)"):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
``ResolvedPlot``
|
|
2
|
+
``ResolvedPlot`` â a spec plus data, reduced to exactly what a renderer draws.
|
|
3
3
|
|
|
4
4
|
This intermediate is the reason the interactive plotly view and the exported
|
|
5
5
|
matplotlib figure cannot drift apart. Compiling ``PlotSpec`` straight to each
|
|
@@ -34,23 +34,23 @@ Y_LOW = "__y_low"
|
|
|
34
34
|
Y_HIGH = "__y_high"
|
|
35
35
|
COLOR = "__color"
|
|
36
36
|
SERIES = "__series"
|
|
37
|
-
#: The UNCOLOURED part of a series identity
|
|
37
|
+
#: The UNCOLOURED part of a series identity â the grouping layers that are
|
|
38
38
|
#: neither the colour nor the sample. A line or band per level of these is
|
|
39
39
|
#: told apart by a dash style (docs/claude/grouping-and-collapse.md, D4),
|
|
40
40
|
#: which is what a scientist reads off a printed figure without hovering.
|
|
41
41
|
DASH = "__dash"
|
|
42
42
|
#: The overlay's own colour level (``PlotSpec.sample_color``): the shown key
|
|
43
|
-
#: that colours a "Show sample" point, beside ``__color``
|
|
43
|
+
#: that colours a "Show sample" point, beside ``__color`` â the MARK's level,
|
|
44
44
|
#: which paints the point when it has no colour of its own. Two columns
|
|
45
45
|
#: because the two colourings are independent by construction (a shown key
|
|
46
46
|
#: is collapsed, a coloured layer groups; no factor is both).
|
|
47
47
|
SAMPLE_COLOR = "__sample_color"
|
|
48
|
-
#: On a spaghetti figure, the id of the LINE an overlay point belongs to
|
|
48
|
+
#: On a spaghetti figure, the id of the LINE an overlay point belongs to â
|
|
49
49
|
#: the marks' ``__series`` (the lines layer, plus the units when a collapsed
|
|
50
50
|
#: key is drawn one line each) recomposed on the overlay rows, so the point
|
|
51
51
|
#: is placed at its line's ``series_offsets`` shift rather than on the tick.
|
|
52
52
|
SAMPLE_LINE = "__line"
|
|
53
|
-
#: The RUN a joined point belongs to
|
|
53
|
+
#: The RUN a joined point belongs to â the bracket a polyline stays inside.
|
|
54
54
|
#: A line spans the innermost tick only and never crosses a bracket (every
|
|
55
55
|
#: tick layer above the innermost, ``roles.GroupingLayers.brackets``; user
|
|
56
56
|
#: decision 2026-09-21), so a spaghetti's marks carry the brackets' values
|
|
@@ -60,13 +60,13 @@ SAMPLE_LINE = "__line"
|
|
|
60
60
|
#: subject keeps its slot in every bracket. Empty when nothing splits the runs.
|
|
61
61
|
RUN = "__run"
|
|
62
62
|
Z = "__z"
|
|
63
|
-
#: The one x level a categorical figure has when NO tick layer was chosen
|
|
63
|
+
#: The one x level a categorical figure has when NO tick layer was chosen â
|
|
64
64
|
#: a scalar measure grouped by its colour alone, or not grouped at all. The
|
|
65
65
|
#: mark frame and the overlay frame both write it as their ``__x``
|
|
66
66
|
#: (``reduce._panel_frame`` / ``reduce._overlay_frame``), and
|
|
67
67
|
#: ``ResolvedPlot.x_order`` lists it as the axis' single level so both
|
|
68
68
|
#: renderers read the axis as categorical (``render.base.is_categorical_x``)
|
|
69
|
-
#:
|
|
69
|
+
#: â a "Show sample" overlay only draws on a categorical axis, and
|
|
70
70
|
#: ``x_positions`` would coerce the label to NaN on a numeric one.
|
|
71
71
|
UNLABELLED_X = ""
|
|
72
72
|
|
|
@@ -118,8 +118,8 @@ class Labels:
|
|
|
118
118
|
x: str = ""
|
|
119
119
|
y: str = ""
|
|
120
120
|
color: str | None = None
|
|
121
|
-
#: What the dash styles tell apart
|
|
122
|
-
#: display names, outermost first
|
|
121
|
+
#: What the dash styles tell apart â the uncoloured series layers'
|
|
122
|
+
#: display names, outermost first â for the legend's second block.
|
|
123
123
|
dash: str | None = None
|
|
124
124
|
#: The overlay's own colour key (``PlotSpec.sample_color``), display
|
|
125
125
|
#: name, for the legend's block of overlay levels.
|
|
@@ -140,14 +140,14 @@ class Panel:
|
|
|
140
140
|
key: dict[str, Any] = field(default_factory=dict)
|
|
141
141
|
#: This panel's y range, or None to autoscale.
|
|
142
142
|
#:
|
|
143
|
-
#: **The authority**
|
|
143
|
+
#: **The authority** â ``ResolvedPlot.y_limits`` is derived from these and
|
|
144
144
|
#: exists only for the case where they all agree. Per panel rather than per
|
|
145
145
|
#: figure because ``PlotSpec.y_axis.scope`` may separate limits by a FACET
|
|
146
146
|
#: factor, which is what "autoscale each panel" means.
|
|
147
147
|
y_limits: tuple[float, float] | None = None
|
|
148
148
|
#: "Show sample" overlay rows for this panel, or None: ``__x`` (the
|
|
149
149
|
#: same leaf key the marks use), ``__y``, ``__color`` when coloured,
|
|
150
|
-
#: ``__series`` (the shown keys composed, outermost first
|
|
150
|
+
#: ``__series`` (the shown keys composed, outermost first â one line's
|
|
151
151
|
#: identity when joined) and the shown key columns themselves, for hover.
|
|
152
152
|
#: Built by ``reduce._overlay_frame``; drawn after the marks.
|
|
153
153
|
sample: pd.DataFrame | None = None
|
|
@@ -157,18 +157,18 @@ class Panel:
|
|
|
157
157
|
"""
|
|
158
158
|
What names this panel: the facet VALUES only.
|
|
159
159
|
|
|
160
|
-
The key is already obvious from the figure
|
|
160
|
+
The key is already obvious from the figure â every panel in a grid is
|
|
161
161
|
faceted by the same factor, so repeating "ColName=" on all 13 subplots
|
|
162
162
|
is noise. (``ResolvedPlot.figure_label`` keeps ``key=value``: there the
|
|
163
163
|
figures are separate files and the key is not otherwise visible.)
|
|
164
164
|
|
|
165
165
|
Renderers draw this as the panel's **y-axis title**, not as a caption
|
|
166
|
-
above it (``render.base.panel_y_title``)
|
|
166
|
+
above it (``render.base.panel_y_title``) â the axis title is room the
|
|
167
167
|
panel already spends, so the name costs the grid no height. The property
|
|
168
168
|
keeps its name because it is also the panel's identity in logs, in
|
|
169
169
|
``to_dict`` and in the GUI.
|
|
170
170
|
"""
|
|
171
|
-
return "
|
|
171
|
+
return " · ".join(str(v) for v in self.key.values())
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
@dataclass
|
|
@@ -176,7 +176,7 @@ class ResolvedPlot:
|
|
|
176
176
|
"""
|
|
177
177
|
One figure, fully reduced.
|
|
178
178
|
|
|
179
|
-
``resolve()`` returns a LIST of these
|
|
179
|
+
``resolve()`` returns a LIST of these â one per combination of the spec's
|
|
180
180
|
ITERATE factors, which is the interactive equivalent of the pipeline's
|
|
181
181
|
``for_each`` fan-out over iterated schema keys.
|
|
182
182
|
"""
|
|
@@ -195,7 +195,7 @@ class ResolvedPlot:
|
|
|
195
195
|
#: draw the group labels and brackets beneath the ticks.
|
|
196
196
|
x_plan: Any = None
|
|
197
197
|
#: x levels of the plot (its filtered data, every figure of the fan-out)
|
|
198
|
-
#: this figure has no data for, in axis order
|
|
198
|
+
#: this figure has no data for, in axis order â a nested leaf as its
|
|
199
199
|
#: layer values joined by ``reduce.MISSING_X_JOIN``. Listed whether or not
|
|
200
200
|
#: ``PlotSpec.show_missing_levels`` draws them, so the GUI can offer the
|
|
201
201
|
#: toggle exactly when it would change something.
|
|
@@ -214,12 +214,12 @@ class ResolvedPlot:
|
|
|
214
214
|
#: Headers for rule-defined rows/columns (empty when the panels just flow).
|
|
215
215
|
row_labels: list[str] = field(default_factory=list)
|
|
216
216
|
col_labels: list[str] = field(default_factory=list)
|
|
217
|
-
#: Human-readable notes about placement decisions the user did not ask for
|
|
217
|
+
#: Human-readable notes about placement decisions the user did not ask for â
|
|
218
218
|
#: a panel that spilled out of its ruled cell, a grid that had to grow, a
|
|
219
219
|
#: panel that matched no rule. The layout never silently disobeys a rule;
|
|
220
220
|
#: it says what it did instead. Surfaced in the GUI's Layout section.
|
|
221
221
|
layout_notes: list[str] = field(default_factory=list)
|
|
222
|
-
#: The whole figure's y range
|
|
222
|
+
#: The whole figure's y range â set only when every panel shares it, and
|
|
223
223
|
#: ``None`` when they differ. Derived from the panels in ``reduce``, never
|
|
224
224
|
#: computed separately, so the two can never disagree. A renderer reads the
|
|
225
225
|
#: None as "give each panel its own axis" (``render.base.shares_y_axis``).
|
|
@@ -229,20 +229,24 @@ class ResolvedPlot:
|
|
|
229
229
|
y_scope: list[str] = field(default_factory=list)
|
|
230
230
|
#: Every factor that separates PANELS, as resolved: the figure's ITERATE
|
|
231
231
|
#: keys in fan-out order, then its FACET factors. The factors a y-limit
|
|
232
|
-
#: scope may name
|
|
232
|
+
#: scope may name â offered by the GUI from HERE rather than from
|
|
233
233
|
#: `spec.roles`, because a schema key promoted to ITERATE or a facet the
|
|
234
234
|
#: table defaulted is a panel factor the spec never mentions.
|
|
235
235
|
panel_factors: list[str] = field(default_factory=list)
|
|
236
236
|
#: Set when the data was reduced for transport (see reduce.MAX_TRANSPORT_POINTS).
|
|
237
237
|
downsampled_from: int | None = None
|
|
238
|
-
#: Notes about the FIGURE SET rather than about this figure's layout
|
|
238
|
+
#: Notes about the FIGURE SET rather than about this figure's layout â at
|
|
239
239
|
#: present, schema keys promoted to ITERATE because a nested key iterates.
|
|
240
240
|
#: Identical on every figure of a fan-out (it describes the fan-out), which
|
|
241
241
|
#: is why the GUI reads it from the first one.
|
|
242
242
|
fanout_notes: list[str] = field(default_factory=list)
|
|
243
|
+
#: Problems with the DATA the user should see even if the canvas is out
|
|
244
|
+
#: of view — the GUI shows each as a popup. Like `fanout_notes`, about the
|
|
245
|
+
#: whole figure set; at present only `cell.all_nan_warning`.
|
|
246
|
+
data_warnings: list[str] = field(default_factory=list)
|
|
243
247
|
#: SPAGHETTI only: the horizontal shift each series (subject) keeps at every
|
|
244
248
|
#: x position, keyed by its ``SERIES`` id. Decided once per FIGURE, not per
|
|
245
|
-
#: panel, so a subject sits at the same offset in every facet
|
|
249
|
+
#: panel, so a subject sits at the same offset in every facet â and by one
|
|
246
250
|
#: function (:func:`scistackplot.spaghetti.series_offsets`) so both
|
|
247
251
|
#: renderers and the generated code place the same marker in the same spot.
|
|
248
252
|
series_offsets: dict[str, float] = field(default_factory=dict)
|
|
@@ -251,7 +255,7 @@ class ResolvedPlot:
|
|
|
251
255
|
#: same level in the same style, and the legend can list them.
|
|
252
256
|
dash_styles: dict[str, str] = field(default_factory=dict)
|
|
253
257
|
#: "Show sample" (``PlotSpec.show_sample``): the shown keys, outermost
|
|
254
|
-
#: first
|
|
258
|
+
#: first â the identity of one overlay point â empty when no overlay is
|
|
255
259
|
#: drawn. ``sample_join`` says whether the points sharing that identity
|
|
256
260
|
#: are joined across x, ``sample_join_reason`` why (``roles.overlay_join``),
|
|
257
261
|
#: and ``sample_offsets`` the per-identity horizontal shift inside its
|
|
@@ -264,7 +268,7 @@ class ResolvedPlot:
|
|
|
264
268
|
sample_offsets: dict[str, float] = field(default_factory=dict)
|
|
265
269
|
#: The shown key colouring the overlay (``roles.overlay_color``), None
|
|
266
270
|
#: when a point takes its mark's colour; and that key's levels in
|
|
267
|
-
#: declared order across the WHOLE figure
|
|
271
|
+
#: declared order across the WHOLE figure â the same rule as
|
|
268
272
|
#: ``color_order``, so subject 03 is the same colour in every panel
|
|
269
273
|
#: (``render.base.sample_palette_for`` indexes it, never a panel's own
|
|
270
274
|
#: enumeration).
|
|
@@ -323,7 +327,7 @@ class ResolvedPlot:
|
|
|
323
327
|
return sum(len(p.frame) for p in self.panels)
|
|
324
328
|
|
|
325
329
|
def to_dict(self) -> dict:
|
|
326
|
-
"""JSON-serializable form
|
|
330
|
+
"""JSON-serializable form â used by the GUI transport and by tests."""
|
|
327
331
|
# Lazily: render.base imports this module. The panel's drawn y title
|
|
328
332
|
# comes from the renderers' own rule, so the GUI shows what is drawn.
|
|
329
333
|
from .panels import override_for, unmatched
|
|
@@ -381,6 +385,7 @@ class ResolvedPlot:
|
|
|
381
385
|
],
|
|
382
386
|
"downsampled_from": self.downsampled_from,
|
|
383
387
|
"fanout_notes": list(self.fanout_notes),
|
|
388
|
+
"data_warnings": list(self.data_warnings),
|
|
384
389
|
"series_offsets": dict(self.series_offsets),
|
|
385
390
|
"dash_styles": dict(self.dash_styles),
|
|
386
391
|
"sample": {
|
|
@@ -411,7 +416,7 @@ class ResolvedPlot:
|
|
|
411
416
|
"grid_row": panel.grid_row,
|
|
412
417
|
"grid_col": panel.grid_col,
|
|
413
418
|
# Per panel, because the figure-level value is absent
|
|
414
|
-
# exactly when the panels differ
|
|
419
|
+
# exactly when the panels differ â which is the case the
|
|
415
420
|
# interactive view most needs to draw correctly.
|
|
416
421
|
"y_limits": list(panel.y_limits) if panel.y_limits else None,
|
|
417
422
|
"rows": _frame_records(panel.frame),
|
|
@@ -185,7 +185,9 @@ def eligible_scope(
|
|
|
185
185
|
|
|
186
186
|
dropped = [name for name in wanted if name not in allowed]
|
|
187
187
|
if dropped:
|
|
188
|
-
|
|
188
|
+
# DEBUG, not WARN: the panel shows the eligible scope, and this fired
|
|
189
|
+
# on every resolve of an unchanged plot (31x in scidb.log 2026-10-19).
|
|
190
|
+
Log.debug(
|
|
189
191
|
"y-limit scope ignores %s: only factors that separate PANELS "
|
|
190
192
|
"(iterate, facet) can separate y limits — %s. Limits will be shared "
|
|
191
193
|
"across %s.",
|
|
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
|
|
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
|