scistackplot 0.1.33__tar.gz → 0.1.35__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.
Files changed (50) hide show
  1. {scistackplot-0.1.33 → scistackplot-0.1.35}/PKG-INFO +68 -12
  2. {scistackplot-0.1.33 → scistackplot-0.1.35}/README.md +63 -10
  3. {scistackplot-0.1.33 → scistackplot-0.1.35}/pyproject.toml +7 -0
  4. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/__init__.py +41 -2
  5. scistackplot-0.1.35/src/scistackplot/autosize.py +465 -0
  6. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/capability.py +2 -2
  7. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/codegen.py +115 -59
  8. scistackplot-0.1.35/src/scistackplot/custom.py +1023 -0
  9. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/figure_file.py +49 -1
  10. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/presets.py +50 -3
  11. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/reduce.py +132 -29
  12. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/render/base.py +49 -0
  13. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/render/mpl.py +249 -104
  14. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/render/plotly_.py +70 -1
  15. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/resolved.py +14 -0
  16. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/roles.py +29 -0
  17. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/spec.py +153 -21
  18. scistackplot-0.1.35/src/scistackplot/textsize.py +309 -0
  19. scistackplot-0.1.33/src/scistackplot/autosize.py +0 -557
  20. scistackplot-0.1.33/src/scistackplot/textsize.py +0 -209
  21. {scistackplot-0.1.33 → scistackplot-0.1.35}/.gitignore +0 -0
  22. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/aliases.py +0 -0
  23. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/cell.py +0 -0
  24. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/colors.py +0 -0
  25. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/compare.py +0 -0
  26. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/dedup.py +0 -0
  27. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/diffbars.py +0 -0
  28. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/export.py +0 -0
  29. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/figsize.py +0 -0
  30. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/framesize.py +0 -0
  31. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/groups.py +0 -0
  32. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/numeric.py +0 -0
  33. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/panels.py +0 -0
  34. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/paper.py +0 -0
  35. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/reducer.py +0 -0
  36. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/render/__init__.py +0 -0
  37. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/restore.py +0 -0
  38. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/series_stats.py +0 -0
  39. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/shape.py +0 -0
  40. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/sources/__init__.py +0 -0
  41. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/sources/base.py +0 -0
  42. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/sources/csv.py +0 -0
  43. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/sources/frame.py +0 -0
  44. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/spaghetti.py +0 -0
  45. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/table.py +0 -0
  46. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/ticklabels.py +0 -0
  47. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/variants.py +0 -0
  48. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/weights.py +0 -0
  49. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/xaxis.py +0 -0
  50. {scistackplot-0.1.33 → scistackplot-0.1.35}/src/scistackplot/ylimits.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scistackplot
3
- Version: 0.1.33
3
+ Version: 0.1.35
4
4
  Summary: Spec-driven plotting for long-format scientific data — standalone, GUI-friendly
5
5
  Author: SciStack Contributors
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: numpy>=1.21
21
21
  Requires-Dist: pandas>=1.5
22
- Requires-Dist: scistacklog==0.1.33
22
+ Requires-Dist: scistacklog==0.1.35
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: matplotlib>=3.6; extra == 'dev'
25
25
  Requires-Dist: plotly>=5.0; extra == 'dev'
@@ -32,6 +32,9 @@ Requires-Dist: plotly>=5.0; extra == 'interactive'
32
32
  Provides-Extra: mpl
33
33
  Requires-Dist: matplotlib>=3.6; extra == 'mpl'
34
34
  Requires-Dist: seaborn>=0.13; extra == 'mpl'
35
+ Provides-Extra: plotly-export
36
+ Requires-Dist: kaleido>=1.0; extra == 'plotly-export'
37
+ Requires-Dist: plotly>=6.1; extra == 'plotly-export'
35
38
  Description-Content-Type: text/markdown
36
39
 
37
40
  # scistackplot
@@ -259,6 +262,18 @@ that looks like a data problem. `LongTable` carries each factor's real level
259
262
  order; sources that know better (SciDB knows its declared `schema_key_types`)
260
263
  supply it explicitly, and everything else falls back to a natural sort.
261
264
 
265
+ ## Missing levels stay visible
266
+
267
+ A figure with no data for an x level that the rest of the plot has keeps an
268
+ empty tick for it: under `subject=ITERATE`, the figure of a subject who never
269
+ ran session 2 still shows a session 2 slot, so the gap reads as missing data.
270
+ `PlotSpec.show_missing_levels=False` leaves those ticks off, which makes the
271
+ remaining marks wider. "Missing" is measured against the plot's filtered
272
+ rows, so a level you filtered out never comes back as an empty tick. Leaves
273
+ of a nested axis work the same way, and no position is ever made for a
274
+ combination no figure has. Either way `ResolvedPlot.missing_x` lists the
275
+ levels, and the exported code draws the same ticks as the preview.
276
+
262
277
  ## Rendering
