scistackplot 0.1.29__tar.gz → 0.1.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.
- {scistackplot-0.1.29 → scistackplot-0.1.31}/.gitignore +1 -0
- scistackplot-0.1.31/PKG-INFO +486 -0
- scistackplot-0.1.31/README.md +450 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/pyproject.toml +2 -2
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/__init__.py +181 -4
- scistackplot-0.1.31/src/scistackplot/aliases.py +415 -0
- scistackplot-0.1.31/src/scistackplot/autosize.py +557 -0
- scistackplot-0.1.31/src/scistackplot/capability.py +917 -0
- scistackplot-0.1.31/src/scistackplot/cell.py +209 -0
- scistackplot-0.1.31/src/scistackplot/codegen.py +2821 -0
- scistackplot-0.1.31/src/scistackplot/colors.py +333 -0
- scistackplot-0.1.31/src/scistackplot/compare.py +645 -0
- scistackplot-0.1.31/src/scistackplot/diffbars.py +1310 -0
- scistackplot-0.1.31/src/scistackplot/export.py +610 -0
- scistackplot-0.1.31/src/scistackplot/figsize.py +118 -0
- scistackplot-0.1.31/src/scistackplot/figure_file.py +128 -0
- scistackplot-0.1.31/src/scistackplot/groups.py +198 -0
- scistackplot-0.1.31/src/scistackplot/panels.py +197 -0
- scistackplot-0.1.31/src/scistackplot/paper.py +200 -0
- scistackplot-0.1.31/src/scistackplot/presets.py +495 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/reduce.py +1269 -240
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/reducer.py +206 -116
- scistackplot-0.1.31/src/scistackplot/render/__init__.py +38 -0
- scistackplot-0.1.31/src/scistackplot/render/base.py +1064 -0
- scistackplot-0.1.31/src/scistackplot/render/mpl.py +1874 -0
- scistackplot-0.1.31/src/scistackplot/render/plotly_.py +1498 -0
- scistackplot-0.1.31/src/scistackplot/resolved.py +434 -0
- scistackplot-0.1.31/src/scistackplot/restore.py +552 -0
- scistackplot-0.1.31/src/scistackplot/roles.py +1348 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/series_stats.py +29 -1
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/sources/__init__.py +2 -2
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/sources/base.py +164 -13
- scistackplot-0.1.31/src/scistackplot/spaghetti.py +65 -0
- scistackplot-0.1.31/src/scistackplot/spec.py +1410 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/table.py +144 -5
- scistackplot-0.1.31/src/scistackplot/textsize.py +209 -0
- scistackplot-0.1.31/src/scistackplot/ticklabels.py +632 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/variants.py +142 -23
- scistackplot-0.1.31/src/scistackplot/weights.py +125 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/xaxis.py +14 -1
- scistackplot-0.1.31/src/scistackplot/ylimits.py +806 -0
- scistackplot-0.1.29/PKG-INFO +0 -213
- scistackplot-0.1.29/README.md +0 -177
- scistackplot-0.1.29/src/scistackplot/capability.py +0 -685
- scistackplot-0.1.29/src/scistackplot/codegen.py +0 -943
- scistackplot-0.1.29/src/scistackplot/groups.py +0 -110
- scistackplot-0.1.29/src/scistackplot/render/__init__.py +0 -26
- scistackplot-0.1.29/src/scistackplot/render/base.py +0 -262
- scistackplot-0.1.29/src/scistackplot/render/mpl.py +0 -458
- scistackplot-0.1.29/src/scistackplot/render/plotly_.py +0 -537
- scistackplot-0.1.29/src/scistackplot/resolved.py +0 -240
- scistackplot-0.1.29/src/scistackplot/roles.py +0 -431
- scistackplot-0.1.29/src/scistackplot/spec.py +0 -788
- scistackplot-0.1.29/src/scistackplot/ylimits.py +0 -452
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/dedup.py +0 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/framesize.py +0 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/numeric.py +0 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/shape.py +0 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/sources/csv.py +0 -0
- {scistackplot-0.1.29 → scistackplot-0.1.31}/src/scistackplot/sources/frame.py +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scistackplot
|
|
3
|
+
Version: 0.1.31
|
|
4
|
+
Summary: Spec-driven plotting for long-format scientific data — standalone, GUI-friendly
|
|
5
|
+
Author: SciStack Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: data-science,matplotlib,plotly,plotting,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.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: numpy>=1.21
|
|
21
|
+
Requires-Dist: pandas>=1.5
|
|
22
|
+
Requires-Dist: scistacklog>=0.1.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: matplotlib>=3.6; extra == 'dev'
|
|
25
|
+
Requires-Dist: plotly>=5.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: seaborn>=0.13; extra == 'dev'
|
|
29
|
+
Requires-Dist: tomli-w>=1.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: interactive
|
|
31
|
+
Requires-Dist: plotly>=5.0; extra == 'interactive'
|
|
32
|
+
Provides-Extra: mpl
|
|
33
|
+
Requires-Dist: matplotlib>=3.6; extra == 'mpl'
|
|
34
|
+
Requires-Dist: seaborn>=0.13; extra == 'mpl'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# scistackplot
|
|
38
|
+
|
|
39
|
+
## Build the figure by looking at it, then keep it
|
|
40
|
+
|
|
41
|
+
`scistackplot` turns a long-format table into a figure from a small,
|
|
42
|
+
serializable description — a `PlotSpec`. It works standalone on a CSV or a
|
|
43
|
+
DataFrame with no database and no configuration, and the same `PlotSpec` is
|
|
44
|
+
exactly what the body of a SciDB `plot_` endpoint needs, so an interactive
|
|
45
|
+
exploration can be frozen into a lineage-tracked pipeline step.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install scistackplot
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## The idea
|
|
52
|
+
|
|
53
|
+
A plotting GUI looks like it produces pictures. It doesn't — it produces a
|
|
54
|
+
**specification**, and the picture is a view of it. That is what lets an
|
|
55
|
+
inherently visual tool live inside a reproducible pipeline:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import pandas as pd
|
|
59
|
+
from scistackplot import DataFrameSource, PlotSpec, Role, PlotKind, render
|
|
60
|
+
|
|
61
|
+
source = DataFrameSource(pd.read_csv("gait.csv"))
|
|
62
|
+
spec = PlotSpec(
|
|
63
|
+
measures=["StepLength"],
|
|
64
|
+
roles={"session": Role.GROUP, "limb": Role.GROUP, "subject": Role.COLLAPSE},
|
|
65
|
+
groups=["limb", "session"], # innermost first: limbs inside each session tick
|
|
66
|
+
color="limb",
|
|
67
|
+
kind=PlotKind.BOX,
|
|
68
|
+
)
|
|
69
|
+
figure = render(source, spec)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Every factor does exactly one thing
|
|
73
|
+
|
|
74
|
+
The whole control surface is one rule: each categorical column carries exactly
|
|
75
|
+
one role, in one of two panes (`docs/claude/grouping-and-collapse.md`).
|
|
76
|
+
|
|
77
|
+
| Role | Meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `GROUP` | one **mark** per level — a bar, a box, a line. Ordered by `PlotSpec.groups`, innermost first; one layer may be the `color` |
|
|
80
|
+
| `FACET` | one subplot per level (arranged by `FacetOptions`) |
|
|
81
|
+
| `ITERATE` | a separate **figure** per level — the default for an unmentioned factor |
|
|
82
|
+
| `COLLAPSE` | averaged away. The **last** collapsed key is the *sample* |
|
|
83
|
+
|
|
84
|
+
Collapsed keys average away deepest first, nested and unweighted — "trial
|
|
85
|
+
within subject, then subject" — so each subject counts once however many
|
|
86
|
+
trials it has (`Aggregation(pooled=True)` is the deliberate alternative). What
|
|
87
|
+
remains at each mark is the sample, and **every kind draws the sample**:
|
|
88
|
+
bar and band draw its centre ± spread, box and violin its distribution,
|
|
89
|
+
scatter and strip one point per sample row, line one line per sample level.
|
|
90
|
+
(A spaghetti whose sample cannot be joined across its x axis, such as trials
|
|
91
|
+
under session ticks, draws each line through their mean.)
|
|
92
|
+
|
|
93
|
+
Which plot kinds are available follows from that assignment plus the measure's
|
|
94
|
+
shape, through one pure function:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from scistackplot import available_plots, default_plot, Shape
|
|
98
|
+
|
|
99
|
+
available_plots(Shape.SCALAR, {"session": Role.GROUP}) # scatter, strip, bar
|
|
100
|
+
available_plots(Shape.SCALAR, {"session": Role.GROUP, "trial": Role.COLLAPSE}) # + box, violin
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A distribution needs a sample, and a sample exists only when some factor is
|
|
104
|
+
collapsed. That single rule produces both defaults and availability:
|
|
105
|
+
|
|
106
|
+
| Measure shape | no sample | with a sample |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| scalar | scatter, or a bar with no error bar | box / violin / bar + CI |
|
|
109
|
+
| 1-D array | one line per leaf group | mean line + shaded error band |
|
|
110
|
+
| 2-D | heatmap | mean heatmap |
|
|
111
|
+
|
|
112
|
+
"Average over trials, then show the spread across subjects" is
|
|
113
|
+
`trial=COLLAPSE, subject=COLLAPSE`: trial averages within each subject
|
|
114
|
+
first, and subject — the last — is the sample the error bars are drawn over.
|
|
115
|
+
|
|
116
|
+
**Spaghetti** is the repeated-measures view: one marker per level of the
|
|
117
|
+
**first** grouping layer at each x position, joined by a line — `groups =
|
|
118
|
+
["subject", "session", "Intervention"]` draws every subject's line across
|
|
119
|
+
sessions inside their group. It needs two grouping layers, and the same list
|
|
120
|
+
read as a bar plot nests subject bars inside session ticks inside Intervention
|
|
121
|
+
brackets. Each subject keeps one small deterministic offset at every x
|
|
122
|
+
position (never a random jitter — a line must end on its own markers),
|
|
123
|
+
decided once per figure by `scistackplot.spaghetti.series_offsets` and read by
|
|
124
|
+
both renderers and the generated code.
|
|
125
|
+
|
|
126
|
+
**Show sample** puts the data behind a summary on top of it. `show_sample =
|
|
127
|
+
["trial"]` on that box plot cuts the collapse chain before `trial`: every
|
|
128
|
+
trial of every subject is drawn as a small point inside its box (a deeper
|
|
129
|
+
key implies the shallower ones — a trial is a trial *of* a subject), and
|
|
130
|
+
`["subject"]` shows one point per subject, the sample itself. Points are
|
|
131
|
+
joined into lines when they are repeated measures — the shown key sits above
|
|
132
|
+
the x axis's key in the schema, so each subject has a value at every session
|
|
133
|
+
— and left as points otherwise; `join_sample=True/False` overrides the rule.
|
|
134
|
+
The marks never change, the y axis grows to hold the points, and the exported
|
|
135
|
+
code draws the same overlay.
|
|
136
|
+
|
|
137
|
+
## Compare to a reference level
|
|
138
|
+
|
|
139
|
+
Draw each level of a grouping layer relative to one of them, as a difference
|
|
140
|
+
or a % change. The reference sits at 0:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from scistackplot import Comparison, CompareMode
|
|
144
|
+
|
|
145
|
+
spec = replace(spec, comparison=Comparison(layer="session", level="1", mode=CompareMode.PERCENT))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
It is **paired** when each unit has its own reference value (a subject is
|
|
149
|
+
measured at every session), which is the rule "Show sample" uses to join
|
|
150
|
+
points. Otherwise each value is compared with the reference mark's centre.
|
|
151
|
+
Units with no reference, a % change from a reference <= 0, or a 1-D series
|
|
152
|
+
whose length differs from its reference are dropped and named in the log.
|
|
153
|
+
A comparison that cannot apply (a log axis, an x-y plot, a layer that no
|
|
154
|
+
longer groups) is inert: the raw values are drawn and the reason is
|
|
155
|
+
reported. The exported code and "Save data" compare the same way, and the saved value column is named after what it holds (`M, % change from session 1`). See
|
|
156
|
+
`docs/claude/compare-to-reference.md`.
|
|
157
|
+
|
|
158
|
+
## Save the data behind a figure
|
|
159
|
+
|
|
160
|
+
The rows a plot is drawn from are available as a long table, so the
|
|
161
|
+
statistics run on exactly what the figure shows:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from scistackplot import plot_data
|
|
165
|
+
|
|
166
|
+
data = plot_data(spec, table) # the plotted sample
|
|
167
|
+
data.to_csv("step_length.csv", index=False)
|
|
168
|
+
deeper = plot_data(spec, table, depth="trial") # keep trials, average cycles only
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Every figure of a Separate-figures fan-out is in one frame, with the figure
|
|
172
|
+
keys as columns. The rows come from the same plan and the same sample step
|
|
173
|
+
the figure uses: filters, variants and the cell statistic are all applied
|
|
174
|
+
already. `data_export_options` lists the depths and the header each one
|
|
175
|
+
writes. A struct variable's fields (`ColName`) are written one column per
|
|
176
|
+
field by default; pass `fields_as_columns=False` for one row per field.
|
|
177
|
+
Scalar plots only; a raw 1-D or 2-D plot is refused with the reason.
|
|
178
|
+
See `docs/claude/plot-data-export.md`.
|
|
179
|
+
|
|
180
|
+
## Reopen a spec an older version wrote
|
|
181
|
+
|
|
182
|
+
A spec saved last month may name settings this version has renamed or
|
|
183
|
+
removed. `PlotSpec.from_dict` is strict and refuses it. `restore_spec`
|
|
184
|
+
keeps everything that still reads and tells you what did not:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from scistackplot import reconcile, restore_spec
|
|
188
|
+
|
|
189
|
+
restored = restore_spec(stored_dict, fallback_measure="StepLength")
|
|
190
|
+
for note in restored.notes: # e.g. "style.widht: 'widht' is no longer a setting; ignored"
|
|
191
|
+
print(note.path, note.kind, note.message)
|
|
192
|
+
|
|
193
|
+
checked = reconcile(restored.spec, table) # names today's data no longer has
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
It never migrates. A renamed setting takes its default and gets a note. It
|
|
197
|
+
never raises, except when there is no usable measure and no
|
|
198
|
+
`fallback_measure`. `reconcile` removes `show_sample` keys and a
|
|
199
|
+
`sample_color` that are no longer factors, because the figure would refuse
|
|
200
|
+
them. It reports stale roles and leaves them in place, because resolve drops
|
|
201
|
+
them itself. See `docs/claude/saved-plots.md`.
|
|
202
|
+
|
|
203
|
+
## Presets: the settings without the data
|
|
204
|
+
|
|
205
|
+
`make_template` takes a spec's settings without the variable: no measure,
|
|
206
|
+
no variant rows, no title, y label or y limits. `apply_preset` draws another
|
|
207
|
+
variable with them and checks the result against that variable's data:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from scistackplot import apply_preset, make_template
|
|
211
|
+
|
|
212
|
+
template = make_template(step_length_spec) # plain JSON
|
|
213
|
+
applied = apply_preset(template, PlotSpec(measures=["StepWidth"]), table=width_table)
|
|
214
|
+
applied.spec, applied.notes # notes: roles the data lacks, a kind it cannot draw, ...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`presets.FIELD_CLASSES` is the one statement of which settings travel. The
|
|
218
|
+
GUI's Presets section and `scistackplotdb.save_preset` store templates in
|
|
219
|
+
the project database. See `docs/claude/plot-presets.md`.
|
|
220
|
+
|
|
221
|
+
## Arranging the subplots
|
|
222
|
+
|
|
223
|
+
Faceted panels flow in order by default, wrapping at `FacetOptions.wrap`. When
|
|
224
|
+
the arrangement matters, describe it with **rules** instead of positions:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from scistackplot import FacetOptions, MatchOp, Matcher, PlotSpec, Role
|
|
228
|
+
|
|
229
|
+
spec = PlotSpec(
|
|
230
|
+
measures=["RawEMG"],
|
|
231
|
+
roles={"ColName": Role.FACET, "subject": Role.GROUP},
|
|
232
|
+
color="subject",
|
|
233
|
+
facet=FacetOptions(
|
|
234
|
+
rows=[Matcher(op=MatchOp.STARTS_WITH, value="R"),
|
|
235
|
+
Matcher(op=MatchOp.STARTS_WITH, value="L")],
|
|
236
|
+
cols=[Matcher(op=MatchOp.ENDS_WITH, value="HAM"),
|
|
237
|
+
Matcher(op=MatchOp.ENDS_WITH, value="TA")],
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Rules describe a layout rather than a hand-arrangement, so the same
|
|
243
|
+
`FacetOptions` applies to any variable whose panels are named the same way.
|
|
244
|
+
Ops are `starts_with`, `ends_with`, `contains`, `not_contains`, `equals` and
|
|
245
|
+
`regex`; a panel matching no rule lands in a trailing "other" row or column
|
|
246
|
+
rather than vanishing.
|
|
247
|
+
|
|
248
|
+
Each panel is named on its **y axis**, not by a caption above it. A caption
|
|
249
|
+
spends a strip of every row of the grid on text; the axis title is room the
|
|
250
|
+
panel was already spending, so a 4x3 grid gets that height back for the data.
|
|
251
|
+
The generated seaborn code says the same thing (`g.set_titles("")`), because
|
|
252
|
+
the export must be the figure you previewed.
|
|
253
|
+
|
|
254
|
+
## Ordering is not cosmetic
|
|
255
|
+
|
|
256
|
+
Zero-padded IDs (`"01"`, `"02"`, … `"10"`) sort lexicographically into
|
|
257
|
+
1, 10, 2 under pandas' default — visibly wrong on an axis, and wrong in a way
|
|
258
|
+
that looks like a data problem. `LongTable` carries each factor's real level
|
|
259
|
+
order; sources that know better (SciDB knows its declared `schema_key_types`)
|
|
260
|
+
supply it explicitly, and everything else falls back to a natural sort.
|
|
261
|
+
|
|
262
|
+
## Rendering
|
|
263
|
+
|
|
264
|
+
Two backends translate the same reduced plot, so the interactive view and the
|
|
265
|
+
exported figure cannot disagree:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from scistackplot import resolve, render_matplotlib, render_plotly
|
|
269
|
+
|
|
270
|
+
resolved = resolve(spec, table) # all reduction happens here
|
|
271
|
+
figure = render_matplotlib(resolved[0]) # export / pipeline — a Figure
|
|
272
|
+
payload = render_plotly(resolved[0]) # interactive — a plotly.js dict
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`render_plotly` builds plain JSON and needs no plotly package.
|
|
276
|
+
|
|
277
|
+
## Legible x labels
|
|
278
|
+
|
|
279
|
+
Crowded tick labels are fitted, not left to collide. One pure function,
|
|
280
|
+
`scistackplot.fit_labels`, decides for the whole figure (the most crowded
|
|
281
|
+
panel decides for every panel), trying the least destructive fix first:
|
|
282
|
+
|
|
283
|
+
1. **Numbered labels** (every label is, or ends in, a number) drop the prefix
|
|
284
|
+
they share: `SS01 … SS40` → `01 … 40`. Names are never touched.
|
|
285
|
+
2. Wrap at `_ - / . space` onto two lines.
|
|
286
|
+
3. Shrink the font, never below 8pt or 70% of the tick size.
|
|
287
|
+
4. Rotate 45°, then 90°.
|
|
288
|
+
5. Numbered labels only: show every k-th, keeping each bracket's first and
|
|
289
|
+
last. Names stop at step 4 and the log WARNs that they still overlap.
|
|
290
|
+
|
|
291
|
+
The matplotlib export measures the real text; bracket rows are placed a fixed
|
|
292
|
+
number of points below the fitted tick labels and fitted too (shrink and wrap
|
|
293
|
+
only). A nested axis has **no x title**: its tick and bracket rows already name
|
|
294
|
+
every level. `StyleOptions.hide_legend_ticks=True` blanks the labels of a
|
|
295
|
+
layer that is also the colour while the legend lists it. Any of the fitted
|
|
296
|
+
settings can be fixed instead: `tick_rotation`, `text.x_ticks`,
|
|
297
|
+
`tick_every` (None = fitted). A fixed value is kept even where it overlaps, and
|
|
298
|
+
the fit reports that it does. The generated seaborn code replays the fit as
|
|
299
|
+
operations on seaborn's own labels, and saves without `bbox_inches="tight"`. See
|
|
300
|
+
`.claude/plan-tick-label-legibility.md`.
|
|
301
|
+
|
|
302
|
+
The exported legend fits too. At the right it may take 30% of the width
|
|
303
|
+
(`LEGEND_BUDGET`); past that the title wraps at " / ", the line samples
|
|
304
|
+
shorten and the text shrinks (never below the tick labels' floor). Still too
|
|
305
|
+
wide, or leaving the x labels no room, it moves below the panels in as many
|
|
306
|
+
columns as fit. `PlotSpec.sample_in_legend=False` (the Show sample pane's
|
|
307
|
+
"Show in legend") leaves the overlay's colours out of it.
|
|
308
|
+
|
|
309
|
+
## Figure size
|
|
310
|
+
|
|
311
|
+
`StyleOptions.width` / `height` are inches and size the **saved** figure and
|
|
312
|
+
the generated code; the interactive preview fills whatever pane it is in.
|
|
313
|
+
`scistackplot.figsize` is the one vocabulary of aspect-ratio presets (`4:3`,
|
|
314
|
+
`16:9`, `3:2`, golden, `2:1`, `1:1`, two portrait ratios, `custom`): pick a
|
|
315
|
+
ratio, name a width — a journal column is 3.5 in, a double column 7.2 in — and
|
|
316
|
+
`height_for` gives the height. `aspect_name` runs the other way, so a reopened
|
|
317
|
+
spec reports the ratio it was saved with, and `render_matplotlib` logs the
|
|
318
|
+
size it drew at INFO. The Plot Studio panel's "Figure size" section is this
|
|
319
|
+
module with a dropdown on it.
|
|
320
|
+
|
|
321
|
+
`write_figure(resolved, path, dpi=200)` writes the file at exactly that size.
|
|
322
|
+
There's no whitespace trim: it refuses `bbox_inches`, and the renderer fits the
|
|
323
|
+
labels and legend inside the canvas. Anything still drawn past the edge is
|
|
324
|
+
measured (`canvas_overflow`) and WARNed. The file is never resized to make room.
|
|
325
|
+
The GUI's Save goes through it.
|
|
326
|
+
|
|
327
|
+
The preview never decides for itself. `layout_decisions(resolved, width_in=,
|
|
328
|
+
height_in=)` lays the figure out with matplotlib at a size and returns the tick,
|
|
329
|
+
bracket and legend decisions; `render_plotly(resolved, decisions=...,
|
|
330
|
+
fixed_size_px=...)` draws exactly those. The Plot Studio previews at the export
|
|
331
|
+
size (drawn at Width x Height, 1 pt = 1 px) or at the pane's size ("Fit pane").
|
|
332
|
+
|
|
333
|
+
`StyleOptions.text` (`TextSizes`) sizes each piece of text separately:
|
|
334
|
+
`base` (points) is matplotlib's `font.size`, and `title`,
|
|
335
|
+
`x_label`, `y_label`, `x_ticks`, `y_ticks`, `groups` (the bracket rows),
|
|
336
|
+
`legend` and `legend_title` are each either fixed or `None`, which derives
|
|
337
|
+
them from `base` with matplotlib's own ratios. Only `base` set draws exactly
|
|
338
|
+
what one font knob would. A fixed size is never shrunk by the label or legend
|
|
339
|
+
fit, which may still rotate, wrap or move it. `scistackplot.textsize` is the
|
|
340
|
+
one owner (`resolve_sizes`, `rc_params`). The export, the generated code and
|
|
341
|
+
the plotly preview (as px) all read it, under `rc_context` so nothing leaks
|
|
342
|
+
into the next figure. See `docs/claude/plot-text-and-labels.md`.
|
|
343
|
+
|
|
344
|
+
**Automatic size (the default).** With `base` unset (`None`), `scistackplot.autosize`
|
|
345
|
+
makes each unfixed element as large as the figure lays it out cleanly, inside
|
|
346
|
+
the band of `TextSizes.target`: `"print"` (8–12 pt, the default) or `"slide"`
|
|
347
|
+
(14–28 pt), in points at the saved size. It searches per element on real
|
|
348
|
+
matplotlib layouts. A rotated or shrunk x tick limits only `x_ticks`, a legend
|
|
349
|
+
moved below limits only `legend`, and a plot area under 55 % of the canvas
|
|
350
|
+
limits all of them. The chosen sizes ride on `ResolvedPlot.auto_text`, and
|
|
351
|
+
every renderer reads them through `textsize.sizes_for`.
|
|
352
|
+
`settle_text_size(resolved)` runs the search by hand. A number in `base`
|
|
353
|
+
turns it off.
|
|
354
|
+
|
|
355
|
+
The figure's **paper** (white background, black text, a black frame round every
|
|
356
|
+
panel, outward ticks, no grid, and the error-bar ink) has one owner,
|
|
357
|
+
`scistackplot.paper.PAPER`. Its values are matplotlib's defaults, written
|
|
358
|
+
out. The export, the generated code and the plotly preview all draw it, so
|
|
359
|
+
the preview shows the saved figure's page rather than plotly's grid. A
|
|
360
|
+
personal `matplotlibrc` cannot change an export. The Plot Studio shows the
|
|
361
|
+
figure untouched in light mode; dark mode recolours the ink for the screen.
|
|
362
|
+
See `docs/claude/preview-paper-parity.md`.
|
|
363
|
+
|
|
364
|
+
`StyleOptions.sample_weight` and `StyleOptions.line_weight` are multipliers
|
|
365
|
+
(default 1) on the point size AND line thickness together: the first for the
|
|
366
|
+
"Show sample" overlay's points and joining lines, the second for a
|
|
367
|
+
spaghetti's own points and lines. `scistackplot.weights` is the one owner;
|
|
368
|
+
matplotlib, the plotly preview and the generated code all read it.
|
|
369
|
+
|
|
370
|
+
## What the text reads as: display aliases
|
|
371
|
+
|
|
372
|
+
A figure shows the data's own names, such as `BL`, `F` and `StepLength`.
|
|
373
|
+
An alias says what one reads as, and it changes only the text:
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from scistackplot import Alias
|
|
377
|
+
|
|
378
|
+
spec = PlotSpec(
|
|
379
|
+
measures=["StepLength"],
|
|
380
|
+
roles={"session": Role.GROUP, "subject": Role.COLLAPSE},
|
|
381
|
+
groups=["session"],
|
|
382
|
+
aliases={
|
|
383
|
+
"session": Alias(name="Visit", levels={"BL": "Baseline", "POST": "After"}),
|
|
384
|
+
"StepLength": Alias(name="Step length (cm)"),
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`name` is used where the figure says what a thing is (axis and legend
|
|
390
|
+
titles). `levels` is used wherever a value appears: ticks, brackets,
|
|
391
|
+
legend entries, panel titles and figure titles.
|
|
392
|
+
|
|
393
|
+
A scidb-backed table also carries the project's aliases (`[aliases]` in
|
|
394
|
+
`scistack.toml`, read live through `scidb.aliases`). The plot's own entries
|
|
395
|
+
override them field by field, and `""` shows the raw text again.
|
|
396
|
+
|
|
397
|
+
Only the text changes. Filters, facet layout rules, the level order and
|
|
398
|
+
`plot_data` all keep raw levels, and so does the data the exported code
|
|
399
|
+
draws. The exported code writes the merged aliases in as a literal and
|
|
400
|
+
relabels what it drew. Two levels of one factor that would read the same are
|
|
401
|
+
refused (`AliasError`), because the figure could not tell them apart.
|
|
402
|
+
`scistackplot.aliases.DisplayText` (on `ResolvedPlot.text`) is the one owner
|
|
403
|
+
of how every level and name reads. See `docs/claude/plot-text-and-labels.md`.
|
|
404
|
+
|
|
405
|
+
## What colour a mark is: pinned colours
|
|
406
|
+
|
|
407
|
+
The palette (`StyleOptions.palette`, Okabe-Ito by default) paints each level
|
|
408
|
+
of the colour layer. You can pin a level's colour with `PlotSpec.colors`,
|
|
409
|
+
and set the one colour of an uncoloured figure with `StyleOptions.mark_color`:
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
spec = PlotSpec(
|
|
413
|
+
...,
|
|
414
|
+
color="session",
|
|
415
|
+
colors={"session": {"BL": "#0072b2", "POST": "rgb(213, 94, 0)"}},
|
|
416
|
+
)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Colours may be `#rrggbb`, `#rgb`, `rgb(r, g, b)` or a matplotlib colour
|
|
420
|
+
name. `scistackplot.colors.parse_color` is the one reader, and it
|
|
421
|
+
canonicalises every colour to `#rrggbb`.
|
|
422
|
+
|
|
423
|
+
A scidb-backed table also carries the project's `[colors]` (read live
|
|
424
|
+
through `scidb.colors`). The plot's pins override them level by level.
|
|
425
|
+
|
|
426
|
+
A level you don't pin keeps its palette colour at its declared position, so
|
|
427
|
+
pinning one level never moves another's.
|
|
428
|
+
|
|
429
|
+
Pins also paint the "Show sample" overlay when it is coloured by the same
|
|
430
|
+
key. The exported code writes the merged pins in as literals.
|
|
431
|
+
|
|
432
|
+
`render.base.palette_for` stays the one owner of the colour a mark is drawn
|
|
433
|
+
in. See `docs/claude/plot-colors.md`.
|
|
434
|
+
|
|
435
|
+
## Export: real code, not a call back into this library
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from scistackplot import generate_plot_function
|
|
439
|
+
|
|
440
|
+
print(generate_plot_function(spec, table))
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
def plot_steplength(df, filename):
|
|
445
|
+
import matplotlib.pyplot as plt
|
|
446
|
+
import pandas as pd
|
|
447
|
+
import seaborn as sns
|
|
448
|
+
|
|
449
|
+
g = sns.catplot(
|
|
450
|
+
data=df,
|
|
451
|
+
x='session',
|
|
452
|
+
y='StepLength',
|
|
453
|
+
hue='limb',
|
|
454
|
+
kind="box",
|
|
455
|
+
)
|
|
456
|
+
g.set_axis_labels('session', 'StepLength')
|
|
457
|
+
return g.figure
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Your pipeline gets ordinary seaborn code it can keep, edit, and read — no
|
|
461
|
+
runtime dependency on this package. The spec is embedded in the docstring, so
|
|
462
|
+
the GUI can reopen a figure you have since hand-edited.
|
|
463
|
+
|
|
464
|
+
## Data sources
|
|
465
|
+
|
|
466
|
+
`DataSource` is a three-method protocol (`describe`, `get_table`,
|
|
467
|
+
`joinable_with`). `scistackplot` ships `CsvSource` and `DataFrameSource`;
|
|
468
|
+
[`scistackplotdb`](../scistackplotdb/README.md) ships the SciDB one. Anything
|
|
469
|
+
consuming the protocol — including the Plot Studio panel in the SciStack GUI —
|
|
470
|
+
works identically against a lone CSV and a full project database.
|
|
471
|
+
|
|
472
|
+
## Relationship to SciDB endpoints
|
|
473
|
+
|
|
474
|
+
Recording a figure is SciDB's job and is unchanged: name a function `plot_`,
|
|
475
|
+
return a Figure, and `finalized=True` stores it as a queryable record with an
|
|
476
|
+
embedded provenance stamp. `scistackplot` supplies the body of that function;
|
|
477
|
+
`scistackplotdb` generates the `for_each` call around it.
|
|
478
|
+
|
|
479
|
+
See [`docs/claude/plotting-library-design.md`](../docs/claude/plotting-library-design.md).
|
|
480
|
+
|
|
481
|
+
## Optional extras
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
pip install "scistackplot[mpl]" # matplotlib + seaborn (export)
|
|
485
|
+
pip install "scistackplot[interactive]" # plotly Figure objects
|
|
486
|
+
```
|