scistackplot 0.1.56__tar.gz → 0.1.58__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.56 → scistackplot-0.1.58}/PKG-INFO +21 -4
- {scistackplot-0.1.56 → scistackplot-0.1.58}/README.md +18 -1
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/__init__.py +6 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/capability.py +6 -4
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/codegen.py +91 -37
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/diffbars.py +11 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/reduce.py +6 -5
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/render/base.py +35 -3
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/render/mpl.py +335 -26
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/render/plotly_.py +247 -16
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/roles.py +2 -2
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/spec.py +30 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/ticklabels.py +13 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/ylimits.py +3 -3
- {scistackplot-0.1.56 → scistackplot-0.1.58}/.gitignore +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/pyproject.toml +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/aliases.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/autosize.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/bins.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/cell.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/colors.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/compare.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/custom.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/dedup.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/design.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/export.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/figsize.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/figure_file.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/fits.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/framesize.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/groups.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/numeric.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/panels.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/paper.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/presets.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/reducer.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/render/__init__.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/resolved.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/restore.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/series_stats.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/shape.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/sources/__init__.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/sources/base.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/sources/csv.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/sources/frame.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/spaghetti.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/spread.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/table.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/temporal.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/textsize.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/variants.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/src/scistackplot/weights.py +0 -0
- {scistackplot-0.1.56 → scistackplot-0.1.58}/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.58
|
|
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,8 +20,8 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Requires-Dist: csv-stats>=0.5.0
|
|
21
21
|
Requires-Dist: numpy>=1.21
|
|
22
22
|
Requires-Dist: pandas>=1.5
|
|
23
|
-
Requires-Dist: scicanonicalhash==0.1.
|
|
24
|
-
Requires-Dist: scistacklog==0.1.
|
|
23
|
+
Requires-Dist: scicanonicalhash==0.1.58
|
|
24
|
+
Requires-Dist: scistacklog==0.1.58
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: matplotlib>=3.6; extra == 'dev'
|
|
27
27
|
Requires-Dist: plotly>=5.0; extra == 'dev'
|
|
@@ -101,7 +101,7 @@ shape, through one pure function:
|
|
|
101
101
|
```python
|
|
102
102
|
from scistackplot import available_plots, default_plot, Shape
|
|
103
103
|
|
|
104
|
-
available_plots(Shape.SCALAR, {"session": Role.GROUP}) # scatter, strip, bar
|
|
104
|
+
available_plots(Shape.SCALAR, {"session": Role.GROUP}) # scatter, strip, bar, hbar
|
|
105
105
|
available_plots(Shape.SCALAR, {"session": Role.GROUP, "trial": Role.COLLAPSE}) # + box, violin
|
|
106
106
|
```
|
|
107
107
|
|
|
@@ -209,6 +209,23 @@ datetime `x_measure` is drawn on a date axis. Log axes are ignored on both
|
|
|
209
209
|
kinds. "Save data" adds the bars or the fits as a table. See
|
|
210
210
|
`docs/claude/histogram-and-regression.md`.
|
|
211
211
|
|
|
212
|
+
## Horizontal bars
|
|
213
|
+
|
|
214
|
+
`PlotKind.HBAR` is the bar plot turned: the same bars, error bars, colours,
|
|
215
|
+
grouping, Show sample, comparison and limits, with the categories down the
|
|
216
|
+
left (first level at the top) and the values along the bottom.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
spec = PlotSpec(measures=["StepLength"], roles=..., groups=["limb", "session"],
|
|
220
|
+
kind=PlotKind.HBAR)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Nothing upstream of drawing changes — "x" is still the category axis and
|
|
224
|
+
"y" the value axis, so `y_axis`, `style.y_label` and `style.log_y` apply to
|
|
225
|
+
the bottom axis. Outer grouping layers become brackets at the left, and a
|
|
226
|
+
faceted panel's name is the title under it. Difference bars are not drawn on
|
|
227
|
+
it. See `docs/claude/horizontal-bar.md`.
|
|
228
|
+
|
|
212
229
|
## Save the data behind a figure
|
|
213
230
|
|
|
214
231
|
The rows a plot is drawn from are available as a long table, so the
|
|
@@ -60,7 +60,7 @@ shape, through one pure function:
|
|
|
60
60
|
```python
|
|
61
61
|
from scistackplot import available_plots, default_plot, Shape
|
|
62
62
|
|
|
63
|
-
available_plots(Shape.SCALAR, {"session": Role.GROUP}) # scatter, strip, bar
|
|
63
|
+
available_plots(Shape.SCALAR, {"session": Role.GROUP}) # scatter, strip, bar, hbar
|
|
64
64
|
available_plots(Shape.SCALAR, {"session": Role.GROUP, "trial": Role.COLLAPSE}) # + box, violin
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -168,6 +168,23 @@ datetime `x_measure` is drawn on a date axis. Log axes are ignored on both
|
|
|
168
168
|
kinds. "Save data" adds the bars or the fits as a table. See
|
|
169
169
|
`docs/claude/histogram-and-regression.md`.
|
|
170
170
|
|
|
171
|
+
## Horizontal bars
|
|
172
|
+
|
|
173
|
+
`PlotKind.HBAR` is the bar plot turned: the same bars, error bars, colours,
|
|
174
|
+
grouping, Show sample, comparison and limits, with the categories down the
|
|
175
|
+
left (first level at the top) and the values along the bottom.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
spec = PlotSpec(measures=["StepLength"], roles=..., groups=["limb", "session"],
|
|
179
|
+
kind=PlotKind.HBAR)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Nothing upstream of drawing changes — "x" is still the category axis and
|
|
183
|
+
"y" the value axis, so `y_axis`, `style.y_label` and `style.log_y` apply to
|
|
184
|
+
the bottom axis. Outer grouping layers become brackets at the left, and a
|
|
185
|
+
faceted panel's name is the title under it. Difference bars are not drawn on
|
|
186
|
+
it. See `docs/claude/horizontal-bar.md`.
|
|
187
|
+
|
|
171
188
|
## Save the data behind a figure
|
|
172
189
|
|
|
173
190
|
The rows a plot is drawn from are available as a long table, so the
|
|
@@ -137,6 +137,8 @@ from .spec import (
|
|
|
137
137
|
PlotKind,
|
|
138
138
|
PlotSpec,
|
|
139
139
|
Role,
|
|
140
|
+
BAR_KINDS,
|
|
141
|
+
HORIZONTAL_KINDS,
|
|
140
142
|
OVERLAY_KINDS,
|
|
141
143
|
SCALAR_KINDS,
|
|
142
144
|
Statistic,
|
|
@@ -148,6 +150,7 @@ from .spec import (
|
|
|
148
150
|
VariantSet,
|
|
149
151
|
YAxis,
|
|
150
152
|
grid_shape_for,
|
|
153
|
+
is_horizontal,
|
|
151
154
|
)
|
|
152
155
|
from .bins import BinEdges, BinError, bin_edges
|
|
153
156
|
from .fits import PanelFit, annotation_text
|
|
@@ -315,6 +318,9 @@ __all__ = [
|
|
|
315
318
|
"VariantSet",
|
|
316
319
|
"SCALAR_KINDS",
|
|
317
320
|
"OVERLAY_KINDS",
|
|
321
|
+
"BAR_KINDS",
|
|
322
|
+
"HORIZONTAL_KINDS",
|
|
323
|
+
"is_horizontal",
|
|
318
324
|
# 1-D -> scalar collapse
|
|
319
325
|
"cell_collapses",
|
|
320
326
|
"effective_shape",
|
|
@@ -38,12 +38,12 @@ from .roles import (
|
|
|
38
38
|
sample_key,
|
|
39
39
|
)
|
|
40
40
|
from .shape import Shape
|
|
41
|
-
from .spec import BUILTIN_KINDS, MAX_X_LAYERS, SCALAR_KINDS, PlotKind, PlotSpec, Role
|
|
41
|
+
from .spec import BAR_KINDS, BUILTIN_KINDS, MAX_X_LAYERS, SCALAR_KINDS, PlotKind, PlotSpec, Role
|
|
42
42
|
from .table import CODE_FACTOR_PREFIX, RUN_FACTOR_PREFIX, LongTable
|
|
43
43
|
from .temporal import BAR_REASON, time_zone_summary
|
|
44
44
|
|
|
45
45
|
#: Kinds that summarize several rows per x position into one mark.
|
|
46
|
-
DISTRIBUTION_KINDS = (PlotKind.BOX, PlotKind.VIOLIN,
|
|
46
|
+
DISTRIBUTION_KINDS = (PlotKind.BOX, PlotKind.VIOLIN, *BAR_KINDS, PlotKind.BAND)
|
|
47
47
|
|
|
48
48
|
#: Kinds that cannot be drawn without a sample — ``roles.SAMPLE_NEEDING_KINDS``,
|
|
49
49
|
#: read by ``roles.kind_requirement``, the one statement of what each kind
|
|
@@ -96,6 +96,7 @@ _ROLE_HINTS_BY_SHAPE: dict[Shape, dict[Role, str]] = {
|
|
|
96
96
|
#: kind's (``roles.grouping_layers``); this is the reading, said out loud.
|
|
97
97
|
_GROUPING_HINTS: dict[PlotKind, str] = {
|
|
98
98
|
PlotKind.BAR: "One bar per combination — first entry innermost",
|
|
99
|
+
PlotKind.HBAR: "One bar per combination, top to bottom — first entry innermost",
|
|
99
100
|
PlotKind.BOX: "One box per combination — first entry innermost",
|
|
100
101
|
PlotKind.VIOLIN: "One violin per combination — first entry innermost",
|
|
101
102
|
PlotKind.STRIP: "One point per combination — first entry innermost",
|
|
@@ -245,13 +246,14 @@ def available_plots(
|
|
|
245
246
|
PlotKind.SCATTER,
|
|
246
247
|
PlotKind.STRIP,
|
|
247
248
|
PlotKind.BAR,
|
|
249
|
+
PlotKind.HBAR,
|
|
248
250
|
PlotKind.SPAGHETTI,
|
|
249
251
|
PlotKind.BOX,
|
|
250
252
|
PlotKind.VIOLIN,
|
|
251
253
|
PlotKind.HISTOGRAM,
|
|
252
254
|
)
|
|
253
255
|
if kind_requirement(kind, Shape.SCALAR, roles, n_groups) is None
|
|
254
|
-
and not (temporal and kind
|
|
256
|
+
and not (temporal and kind in BAR_KINDS)
|
|
255
257
|
]
|
|
256
258
|
|
|
257
259
|
if shape is Shape.SERIES_1D:
|
|
@@ -334,7 +336,7 @@ def why_unavailable(
|
|
|
334
336
|
if kind is PlotKind.HISTOGRAM:
|
|
335
337
|
return HISTOGRAM_NO_X
|
|
336
338
|
return "With an X variable, the kinds are scatter, line and regression; clear it for the others."
|
|
337
|
-
if temporal and kind
|
|
339
|
+
if temporal and kind in BAR_KINDS:
|
|
338
340
|
return BAR_REASON
|
|
339
341
|
if shape is Shape.MATRIX_2D:
|
|
340
342
|
return "2-D measures render as a heatmap."
|
|
@@ -56,13 +56,16 @@ from .roles import (
|
|
|
56
56
|
)
|
|
57
57
|
from .shape import Shape
|
|
58
58
|
from .spec import (
|
|
59
|
+
BAR_KINDS,
|
|
59
60
|
CONTINUOUS_X_KINDS,
|
|
61
|
+
SUMMARY_KINDS,
|
|
60
62
|
ErrorBand,
|
|
61
63
|
FactorVariable,
|
|
62
64
|
PlotKind,
|
|
63
65
|
PlotSpec,
|
|
64
66
|
Role,
|
|
65
67
|
Statistic,
|
|
68
|
+
is_horizontal,
|
|
66
69
|
value_spellings,
|
|
67
70
|
)
|
|
68
71
|
from .table import MISSING_LEVEL, LongTable
|
|
@@ -535,7 +538,7 @@ def _docstring(spec: PlotSpec, table: LongTable, roles: dict, y_plan: _YLimitPla
|
|
|
535
538
|
steps = collapse_steps(spec, roles, table)
|
|
536
539
|
if steps.all:
|
|
537
540
|
chain = " -> ".join([*steps.pre, " x ".join(steps.sample)])
|
|
538
|
-
if spec.kind in
|
|
541
|
+
if spec.kind in SUMMARY_KINDS:
|
|
539
542
|
what = (
|
|
540
543
|
f"error bars: {spec.aggregate.error}"
|
|
541
544
|
f"{' (t-based)' if spec.aggregate.error is ErrorBand.CI95 else ''} across "
|
|
@@ -851,7 +854,7 @@ def _preamble(spec, table, roles, shape) -> list[str]:
|
|
|
851
854
|
)
|
|
852
855
|
if comparison is not None and not compared:
|
|
853
856
|
lines.extend(_compare_lines(comparison, spec, kept, shape, index_column, overlay_drawn))
|
|
854
|
-
if steps.sample and spec.kind in
|
|
857
|
+
if steps.sample and spec.kind in SUMMARY_KINDS:
|
|
855
858
|
lines.append(
|
|
856
859
|
f"# the sample: {' x '.join(steps.sample)} — seaborn's estimator and "
|
|
857
860
|
f"errorbar run over its levels{' (pooled)' if len(steps.sample) > 1 else ''}"
|
|
@@ -1425,6 +1428,13 @@ def _sample_draw_lines(
|
|
|
1425
1428
|
nested = _nested_x_order(spec, table, roles, shape)
|
|
1426
1429
|
weight = sample_weight(spec.style)
|
|
1427
1430
|
linestyle = "-" if join.join else "none"
|
|
1431
|
+
# The screen (x, y) of a sample point: (position, value), or (value,
|
|
1432
|
+
# position) on a horizontal kind (spec.is_horizontal).
|
|
1433
|
+
xy = (
|
|
1434
|
+
f"_part[{y!r}], _part[{_X_POSITION!r}]"
|
|
1435
|
+
if is_horizontal(spec.kind)
|
|
1436
|
+
else f"_part[{_X_POSITION!r}], _part[{y!r}]"
|
|
1437
|
+
)
|
|
1428
1438
|
|
|
1429
1439
|
spaghetti = spec.kind is PlotKind.SPAGHETTI
|
|
1430
1440
|
line_layers = _line_layers(spec, table, roles, shape) if spaghetti else []
|
|
@@ -1546,14 +1556,14 @@ def _sample_draw_lines(
|
|
|
1546
1556
|
*(
|
|
1547
1557
|
[
|
|
1548
1558
|
" if len(_part) > 1:",
|
|
1549
|
-
f" _ax.plot(
|
|
1559
|
+
f" _ax.plot({xy}, "
|
|
1550
1560
|
f"color=_line_color, alpha={SAMPLE_ALPHA}, "
|
|
1551
1561
|
f"linewidth={weight.line_pt:.3f}, zorder=3)",
|
|
1552
1562
|
]
|
|
1553
1563
|
if join.join
|
|
1554
1564
|
else []
|
|
1555
1565
|
),
|
|
1556
|
-
f" _ax.scatter(
|
|
1566
|
+
f" _ax.scatter({xy}, "
|
|
1557
1567
|
f"s={weight.marker_area:.3f}, color=_point_colors, alpha={SAMPLE_ALPHA}, "
|
|
1558
1568
|
f"edgecolors={SAMPLE_EDGE_COLOR!r}, linewidths={SAMPLE_EDGE_WIDTH:g}, "
|
|
1559
1569
|
+ (
|
|
@@ -1569,7 +1579,7 @@ def _sample_draw_lines(
|
|
|
1569
1579
|
else []
|
|
1570
1580
|
),
|
|
1571
1581
|
" _ax.plot(",
|
|
1572
|
-
f"
|
|
1582
|
+
f" {xy},",
|
|
1573
1583
|
f" linestyle={linestyle!r}, marker='o', markersize={weight.marker_pt:.3f},",
|
|
1574
1584
|
f" markeredgecolor={SAMPLE_EDGE_COLOR!r}, "
|
|
1575
1585
|
f"markeredgewidth={SAMPLE_EDGE_WIDTH:g},",
|
|
@@ -1868,8 +1878,11 @@ def _alias_definition_lines(aliases: _ExportAliases) -> list[str]:
|
|
|
1868
1878
|
]
|
|
1869
1879
|
|
|
1870
1880
|
|
|
1871
|
-
def _alias_relabel_lines(
|
|
1872
|
-
|
|
1881
|
+
def _alias_relabel_lines(
|
|
1882
|
+
aliases: _ExportAliases, x_categorical: bool, cat: str = "x"
|
|
1883
|
+
) -> list[str]:
|
|
1884
|
+
"""Relabel the drawn category ticks (on ``cat``, the screen axis they are
|
|
1885
|
+
drawn on: y for a horizontal kind) and legend entries through ``_alias``.
|
|
1873
1886
|
|
|
1874
1887
|
Runs after everything that draws or rebuilds text (the overlay rebuilds
|
|
1875
1888
|
the legend) and before the fitted-tick replay, which then works on the
|
|
@@ -1891,8 +1904,8 @@ def _alias_relabel_lines(aliases: _ExportAliases, x_categorical: bool) -> list[s
|
|
|
1891
1904
|
f"_x_text = {text}",
|
|
1892
1905
|
"g.figure.canvas.draw() # categorical tick labels exist once drawn",
|
|
1893
1906
|
"for _ax in g.axes.flat:",
|
|
1894
|
-
" _ax.
|
|
1895
|
-
" _ax.
|
|
1907
|
+
f" _ax.set_{cat}ticks(_ax.get_{cat}ticks())",
|
|
1908
|
+
f" _ax.set_{cat}ticklabels([_x_text(t.get_text()) for t in _ax.get_{cat}ticklabels()])",
|
|
1896
1909
|
]
|
|
1897
1910
|
)
|
|
1898
1911
|
dashes = aliases.aliased(*aliases.dash_layers)
|
|
@@ -1945,7 +1958,7 @@ def _uses_ci95(spec, shape) -> bool:
|
|
|
1945
1958
|
"""True when the plot call passes `errorbar=_ci95` (bar and band only)."""
|
|
1946
1959
|
return (
|
|
1947
1960
|
shape is not Shape.MATRIX_2D
|
|
1948
|
-
and spec.kind in
|
|
1961
|
+
and spec.kind in SUMMARY_KINDS
|
|
1949
1962
|
and spec.aggregate.error is ErrorBand.CI95
|
|
1950
1963
|
)
|
|
1951
1964
|
|
|
@@ -1990,6 +2003,13 @@ def _plot_call(
|
|
|
1990
2003
|
if kind is PlotKind.HISTOGRAM
|
|
1991
2004
|
else [f"data=df", f"x={x_column!r}", f"y={spec.y_measure!r}"]
|
|
1992
2005
|
)
|
|
2006
|
+
if is_horizontal(kind):
|
|
2007
|
+
# The same bars turned (spec.is_horizontal): the categories on y,
|
|
2008
|
+
# the measure along x, and the orientation stated, not inferred.
|
|
2009
|
+
args = [f"data=df", f"x={spec.y_measure!r}", f"y={x_column!r}", 'orient="h"']
|
|
2010
|
+
# The screen axis each logical axis is drawn on: the CATEGORY axis and
|
|
2011
|
+
# the VALUE axis (y limits, y scale, the y title) — swapped when horizontal.
|
|
2012
|
+
cat, val = _screen_axes(spec)
|
|
1993
2013
|
if color:
|
|
1994
2014
|
args.append(f"hue={color!r}")
|
|
1995
2015
|
if _color_level_count(spec, table, color) < 2:
|
|
@@ -2011,11 +2031,12 @@ def _plot_call(
|
|
|
2011
2031
|
)
|
|
2012
2032
|
errorbar = _SEABORN_ERRORBAR[spec.aggregate.error]
|
|
2013
2033
|
|
|
2014
|
-
if kind in (PlotKind.BOX, PlotKind.VIOLIN,
|
|
2034
|
+
if kind in (PlotKind.BOX, PlotKind.VIOLIN, *BAR_KINDS, PlotKind.STRIP):
|
|
2015
2035
|
seaborn_kind = {
|
|
2016
2036
|
PlotKind.BOX: "box",
|
|
2017
2037
|
PlotKind.VIOLIN: "violin",
|
|
2018
2038
|
PlotKind.BAR: "bar",
|
|
2039
|
+
PlotKind.HBAR: "bar",
|
|
2019
2040
|
PlotKind.STRIP: "strip",
|
|
2020
2041
|
}[kind]
|
|
2021
2042
|
args.append(f'kind="{seaborn_kind}"')
|
|
@@ -2027,7 +2048,7 @@ def _plot_call(
|
|
|
2027
2048
|
# mark there narrow and off-centre — the preview draws it on its
|
|
2028
2049
|
# tick, full width (`render.mpl._draw_bars`).
|
|
2029
2050
|
args.append("dodge=False")
|
|
2030
|
-
if kind
|
|
2051
|
+
if kind in BAR_KINDS:
|
|
2031
2052
|
args.append(f"estimator={estimator}")
|
|
2032
2053
|
args.append(f"errorbar={errorbar}")
|
|
2033
2054
|
# The export's error-bar ink and caps (paper.seaborn_err_kws);
|
|
@@ -2160,7 +2181,7 @@ def _plot_call(
|
|
|
2160
2181
|
# argument" (found 2026-09-24 — every faceted per-panel catplot export
|
|
2161
2182
|
# raised). relplot has no `sharey` and takes it through facet_kws.
|
|
2162
2183
|
args.append(
|
|
2163
|
-
"
|
|
2184
|
+
f"share{val}=False" if call == "sns.catplot" else 'facet_kws={"sharey": False}'
|
|
2164
2185
|
)
|
|
2165
2186
|
|
|
2166
2187
|
aliases = _export_aliases(spec, table, roles, shape)
|
|
@@ -2185,9 +2206,9 @@ def _plot_call(
|
|
|
2185
2206
|
if kind is PlotKind.REGRESSION:
|
|
2186
2207
|
lines.extend(_fit_lines(spec, table, color, facets, aliases))
|
|
2187
2208
|
if ylim is not None:
|
|
2188
|
-
lines.append(f"g.set(
|
|
2209
|
+
lines.append(f"g.set({val}lim={(float(ylim[0]), float(ylim[1]))!r})")
|
|
2189
2210
|
if y_plan.by_panel is not None:
|
|
2190
|
-
lines.extend(_panel_ylim_lines(y_plan, facets))
|
|
2211
|
+
lines.extend(_panel_ylim_lines(y_plan, facets, val))
|
|
2191
2212
|
# The one owner (reduce.x_axis_title): no title on a nested axis, none
|
|
2192
2213
|
# for the constant-x scaffolding column, the factor's display name
|
|
2193
2214
|
# otherwise — exactly what the preview labels the axis.
|
|
@@ -2227,7 +2248,10 @@ def _plot_call(
|
|
|
2227
2248
|
and table.measure(spec.x_measure).temporal
|
|
2228
2249
|
):
|
|
2229
2250
|
x_label += zone_suffix
|
|
2230
|
-
|
|
2251
|
+
# Positional (x title, y title) on the SCREEN: a horizontal kind's value
|
|
2252
|
+
# title is the one along x.
|
|
2253
|
+
titles = (y_label, x_label) if val == "x" else (x_label, y_label)
|
|
2254
|
+
lines.append(f"g.set_axis_labels({titles[0]!r}, {titles[1]!r})")
|
|
2231
2255
|
if facets:
|
|
2232
2256
|
# Same rule as the preview (render.base.panel_y_title): the facet values
|
|
2233
2257
|
# ARE the panel's y-axis title, and no caption sits above it. seaborn
|
|
@@ -2243,14 +2267,14 @@ def _plot_call(
|
|
|
2243
2267
|
else "str(v) for v in reversed(_values)"
|
|
2244
2268
|
)
|
|
2245
2269
|
if overrides.titles or overrides.first_column:
|
|
2246
|
-
lines.extend(['g.set_titles("")', *_panel_title_lines(overrides, shown)])
|
|
2270
|
+
lines.extend(['g.set_titles("")', *_panel_title_lines(overrides, shown, val)])
|
|
2247
2271
|
else:
|
|
2248
2272
|
lines.extend(
|
|
2249
2273
|
[
|
|
2250
2274
|
'g.set_titles("")',
|
|
2251
2275
|
"for _key, _ax in g.axes_dict.items():",
|
|
2252
2276
|
" _values = _key if isinstance(_key, tuple) else (_key,)",
|
|
2253
|
-
f' _ax.
|
|
2277
|
+
f' _ax.set_{val}label(" · ".join({shown}))',
|
|
2254
2278
|
]
|
|
2255
2279
|
)
|
|
2256
2280
|
sizes = _export_sizes(style)
|
|
@@ -2273,14 +2297,16 @@ def _plot_call(
|
|
|
2273
2297
|
and spec.x_measure in table.measure_names
|
|
2274
2298
|
and table.measure(spec.x_measure).temporal
|
|
2275
2299
|
)
|
|
2276
|
-
|
|
2300
|
+
# A horizontal kind's category axis is y and takes no log scale (as the
|
|
2301
|
+
# preview, mpl._apply_horizontal_cosmetics); its value axis is x.
|
|
2302
|
+
if style.log_x and not log_inert and not temporal_x_measure and cat == "x":
|
|
2277
2303
|
lines.append('g.set(xscale="log")')
|
|
2278
2304
|
if style.log_y and not log_inert:
|
|
2279
|
-
lines.append('g.set(
|
|
2280
|
-
if y_plan.autoscaled and kind
|
|
2281
|
-
lines.extend(_bar_autoscale_lines())
|
|
2305
|
+
lines.append(f'g.set({val}scale="log")')
|
|
2306
|
+
if y_plan.autoscaled and kind in BAR_KINDS and not style.log_y:
|
|
2307
|
+
lines.extend(_bar_autoscale_lines(val))
|
|
2282
2308
|
# Last of the y-range lines, so nothing after it rescales a pinned panel.
|
|
2283
|
-
lines.extend(_panel_override_ylim_lines(overrides, facets))
|
|
2309
|
+
lines.extend(_panel_override_ylim_lines(overrides, facets, val))
|
|
2284
2310
|
# Difference bars set the final range of every panel they sit in (and
|
|
2285
2311
|
# of every panel sharing that range), so they come after the pins.
|
|
2286
2312
|
lines.extend(
|
|
@@ -2290,7 +2316,9 @@ def _plot_call(
|
|
|
2290
2316
|
)
|
|
2291
2317
|
if style.title:
|
|
2292
2318
|
lines.append(f"g.figure.suptitle({style.title!r})")
|
|
2293
|
-
lines.extend(
|
|
2319
|
+
lines.extend(
|
|
2320
|
+
_alias_relabel_lines(aliases, _x_is_categorical(spec, table, roles, shape), cat)
|
|
2321
|
+
)
|
|
2294
2322
|
# The frame, ticks and background the preview and render_matplotlib draw
|
|
2295
2323
|
# (paper.py). Also undoes seaborn's default despine, which would export
|
|
2296
2324
|
# an open frame beside a boxed preview.
|
|
@@ -2341,13 +2369,16 @@ def _fitted_tick_lines(spec, table: LongTable, roles, shape) -> list[str]:
|
|
|
2341
2369
|
wrapped[original[len(fit.prefix):]] = text
|
|
2342
2370
|
rotated = fit.rotation != 0
|
|
2343
2371
|
style = spec.style
|
|
2372
|
+
# The category axis: y on a horizontal kind, whose labels are stacked
|
|
2373
|
+
# and never rotated (ticklabels.STACKED_TICK_POLICY).
|
|
2374
|
+
cat, _val = _screen_axes(spec)
|
|
2344
2375
|
lines = [
|
|
2345
|
-
f"#
|
|
2376
|
+
f"# {cat} tick labels as scistackplot fitted them at {style.width} x {style.height} in:"
|
|
2346
2377
|
f" {fit.describe()}"
|
|
2347
2378
|
+ (" (nested axis: rotation, font and every-k only)" if nested else ""),
|
|
2348
2379
|
"g.figure.canvas.draw() # categorical tick labels exist once drawn",
|
|
2349
2380
|
"for _ax in g.axes.flat:",
|
|
2350
|
-
" _labels = [t.get_text() for t in _ax.
|
|
2381
|
+
f" _labels = [t.get_text() for t in _ax.get_{cat}ticklabels()]",
|
|
2351
2382
|
]
|
|
2352
2383
|
if fit.prefix and not nested:
|
|
2353
2384
|
lines.append(
|
|
@@ -2361,6 +2392,14 @@ def _fitted_tick_lines(spec, table: LongTable, roles, shape) -> list[str]:
|
|
|
2361
2392
|
f" _labels = [s if i % {fit.every} == 0 or i == len(_labels) - 1 else '' "
|
|
2362
2393
|
"for i, s in enumerate(_labels)]"
|
|
2363
2394
|
)
|
|
2395
|
+
if cat == "y":
|
|
2396
|
+
lines.extend(
|
|
2397
|
+
[
|
|
2398
|
+
" _ax.set_yticks(_ax.get_yticks())",
|
|
2399
|
+
f" _ax.set_yticklabels(_labels, fontsize={fit.font_pt})",
|
|
2400
|
+
]
|
|
2401
|
+
)
|
|
2402
|
+
return lines
|
|
2364
2403
|
lines.extend(
|
|
2365
2404
|
[
|
|
2366
2405
|
" _ax.set_xticks(_ax.get_xticks())",
|
|
@@ -2429,7 +2468,7 @@ def _declared_levels(table: LongTable, name: str) -> list[str]:
|
|
|
2429
2468
|
|
|
2430
2469
|
def _seaborn_call(spec, table, roles, shape) -> str:
|
|
2431
2470
|
"""Which seaborn function ``_plot_call`` draws with (heatmaps aside)."""
|
|
2432
|
-
if spec.kind in (PlotKind.BOX, PlotKind.VIOLIN,
|
|
2471
|
+
if spec.kind in (PlotKind.BOX, PlotKind.VIOLIN, *BAR_KINDS, PlotKind.STRIP):
|
|
2433
2472
|
return "sns.catplot"
|
|
2434
2473
|
if spec.kind is PlotKind.SCATTER and _x_is_categorical(spec, table, roles, shape):
|
|
2435
2474
|
return "sns.catplot"
|
|
@@ -2504,7 +2543,7 @@ def _heatmap_call(spec, table: LongTable) -> list[str]:
|
|
|
2504
2543
|
]
|
|
2505
2544
|
|
|
2506
2545
|
|
|
2507
|
-
def _bar_autoscale_lines() -> list[str]:
|
|
2546
|
+
def _bar_autoscale_lines(val: str = "y") -> list[str]:
|
|
2508
2547
|
"""Let an autoscaled bar axis pad below zero, as the preview's does.
|
|
2509
2548
|
|
|
2510
2549
|
The preview's bar limits are ``min(low, 0)`` .. ``max(high, 0)`` padded by
|
|
@@ -2523,7 +2562,7 @@ def _bar_autoscale_lines() -> list[str]:
|
|
|
2523
2562
|
"# otherwise pins an autoscaled bar axis to exactly 0).",
|
|
2524
2563
|
"for _ax in g.axes.flat:",
|
|
2525
2564
|
" _ax.use_sticky_edges = False",
|
|
2526
|
-
" _ax.autoscale_view(
|
|
2565
|
+
f" _ax.autoscale_view(scale{'y' if val == 'x' else 'x'}=False)",
|
|
2527
2566
|
]
|
|
2528
2567
|
|
|
2529
2568
|
|
|
@@ -2608,7 +2647,7 @@ def _panel_override_export(
|
|
|
2608
2647
|
|
|
2609
2648
|
|
|
2610
2649
|
def _panel_override_ylim_lines(
|
|
2611
|
-
overrides: _PanelOverrideExport, facets: list[str]
|
|
2650
|
+
overrides: _PanelOverrideExport, facets: list[str], val: str = "y"
|
|
2612
2651
|
) -> list[str]:
|
|
2613
2652
|
"""Each overridden panel's own range, after every other y-range line.
|
|
2614
2653
|
|
|
@@ -2623,11 +2662,17 @@ def _panel_override_ylim_lines(
|
|
|
2623
2662
|
" _values = _key if isinstance(_key, tuple) else (_key,)",
|
|
2624
2663
|
" _pin = _panel_ylims.get(tuple(str(_v) for _v in reversed(_values)))",
|
|
2625
2664
|
" if _pin is not None:",
|
|
2626
|
-
|
|
2665
|
+
(
|
|
2666
|
+
" _ax.set_ylim(bottom=_pin[0], top=_pin[1])"
|
|
2667
|
+
if val == "y"
|
|
2668
|
+
else " _ax.set_xlim(left=_pin[0], right=_pin[1])"
|
|
2669
|
+
),
|
|
2627
2670
|
]
|
|
2628
2671
|
|
|
2629
2672
|
|
|
2630
|
-
def _panel_title_lines(
|
|
2673
|
+
def _panel_title_lines(
|
|
2674
|
+
overrides: _PanelOverrideExport, shown: str, val: str = "y"
|
|
2675
|
+
) -> list[str]:
|
|
2631
2676
|
"""Each panel's y title with its override and the ``y_titles`` toggle.
|
|
2632
2677
|
|
|
2633
2678
|
The text and the hide flag belong to the panel's facet values, so they are
|
|
@@ -2650,7 +2695,7 @@ def _panel_title_lines(overrides: _PanelOverrideExport, shown: str) -> list[str]
|
|
|
2650
2695
|
f" _hidden = {unset_hidden}",
|
|
2651
2696
|
" if _text is None:",
|
|
2652
2697
|
f' _text = " · ".join({shown})',
|
|
2653
|
-
' _ax.
|
|
2698
|
+
f' _ax.set_{val}label("" if _hidden else _text)',
|
|
2654
2699
|
]
|
|
2655
2700
|
|
|
2656
2701
|
|
|
@@ -2689,7 +2734,8 @@ def _difference_bar_export(
|
|
|
2689
2734
|
would move every bar anyway: the code says the positions are fitted to
|
|
2690
2735
|
this data.
|
|
2691
2736
|
"""
|
|
2692
|
-
if not spec.difference_bars or shape is not Shape.SCALAR:
|
|
2737
|
+
if not spec.difference_bars or shape is not Shape.SCALAR or is_horizontal(spec.kind):
|
|
2738
|
+
# A horizontal kind draws none (diffbars.HORIZONTAL_REASON), as the preview.
|
|
2693
2739
|
return None
|
|
2694
2740
|
import matplotlib.pyplot as plt
|
|
2695
2741
|
|
|
@@ -2827,8 +2873,9 @@ def _difference_bar_lines(export: _DifferenceExport | None, style) -> list[str]:
|
|
|
2827
2873
|
]
|
|
2828
2874
|
|
|
2829
2875
|
|
|
2830
|
-
def _panel_ylim_lines(y_plan: _YLimitPlan, facets: list[str]) -> list[str]:
|
|
2831
|
-
"""Set each panel's y limits from the preview's per-facet table
|
|
2876
|
+
def _panel_ylim_lines(y_plan: _YLimitPlan, facets: list[str], val: str = "y") -> list[str]:
|
|
2877
|
+
"""Set each panel's y limits from the preview's per-facet table (on
|
|
2878
|
+
``val``, the screen axis the values run along: x for a horizontal kind).
|
|
2832
2879
|
|
|
2833
2880
|
``axes_dict`` is keyed by the ``col`` value, or by ``(row, col)`` for two
|
|
2834
2881
|
facets — the reverse of ``facets``' order, hence the ``reversed()`` (the
|
|
@@ -2848,7 +2895,7 @@ def _panel_ylim_lines(y_plan: _YLimitPlan, facets: list[str]) -> list[str]:
|
|
|
2848
2895
|
" _values = dict(",
|
|
2849
2896
|
f" zip({facets!r}, reversed(_key if isinstance(_key, tuple) else (_key,)))",
|
|
2850
2897
|
" )",
|
|
2851
|
-
" _ax.
|
|
2898
|
+
f" _ax.set_{val}lim(",
|
|
2852
2899
|
f" _ylims.get(tuple(str(_values[_f]) for _f in {list(y_plan.panel_scope)!r}), "
|
|
2853
2900
|
f"{fallback!r})",
|
|
2854
2901
|
" )",
|
|
@@ -3137,6 +3184,13 @@ def _x_expression(spec, table, roles, shape) -> str:
|
|
|
3137
3184
|
return ticks[0] if ticks else _X_CONSTANT
|
|
3138
3185
|
|
|
3139
3186
|
|
|
3187
|
+
def _screen_axes(spec) -> tuple[str, str]:
|
|
3188
|
+
"""``(category axis, value axis)`` as the screen letters the generated
|
|
3189
|
+
code addresses: ``("x", "y")``, or ``("y", "x")`` for a horizontal kind
|
|
3190
|
+
(``spec.is_horizontal``, the one owner of orientation)."""
|
|
3191
|
+
return ("y", "x") if is_horizontal(spec.kind) else ("x", "y")
|
|
3192
|
+
|
|
3193
|
+
|
|
3140
3194
|
def _x_is_categorical(spec, table, roles, shape) -> bool:
|
|
3141
3195
|
if spec.x_measure or shape is Shape.SERIES_1D or spec.kind in CONTINUOUS_X_KINDS:
|
|
3142
3196
|
return False
|
|
@@ -168,6 +168,14 @@ DIFFERENCE_BAR_KINDS: tuple[str, ...] = (
|
|
|
168
168
|
"spaghetti",
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
+
#: Why a horizontal kind (``spec.HORIZONTAL_KINDS``) carries no difference
|
|
172
|
+
#: bars: they are stacked ABOVE the marks in points, a geometry with no
|
|
173
|
+
#: horizontal counterpart yet (user, 2026-10-26: omit them for HBAR).
|
|
174
|
+
HORIZONTAL_REASON = (
|
|
175
|
+
"Difference bars are drawn above vertical marks; use the vertical bar "
|
|
176
|
+
"plot for them."
|
|
177
|
+
)
|
|
178
|
+
|
|
171
179
|
|
|
172
180
|
def unavailable(spec: "PlotSpec", shape: "Shape", *, temporal: bool = False) -> str | None:
|
|
173
181
|
"""Why this figure cannot carry difference bars, or None.
|
|
@@ -178,7 +186,10 @@ def unavailable(spec: "PlotSpec", shape: "Shape", *, temporal: bool = False) ->
|
|
|
178
186
|
answers it.
|
|
179
187
|
"""
|
|
180
188
|
from .shape import Shape
|
|
189
|
+
from .spec import is_horizontal
|
|
181
190
|
|
|
191
|
+
if is_horizontal(spec.kind):
|
|
192
|
+
return HORIZONTAL_REASON
|
|
182
193
|
if str(spec.kind) not in DIFFERENCE_BAR_KINDS:
|
|
183
194
|
return (
|
|
184
195
|
f"A {spec.kind} plot has no tick-by-tick marks to join; difference "
|
|
@@ -79,6 +79,7 @@ from .spec import (
|
|
|
79
79
|
PlotKind,
|
|
80
80
|
PlotSpec,
|
|
81
81
|
Role,
|
|
82
|
+
SUMMARY_KINDS,
|
|
82
83
|
Statistic,
|
|
83
84
|
grid_shape_for,
|
|
84
85
|
value_spellings,
|
|
@@ -1354,14 +1355,14 @@ def _build_figure(
|
|
|
1354
1355
|
overlay_frame: pd.DataFrame | None = None
|
|
1355
1356
|
summarize_nested = (
|
|
1356
1357
|
explode
|
|
1357
|
-
and spec.kind in
|
|
1358
|
+
and spec.kind in SUMMARY_KINDS
|
|
1358
1359
|
# A compared 1-D band/bar takes the exploded route: the baseline
|
|
1359
1360
|
# is `compare`'s, over long rows, never restated over cells. The
|
|
1360
1361
|
# transport stride then runs after the summary is built from the
|
|
1361
1362
|
# compared rows (`downsample`), not inside the explode.
|
|
1362
1363
|
and comparison is None
|
|
1363
1364
|
)
|
|
1364
|
-
if comparison is not None and explode and spec.kind in
|
|
1365
|
+
if comparison is not None and explode and spec.kind in SUMMARY_KINDS:
|
|
1365
1366
|
Log.info(
|
|
1366
1367
|
"compare: 1-D %s summarised from exploded rows (the nested-cell "
|
|
1367
1368
|
"summary does not compare)",
|
|
@@ -2231,7 +2232,7 @@ def _panel_frame(
|
|
|
2231
2232
|
out = out.dropna(subset=[Y])
|
|
2232
2233
|
|
|
2233
2234
|
# --- summarize the sample into centre + error ------------------------
|
|
2234
|
-
if spec.kind in
|
|
2235
|
+
if spec.kind in SUMMARY_KINDS:
|
|
2235
2236
|
# Carry the nested axis's LAYER columns through the summary. They are a
|
|
2236
2237
|
# function of the composed key, so grouping by them as well changes no
|
|
2237
2238
|
# group — but without them `_summarize`'s `reset_index` returns three
|
|
@@ -2944,8 +2945,8 @@ def _encoding_for(
|
|
|
2944
2945
|
x=X,
|
|
2945
2946
|
y=Y,
|
|
2946
2947
|
color=COLOR if color else None,
|
|
2947
|
-
y_low=Y_LOW if kind in
|
|
2948
|
-
y_high=Y_HIGH if kind in
|
|
2948
|
+
y_low=Y_LOW if kind in SUMMARY_KINDS else None,
|
|
2949
|
+
y_high=Y_HIGH if kind in SUMMARY_KINDS else None,
|
|
2949
2950
|
series=SERIES if series else None,
|
|
2950
2951
|
dash=DASH if has_dash else None,
|
|
2951
2952
|
)
|
|
@@ -27,7 +27,7 @@ from ..panels import (
|
|
|
27
27
|
)
|
|
28
28
|
from ..resolved import DASH_CYCLE, SAMPLE_COLOR, SAMPLE_LINE, RUN, SERIES, ResolvedPlot
|
|
29
29
|
from ..roles import overlay_in_legend
|
|
30
|
-
from ..spec import PlotKind
|
|
30
|
+
from ..spec import BAR_KINDS, PlotKind, is_horizontal
|
|
31
31
|
|
|
32
32
|
LAYER = "scistackplot"
|
|
33
33
|
|
|
@@ -227,6 +227,38 @@ def shows_y_labels(resolved: ResolvedPlot, row: int, col: int) -> bool:
|
|
|
227
227
|
return is_leftmost(resolved, row, col)
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
def shows_category_labels(resolved: ResolvedPlot, row: int, col: int) -> bool:
|
|
231
|
+
"""Whether this cell labels the CATEGORY axis (logical x: its ticks, its
|
|
232
|
+
brackets, its title) — the one rule both renderers read, whichever way
|
|
233
|
+
the kind points. Vertical, that is the axis at the bottom
|
|
234
|
+
(:func:`shows_x_labels`); horizontal (``spec.is_horizontal``) it runs
|
|
235
|
+
down the left, labelled where nothing sits to the left
|
|
236
|
+
(:func:`is_leftmost`)."""
|
|
237
|
+
if is_horizontal(resolved.kind):
|
|
238
|
+
return is_leftmost(resolved, row, col)
|
|
239
|
+
return shows_x_labels(resolved, row, col)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def axis_sides(resolved: ResolvedPlot) -> dict[str, str]:
|
|
243
|
+
"""Which side of each panel the logical axes are drawn on —
|
|
244
|
+
``{"x": "bottom", "y": "left"}``, or swapped for a horizontal kind. The
|
|
245
|
+
GUI says where its "X label" / "Y axis" controls land from this
|
|
246
|
+
(``layout.meta.axis_sides``), never from the kind."""
|
|
247
|
+
if is_horizontal(resolved.kind):
|
|
248
|
+
return {"x": "left", "y": "bottom"}
|
|
249
|
+
return {"x": "bottom", "y": "left"}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def shows_value_labels(resolved: ResolvedPlot, row: int, col: int) -> bool:
|
|
253
|
+
"""Whether this cell numbers the VALUE axis (logical y). Vertical:
|
|
254
|
+
:func:`shows_y_labels`. Horizontal: the same rule turned — nothing
|
|
255
|
+
directly below, unless the panels are on different scales, when every
|
|
256
|
+
panel keeps its numbers."""
|
|
257
|
+
if is_horizontal(resolved.kind):
|
|
258
|
+
return not shares_y_axis(resolved) or shows_x_labels(resolved, row, col)
|
|
259
|
+
return shows_y_labels(resolved, row, col)
|
|
260
|
+
|
|
261
|
+
|
|
230
262
|
def is_leftmost(resolved: ResolvedPlot, row: int, col: int) -> bool:
|
|
231
263
|
"""Whether nothing sits directly to the left of this cell: the first
|
|
232
264
|
column, including a panel in a partial wrapped row whose left cell is
|
|
@@ -295,7 +327,7 @@ def panel_override_meta(resolved: ResolvedPlot) -> dict:
|
|
|
295
327
|
``unmatched`` lists overrides for panels not in this figure (inert).
|
|
296
328
|
"""
|
|
297
329
|
at_left = {
|
|
298
|
-
id(panel):
|
|
330
|
+
id(panel): shows_value_labels(resolved, panel.grid_row, panel.grid_col)
|
|
299
331
|
for panel in resolved.panels
|
|
300
332
|
}
|
|
301
333
|
panels = []
|
|
@@ -693,7 +725,7 @@ def fill_alpha(kind: PlotKind, style) -> float | None:
|
|
|
693
725
|
and its own half-transparent box fill, and the export drew every fill
|
|
694
726
|
opaque.
|
|
695
727
|
"""
|
|
696
|
-
if kind
|
|
728
|
+
if kind in BAR_KINDS:
|
|
697
729
|
return style.alpha
|
|
698
730
|
if kind is PlotKind.BOX:
|
|
699
731
|
return BOX_FILL_ALPHA
|