263
278
 
264
279
  Two backends translate the same reduced plot, so the interactive view and the
@@ -341,16 +356,20 @@ one owner (`resolve_sizes`, `rc_params`). The export, the generated code and
341
356
  the plotly preview (as px) all read it, under `rc_context` so nothing leaks
342
357
  into the next figure. See `docs/claude/plot-text-and-labels.md`.
343
358
 
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.
359
+ **Automatic size (the default).** `base` is ONE reference size; every unfixed
360
+ element is drawn at reference × its ratio (`DEFAULT_TEXT_RATIOS`: title 1.44,
361
+ axis and legend titles 1.2, the rest 1.0, overridable per element in
362
+ `TextSizes.ratios`), clamped into that element's floor/ceiling (the band of
363
+ `TextSizes.target` × the ratio — `"print"` 8–12 pt, `"slide"` 14–28 pt — or
364
+ `TextSizes.floors` / `ceilings` in pt). With `base` unset (`None`),
365
+ `scistackplot.autosize` searches the largest reference whose real matplotlib
366
+ layout breaks no rule: no text into text (tick fit, numeric tick collisions,
367
+ neighbouring panels, brackets, legend), nothing off the canvas, at least 3
368
+ tick labels per numeric axis, no y title taller than its panel. One failing
369
+ rule steps all the text down together, so the proportions hold. The result
370
+ rides on `ResolvedPlot.auto_text`, and every renderer reads it through
371
+ `textsize.sizes_for`. `settle_text_size(resolved)` runs the search by hand. A
372
+ number in `base` fixes the reference (unclamped).
354
373
 
355
374
  The figure's **paper** (white background, black text, a black frame round every
356
375
  panel, outward ticks, no grid, and the error-bar ink) has one owner,
@@ -432,6 +451,43 @@ key. The exported code writes the merged pins in as literals.
432
451
  `render.base.palette_for` stays the one owner of the colour a mark is drawn
433
452
  in. See `docs/claude/plot-colors.md`.
434
453
 
454
+ ## Your own plot function
455
+
456
+ A `plot_` function of your project can draw the figure instead of a built-in
457
+ kind. Plot Studio opens on it from the `plot_` node, and the node's edges bind
458
+ its inputs. The same works from code:
459
+
460
+ ```python
461
+ from dataclasses import replace
462
+
463
+ from scistackplot import CustomCall, CustomPlot, PlotKind, resolve_custom, write_figure
464
+
465
+ def plot_steps(steps, mass, ax, context):
466
+ for _, row in steps.iterrows():
467
+ ax.plot([context.x(row)], [row["StepLength"]], "o", color=context.color_of(row["session"]))
468
+
469
+ call = CustomCall(fn=plot_steps, inputs={"steps": steps_table, "mass": mass_table},
470
+ variables={"steps": "StepLength", "mass": "Mass"})
471
+ spec = replace(spec, kind=PlotKind.CUSTOM, custom=CustomPlot(function="plot_steps"))
472
+ for figure in resolve_custom(spec, call):
473
+ write_figure(figure, f"{figure.figure_label}.png")
474
+ ```
475
+
476
+ **With an `ax` parameter**, the function draws one panel per call, and
477
+ everything else is scistackplot's: panels, ticks, legend, text, size and
478
+ colours. `context.x(...)` places a mark on the tick its group levels name.
479
+ **Without `ax`**, it returns a whole matplotlib or plotly figure; only the
480
+ data settings and the size apply.
481
+
482
+ **What the function receives:**
483
+ - Each input is its own table: rows selected by the figure's and panel's
484
+ levels, with the inner collapsed keys averaged away.
485
+ - An input at a coarser level is passed whole to every call.
486
+ - With `as_table` off, an input arrives as the bare value instead of a table.
487
+
488
+ Writing a plotly figure needs `pip install "scistackplot[plotly-export]"`.
489
+ See `docs/claude/custom-plot-kinds.md`.
490
+
435
491
  ## Export: real code, not a call back into this library
436
492
 
