scistackplotdb 0.1.26__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.
@@ -0,0 +1,28 @@
1
+ .venv
2
+ .DS_Store
3
+ */*/__pycache__
4
+ others_projects/sciforge
5
+ scistack-gui/extension/node_modules/
6
+ scistack-gui/frontend/node_modules/__pycache__/
7
+ *.pyc
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+ *.egg-info/
12
+ # Python package build output (rebuilt fresh by CI on every publish; a
13
+ # committed dist/ gets deleted by `rm -rf dist` mid-build and dirties the
14
+ # tree for hatch-vcs). Excludes scistack-gui/extension/dist/, which is an
15
+ # intentionally committed compiled bundle, not a Python build artifact.
16
+ /dist/
17
+ /*/dist/
18
+ # Generated database artifacts (DuckDB data/lineage + write-ahead logs)
19
+ *.duckdb
20
+ *.duckdb.wal
21
+ *.wal
22
+ scistack-gui/frontend/node_modules/
23
+ # mkdocs build output (regenerated by `mkdocs build`)
24
+ /site/
25
+ # Runtime output of code_export_service (pipeline-to-code export) — timestamped
26
+ # per-run files, not source.
27
+ /exports/
28
+ *.log
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: scistackplotdb
3
+ Version: 0.1.26
4
+ Summary: scidb-backed plotting: load variables into long tables and plot them with scistackplot
5
+ Author: SciStack Contributors
6
+ License-Expression: MIT
7
+ Keywords: plotting,provenance,scidb,scistack,visualization
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Visualization
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: scistack-db>=0.1.0
19
+ Requires-Dist: scistackplot>=0.1.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: matplotlib>=3.6; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Requires-Dist: seaborn>=0.12; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # scistackplotdb
28
+
29
+ ## Plot what's in the database
30
+
31
+ `scistackplotdb` loads SciDB variables into the long format
32
+ [`scistackplot`](../scistackplot/README.md) consumes, and generates pipeline
33
+ endpoints from a finished plot spec.
34
+
35
+ ```bash
36
+ pip install scistackplotdb
37
+ ```
38
+
39
+ ```python
40
+ from scidb import configure_database
41
+ from scistackplot import PlotSpec, Role, PlotKind, render
42
+ from scistackplotdb import ScidbSource
43
+
44
+ db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
45
+ source = ScidbSource(db)
46
+
47
+ table = source.get_table(["StepLength"])
48
+ spec = PlotSpec(
49
+ measures=["StepLength"],
50
+ roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
51
+ kind=PlotKind.BOX,
52
+ )
53
+ figure = render(table, spec)
54
+ ```
55
+
56
+ ## What this layer actually solves
57
+
58
+ The long format is nearly free — schema keys are already columns once a
59
+ variable is joined to `_schema`, the same shape `stat_` functions receive. The
60
+ real work is the four things a flat CSV never had.
61
+
62
+ **Shape classification.** Scalar, 1-D, or 2-D, decided from observed values
63
+ rather than declared SQL type names, and cached. It determines which plot kinds
64
+ are offered at all.
65
+
66
+ **Joins across schema depth.** Plotting trial-level `Speed` against
67
+ subject-level `Mass` broadcasts the shallower variable down the hierarchy:
68
+
69
+ ```python
70
+ source.joinable_with("StepLength") # -> ["Mass"] (Signal is 1-D: no x axis)
71
+ table = source.get_table(["StepLength", "Mass"]) # one Mass value per trial row
72
+ ```
73
+
74
+ Because the dataset schema is an ordered, contiguous hierarchy, one variable's
75
+ levels are always a prefix of the other's or the two cannot be joined — and
76
+ `join_frames` refuses the latter with a message saying why.
77
+
78
+ **Variants are factors — this one is a correctness trap.** A variable produced
79
+ at two filter cutoffs has *two records per schema combination*. Treating those
80
+ branch params as ordinary columns silently plots two pipelines' results as if
81
+ they were replicates of one:
82
+
83
+ ```python
84
+ spec = PlotSpec(measures=["Scaled"], roles={"session": Role.X})
85
+ validate(spec, table)
86
+ # RoleError: Variant factor(s) ['scale.factor'] would be pooled: their levels
87
+ # are different pipeline variants, not replicates... Assign them
88
+ # 'color'/'facet'/'iterate', select the variants you want with
89
+ # PlotSpec.variant_sets, or — to pool them deliberately — set them to
90
+ # 'aggregate' or 'free' yourself.
91
+ ```
92
+
93
+ **A transport budget.** 1-D data across hundreds of trials is megabytes.
94
+ `resolve(..., max_points=N)` downsamples for the interactive panel; export
95
+ never does.
96
+
97
+ ## From spec to pipeline endpoint
98
+
99
+ ```python
100
+ from scistackplotdb import generate_endpoint
101
+
102
+ code = generate_endpoint(spec, table, input_variable="StepLength")
103
+ print(code.source)
104
+ ```
105
+
106
+ ```python
107
+ def plot_steplength(df, filename):
108
+ ...
109
+ return g.figure
110
+
111
+ for_each(
112
+ plot_steplength,
113
+ inputs={
114
+ "df": StepLength,
115
+ "filename": PathOutput("plots/steplength_{subject}.png"),
116
+ },
117
+ outputs=[StepLengthFigure],
118
+ as_table=['df'],
119
+ finalized=True,
120
+ subject=[],
121
+ )
122
+ ```
123
+
124
+ The one translation that has to be exactly right is `Role.ITERATE` → a
125
+ `for_each` iteration keyword. Interactively, ITERATE fans out through a pandas
126
+ `groupby`; in the pipeline it fans out through `for_each` + `PathOutput`. If
127
+ those disagree, the exported pipeline is not what you previewed —
128
+ `tests/test_fanout_parity.py` runs both paths against the same database and
129
+ compares the figure sets.
130
+
131
+ Everything about *recording* the figure — `finalized`, artifact stamping,
132
+ `skip_computed`, `scidb report` — is SciDB's existing endpoint machinery and is
133
+ untouched.
134
+
135
+ ## Ordering
136
+
137
+ Factor levels are ordered by SciDB's declared `schema_key_types`, not by
138
+ pandas' default: a key declared `numeric` sorts numerically, and everything
139
+ else goes through a natural sort so zero-padded IDs land as
140
+ `01, 02, … 10` instead of `01, 10, 02`.
141
+
142
+ See [`docs/claude/plotting-library-design.md`](../docs/claude/plotting-library-design.md).
@@ -0,0 +1,116 @@
1
+ # scistackplotdb
2
+
3
+ ## Plot what's in the database
4
+
5
+ `scistackplotdb` loads SciDB variables into the long format
6
+ [`scistackplot`](../scistackplot/README.md) consumes, and generates pipeline
7
+ endpoints from a finished plot spec.
8
+
9
+ ```bash
10
+ pip install scistackplotdb
11
+ ```
12
+
13
+ ```python
14
+ from scidb import configure_database
15
+ from scistackplot import PlotSpec, Role, PlotKind, render
16
+ from scistackplotdb import ScidbSource
17
+
18
+ db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
19
+ source = ScidbSource(db)
20
+
21
+ table = source.get_table(["StepLength"])
22
+ spec = PlotSpec(
23
+ measures=["StepLength"],
24
+ roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
25
+ kind=PlotKind.BOX,
26
+ )
27
+ figure = render(table, spec)
28
+ ```
29
+
30
+ ## What this layer actually solves
31
+
32
+ The long format is nearly free — schema keys are already columns once a
33
+ variable is joined to `_schema`, the same shape `stat_` functions receive. The
34
+ real work is the four things a flat CSV never had.
35
+
36
+ **Shape classification.** Scalar, 1-D, or 2-D, decided from observed values
37
+ rather than declared SQL type names, and cached. It determines which plot kinds
38
+ are offered at all.
39
+
40
+ **Joins across schema depth.** Plotting trial-level `Speed` against
41
+ subject-level `Mass` broadcasts the shallower variable down the hierarchy:
42
+
43
+ ```python
44
+ source.joinable_with("StepLength") # -> ["Mass"] (Signal is 1-D: no x axis)
45
+ table = source.get_table(["StepLength", "Mass"]) # one Mass value per trial row
46
+ ```
47
+
48
+ Because the dataset schema is an ordered, contiguous hierarchy, one variable's
49
+ levels are always a prefix of the other's or the two cannot be joined — and
50
+ `join_frames` refuses the latter with a message saying why.
51
+
52
+ **Variants are factors — this one is a correctness trap.** A variable produced
53
+ at two filter cutoffs has *two records per schema combination*. Treating those
54
+ branch params as ordinary columns silently plots two pipelines' results as if
55
+ they were replicates of one:
56
+
57
+ ```python
58
+ spec = PlotSpec(measures=["Scaled"], roles={"session": Role.X})
59
+ validate(spec, table)
60
+ # RoleError: Variant factor(s) ['scale.factor'] would be pooled: their levels
61
+ # are different pipeline variants, not replicates... Assign them
62
+ # 'color'/'facet'/'iterate', select the variants you want with
63
+ # PlotSpec.variant_sets, or — to pool them deliberately — set them to
64
+ # 'aggregate' or 'free' yourself.
65
+ ```
66
+
67
+ **A transport budget.** 1-D data across hundreds of trials is megabytes.
68
+ `resolve(..., max_points=N)` downsamples for the interactive panel; export
69
+ never does.
70
+
71
+ ## From spec to pipeline endpoint
72
+
73
+ ```python
74
+ from scistackplotdb import generate_endpoint
75
+
76
+ code = generate_endpoint(spec, table, input_variable="StepLength")
77
+ print(code.source)
78
+ ```
79
+
80
+ ```python
81
+ def plot_steplength(df, filename):
82
+ ...
83
+ return g.figure
84
+
85
+ for_each(
86
+ plot_steplength,
87
+ inputs={
88
+ "df": StepLength,
89
+ "filename": PathOutput("plots/steplength_{subject}.png"),
90
+ },
91
+ outputs=[StepLengthFigure],
92
+ as_table=['df'],
93
+ finalized=True,
94
+ subject=[],
95
+ )
96
+ ```
97
+
98
+ The one translation that has to be exactly right is `Role.ITERATE` → a
99
+ `for_each` iteration keyword. Interactively, ITERATE fans out through a pandas
100
+ `groupby`; in the pipeline it fans out through `for_each` + `PathOutput`. If
101
+ those disagree, the exported pipeline is not what you previewed —
102
+ `tests/test_fanout_parity.py` runs both paths against the same database and
103
+ compares the figure sets.
104
+
105
+ Everything about *recording* the figure — `finalized`, artifact stamping,
106
+ `skip_computed`, `scidb report` — is SciDB's existing endpoint machinery and is
107
+ untouched.
108
+
109
+ ## Ordering
110
+
111
+ Factor levels are ordered by SciDB's declared `schema_key_types`, not by
112
+ pandas' default: a key declared `numeric` sorts numerically, and everything
113
+ else goes through a natural sort so zero-padded IDs land as
114
+ `01, 02, … 10` instead of `01, 10, 02`.
115
+
116
+ See [`docs/claude/plotting-library-design.md`](../docs/claude/plotting-library-design.md).
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.version]
6
+ source = "vcs"
7
+ raw-options = { search_parent_directories = true, local_scheme = "no-local-version" }
8
+
9
+ [project]
10
+ name = "scistackplotdb"
11
+ dynamic = ["version"]
12
+ description = "scidb-backed plotting: load variables into long tables and plot them with scistackplot"
13
+ readme = "README.md"
14
+ license = "MIT"
15
+ requires-python = ">=3.10"
16
+ authors = [
17
+ { name = "SciStack Contributors" }
18
+ ]
19
+ keywords = [
20
+ "plotting",
21
+ "visualization",
22
+ "scidb",
23
+ "scistack",
24
+ "provenance",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Science/Research",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Topic :: Scientific/Engineering :: Visualization",
35
+ "Typing :: Typed",
36
+ ]
37
+ dependencies = [
38
+ "scistackplot>=0.1.0",
39
+ "scistack-db>=0.1.0",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=7.0",
45
+ "pytest-cov>=4.0",
46
+ "matplotlib>=3.6",
47
+ "seaborn>=0.12",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.sdist]
51
+ include = [
52
+ "/src",
53
+ ]
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/scistackplotdb"]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
@@ -0,0 +1,80 @@
1
+ """
2
+ scistackplotdb — plot scidb variables.
3
+
4
+ Loads variables into the long format ``scistackplot`` consumes, adds the four
5
+ things a flat table never needed (shape classification, schema-depth joins,
6
+ variants as factors, and a transport budget), and generates pipeline endpoints
7
+ from a finished spec.
8
+
9
+ ::
10
+
11
+ from scidb import configure_database
12
+ from scistackplot import PlotSpec, Role, PlotKind, render
13
+ from scistackplotdb import ScidbSource
14
+
15
+ db = configure_database("experiment.duckdb", ["subject", "session", "trial"])
16
+ source = ScidbSource(db)
17
+
18
+ table = source.get_table(["StepLength"])
19
+ spec = PlotSpec(
20
+ measures=["StepLength"],
21
+ roles={"session": Role.X, "subject": Role.FREE, "trial": Role.FREE},
22
+ kind=PlotKind.BOX,
23
+ )
24
+ figure = render(table, spec)
25
+
26
+ Everything about recording a figure — ``finalized``, artifact stamping,
27
+ ``scidb report`` — belongs to scidb's existing ``plot_`` endpoint machinery and
28
+ is unchanged. See ``docs/claude/plotting-library-design.md``.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from .endpoint import (
34
+ EndpointCode,
35
+ default_output_variable,
36
+ default_path_template,
37
+ generate_endpoint,
38
+ )
39
+ from .hierarchy import join_frames, join_kind, joinable, joined_levels
40
+ from .load import (
41
+ LATEST_COLUMN,
42
+ MISSING_VERSION_LEVEL,
43
+ VERSION_FACTOR_PREFIX,
44
+ VariableFrame,
45
+ attach_variants,
46
+ data_columns_for,
47
+ load_variable,
48
+ registered_variables,
49
+ sample_value,
50
+ schema_keys,
51
+ )
52
+ from .source import ScidbSource
53
+ from .variants import selection_for, variant_graph, variant_set
54
+
55
+ __all__ = [
56
+ "ScidbSource",
57
+ "variant_set",
58
+ "variant_graph",
59
+ "selection_for",
60
+ "VariableFrame",
61
+ "VERSION_FACTOR_PREFIX",
62
+ "MISSING_VERSION_LEVEL",
63
+ "LATEST_COLUMN",
64
+ "load_variable",
65
+ "attach_variants",
66
+ "registered_variables",
67
+ "schema_keys",
68
+ "data_columns_for",
69
+ "sample_value",
70
+ "join_kind",
71
+ "joinable",
72
+ "join_frames",
73
+ "joined_levels",
74
+ "generate_endpoint",
75
+ "EndpointCode",
76
+ "default_output_variable",
77
+ "default_path_template",
78
+ ]
79
+
80
+ __version__ = "0.1.0"
@@ -0,0 +1,257 @@
1
+ """
2
+ Turning a ``PlotSpec`` into a pipeline endpoint.
3
+
4
+ The GUI's "Add to pipeline" produces two things: a ``plot_`` function (generated
5
+ by ``scistackplot.codegen`` — literal seaborn, no runtime dependency on this
6
+ package) and the ``for_each`` call that runs it. This module owns the second
7
+ half, and with it the one translation that has to be exactly right:
8
+
9
+ Role.ITERATE -> a for_each iteration keyword
10
+
11
+ Interactively, ITERATE fans out through a pandas ``groupby`` inside
12
+ ``resolve()``. In the pipeline it fans out through ``for_each`` + ``PathOutput``.
13
+ If those two ever disagree, the exported pipeline is not what the user
14
+ previewed — the worst failure mode this layer has, and what
15
+ ``tests/test_fanout_parity.py`` exists to prevent.
16
+
17
+ Everything else the endpoint needs already exists in scidb: ``finalized``,
18
+ artifact stamping, ``skip_computed`` and ``scidb report`` are untouched.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from dataclasses import dataclass, field
25
+
26
+ from scistacklog import Log
27
+ from scistackplot import LongTable, PlotSpec, default_function_name
28
+ from scistackplot import generate_plot_function
29
+ from scistackplot.codegen import group_param, variant_params
30
+ from scistackplot.roles import fanout_keys
31
+ from scistackplot.variants import defined_sets
32
+
33
+ from .load import LATEST_COLUMN
34
+
35
+ LAYER = "scistackplotdb"
36
+
37
+
38
+ @dataclass
39
+ class EndpointCode:
40
+ """Generated source for one plotting endpoint."""
41
+
42
+ function_name: str
43
+ function_source: str
44
+ foreach_source: str
45
+ iterate_keys: list[str] = field(default_factory=list)
46
+ path_template: str = ""
47
+ output_variable: str = ""
48
+
49
+ @property
50
+ def source(self) -> str:
51
+ """Function and call together, ready to append to a pipeline module."""
52
+ return f"{self.function_source}\n\n{self.foreach_source}"
53
+
54
+
55
+ def generate_endpoint(
56
+ spec: PlotSpec,
57
+ table: LongTable,
58
+ *,
59
+ input_variable: str,
60
+ function_name: str | None = None,
61
+ output_variable: str | None = None,
62
+ path_template: str | None = None,
63
+ finalized: bool = True,
64
+ x_variable: str | None = None,
65
+ ) -> EndpointCode:
66
+ """
67
+ Generate the ``plot_`` function and its ``for_each`` call.
68
+
69
+ ``input_variable`` is the scidb variable type supplying the data;
70
+ ``x_variable`` is the optional second measure for an x–y scatter.
71
+ """
72
+ name = function_name or default_function_name(spec)
73
+ # NOT spec.iterate_factors: the fan-out includes schema keys promoted
74
+ # because a nested key iterates, and runs in schema order. The preview and
75
+ # the generated for_each have to agree on both — that is what
76
+ # tests/test_fanout_parity.py checks.
77
+ iterate_keys = fanout_keys(spec, table)
78
+ output = output_variable or default_output_variable(input_variable)
79
+ template = path_template or default_path_template(name, iterate_keys)
80
+
81
+ function_source = generate_plot_function(spec, table, function_name=name)
82
+ foreach_source = _foreach_call(
83
+ spec=spec,
84
+ function_name=name,
85
+ input_variable=input_variable,
86
+ x_variable=x_variable,
87
+ output_variable=output,
88
+ path_template=template,
89
+ iterate_keys=iterate_keys,
90
+ finalized=finalized,
91
+ )
92
+
93
+ Log.info(
94
+ "generated endpoint %s: input=%s output=%s iterate=%s finalized=%s",
95
+ name,
96
+ input_variable,
97
+ output,
98
+ iterate_keys or "none",
99
+ finalized,
100
+ layer=LAYER,
101
+ )
102
+ return EndpointCode(
103
+ function_name=name,
104
+ function_source=function_source,
105
+ foreach_source=foreach_source,
106
+ iterate_keys=iterate_keys,
107
+ path_template=template,
108
+ output_variable=output,
109
+ )
110
+
111
+
112
+ def default_output_variable(input_variable: str) -> str:
113
+ """``StepLength`` -> ``StepLengthFigure``."""
114
+ return f"{input_variable}Figure"
115
+
116
+
117
+ def default_path_template(function_name: str, iterate_keys: list[str]) -> str:
118
+ """
119
+ Build a PathOutput template that cannot collide.
120
+
121
+ Every ITERATE key goes into the filename. Omitting one would make two
122
+ figures write the same file; for schema keys scidb treats that as
123
+ pre-existing overwrite behavior (no error), and for variants its collision
124
+ guard raises before anything renders. Including them all avoids both.
125
+ """
126
+ slug = re.sub(r"^plot_", "", function_name)
127
+ parts = "".join(f"_{{{key}}}" for key in iterate_keys)
128
+ return f"plots/{slug}{parts}.png"
129
+
130
+
131
+ def variant_expression(input_variable: str, variant_set) -> str:
132
+ """A ``Variant(...)`` call selecting one named variant's records.
133
+
134
+ The inverse of :func:`~scistackplotdb.variants.selection_for`, and the thing
135
+ that makes a variant figure reproducible by hand: what the popup's
136
+ checkboxes and version dropdowns produced comes back out as the same
137
+ wrapper a scientist would have typed.
138
+
139
+ Selections spanning two producing functions **nest** rather than resorting
140
+ to dotted-string kwargs::
141
+
142
+ Variant(Variant(EMG, fn="loadEMG", code_version="v1"), fn="bandpass", low_hz=20)
143
+
144
+ Nesting is documented, merges the two filters (``scidb.variant.Variant``
145
+ handles it explicitly), and keeps every keyword readable — where
146
+ ``**{"__code__.loadEMG": "v1"}`` would leak a reserved namespace into code a
147
+ user is meant to edit.
148
+ """
149
+ from scistackplot import CODE_FACTOR_PREFIX, LATEST
150
+
151
+ by_function: dict[str, dict[str, object]] = {}
152
+ for column, value in (variant_set.selection or {}).items():
153
+ if column.startswith(CODE_FACTOR_PREFIX):
154
+ fn_name = column[len(CODE_FACTOR_PREFIX) :]
155
+ by_function.setdefault(fn_name, {})["code_version"] = value
156
+ elif column == LATEST_COLUMN:
157
+ # The source's "these are the current records" recommendation. scidb
158
+ # spells the same thing `code_version="latest"`, resolved per schema
159
+ # location by the same rule — not "the highest ordinal".
160
+ by_function.setdefault(None, {})["code_version"] = LATEST
161
+ else:
162
+ fn_name, _, param = column.rpartition(".")
163
+ by_function.setdefault(fn_name or None, {})[param] = value
164
+
165
+ expression = input_variable
166
+ for fn_name in sorted(by_function, key=lambda n: (n is None, n or "")):
167
+ arguments = []
168
+ if fn_name:
169
+ arguments.append(f"fn={fn_name!r}")
170
+ arguments.extend(f"{key}={value!r}" for key, value in by_function[fn_name].items())
171
+ expression = f"Variant({expression}, {', '.join(arguments)})"
172
+ return expression
173
+
174
+
175
+ def _single_variant_expression(input_variable: str, spec) -> str:
176
+ """The lone variant's pin, or the bare variable when nothing is selected.
177
+
178
+ The row may name its own variable, in which case that is what the endpoint
179
+ loads — ``input_variable`` is only the default.
180
+ """
181
+ sets = defined_sets(spec.variant_sets)
182
+ if len(sets) != 1:
183
+ return input_variable
184
+ return variant_expression(sets[0].variable or input_variable, sets[0])
185
+
186
+
187
+ def _foreach_call(
188
+ *,
189
+ spec: PlotSpec,
190
+ function_name: str,
191
+ input_variable: str,
192
+ x_variable: str | None,
193
+ output_variable: str,
194
+ path_template: str,
195
+ iterate_keys: list[str],
196
+ finalized: bool,
197
+ ) -> str:
198
+ variant_inputs = variant_params(spec)
199
+ if variant_inputs:
200
+ # One input per named variant, each loaded through its own pin. See
201
+ # `codegen.variant_params` for why this cannot be a single `df`.
202
+ # Zipped against `defined_sets`, not `spec.variant_sets`: unfilled rows
203
+ # produce no input, so indexing the raw list would pair a parameter with
204
+ # the wrong variant's selection.
205
+ inputs = [
206
+ f' "{generated.param}": '
207
+ f"{variant_expression(generated.variable, variant)},"
208
+ for generated, variant in zip(
209
+ variant_inputs, defined_sets(spec.variant_sets), strict=True
210
+ )
211
+ ]
212
+ table_inputs = [generated.param for generated in variant_inputs]
213
+ else:
214
+ pinned = _single_variant_expression(input_variable, spec)
215
+ inputs = [f' "df": {pinned},']
216
+ table_inputs = ["df"]
217
+ if x_variable:
218
+ inputs.append(f' "df_x": {x_variable},')
219
+ table_inputs.append("df_x")
220
+ for group in spec.factor_variables:
221
+ # A grouping variable arrives as its own input and is merged onto the
222
+ # data inside the function: `as_table` hands a function schema keys and
223
+ # data columns only, so a subject-level Condition cannot ride along on
224
+ # the measure's frame.
225
+ inputs.append(f' "{group_param(group)}": {group},')
226
+ table_inputs.append(group_param(group))
227
+ inputs.append(f' "filename": PathOutput("{path_template}"),')
228
+
229
+ lines = [
230
+ "for_each(",
231
+ f" {function_name},",
232
+ " inputs={",
233
+ *inputs,
234
+ " },",
235
+ f" outputs=[{output_variable}],",
236
+ # A plot_ function receives the long-format table (schema keys as
237
+ # columns) — as_table defaults ON only for stat_, so say it explicitly.
238
+ f" as_table={table_inputs!r},",
239
+ f" finalized={finalized},",
240
+ ]
241
+ for key in iterate_keys:
242
+ # [] means "every value present" — the same all-values resolution
243
+ # scifor applies to an empty iteration list.
244
+ lines.append(f" {key}=[],")
245
+ lines.append(")")
246
+ return "\n".join(lines)
247
+
248
+
249
+ def required_declarations(code: EndpointCode) -> list[str]:
250
+ """
251
+ Variable types the generated call needs that may not exist yet.
252
+
253
+ The GUI declares these through the normal entity-declaration path before
254
+ writing the code, so a generated endpoint never references an undeclared
255
+ type.
256
+ """
257
+ return [code.output_variable]