plotlet 0.4.0__tar.gz → 0.5.0__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.
- plotlet-0.5.0/PKG-INFO +82 -0
- plotlet-0.5.0/README.md +50 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/pyproject.toml +9 -3
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet/__init__.py +17 -8
- plotlet-0.5.0/src/plotlet/_attachments.py +237 -0
- plotlet-0.5.0/src/plotlet/_datasets/penguins.csv +345 -0
- plotlet-0.4.0/src/plotlet/layout.py → plotlet-0.5.0/src/plotlet/_layout_engine.py +207 -194
- plotlet-0.5.0/src/plotlet/_spec.py +105 -0
- plotlet-0.5.0/src/plotlet/_splits.py +190 -0
- plotlet-0.5.0/src/plotlet/artists/__init__.py +52 -0
- plotlet-0.5.0/src/plotlet/artists/_shared.py +191 -0
- plotlet-0.5.0/src/plotlet/artists/bar.py +243 -0
- plotlet-0.5.0/src/plotlet/artists/boxplot.py +254 -0
- plotlet-0.5.0/src/plotlet/artists/contour.py +129 -0
- plotlet-0.5.0/src/plotlet/artists/dendrogram.py +230 -0
- plotlet-0.5.0/src/plotlet/artists/density_1d.py +139 -0
- plotlet-0.5.0/src/plotlet/artists/ecdf.py +118 -0
- plotlet-0.5.0/src/plotlet/artists/errorbar.py +183 -0
- plotlet-0.5.0/src/plotlet/artists/fills.py +235 -0
- plotlet-0.5.0/src/plotlet/artists/freqpoly.py +147 -0
- plotlet-0.5.0/src/plotlet/artists/heatmap.py +436 -0
- plotlet-0.5.0/src/plotlet/artists/hexbin.py +114 -0
- plotlet-0.5.0/src/plotlet/artists/hist.py +193 -0
- plotlet-0.5.0/src/plotlet/artists/imshow.py +239 -0
- plotlet-0.5.0/src/plotlet/artists/kde_2d.py +145 -0
- plotlet-0.5.0/src/plotlet/artists/line.py +152 -0
- plotlet-0.5.0/src/plotlet/artists/pointplot.py +150 -0
- plotlet-0.5.0/src/plotlet/artists/qq.py +83 -0
- plotlet-0.5.0/src/plotlet/artists/references.py +224 -0
- plotlet-0.5.0/src/plotlet/artists/regression.py +179 -0
- plotlet-0.5.0/src/plotlet/artists/ridge.py +98 -0
- plotlet-0.5.0/src/plotlet/artists/rug.py +108 -0
- plotlet-0.5.0/src/plotlet/artists/scatter.py +279 -0
- plotlet-0.5.0/src/plotlet/artists/shapes.py +126 -0
- plotlet-0.5.0/src/plotlet/artists/strip.py +168 -0
- plotlet-0.5.0/src/plotlet/artists/swarm.py +174 -0
- plotlet-0.5.0/src/plotlet/artists/text.py +267 -0
- plotlet-0.5.0/src/plotlet/artists/violin.py +226 -0
- plotlet-0.5.0/src/plotlet/chart.py +1060 -0
- plotlet-0.5.0/src/plotlet/cluster.py +461 -0
- plotlet-0.5.0/src/plotlet/core.py +1973 -0
- plotlet-0.5.0/src/plotlet/data.py +83 -0
- plotlet-0.5.0/src/plotlet/draw/__init__.py +33 -0
- {plotlet-0.4.0/src/plotlet → plotlet-0.5.0/src/plotlet/draw}/colormaps.py +2 -2
- plotlet-0.5.0/src/plotlet/draw/colors.py +41 -0
- plotlet-0.5.0/src/plotlet/draw/font.py +117 -0
- plotlet-0.5.0/src/plotlet/draw/linestyles.py +27 -0
- plotlet-0.5.0/src/plotlet/draw/primitives.py +509 -0
- plotlet-0.5.0/src/plotlet/extensions/__init__.py +22 -0
- plotlet-0.5.0/src/plotlet/extensions/_gallery.py +205 -0
- plotlet-0.5.0/src/plotlet/extensions/alluvial.py +202 -0
- plotlet-0.5.0/src/plotlet/extensions/annotation_strip.py +305 -0
- plotlet-0.5.0/src/plotlet/extensions/bland_altman.py +93 -0
- plotlet-0.5.0/src/plotlet/extensions/boxenplot.py +251 -0
- plotlet-0.5.0/src/plotlet/extensions/bubble_grid.py +106 -0
- plotlet-0.5.0/src/plotlet/extensions/bump.py +116 -0
- plotlet-0.5.0/src/plotlet/extensions/calendar_heatmap.py +130 -0
- plotlet-0.5.0/src/plotlet/extensions/calibration_plot.py +135 -0
- plotlet-0.5.0/src/plotlet/extensions/cleveland_dot.py +96 -0
- plotlet-0.5.0/src/plotlet/extensions/confusion_matrix.py +149 -0
- plotlet-0.5.0/src/plotlet/extensions/crossbar.py +98 -0
- plotlet-0.5.0/src/plotlet/extensions/curved_tree.py +227 -0
- plotlet-0.5.0/src/plotlet/extensions/diverging_bar.py +99 -0
- plotlet-0.5.0/src/plotlet/extensions/dumbbell.py +90 -0
- plotlet-0.5.0/src/plotlet/extensions/eventplot.py +114 -0
- plotlet-0.5.0/src/plotlet/extensions/forest_plot.py +127 -0
- plotlet-0.5.0/src/plotlet/extensions/funnel_plot.py +126 -0
- plotlet-0.5.0/src/plotlet/extensions/gene_arrow.py +109 -0
- plotlet-0.5.0/src/plotlet/extensions/horizon.py +132 -0
- plotlet-0.5.0/src/plotlet/extensions/horizontal_bar.py +80 -0
- plotlet-0.5.0/src/plotlet/extensions/jointplot.py +90 -0
- plotlet-0.5.0/src/plotlet/extensions/km_curve.py +206 -0
- plotlet-0.5.0/src/plotlet/extensions/labels_strip.py +246 -0
- plotlet-0.5.0/src/plotlet/extensions/loess.py +95 -0
- plotlet-0.5.0/src/plotlet/extensions/lollipop.py +95 -0
- plotlet-0.5.0/src/plotlet/extensions/ma_plot.py +113 -0
- plotlet-0.5.0/src/plotlet/extensions/manhattan.py +133 -0
- plotlet-0.5.0/src/plotlet/extensions/mosaic.py +143 -0
- plotlet-0.5.0/src/plotlet/extensions/numeric_bar.py +98 -0
- plotlet-0.5.0/src/plotlet/extensions/pair_plot.py +90 -0
- plotlet-0.5.0/src/plotlet/extensions/parallel_coordinates.py +126 -0
- plotlet-0.5.0/src/plotlet/extensions/pca_biplot.py +113 -0
- plotlet-0.5.0/src/plotlet/extensions/percentile_band.py +108 -0
- plotlet-0.5.0/src/plotlet/extensions/pr_curve.py +126 -0
- plotlet-0.5.0/src/plotlet/extensions/pyramid_plot.py +97 -0
- plotlet-0.5.0/src/plotlet/extensions/raincloud.py +309 -0
- plotlet-0.5.0/src/plotlet/extensions/residual_diagnostics.py +112 -0
- plotlet-0.5.0/src/plotlet/extensions/roc_curve.py +123 -0
- plotlet-0.5.0/src/plotlet/extensions/sales_funnel.py +86 -0
- plotlet-0.5.0/src/plotlet/extensions/sankey.py +257 -0
- plotlet-0.5.0/src/plotlet/extensions/significance_brackets.py +132 -0
- plotlet-0.5.0/src/plotlet/extensions/slope_chart.py +88 -0
- plotlet-0.5.0/src/plotlet/extensions/split_violin.py +177 -0
- plotlet-0.5.0/src/plotlet/extensions/text_label.py +78 -0
- plotlet-0.5.0/src/plotlet/extensions/upset_plot.py +152 -0
- plotlet-0.5.0/src/plotlet/extensions/volcano.py +133 -0
- plotlet-0.5.0/src/plotlet/extensions/waterfall.py +131 -0
- plotlet-0.5.0/src/plotlet/facet.py +163 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet/legend.py +111 -64
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet/registry.py +18 -1
- plotlet-0.5.0/src/plotlet/scales.py +422 -0
- plotlet-0.5.0/src/plotlet/spec.json +109 -0
- plotlet-0.5.0/src/plotlet/themes/dark.json +24 -0
- plotlet-0.5.0/src/plotlet/themes/minimal.json +26 -0
- plotlet-0.5.0/src/plotlet/themes/void.json +15 -0
- plotlet-0.5.0/src/plotlet/themes.py +99 -0
- plotlet-0.5.0/src/plotlet/utils.py +319 -0
- plotlet-0.5.0/src/plotlet.egg-info/PKG-INFO +82 -0
- plotlet-0.5.0/src/plotlet.egg-info/SOURCES.txt +124 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet.egg-info/requires.txt +3 -0
- plotlet-0.5.0/tests/test_attachments.py +193 -0
- plotlet-0.5.0/tests/test_chart.py +1844 -0
- plotlet-0.5.0/tests/test_extensions.py +74 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/tests/test_layout_diagram.py +6 -4
- {plotlet-0.4.0 → plotlet-0.5.0}/tests/test_legend.py +11 -9
- {plotlet-0.4.0 → plotlet-0.5.0}/tests/test_subplots.py +85 -43
- plotlet-0.5.0/tests/test_themes.py +88 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/tests/test_units.py +16 -13
- plotlet-0.4.0/PKG-INFO +0 -176
- plotlet-0.4.0/README.md +0 -146
- plotlet-0.4.0/src/plotlet/_spec.py +0 -23
- plotlet-0.4.0/src/plotlet/artists.py +0 -503
- plotlet-0.4.0/src/plotlet/builtin_artists.py +0 -570
- plotlet-0.4.0/src/plotlet/chart.py +0 -718
- plotlet-0.4.0/src/plotlet/colors.py +0 -20
- plotlet-0.4.0/src/plotlet/core.py +0 -1055
- plotlet-0.4.0/src/plotlet/dendrogram.py +0 -197
- plotlet-0.4.0/src/plotlet/font.py +0 -63
- plotlet-0.4.0/src/plotlet/scales.py +0 -123
- plotlet-0.4.0/src/plotlet/spec.json +0 -100
- plotlet-0.4.0/src/plotlet.egg-info/PKG-INFO +0 -176
- plotlet-0.4.0/src/plotlet.egg-info/SOURCES.txt +0 -32
- plotlet-0.4.0/tests/test_chart.py +0 -564
- {plotlet-0.4.0 → plotlet-0.5.0}/LICENSE +0 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/setup.cfg +0 -0
- {plotlet-0.4.0/src/plotlet → plotlet-0.5.0/src/plotlet/draw}/_cm_data.py +0 -0
- {plotlet-0.4.0/src/plotlet → plotlet-0.5.0/src/plotlet/draw}/_png.py +0 -0
- {plotlet-0.4.0/src/plotlet → plotlet-0.5.0/src/plotlet/draw}/fonts/DejaVuSans.ttf +0 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet/layout_diagram.py +0 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet.egg-info/dependency_links.txt +0 -0
- {plotlet-0.4.0 → plotlet-0.5.0}/src/plotlet.egg-info/top_level.txt +0 -0
plotlet-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotlet
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Python library for SVG plots, with multi-panel composition and an extension API for custom plot types.
|
|
5
|
+
Author: gitbamboo42
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gitbamboo42/plotlet
|
|
8
|
+
Project-URL: Repository, https://github.com/gitbamboo42/plotlet
|
|
9
|
+
Project-URL: Issues, https://github.com/gitbamboo42/plotlet/issues
|
|
10
|
+
Keywords: plot,svg,scientific,visualization,jupyter,reproducible
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: fonttools>=4.0
|
|
25
|
+
Requires-Dist: scipy>=1.10
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
28
|
+
Requires-Dist: nbconvert; extra == "dev"
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# plotlet
|
|
34
|
+
|
|
35
|
+
plotlet is a Python library for SVG plots. It provides reproducible, byte-identical multi-panel scientific figures with a standard plotting vocabulary and a first-class extension story for custom plot types.
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
Hands-on tour with executable examples:
|
|
40
|
+
|
|
41
|
+
- [notebooks/00_introduction.ipynb](notebooks/00_introduction.ipynb) — long-form data, aesthetic inheritance, layering, composition
|
|
42
|
+
|
|
43
|
+
Reference docs in [`docs/`](docs/):
|
|
44
|
+
|
|
45
|
+
- [API reference](docs/API.md) — mark methods, frame options, scales, tick overrides
|
|
46
|
+
- [Philosophy](docs/PHILOSOPHY.md) — what's in core, what's not, and why
|
|
47
|
+
- [Subplots](docs/SUBPLOTS.md) — multi-panel composition, shared scales
|
|
48
|
+
- [Extending](docs/EXTENDING.md) — write your own plot type
|
|
49
|
+
- [Themes](docs/THEMES.md) — visual presets
|
|
50
|
+
- [AI attributes](docs/AI_ATTRS.md) — `data-plotlet-*` schema for automation
|
|
51
|
+
|
|
52
|
+
Reference plot types beyond the standard vocabulary live in [`src/plotlet/extensions/`](src/plotlet/extensions/) (single-file) and [`cookbook/`](cookbook/) (multi-file projects).
|
|
53
|
+
|
|
54
|
+
## Dependencies
|
|
55
|
+
|
|
56
|
+
plotlet supports Python 3.10+.
|
|
57
|
+
|
|
58
|
+
Required: `fonttools`, `scipy` (used by regression, qq, pointplot, dendrogram). numpy / pandas / polars inputs work transparently.
|
|
59
|
+
|
|
60
|
+
Optional: `cairosvg` for PNG / PDF export.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install plotlet
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Testing
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install -e ".[test]"
|
|
72
|
+
pytest tests/ # check vs. committed baselines
|
|
73
|
+
pytest tests/ --update # regenerate (review the diff!)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
plotlet-0.5.0/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# plotlet
|
|
2
|
+
|
|
3
|
+
plotlet is a Python library for SVG plots. It provides reproducible, byte-identical multi-panel scientific figures with a standard plotting vocabulary and a first-class extension story for custom plot types.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Hands-on tour with executable examples:
|
|
8
|
+
|
|
9
|
+
- [notebooks/00_introduction.ipynb](notebooks/00_introduction.ipynb) — long-form data, aesthetic inheritance, layering, composition
|
|
10
|
+
|
|
11
|
+
Reference docs in [`docs/`](docs/):
|
|
12
|
+
|
|
13
|
+
- [API reference](docs/API.md) — mark methods, frame options, scales, tick overrides
|
|
14
|
+
- [Philosophy](docs/PHILOSOPHY.md) — what's in core, what's not, and why
|
|
15
|
+
- [Subplots](docs/SUBPLOTS.md) — multi-panel composition, shared scales
|
|
16
|
+
- [Extending](docs/EXTENDING.md) — write your own plot type
|
|
17
|
+
- [Themes](docs/THEMES.md) — visual presets
|
|
18
|
+
- [AI attributes](docs/AI_ATTRS.md) — `data-plotlet-*` schema for automation
|
|
19
|
+
|
|
20
|
+
Reference plot types beyond the standard vocabulary live in [`src/plotlet/extensions/`](src/plotlet/extensions/) (single-file) and [`cookbook/`](cookbook/) (multi-file projects).
|
|
21
|
+
|
|
22
|
+
## Dependencies
|
|
23
|
+
|
|
24
|
+
plotlet supports Python 3.10+.
|
|
25
|
+
|
|
26
|
+
Required: `fonttools`, `scipy` (used by regression, qq, pointplot, dendrogram). numpy / pandas / polars inputs work transparently.
|
|
27
|
+
|
|
28
|
+
Optional: `cairosvg` for PNG / PDF export.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install plotlet
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Testing
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -e ".[test]"
|
|
40
|
+
pytest tests/ # check vs. committed baselines
|
|
41
|
+
pytest tests/ --update # regenerate (review the diff!)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plotlet"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Python library for SVG plots, with multi-panel composition and an extension API for custom plot types."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -16,7 +16,6 @@ authors = [
|
|
|
16
16
|
keywords = [
|
|
17
17
|
"plot",
|
|
18
18
|
"svg",
|
|
19
|
-
"matplotlib",
|
|
20
19
|
"scientific",
|
|
21
20
|
"visualization",
|
|
22
21
|
"jupyter",
|
|
@@ -44,6 +43,13 @@ dev = [
|
|
|
44
43
|
"jupyter",
|
|
45
44
|
"nbconvert",
|
|
46
45
|
]
|
|
46
|
+
test = [
|
|
47
|
+
"pytest>=7",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
addopts = "-ra"
|
|
47
53
|
|
|
48
54
|
[project.urls]
|
|
49
55
|
Homepage = "https://github.com/gitbamboo42/plotlet"
|
|
@@ -54,4 +60,4 @@ Issues = "https://github.com/gitbamboo42/plotlet/issues"
|
|
|
54
60
|
where = ["src"]
|
|
55
61
|
|
|
56
62
|
[tool.setuptools.package-data]
|
|
57
|
-
plotlet = ["spec.json", "fonts/*.ttf"]
|
|
63
|
+
plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "_datasets/*.csv"]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""plotlet — pure-Python SVG
|
|
1
|
+
"""plotlet — pure-Python deferred-rendering SVG plot library.
|
|
2
2
|
|
|
3
3
|
import plotlet as pt
|
|
4
4
|
c = pt.chart(df, title="...", xlabel="x", ylabel="y", legend=True, grid=True)
|
|
5
|
-
c.line(x="time", y="value",
|
|
5
|
+
c.line(x="time", y="value", color="series")
|
|
6
6
|
c # auto-renders in Jupyter
|
|
7
7
|
|
|
8
8
|
For non-tabular use, the chained form works directly on the same Chart:
|
|
@@ -13,17 +13,26 @@ For non-tabular use, the chained form works directly on the same Chart:
|
|
|
13
13
|
c
|
|
14
14
|
"""
|
|
15
15
|
from ._spec import SPEC
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .chart import Chart, chart
|
|
19
|
-
from .layout import grid
|
|
16
|
+
from .draw import TAB10, colors
|
|
17
|
+
from .draw import colormap, list_colormaps
|
|
18
|
+
from .chart import Chart, Layout, chart, grid
|
|
20
19
|
from .legend import legend
|
|
21
20
|
from .registry import ArtistSpec, add_artist
|
|
22
21
|
from .layout_diagram import layout_diagram
|
|
22
|
+
from .themes import load_theme, available_themes, register_theme
|
|
23
|
+
from .facet import facet, FacetGrid
|
|
24
|
+
from .data import load, list_datasets
|
|
25
|
+
from .cluster import cluster, cluster_split, SplitTree
|
|
26
|
+
from . import draw, utils
|
|
23
27
|
|
|
24
|
-
__all__ = ["chart", "Chart", "SPEC", "TAB10", "colors",
|
|
28
|
+
__all__ = ["chart", "Chart", "Layout", "SPEC", "TAB10", "colors",
|
|
25
29
|
"colormap", "list_colormaps", "grid", "legend",
|
|
26
|
-
"ArtistSpec", "add_artist", "layout_diagram"
|
|
30
|
+
"ArtistSpec", "add_artist", "layout_diagram",
|
|
31
|
+
"load_theme", "available_themes", "register_theme",
|
|
32
|
+
"facet", "FacetGrid",
|
|
33
|
+
"load", "list_datasets",
|
|
34
|
+
"cluster", "cluster_split", "SplitTree",
|
|
35
|
+
"draw", "utils"]
|
|
27
36
|
|
|
28
37
|
# Single source of truth: pyproject.toml. importlib.metadata reads it at
|
|
29
38
|
# runtime from the installed package metadata (works for `pip install` and
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Helpers for chart-level attachments — sub-charts placed in the host
|
|
2
|
+
chart's margin space, like extended axis decorations.
|
|
3
|
+
|
|
4
|
+
The public API lives on `Chart`: `attach_left/right/above/below`. This
|
|
5
|
+
module owns the layout-time concerns those methods imply:
|
|
6
|
+
|
|
7
|
+
* **Size discovery** — how much extra horizontal/vertical space the
|
|
8
|
+
figure needs beyond the host's own canvas to fit the attachments.
|
|
9
|
+
* **Allocation** — positioning each attachment so its data area lines
|
|
10
|
+
up with the host's data area on the shared axis.
|
|
11
|
+
* **Joined-pair hiding** — suppressing duplicated tick / axis labels
|
|
12
|
+
on each host-attachment seam (and along chained attachments).
|
|
13
|
+
|
|
14
|
+
The render engine in `_layout_engine.py` calls into here from a few
|
|
15
|
+
small hook points (`_iter_leaves`, `_measure`, `_allocate`, the
|
|
16
|
+
panel-opts pre-pass). Engine internals that this module needs
|
|
17
|
+
(`_allocate`, `_mark_joined_pair`) are lazy-imported inside the
|
|
18
|
+
functions to keep the import graph one-way: engine imports
|
|
19
|
+
attachments, attachments imports engine only at call time.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .chart import Chart
|
|
24
|
+
from .core import _PanelOpts
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DEFAULT_M = {"left": 0, "right": 0, "top": 0, "bottom": 0}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _M_eff(leaf: Chart) -> dict:
|
|
31
|
+
"""Per-side margin for a leaf, defaulting to zeros before the
|
|
32
|
+
pre-pass has run. Returns a fresh dict-shaped value so callers can
|
|
33
|
+
treat the four sides uniformly."""
|
|
34
|
+
return leaf._last_M_eff or _DEFAULT_M
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def has_attachments(chart: Chart) -> bool:
|
|
38
|
+
"""True if `chart` is a host with any side attachments."""
|
|
39
|
+
return bool(chart._attached_left or chart._attached_right
|
|
40
|
+
or chart._attached_above or chart._attached_below)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def all_attachments(chart: Chart) -> list[Chart]:
|
|
44
|
+
return (chart._attached_left + chart._attached_right
|
|
45
|
+
+ chart._attached_above + chart._attached_below)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _gap(c: Chart) -> float:
|
|
49
|
+
"""Per-attachment gap to inward neighbor (host or previous attachment).
|
|
50
|
+
Returns 0 for unattached charts (defensive — placement only iterates
|
|
51
|
+
over already-attached lists)."""
|
|
52
|
+
return getattr(c, "_attachment_gap", 0.0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def attached_size_h(host: Chart) -> tuple[float, float]:
|
|
56
|
+
"""Extra horizontal space the figure needs beyond `host`'s own canvas
|
|
57
|
+
to fit left/right attachments AND any perpendicular-margin overflow
|
|
58
|
+
from above/below attachments. Left/right attachments occupy the
|
|
59
|
+
host's collapsed left/right margin first (their innermost right edge
|
|
60
|
+
sits at the host's data-area left edge minus its `gap`), so they
|
|
61
|
+
only grow the figure once cumulative width-plus-gaps exceeds it.
|
|
62
|
+
Above/below attachments keep their own left/right margins; if a top
|
|
63
|
+
track's left margin exceeds the host's, its labels need extra figure
|
|
64
|
+
space unless a left attachment already provides it.
|
|
65
|
+
Returns (left_extra, right_extra)."""
|
|
66
|
+
M = _M_eff(host)
|
|
67
|
+
sum_left = sum(c._canvas_width + _gap(c) for c in host._attached_left)
|
|
68
|
+
sum_right = sum(c._canvas_width + _gap(c) for c in host._attached_right)
|
|
69
|
+
stack = host._attached_above + host._attached_below
|
|
70
|
+
max_left_overflow = max((_M_eff(c)["left"] - M["left"]
|
|
71
|
+
for c in stack), default=0.0)
|
|
72
|
+
max_right_overflow = max((_M_eff(c)["right"] - M["right"]
|
|
73
|
+
for c in stack), default=0.0)
|
|
74
|
+
return (max(sum_left - M["left"], max_left_overflow, 0.0),
|
|
75
|
+
max(sum_right - M["right"], max_right_overflow, 0.0))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def attached_size_v(host: Chart) -> tuple[float, float]:
|
|
79
|
+
"""Extra vertical space beyond `host`'s canvas needed for above/below
|
|
80
|
+
attachments AND for left/right attachments whose own top/bottom
|
|
81
|
+
margins overflow the host's. Returns (above_extra, below_extra)."""
|
|
82
|
+
M = _M_eff(host)
|
|
83
|
+
sum_above = sum(c._canvas_height + _gap(c) for c in host._attached_above)
|
|
84
|
+
sum_below = sum(c._canvas_height + _gap(c) for c in host._attached_below)
|
|
85
|
+
side = host._attached_left + host._attached_right
|
|
86
|
+
max_top_overflow = max((_M_eff(c)["top"] - M["top"]
|
|
87
|
+
for c in side), default=0.0)
|
|
88
|
+
max_bottom_overflow = max((_M_eff(c)["bottom"] - M["bottom"]
|
|
89
|
+
for c in side), default=0.0)
|
|
90
|
+
return (max(sum_above - M["top"], max_top_overflow, 0.0),
|
|
91
|
+
max(sum_below - M["bottom"], max_bottom_overflow, 0.0))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def allocate(host: Chart, host_x: float, host_y: float,
|
|
95
|
+
host_w: float, host_h: float, out: list) -> None:
|
|
96
|
+
"""Place each attachment so its DATA area aligns with the host's data
|
|
97
|
+
area on the shared axis; the attachment's own margins are independent
|
|
98
|
+
and do not push the host's margins out. Canvases may extend beyond
|
|
99
|
+
the host's canvas on the perpendicular axis — those overflows visually
|
|
100
|
+
sit in the left/right attachment columns (above/below) or top/bottom
|
|
101
|
+
rows (left/right), at a different position on the other axis, so they
|
|
102
|
+
don't visually collide with sibling attachments' content."""
|
|
103
|
+
from ._layout_engine import _allocate
|
|
104
|
+
|
|
105
|
+
host_M = _M_eff(host)
|
|
106
|
+
host_data_x = host_x + host_M["left"]
|
|
107
|
+
host_data_y = host_y + host_M["top"]
|
|
108
|
+
|
|
109
|
+
# Each side places the attachment so its DATA area edge is flush against
|
|
110
|
+
# the host's data area edge. The attachment's inner-facing margin floor
|
|
111
|
+
# (cM["right"] for left, cM["bottom"] for above, etc.) overlaps into the
|
|
112
|
+
# host's collapsed inner margin floor — both are blank floor regions,
|
|
113
|
+
# so no visible collision; this matches the symmetric right/below path.
|
|
114
|
+
|
|
115
|
+
# Each side advances a cursor inward-to-outward. The cursor holds the
|
|
116
|
+
# next "data edge to align to" — initially the host's data edge, then
|
|
117
|
+
# each placed attachment's outward data edge. Per-attachment `gap` is
|
|
118
|
+
# applied by stepping the cursor outward by `gap` BEFORE placing
|
|
119
|
+
# (zero gap → flush join; positive gap → visible separation).
|
|
120
|
+
|
|
121
|
+
# Left: walk outward (decreasing x). Data y/h locks to host (share_y);
|
|
122
|
+
# canvas y is offset so the attachment's data area starts at host_data_y.
|
|
123
|
+
cx_right = host_x + host_M["left"]
|
|
124
|
+
for c in host._attached_left:
|
|
125
|
+
cx_right -= _gap(c)
|
|
126
|
+
cM = _M_eff(c)
|
|
127
|
+
cw = c._data_width + cM["left"] + cM["right"]
|
|
128
|
+
ch = c._data_height + cM["top"] + cM["bottom"]
|
|
129
|
+
c_canvas_x = cx_right - cM["left"] - c._data_width
|
|
130
|
+
c_canvas_y = host_data_y - cM["top"]
|
|
131
|
+
_allocate(c, c_canvas_x, c_canvas_y, cw, ch, out)
|
|
132
|
+
cx_right = c_canvas_x + cM["left"] # c's data-left becomes next reference
|
|
133
|
+
# Right: walk outward (increasing x).
|
|
134
|
+
cx_left = host_x + host_w - host_M["right"]
|
|
135
|
+
for c in host._attached_right:
|
|
136
|
+
cx_left += _gap(c)
|
|
137
|
+
cM = _M_eff(c)
|
|
138
|
+
cw = c._data_width + cM["left"] + cM["right"]
|
|
139
|
+
ch = c._data_height + cM["top"] + cM["bottom"]
|
|
140
|
+
c_canvas_x = cx_left - cM["left"]
|
|
141
|
+
c_canvas_y = host_data_y - cM["top"]
|
|
142
|
+
_allocate(c, c_canvas_x, c_canvas_y, cw, ch, out)
|
|
143
|
+
cx_left = c_canvas_x + cM["left"] + c._data_width # c's data-right
|
|
144
|
+
# Above: walk outward (decreasing y). Data x/w locks to host (share_x).
|
|
145
|
+
cy_bottom = host_y + host_M["top"]
|
|
146
|
+
for c in host._attached_above:
|
|
147
|
+
cy_bottom -= _gap(c)
|
|
148
|
+
cM = _M_eff(c)
|
|
149
|
+
cw = c._data_width + cM["left"] + cM["right"]
|
|
150
|
+
ch = c._data_height + cM["top"] + cM["bottom"]
|
|
151
|
+
c_canvas_x = host_data_x - cM["left"]
|
|
152
|
+
c_canvas_y = cy_bottom - cM["top"] - c._data_height
|
|
153
|
+
_allocate(c, c_canvas_x, c_canvas_y, cw, ch, out)
|
|
154
|
+
cy_bottom = c_canvas_y + cM["top"] # c's data-top
|
|
155
|
+
# Below: walk outward (increasing y).
|
|
156
|
+
cy_top = host_y + host_h - host_M["bottom"]
|
|
157
|
+
for c in host._attached_below:
|
|
158
|
+
cy_top += _gap(c)
|
|
159
|
+
cM = _M_eff(c)
|
|
160
|
+
cw = c._data_width + cM["left"] + cM["right"]
|
|
161
|
+
ch = c._data_height + cM["top"] + cM["bottom"]
|
|
162
|
+
c_canvas_x = host_data_x - cM["left"]
|
|
163
|
+
c_canvas_y = cy_top - cM["top"]
|
|
164
|
+
_allocate(c, c_canvas_x, c_canvas_y, cw, ch, out)
|
|
165
|
+
cy_top = c_canvas_y + cM["top"] + c._data_height # c's data-bottom
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def annotate_joined_pairs(leaves: list[Chart],
|
|
169
|
+
panel_opts: dict[int, _PanelOpts]) -> None:
|
|
170
|
+
"""For each host with attachments, mark the inner-facing edge of each
|
|
171
|
+
host-attachment pair (and each adjacent pair along a chain of
|
|
172
|
+
same-side attachments) as a joined-pair side — duplicated tick
|
|
173
|
+
labels and axis labels suppress on the host-facing edge. Reuses
|
|
174
|
+
`_layout_engine._mark_joined_pair`; the share auto-wired by
|
|
175
|
+
`attach_*` satisfies its share-equivalence precondition. The
|
|
176
|
+
per-leaf `_share_hide_labels_*` flag (set when an attachment opts
|
|
177
|
+
out via `hide_labels=False`) lets either side cancel the
|
|
178
|
+
suppression."""
|
|
179
|
+
from ._layout_engine import _mark_joined_pair
|
|
180
|
+
|
|
181
|
+
for host in leaves:
|
|
182
|
+
if not has_attachments(host):
|
|
183
|
+
continue
|
|
184
|
+
if host._attached_left:
|
|
185
|
+
_mark_joined_pair(host._attached_left[0], host,
|
|
186
|
+
axis="h", out=panel_opts)
|
|
187
|
+
if host._attached_right:
|
|
188
|
+
_mark_joined_pair(host, host._attached_right[0],
|
|
189
|
+
axis="h", out=panel_opts)
|
|
190
|
+
if host._attached_above:
|
|
191
|
+
_mark_joined_pair(host._attached_above[0], host,
|
|
192
|
+
axis="v", out=panel_opts)
|
|
193
|
+
if host._attached_below:
|
|
194
|
+
_mark_joined_pair(host, host._attached_below[0],
|
|
195
|
+
axis="v", out=panel_opts)
|
|
196
|
+
# Chained attachments on the same side: each adjacent pair is a
|
|
197
|
+
# joint. Index 0 is innermost; higher indices extend outward.
|
|
198
|
+
for chain, axis, outward in (
|
|
199
|
+
(host._attached_left, "h", "left"),
|
|
200
|
+
(host._attached_right, "h", "right"),
|
|
201
|
+
(host._attached_above, "v", "above"),
|
|
202
|
+
(host._attached_below, "v", "below"),
|
|
203
|
+
):
|
|
204
|
+
for i in range(len(chain) - 1):
|
|
205
|
+
inner, outer = chain[i], chain[i + 1]
|
|
206
|
+
if outward in ("left", "above"):
|
|
207
|
+
_mark_joined_pair(outer, inner, axis=axis, out=panel_opts)
|
|
208
|
+
else:
|
|
209
|
+
_mark_joined_pair(inner, outer, axis=axis, out=panel_opts)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def promote_titles(leaves: list[Chart], states: dict[int, dict]) -> None:
|
|
213
|
+
"""`c.title("...")` is a figure-title gesture: it should render above
|
|
214
|
+
everything stacked on top of `c`, not buried inside `c`'s own title
|
|
215
|
+
margin under the attached panels. When `c` has `_attached_above` and
|
|
216
|
+
sets a title, move the title's render state to the outermost
|
|
217
|
+
attached_above chart so the layout reserves margin in the right
|
|
218
|
+
panel. The host's own state has the title cleared so the renderer
|
|
219
|
+
doesn't draw it twice.
|
|
220
|
+
|
|
221
|
+
Only the top side is promoted — title is conventionally above the
|
|
222
|
+
data. xlabel/ylabel are not symmetric concepts (axis labels belong
|
|
223
|
+
to the axis they describe, not to the figure), so they stay put.
|
|
224
|
+
"""
|
|
225
|
+
for leaf in leaves:
|
|
226
|
+
if not leaf._attached_above:
|
|
227
|
+
continue
|
|
228
|
+
host_st = states.get(id(leaf))
|
|
229
|
+
if host_st is None or not host_st.get("title"):
|
|
230
|
+
continue
|
|
231
|
+
# Index 0 = innermost, last = outermost — title goes to the very top.
|
|
232
|
+
outermost = leaf._attached_above[-1]
|
|
233
|
+
outer_st = states.get(id(outermost))
|
|
234
|
+
if outer_st is None:
|
|
235
|
+
continue
|
|
236
|
+
outer_st["title"] = host_st["title"]
|
|
237
|
+
host_st["title"] = ""
|