437
493
  ```python
@@ -223,6 +223,18 @@ that looks like a data problem. `LongTable` carries each factor's real level
223
223
  order; sources that know better (SciDB knows its declared `schema_key_types`)
224
224
  supply it explicitly, and everything else falls back to a natural sort.
225
225
 
226
+ ## Missing levels stay visible
227
+
228
+ A figure with no data for an x level that the rest of the plot has keeps an
229
+ empty tick for it: under `subject=ITERATE`, the figure of a subject who never
230
+ ran session 2 still shows a session 2 slot, so the gap reads as missing data.
231
+ `PlotSpec.show_missing_levels=False` leaves those ticks off, which makes the
232
+ remaining marks wider. "Missing" is measured against the plot's filtered
233
+ rows, so a level you filtered out never comes back as an empty tick. Leaves
234
+ of a nested axis work the same way, and no position is ever made for a
235
+ combination no figure has. Either way `ResolvedPlot.missing_x` lists the
236
+ levels, and the exported code draws the same ticks as the preview.
237
+
226
238
  ## Rendering
227
239
 
228
240
  Two backends translate the same reduced plot, so the interactive view and the
@@ -305,16 +317,20 @@ one owner (`resolve_sizes`, `rc_params`). The export, the generated code and
305
317
  the plotly preview (as px) all read it, under `rc_context` so nothing leaks
306
318
  into the next figure. See `docs/claude/plot-text-and-labels.md`.
307
319
 
308
- **Automatic size (the default).** With `base` unset (`None`), `scistackplot.autosize`
309
- makes each unfixed element as large as the figure lays it out cleanly, inside
310
- the band of `TextSizes.target`: `"print"` (8–12 pt, the default) or `"slide"`
311
- (14–28 pt), in points at the saved size. It searches per element on real
312
- matplotlib layouts. A rotated or shrunk x tick limits only `x_ticks`, a legend
313
- moved below limits only `legend`, and a plot area under 55 % of the canvas
314
- limits all of them. The chosen sizes ride on `ResolvedPlot.auto_text`, and
315
- every renderer reads them through `textsize.sizes_for`.
316
- `settle_text_size(resolved)` runs the search by hand. A number in `base`
317
- turns it off.
320
+ **Automatic size (the default).** `base` is ONE reference size; every unfixed
321
+ element is drawn at reference × its ratio (`DEFAULT_TEXT_RATIOS`: title 1.44,
322
+ axis and legend titles 1.2, the rest 1.0, overridable per element in
323
+ `TextSizes.ratios`), clamped into that element's floor/ceiling (the band of
324
+ `TextSizes.target` × the ratio — `"print"` 8–12 pt, `"slide"` 14–28 pt — or
325
+ `TextSizes.floors` / `ceilings` in pt). With `base` unset (`None`),
326
+ `scistackplot.autosize` searches the largest reference whose real matplotlib
327
+ layout breaks no rule: no text into text (tick fit, numeric tick collisions,
328
+ neighbouring panels, brackets, legend), nothing off the canvas, at least 3
329
+ tick labels per numeric axis, no y title taller than its panel. One failing
330
+ rule steps all the text down together, so the proportions hold. The result
331
+ rides on `ResolvedPlot.auto_text`, and every renderer reads it through
332
+ `textsize.sizes_for`. `settle_text_size(resolved)` runs the search by hand. A
333
+ number in `base` fixes the reference (unclamped).
318
334
 
319
335
  The figure's **paper** (white background, black text, a black frame round every
320
336
  panel, outward ticks, no grid, and the error-bar ink) has one owner,
@@ -396,6 +412,43 @@ key. The exported code writes the merged pins in as literals.
396
412
  `render.base.palette_for` stays the one owner of the colour a mark is drawn
397
413
  in. See `docs/claude/plot-colors.md`.
398
414
 
415
+ ## Your own plot function
416
+
417
+ A `plot_` function of your project can draw the figure instead of a built-in
418
+ kind. Plot Studio opens on it from the `plot_` node, and the node's edges bind
419
+ its inputs. The same works from code:
420
+
421
+ ```python
422
+ from dataclasses import replace
423
+
424
+ from scistackplot import CustomCall, CustomPlot, PlotKind, resolve_custom, write_figure
425
+
426
+ def plot_steps(steps, mass, ax, context):
427
+ for _, row in steps.iterrows():
428
+ ax.plot([context.x(row)], [row["StepLength"]], "o", color=context.color_of(row["session"]))
429
+
430
+ call = CustomCall(fn=plot_steps, inputs={"steps": steps_table, "mass": mass_table},
431
+ variables={"steps": "StepLength", "mass": "Mass"})
432
+ spec = replace(spec, kind=PlotKind.CUSTOM, custom=CustomPlot(function="plot_steps"))
433
+ for figure in resolve_custom(spec, call):
434
+ write_figure(figure, f"{figure.figure_label}.png")
435
+ ```
436
+
437
+ **With an `ax` parameter**, the function draws one panel per call, and
438
+ everything else is scistackplot's: panels, ticks, legend, text, size and
439
+ colours. `context.x(...)` places a mark on the tick its group levels name.
440
+ **Without `ax`**, it returns a whole matplotlib or plotly figure; only the
441
+ data settings and the size apply.
442
+
443
+ **What the function receives:**
444
+ - Each input is its own table: rows selected by the figure's and panel's
445
+ levels, with the inner collapsed keys averaged away.
446
+ - An input at a coarser level is passed whole to every call.
447
+ - With `as_table` off, an input arrives as the bare value instead of a table.
448
+
449
+ Writing a plotly figure needs `pip install "scistackplot[plotly-export]"`.
450
+ See `docs/claude/custom-plot-kinds.md`.
451
+
399
452
  ## Export: real code, not a call back into this library
400
453
 
401
454
  ```python
@@ -58,6 +58,13 @@ mpl = [
58
58
  interactive = [
59
59
  "plotly>=5.0",
60
60
  ]
61
+ # Writing a plotly figure a custom plot_ function returned to a file
62
+ # (scistackplot.custom.write_plotly). kaleido 1.x also needs Chrome, which
63
+ # `plotly_get_chrome` installs.
64
+ plotly-export = [
65
+ "plotly>=6.1",
66
+ "kaleido>=1.0",
67
+ ]
61
68
  dev = [
62
69
  "pytest>=7.0",
63
70
  "pytest-cov>=4.0",
@@ -97,6 +97,18 @@ from .roles import (
97
97
  from .restore import NoteKind, RestoreError, RestoreNote, Restored, reconcile, restore_spec
98
98
  from .presets import FieldClass, apply_preset, make_template, shape_warning
99
99
  from .compare import CompareMode, Comparison
100
+ from .custom import (
101
+ CustomCall,
102
+ CustomFunctionError,
103
+ CustomSignature,
104
+ PlotContext,
105
+ custom_capabilities,
106
+ custom_requirement,
107
+ inspect_function,
108
+ preview_payload as custom_preview,
109
+ resolve_custom,
110
+ resolve_custom_one,
111
+ )
100
112
  from .diffbars import DifferenceBar, bars_for_panel, slot_endpoints
101
113
  from .panels import PanelOverride, override_for
102
114
  from .shape import Shape, classify_column, classify_value, is_plottable
@@ -112,6 +124,8 @@ from .spec import (
112
124
  LocationFilter,
113
125
  MatchOp,
114
126
  Matcher,
127
+ BUILTIN_KINDS,
128
+ CustomPlot,
115
129
  PlotKind,
116
130
  PlotSpec,
117
131
  Role,
@@ -120,14 +134,23 @@ from .spec import (
120
134
  Statistic,
121
135
  Alias,
122
136
  StyleOptions,
137
+ TEXT_ELEMENTS,
123
138
  TEXT_TARGETS,
124
139
  TextSizes,
125
140
  VariantSet,
126
141
  YAxis,
127
142
  grid_shape_for,
128
143
  )
129
- from .textsize import ResolvedSizes, rc_params, resolve_sizes, sizes_for
130
- from .autosize import AutoTextSize, BANDS, settle as settle_text_size
144
+ from .textsize import (
145
+ BANDS,
146
+ DEFAULT_TEXT_RATIOS,
147
+ ResolvedSizes,
148
+ drawn_sizes,
149
+ rc_params,
150
+ resolve_sizes,
151
+ sizes_for,
152
+ )
153
+ from .autosize import AutoTextSize, settle as settle_text_size
131
154
  from .aliases import AliasError, DisplayText, display_text
132
155
  from .colors import ColorError, MarkColors, mark_colors, parse_color
133
156
  from .figsize import (
@@ -185,6 +208,8 @@ __all__ = [
185
208
  "PlotSpec",
186
209
  "Role",
187
210
  "PlotKind",
211
+ "BUILTIN_KINDS",
212
+ "CustomPlot",
188
213
  "Statistic",
189
214
  "ErrorBand",
190
215
  "Aggregation",
@@ -209,6 +234,17 @@ __all__ = [
209
234
  # difference bars (.claude/plan-difference-bars.md)
210
235
  "CompareMode",
211
236
  "Comparison",
237
+ # custom plot kinds: a user plot_ function draws the figure
238
+ "CustomCall",
239
+ "CustomFunctionError",
240
+ "CustomSignature",
241
+ "PlotContext",
242
+ "custom_capabilities",
243
+ "custom_requirement",
244
+ "inspect_function",
245
+ "custom_preview",
246
+ "resolve_custom",
247
+ "resolve_custom_one",
212
248
  "DifferenceBar",
213
249
  "bars_for_panel",
214
250
  "slot_endpoints",
@@ -232,9 +268,12 @@ __all__ = [
232
268
  "ResolvedSizes",
233
269
  "resolve_sizes",
234
270
  "sizes_for",
271
+ "drawn_sizes",
272
+ "DEFAULT_TEXT_RATIOS",
235
273
  "rc_params",
236
274
  # automatic text size (autosize)
237
275
  "TEXT_TARGETS",
276
+ "TEXT_ELEMENTS",
238
277
  "BANDS",
239
278
  "AutoTextSize",
240
279
  "settle_text_size",