plotlet 0.6.0__tar.gz → 0.6.4__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.6.4/PKG-INFO +72 -0
- plotlet-0.6.4/README.md +35 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/pyproject.toml +6 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/__init__.py +14 -9
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_coord_registry.py +13 -8
- plotlet-0.6.4/src/plotlet/_datasets/earth.npz +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_json_layer.py +62 -27
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_regions.py +6 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_tree.py +1 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/__init__.py +9 -3
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/_shared.py +151 -1
- plotlet-0.6.4/src/plotlet/artists/annotation_strip.py +742 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/bar.py +225 -150
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/boxplot.py +80 -58
- {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.6.4/src/plotlet/artists}/chord_links.py +9 -36
- {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.6.4/src/plotlet/artists}/chord_ribbon.py +7 -43
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/contour.py +2 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/dendrogram.py +37 -6
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/density_1d.py +4 -4
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/ecdf.py +30 -6
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/errorbar.py +24 -21
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/fills.py +5 -4
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/freqpoly.py +4 -4
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/heatmap.py +147 -72
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/hexbin.py +3 -3
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/hist.py +10 -9
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/hist2d.py +1 -1
- plotlet-0.6.4/src/plotlet/artists/image_cmap.py +323 -0
- plotlet-0.6.4/src/plotlet/artists/image_rgba.py +236 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/kde_2d.py +5 -5
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/line.py +53 -17
- {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.6.4/src/plotlet/artists}/numeric_bar.py +11 -35
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/pointplot.py +7 -5
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/qq.py +46 -12
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/references.py +10 -6
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/regression.py +10 -8
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/ridge.py +6 -5
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/rug.py +50 -18
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/scatter.py +114 -50
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/shapes.py +5 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/strip.py +37 -15
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/swarm.py +34 -13
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/text.py +8 -4
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/violin.py +11 -8
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/cluster.py +19 -4
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/datasets.py +21 -9
- plotlet-0.6.4/src/plotlet/docs/AI_ATTRS.md +170 -0
- plotlet-0.6.4/src/plotlet/docs/API.md +455 -0
- plotlet-0.6.4/src/plotlet/docs/ARCHITECTURE.md +201 -0
- plotlet-0.6.4/src/plotlet/docs/COORDINATES.md +203 -0
- plotlet-0.6.4/src/plotlet/docs/EXTENDING.md +271 -0
- plotlet-0.6.4/src/plotlet/docs/PHILOSOPHY.md +125 -0
- plotlet-0.6.4/src/plotlet/docs/SUBPLOTS.md +136 -0
- plotlet-0.6.4/src/plotlet/docs/THEMES.md +124 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/__init__.py +8 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/_png.py +44 -1
- plotlet-0.6.4/src/plotlet/draw/_raster.py +193 -0
- plotlet-0.6.4/src/plotlet/draw/_simplify.py +100 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/colormaps.py +28 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/colors.py +2 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/primitives.py +1 -1
- plotlet-0.6.4/src/plotlet/examples/01_scatter.py +24 -0
- plotlet-0.6.4/src/plotlet/examples/02_line.py +16 -0
- plotlet-0.6.4/src/plotlet/examples/03_bar.py +20 -0
- plotlet-0.6.4/src/plotlet/examples/04_violin.py +21 -0
- plotlet-0.6.4/src/plotlet/examples/05_anscombe.py +24 -0
- plotlet-0.6.4/src/plotlet/examples/06_heatmap.py +22 -0
- plotlet-0.6.4/src/plotlet/examples/07_annotated_heatmap.py +39 -0
- plotlet-0.6.4/src/plotlet/examples/08_tracks.py +41 -0
- plotlet-0.6.4/src/plotlet/examples/09_circular.py +17 -0
- plotlet-0.6.4/src/plotlet/examples/10_chord.py +30 -0
- plotlet-0.6.4/src/plotlet/examples/12_hist.py +19 -0
- plotlet-0.6.4/src/plotlet/examples/13_ecdf.py +14 -0
- plotlet-0.6.4/src/plotlet/examples/14_box_strip.py +17 -0
- plotlet-0.6.4/src/plotlet/examples/15_swarm.py +15 -0
- plotlet-0.6.4/src/plotlet/examples/16_pointplot.py +26 -0
- plotlet-0.6.4/src/plotlet/examples/17_ridge.py +23 -0
- plotlet-0.6.4/src/plotlet/examples/18_qq.py +20 -0
- plotlet-0.6.4/src/plotlet/examples/19_hexbin.py +20 -0
- plotlet-0.6.4/src/plotlet/examples/20_kde_2d.py +23 -0
- plotlet-0.6.4/src/plotlet/examples/21_image_cmap.py +19 -0
- plotlet-0.6.4/src/plotlet/examples/22_facets.py +21 -0
- plotlet-0.6.4/src/plotlet/examples/23_ring_tree.py +43 -0
- plotlet-0.6.4/src/plotlet/examples/24_image_rgba.py +19 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/layout_diagram.py +3 -3
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/lint.py +1 -1
- plotlet-0.6.4/src/plotlet/record/__init__.py +12 -0
- {plotlet-0.6.0/src/plotlet → plotlet-0.6.4/src/plotlet/record}/chart.py +168 -74
- {plotlet-0.6.0/src/plotlet → plotlet-0.6.4/src/plotlet/record}/facet.py +4 -4
- plotlet-0.6.0/src/plotlet/_ir.py → plotlet-0.6.4/src/plotlet/record/figure_ir.py +38 -32
- plotlet-0.6.0/src/plotlet/_journal.py → plotlet-0.6.4/src/plotlet/record/journal.py +115 -42
- {plotlet-0.6.0/src/plotlet → plotlet-0.6.4/src/plotlet/record}/legend.py +1 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/registry.py +29 -11
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/__init__.py +18 -12
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_attachments.py +6 -6
- plotlet-0.6.4/src/plotlet/render/_chrome_bands.py +392 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_chrome_circular.py +81 -6
- plotlet-0.6.4/src/plotlet/render/_chrome_emit.py +764 -0
- plotlet-0.6.4/src/plotlet/render/_chrome_visibility.py +97 -0
- plotlet-0.6.0/src/plotlet/render/coordinates.py → plotlet-0.6.4/src/plotlet/render/_coord_circular.py +17 -343
- plotlet-0.6.4/src/plotlet/render/_layout_circular.py +324 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_layout_engine.py +121 -121
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_legend.py +12 -9
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_nodes.py +19 -39
- plotlet-0.6.0/src/plotlet/render/core.py → plotlet-0.6.4/src/plotlet/render/_resolution.py +542 -997
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/render/_validate.py +43 -15
- plotlet-0.6.4/src/plotlet/render/emit.py +713 -0
- plotlet-0.6.0/src/plotlet/render/resolved.py → plotlet-0.6.4/src/plotlet/render/resolved_ir.py +104 -96
- plotlet-0.6.4/src/plotlet/skill.py +36 -0
- plotlet-0.6.4/src/plotlet/skills/developers.md +141 -0
- plotlet-0.6.4/src/plotlet/skills/users.md +182 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/spec.json +6 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/themes/minimal.json +2 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/themes/void.json +2 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/utils.py +152 -17
- plotlet-0.6.4/src/plotlet.egg-info/PKG-INFO +72 -0
- plotlet-0.6.4/src/plotlet.egg-info/SOURCES.txt +208 -0
- plotlet-0.6.4/tests/test_aes.py +136 -0
- plotlet-0.6.4/tests/test_chart_annotation_strip.py +199 -0
- plotlet-0.6.4/tests/test_chart_axis.py +473 -0
- plotlet-0.6.4/tests/test_chart_bar.py +353 -0
- plotlet-0.6.4/tests/test_chart_boxplot.py +105 -0
- plotlet-0.6.4/tests/test_chart_chord_links.py +68 -0
- plotlet-0.6.4/tests/test_chart_chord_ribbon.py +56 -0
- plotlet-0.6.4/tests/test_chart_contour.py +84 -0
- plotlet-0.6.4/tests/test_chart_dendrogram.py +186 -0
- plotlet-0.6.4/tests/test_chart_density_1d.py +59 -0
- plotlet-0.6.4/tests/test_chart_ecdf.py +40 -0
- plotlet-0.6.4/tests/test_chart_errorbar.py +57 -0
- plotlet-0.6.4/tests/test_chart_facets.py +238 -0
- plotlet-0.6.4/tests/test_chart_fills.py +119 -0
- plotlet-0.6.4/tests/test_chart_freqpoly.py +40 -0
- plotlet-0.6.4/tests/test_chart_heatmap.py +378 -0
- plotlet-0.6.4/tests/test_chart_hexbin.py +50 -0
- plotlet-0.6.4/tests/test_chart_hist.py +136 -0
- plotlet-0.6.4/tests/test_chart_hist2d.py +108 -0
- plotlet-0.6.4/tests/test_chart_image_cmap.py +284 -0
- plotlet-0.6.4/tests/test_chart_image_rgba.py +214 -0
- plotlet-0.6.4/tests/test_chart_kde_2d.py +77 -0
- plotlet-0.6.4/tests/test_chart_labels.py +136 -0
- plotlet-0.6.4/tests/test_chart_line.py +240 -0
- plotlet-0.6.4/tests/test_chart_numeric_bar.py +50 -0
- plotlet-0.6.4/tests/test_chart_pointplot.py +90 -0
- plotlet-0.6.4/tests/test_chart_qq.py +74 -0
- plotlet-0.6.4/tests/test_chart_references.py +68 -0
- plotlet-0.6.4/tests/test_chart_regression.py +159 -0
- plotlet-0.6.4/tests/test_chart_ridge.py +72 -0
- plotlet-0.6.4/tests/test_chart_rug.py +36 -0
- plotlet-0.6.4/tests/test_chart_scatter.py +146 -0
- plotlet-0.6.4/tests/test_chart_shapes.py +189 -0
- plotlet-0.6.4/tests/test_chart_spines.py +52 -0
- plotlet-0.6.4/tests/test_chart_strip.py +46 -0
- plotlet-0.6.4/tests/test_chart_swarm.py +58 -0
- plotlet-0.6.4/tests/test_chart_text.py +89 -0
- plotlet-0.6.4/tests/test_chart_violin.py +46 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_colors.py +5 -3
- plotlet-0.6.0/tests/test_circular_coordinate.py → plotlet-0.6.4/tests/test_coord_circular.py +302 -116
- plotlet-0.6.0/tests/test_sectors.py → plotlet-0.6.4/tests/test_coord_sectors.py +106 -100
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_datasets.py +7 -1
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_fonts.py +34 -25
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_formatters.py +5 -2
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_help.py +4 -10
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_import_boundary.py +120 -13
- plotlet-0.6.4/tests/test_io.py +187 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_ir.py +42 -36
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_ir_resolved.py +108 -56
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_ir_validate.py +60 -20
- plotlet-0.6.4/tests/test_journal_roundtrip.py +236 -0
- plotlet-0.6.0/tests/test_attachments.py → plotlet-0.6.4/tests/test_layout_attachments.py +149 -79
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_layout_diagram.py +38 -19
- plotlet-0.6.0/tests/test_legend.py → plotlet-0.6.4/tests/test_layout_legend.py +120 -56
- plotlet-0.6.0/tests/test_subplots.py → plotlet-0.6.4/tests/test_layout_subplots.py +219 -110
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_lint.py +8 -5
- plotlet-0.6.4/tests/test_record_signatures.py +225 -0
- plotlet-0.6.4/tests/test_skill.py +35 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_themes.py +22 -12
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_units.py +9 -4
- plotlet-0.6.0/PKG-INFO +0 -95
- plotlet-0.6.0/README.md +0 -58
- plotlet-0.6.0/src/plotlet/artists/imshow.py +0 -237
- plotlet-0.6.0/src/plotlet/extensions/annotation_strip.py +0 -675
- plotlet-0.6.0/src/plotlet/extensions/curved_tree.py +0 -244
- plotlet-0.6.0/src/plotlet/render/_chrome.py +0 -953
- plotlet-0.6.0/src/plotlet.egg-info/PKG-INFO +0 -95
- plotlet-0.6.0/src/plotlet.egg-info/SOURCES.txt +0 -129
- plotlet-0.6.0/tests/test_chart.py +0 -3594
- plotlet-0.6.0/tests/test_extensions.py +0 -40
- plotlet-0.6.0/tests/test_io.py +0 -61
- plotlet-0.6.0/tests/test_journal_roundtrip.py +0 -127
- plotlet-0.6.0/tests/test_record_signatures.py +0 -75
- {plotlet-0.6.0 → plotlet-0.6.4}/LICENSE +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/setup.cfg +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_datasets/anscombe.csv +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_datasets/flights.csv +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_datasets/penguins.csv +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_datasets/tips.csv +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_spec.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/_splits.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/artists/_marching.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/_cm_data.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/_css_colors.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/font.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/Arimo-Bold.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/Arimo-BoldItalic.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/Arimo-Italic.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/Arimo-Regular.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/DejaVuSans-Bold.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/DejaVuSans-Oblique.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/DejaVuSans.ttf +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/LICENSE-Arimo.txt +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/fonts/LICENSE-DejaVuSans.txt +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/format.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/draw/linestyles.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/formatters.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/scales.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/sectors.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/themes/dark.json +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet/themes.py +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet.egg-info/dependency_links.txt +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet.egg-info/requires.txt +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/src/plotlet.egg-info/top_level.txt +0 -0
- {plotlet-0.6.0 → plotlet-0.6.4}/tests/test_cluster.py +0 -0
plotlet-0.6.4/PKG-INFO
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotlet
|
|
3
|
+
Version: 0.6.4
|
|
4
|
+
Summary: Python library for reproducible, multi-panel scientific figures, with byte-identical output across machines 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: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fonttools>=4.0
|
|
26
|
+
Requires-Dist: scipy>=1.10
|
|
27
|
+
Requires-Dist: resvg_py>=0.3
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
30
|
+
Requires-Dist: nbconvert; extra == "dev"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Requires-Dist: pandas; extra == "test"
|
|
34
|
+
Provides-Extra: pdf
|
|
35
|
+
Requires-Dist: cairosvg; extra == "pdf"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# plotlet
|
|
39
|
+
|
|
40
|
+
plotlet is a Python library for reproducible, multi-panel scientific figures, built for AI authorship: figures are written, inspected, and verified as data, not pixels. Byte-identical output across machines, a standard plotting vocabulary, built-in Cartesian and circular coordinate systems, and an easy way to add your own plot types.
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
Online documentation is available at [gitbamboo42.github.io/plotlet](https://gitbamboo42.github.io/plotlet/).
|
|
45
|
+
|
|
46
|
+
The docs include a [tutorial](https://gitbamboo42.github.io/plotlet/tutorial.html), a [plot-type reference](https://gitbamboo42.github.io/plotlet/reference.html), a [cookbook](https://gitbamboo42.github.io/plotlet/cookbook.html), an [extensions gallery](https://gitbamboo42.github.io/plotlet/extensions.html), [deep-dive guides](https://gitbamboo42.github.io/plotlet/docs-api.html), and a [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html).
|
|
47
|
+
|
|
48
|
+
## Dependencies
|
|
49
|
+
|
|
50
|
+
plotlet supports Python 3.10+.
|
|
51
|
+
|
|
52
|
+
Required: `fonttools`, `scipy`, `resvg_py`. numpy / pandas / polars inputs work transparently.
|
|
53
|
+
|
|
54
|
+
Optional: `cairosvg` for PDF export (`pip install plotlet[pdf]`).
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install plotlet
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## For AI assistants
|
|
63
|
+
|
|
64
|
+
Tell your assistant to run plotlet's `skill()` and follow it. The guides, docs, and worked examples ship inside the installed package; the [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html) shows the full workflow.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
plotlet-0.6.4/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# plotlet
|
|
2
|
+
|
|
3
|
+
plotlet is a Python library for reproducible, multi-panel scientific figures, built for AI authorship: figures are written, inspected, and verified as data, not pixels. Byte-identical output across machines, a standard plotting vocabulary, built-in Cartesian and circular coordinate systems, and an easy way to add your own plot types.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Online documentation is available at [gitbamboo42.github.io/plotlet](https://gitbamboo42.github.io/plotlet/).
|
|
8
|
+
|
|
9
|
+
The docs include a [tutorial](https://gitbamboo42.github.io/plotlet/tutorial.html), a [plot-type reference](https://gitbamboo42.github.io/plotlet/reference.html), a [cookbook](https://gitbamboo42.github.io/plotlet/cookbook.html), an [extensions gallery](https://gitbamboo42.github.io/plotlet/extensions.html), [deep-dive guides](https://gitbamboo42.github.io/plotlet/docs-api.html), and a [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html).
|
|
10
|
+
|
|
11
|
+
## Dependencies
|
|
12
|
+
|
|
13
|
+
plotlet supports Python 3.10+.
|
|
14
|
+
|
|
15
|
+
Required: `fonttools`, `scipy`, `resvg_py`. numpy / pandas / polars inputs work transparently.
|
|
16
|
+
|
|
17
|
+
Optional: `cairosvg` for PDF export (`pip install plotlet[pdf]`).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install plotlet
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## For AI assistants
|
|
26
|
+
|
|
27
|
+
Tell your assistant to run plotlet's `skill()` and follow it. The guides, docs, and worked examples ship inside the installed package; the [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html) shows the full workflow.
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plotlet"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
8
8
|
description = "Python library for reproducible, multi-panel scientific figures, with byte-identical output across machines and an extension API for custom plot types."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -70,4 +70,8 @@ namespaces = true
|
|
|
70
70
|
# draw/fonts/*.txt are the font licenses — DejaVu (Bitstream Vera–derived)
|
|
71
71
|
# and Arimo (SIL OFL 1.1) both require the license text to accompany
|
|
72
72
|
# redistribution, so they must ship in the wheel next to the TTFs.
|
|
73
|
-
plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "draw/fonts/*.txt", "_datasets/*.csv"
|
|
73
|
+
plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "draw/fonts/*.txt", "_datasets/*.csv", "_datasets/*.npz",
|
|
74
|
+
# AI onboarding: pt.skill() reads skills/, and the guides point
|
|
75
|
+
# at the shipped docs/ and examples/ so an installed package is
|
|
76
|
+
# self-sufficient — no repo clone, no web access needed.
|
|
77
|
+
"skills/*.md", "docs/*.md", "examples/*.py"]
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""plotlet — pure-Python deferred-rendering SVG plot library.
|
|
2
2
|
|
|
3
3
|
import plotlet as pt
|
|
4
|
+
from plotlet import aes
|
|
5
|
+
|
|
4
6
|
c = pt.chart(df, title="...", xlabel="x", ylabel="y", legend=True, gridlines=True)
|
|
5
|
-
c.
|
|
7
|
+
c.add_line(aes(x="time", y="value", color="series"))
|
|
6
8
|
c # auto-renders in Jupyter
|
|
7
9
|
|
|
8
10
|
Chart methods chain for incremental composition:
|
|
9
11
|
|
|
10
12
|
df = {"x": [1, 2, 3], "y": [1, 4, 9]}
|
|
11
13
|
c = pt.chart()
|
|
12
|
-
c.
|
|
14
|
+
c.add_line(df, aes(x="x", y="y"), label="squares")
|
|
13
15
|
c.title("Hello").legend().gridlines(True)
|
|
14
16
|
c
|
|
15
17
|
"""
|
|
@@ -17,21 +19,23 @@ from ._spec import SPEC
|
|
|
17
19
|
from .draw import TAB10, palette, list_palettes
|
|
18
20
|
from .draw import colormap, list_colormaps, register_colormap
|
|
19
21
|
from . import artists # noqa: F401 — registers built-in artists on import
|
|
20
|
-
from .chart import Chart, Layout, chart, grid
|
|
21
|
-
from .
|
|
22
|
+
from .record.chart import Chart, Layout, chart, grid
|
|
23
|
+
from .utils import aes
|
|
24
|
+
from .record.legend import legend
|
|
22
25
|
from .registry import ArtistSpec, add_artist, artist_table, declare_coord_support
|
|
23
26
|
from .sectors import Sectors
|
|
24
27
|
from .layout_diagram import layout_diagram
|
|
25
28
|
from .themes import load_theme, list_themes, register_theme
|
|
26
|
-
from .facet import facet, FacetGrid
|
|
29
|
+
from .record.facet import facet, FacetGrid
|
|
27
30
|
from .datasets import load_dataset, list_datasets
|
|
28
31
|
from .cluster import linkage, linkage_split, SplitTree
|
|
29
32
|
from .formatters import (register_formatter, list_formatters,
|
|
30
33
|
superscript, subscript)
|
|
31
34
|
from ._coord_registry import register_coord_codec
|
|
32
|
-
from .
|
|
35
|
+
from .record.journal import (to_journal, from_journal, to_json, from_json,
|
|
33
36
|
JournalNode, Journal)
|
|
34
|
-
from .
|
|
37
|
+
from .record.figure_ir import to_ir, from_ir, FigureIR, IRNode
|
|
38
|
+
from .skill import skill
|
|
35
39
|
from . import draw, utils
|
|
36
40
|
|
|
37
41
|
|
|
@@ -41,7 +45,7 @@ def __getattr__(name):
|
|
|
41
45
|
# the render half loads on first render (or first touch of this
|
|
42
46
|
# name). Enforced by tests/test_import_boundary.py.
|
|
43
47
|
if name == "CircularCoordinate":
|
|
44
|
-
from .render.
|
|
48
|
+
from .render._coord_circular import CircularCoordinate
|
|
45
49
|
return CircularCoordinate
|
|
46
50
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
47
51
|
|
|
@@ -60,7 +64,8 @@ __all__ = ["chart", "Chart", "Layout", "SPEC", "TAB10",
|
|
|
60
64
|
"superscript", "subscript",
|
|
61
65
|
"to_json", "from_json", "register_coord_codec",
|
|
62
66
|
"to_journal", "from_journal", "JournalNode", "Journal",
|
|
63
|
-
"to_ir", "from_ir", "FigureIR", "IRNode",
|
|
67
|
+
"to_ir", "from_ir", "FigureIR", "IRNode",
|
|
68
|
+
"skill",
|
|
64
69
|
"draw", "utils"]
|
|
65
70
|
|
|
66
71
|
# Single source of truth: pyproject.toml. importlib.metadata reads it at
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
"""Coord codec registry —
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Coord codec registry — the `class_name → class` map behind
|
|
2
|
+
`{"$coord": ...}` envelopes, for coord objects like
|
|
3
|
+
`CircularCoordinate`.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
`record/journal.py` encodes a coord object to its class name;
|
|
6
|
+
`_json_layer._decode` turns the name back into the class via
|
|
7
|
+
`resolve_coord`, which imports the built-in coord module on the first
|
|
8
|
+
miss so cold processes decode without a prior render. The render half
|
|
9
|
+
reads the map too (`render/_validate.py` cross-checks the `container`
|
|
10
|
+
flag, `render/resolved_ir.py` rehydrates coords), so the registry sits
|
|
11
|
+
at the package root, where `record/` and `render/` may both import it.
|
|
12
|
+
The public API `plotlet.register_coord_codec` re-exports from this
|
|
13
|
+
module directly.
|
|
9
14
|
"""
|
|
10
15
|
from __future__ import annotations
|
|
11
16
|
|
|
@@ -38,7 +43,7 @@ def resolve_coord(name: str) -> type:
|
|
|
38
43
|
return _COORD_REGISTRY[name]
|
|
39
44
|
except KeyError:
|
|
40
45
|
pass
|
|
41
|
-
from .render import
|
|
46
|
+
from .render import _coord_circular # noqa: F401 — registers built-ins
|
|
42
47
|
try:
|
|
43
48
|
return _COORD_REGISTRY[name]
|
|
44
49
|
except KeyError:
|
|
Binary file
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
"""JSON layer
|
|
1
|
+
"""JSON envelope layer — shared by `record/` and `render/`.
|
|
2
2
|
|
|
3
3
|
Envelopes Python values that aren't JSON-native (tuple, set, date,
|
|
4
|
-
datetime, dicts with non-string keys, DataFrameLite) so
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
datetime, dicts with non-string keys, DataFrameLite) so they can be
|
|
5
|
+
dumped through `json.dumps` and rehydrated. Four consumers: the
|
|
6
|
+
journal's JSON form (`record/journal.py`), the FigureIR wire format
|
|
7
|
+
(`record/figure_ir.py`), value-envelope decoding at render hydration
|
|
8
|
+
(`_decode`, `render/_nodes.py`), and the resolved IR's debug view
|
|
9
|
+
(`render/resolved_ir.py`). It sits at the package root because both
|
|
10
|
+
`record/` and `render/` need it and neither may import the other
|
|
11
|
+
(`tests/test_import_boundary.py`). It also keeps JSON support out of
|
|
12
|
+
`journal.py` itself — the journal stays a plain event log.
|
|
8
13
|
|
|
9
|
-
DataFrame-shaped
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
DataFrame-shaped inputs never reach this layer: they're normalized to
|
|
15
|
+
`DataFrameLite` / plain lists at the recorder boundary in
|
|
16
|
+
`record/chart.py` (via `utils._normalize_data`), so it never imports
|
|
17
|
+
pandas or grows a branch per data library. numpy is the one exception:
|
|
18
|
+
a matrix artist's data rides through record as a 2-D ndarray (numpy is
|
|
19
|
+
a guaranteed dependency, via scipy), and `json_safe` lowers it to
|
|
20
|
+
nested lists — the JSON wire format itself carries no array types.
|
|
14
21
|
|
|
15
22
|
Envelope keys used here:
|
|
16
23
|
$dataframe utils.DataFrameLite (canonical DataFrame form)
|
|
@@ -23,15 +30,17 @@ Envelope keys used here:
|
|
|
23
30
|
$dict_pairs dict whose keys aren't all JSON-native strings
|
|
24
31
|
|
|
25
32
|
`_decode` at the bottom handles the other envelope family — the
|
|
26
|
-
*reference* envelopes ($node / $coord / $sectors) that journals
|
|
27
|
-
carry whether or not they ever touch JSON. It lives here because
|
|
28
|
-
halves need it (the render tree's hydrator and the front half's
|
|
29
|
-
expansion) and it resolves only against shared vocabulary.
|
|
33
|
+
*reference* envelopes ($node / $coord / $sectors / $data) that journals
|
|
34
|
+
and IRs carry whether or not they ever touch JSON. It lives here because
|
|
35
|
+
both halves need it (the render tree's hydrator and the front half's
|
|
36
|
+
facet expansion) and it resolves only against shared vocabulary.
|
|
30
37
|
"""
|
|
31
38
|
from __future__ import annotations
|
|
32
39
|
from typing import Any
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
import numpy as np
|
|
42
|
+
|
|
43
|
+
from .utils import DataFrameLite, all_primitive
|
|
35
44
|
|
|
36
45
|
|
|
37
46
|
def json_safe(value: Any) -> Any:
|
|
@@ -63,6 +72,12 @@ def json_safe(value: Any) -> Any:
|
|
|
63
72
|
# values that never passed through the journal emitter, e.g. a
|
|
64
73
|
# resolved-IR state dict.
|
|
65
74
|
return {"$sectors": json_safe(value._to_dict())}
|
|
75
|
+
if isinstance(value, np.ndarray):
|
|
76
|
+
# A matrix artist's data section entry. The wire format stays
|
|
77
|
+
# plain nested lists — identical JSON whether the matrix came
|
|
78
|
+
# in as an array or as lists. Recursing catches the rare
|
|
79
|
+
# object-dtype cell.
|
|
80
|
+
return json_safe(value.tolist())
|
|
66
81
|
if isinstance(value, tuple):
|
|
67
82
|
return {"$tuple": [json_safe(v) for v in value]}
|
|
68
83
|
if isinstance(value, (set, frozenset)):
|
|
@@ -78,6 +93,11 @@ def json_safe(value: Any) -> Any:
|
|
|
78
93
|
return {"$dict_pairs": [[json_safe(k), json_safe(v)]
|
|
79
94
|
for k, v in value.items()]}
|
|
80
95
|
if isinstance(value, list):
|
|
96
|
+
# Bulk numeric rows are already JSON-native — one flat scan
|
|
97
|
+
# instead of per-cell recursion (same bail-out in json_hydrate,
|
|
98
|
+
# _decode, and the journal/IR walkers).
|
|
99
|
+
if all_primitive(value):
|
|
100
|
+
return list(value)
|
|
81
101
|
return [json_safe(v) for v in value]
|
|
82
102
|
return value
|
|
83
103
|
|
|
@@ -110,35 +130,50 @@ def json_hydrate(value: Any) -> Any:
|
|
|
110
130
|
for k, v in value["$dict_pairs"]}
|
|
111
131
|
return {k: json_hydrate(v) for k, v in value.items()}
|
|
112
132
|
if isinstance(value, list):
|
|
133
|
+
if all_primitive(value):
|
|
134
|
+
return list(value)
|
|
113
135
|
return [json_hydrate(v) for v in value]
|
|
114
136
|
return value
|
|
115
137
|
|
|
116
138
|
|
|
117
|
-
def _decode(value: Any, nid_to_node: dict) -> Any:
|
|
139
|
+
def _decode(value: Any, nid_to_node: dict, data_table: dict) -> Any:
|
|
118
140
|
"""Resolve plotlet's *reference* envelopes back to live objects —
|
|
119
141
|
`{"$node"}` via `nid_to_node`, `{"$coord"}` via the coord registry,
|
|
120
|
-
`{"$sectors"}` via `Sectors
|
|
121
|
-
|
|
142
|
+
`{"$sectors"}` via `Sectors`, `{"$data"}` via `data_table` (the
|
|
143
|
+
Journal's / FigureIR's data section). Containers recurse; everything
|
|
144
|
+
else passes through.
|
|
122
145
|
|
|
123
146
|
Distinct from `json_hydrate` above: that undoes the JSON-native
|
|
124
|
-
envelopes at the JSON boundary, while these
|
|
125
|
-
journals and IRs whether or not they ever touch JSON, and decode
|
|
126
|
-
hydration time. Shared vocabulary — used by the render tree's
|
|
147
|
+
envelopes at the JSON boundary, while these reference envelopes live
|
|
148
|
+
in journals and IRs whether or not they ever touch JSON, and decode
|
|
149
|
+
at hydration time. Shared vocabulary — used by the render tree's
|
|
127
150
|
hydrator (`render.hydrate`) and by the facet expansion in
|
|
128
|
-
`
|
|
151
|
+
`record/figure_ir.py`."""
|
|
129
152
|
if isinstance(value, dict):
|
|
130
153
|
if "$node" in value and len(value) == 1:
|
|
131
154
|
return nid_to_node[value["$node"]]
|
|
155
|
+
if "$data" in value and len(value) == 1:
|
|
156
|
+
return data_table[value["$data"]]
|
|
132
157
|
if "$coord" in value:
|
|
133
158
|
from ._coord_registry import resolve_coord
|
|
134
159
|
cls = resolve_coord(value["$coord"])
|
|
135
|
-
return cls._from_dict(
|
|
160
|
+
return cls._from_dict(
|
|
161
|
+
_decode(value.get("kwargs", {}), nid_to_node, data_table))
|
|
136
162
|
if "$sectors" in value:
|
|
137
163
|
from .sectors import Sectors
|
|
138
|
-
return Sectors._from_dict(
|
|
139
|
-
|
|
164
|
+
return Sectors._from_dict(
|
|
165
|
+
_decode(value["$sectors"], nid_to_node, data_table))
|
|
166
|
+
if "$aes" in value and len(value) == 1:
|
|
167
|
+
from .utils import Aes
|
|
168
|
+
return Aes(value["$aes"])
|
|
169
|
+
return {k: _decode(v, nid_to_node, data_table)
|
|
170
|
+
for k, v in value.items()}
|
|
140
171
|
if isinstance(value, list):
|
|
141
|
-
|
|
172
|
+
if all_primitive(value):
|
|
173
|
+
return list(value)
|
|
174
|
+
return [_decode(v, nid_to_node, data_table) for v in value]
|
|
142
175
|
if isinstance(value, tuple):
|
|
143
|
-
|
|
176
|
+
if all_primitive(value):
|
|
177
|
+
return value
|
|
178
|
+
return tuple(_decode(v, nid_to_node, data_table) for v in value)
|
|
144
179
|
return value
|
|
@@ -15,7 +15,9 @@ The mechanism enforces this:
|
|
|
15
15
|
When set, the primitive records its bbox under that name. When
|
|
16
16
|
unset (the default), nothing is recorded — so artist `draw()`
|
|
17
17
|
bodies, which never pass `tag=`, produce no regions for free.
|
|
18
|
-
* Central code (`
|
|
18
|
+
* Central code (`render/_chrome_emit.py`, `render/emit.py`,
|
|
19
|
+
`render/_legend.py`)
|
|
20
|
+
passes `tag="title"` /
|
|
19
21
|
`"spine"` / `"tick-x"` / `"legend-text"` etc. at the chrome
|
|
20
22
|
emission sites. The tag is right there in the call, grep-able,
|
|
21
23
|
no contextvar magic.
|
|
@@ -85,7 +87,9 @@ _CURRENT: contextvars.ContextVar = contextvars.ContextVar(
|
|
|
85
87
|
def record(kind: str, bbox, *, name: str, **meta) -> None:
|
|
86
88
|
"""Record a region into the active sink, if any. Called by
|
|
87
89
|
`draw.*` primitives when they receive a `tag=` kwarg, and by
|
|
88
|
-
central code (`
|
|
90
|
+
central code (`render/_chrome_emit.py`, `render/emit.py`,
|
|
91
|
+
`render/_legend.py`)
|
|
92
|
+
for chrome that has no
|
|
89
93
|
primitive call to wrap (panel boundary, canonical legend swatch).
|
|
90
94
|
No-op when no sink is active — the common render path."""
|
|
91
95
|
sink = _CURRENT.get()
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Both halves walk the same tree shape — the recording side at
|
|
4
4
|
validation time (over `Chart` / `Layout`), the render side when
|
|
5
5
|
deriving field state and building share classes (over the render
|
|
6
|
-
nodes in `
|
|
6
|
+
nodes in `render/_nodes.py`). The protocol is structural:
|
|
7
7
|
`_is_parent`, `_children`, `_effective_children()`, `_layout_kind`,
|
|
8
8
|
`_grid_rows` / `_grid_cols`, `_leaf_kind`, and the `_attached_*`
|
|
9
9
|
lists. Nothing here imports either half.
|
|
@@ -5,10 +5,11 @@ group mirror pairs or shared-machinery siblings — consolidation only
|
|
|
5
5
|
where the related artists share enough structure to earn it.
|
|
6
6
|
|
|
7
7
|
Single-artist files (one plot type each):
|
|
8
|
-
scatter, line, bar, hist,
|
|
8
|
+
scatter, line, bar, hist, image_cmap, image_rgba, heatmap, dendrogram, errorbar
|
|
9
9
|
boxplot, violin, swarm, strip
|
|
10
10
|
pointplot, ecdf, rug, density_1d, regression, kde_2d, hexbin, hist2d
|
|
11
|
-
freqpoly, contour, ridge, qq
|
|
11
|
+
freqpoly, contour, ridge, qq, numeric_bar, annotation_strip
|
|
12
|
+
chord_links, chord_ribbon
|
|
12
13
|
|
|
13
14
|
Category files (closely-related artists grouped):
|
|
14
15
|
references — axhline, axvline, axhspan, axvspan, hlines, vlines
|
|
@@ -25,7 +26,8 @@ from . import scatter # noqa: F401
|
|
|
25
26
|
from . import line # noqa: F401
|
|
26
27
|
from . import bar # noqa: F401
|
|
27
28
|
from . import hist # noqa: F401
|
|
28
|
-
from . import
|
|
29
|
+
from . import image_cmap # noqa: F401
|
|
30
|
+
from . import image_rgba # noqa: F401
|
|
29
31
|
from . import heatmap # noqa: F401
|
|
30
32
|
from . import dendrogram # noqa: F401
|
|
31
33
|
from . import errorbar # noqa: F401
|
|
@@ -45,6 +47,10 @@ from . import freqpoly # noqa: F401
|
|
|
45
47
|
from . import contour # noqa: F401
|
|
46
48
|
from . import ridge # noqa: F401
|
|
47
49
|
from . import qq # noqa: F401
|
|
50
|
+
from . import numeric_bar # noqa: F401
|
|
51
|
+
from . import annotation_strip # noqa: F401
|
|
52
|
+
from . import chord_links # noqa: F401
|
|
53
|
+
from . import chord_ribbon # noqa: F401
|
|
48
54
|
|
|
49
55
|
# Category files
|
|
50
56
|
from . import references # noqa: F401
|
|
@@ -3,13 +3,158 @@
|
|
|
3
3
|
Each built-in artist registers one of the `*_legend_entries` functions so
|
|
4
4
|
the legend dispatch in `_render_inner` can stay generic — no type-string matching.
|
|
5
5
|
Paint logic is a nested closure; there are no shared swatch helpers.
|
|
6
|
+
|
|
7
|
+
The `pool_*` / `rgb_buffer` helpers back the PNG-embedded raster paths of
|
|
8
|
+
image_cmap / image_rgba and heatmap: a grid denser than the display can show is box-pooled
|
|
9
|
+
down before encoding. All pooling rules are deterministic (fixed bin
|
|
10
|
+
edges, fixed iteration order, first-seen tie-breaks), so the
|
|
11
|
+
byte-identical-SVG guarantee holds.
|
|
6
12
|
"""
|
|
7
|
-
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .._spec import _LEGSPEC, _D
|
|
8
18
|
from ..draw import marker, segment, rect
|
|
9
19
|
from ..draw import TAB10
|
|
10
20
|
from ..utils import to_list, resolve_aes, palette_color
|
|
11
21
|
|
|
12
22
|
|
|
23
|
+
def pool_target(n_cells, px):
|
|
24
|
+
"""Output-pixel count along one raster axis: full data resolution
|
|
25
|
+
until cells outnumber `raster_oversample` per display pixel, then
|
|
26
|
+
capped at that. The oversample headroom keeps hi-dpi rendering and
|
|
27
|
+
moderate zoom sharp; past it the viewer can't show the extra cells
|
|
28
|
+
anyway, and its nearest-neighbour downscale would *subsample* them
|
|
29
|
+
(drop cells) where pooling summarizes them."""
|
|
30
|
+
cap = max(1, math.ceil(px * _D["raster_oversample"]))
|
|
31
|
+
return n_cells if n_cells <= cap else cap
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pool_mean(values):
|
|
35
|
+
"""NaN/None-ignoring mean of one pooling bin; an all-absent bin → NaN
|
|
36
|
+
(renders as the artist's absent color, like a NaN cell)."""
|
|
37
|
+
fin = [v for v in values if v is not None and v == v]
|
|
38
|
+
if not fin:
|
|
39
|
+
return float("nan")
|
|
40
|
+
return sum(fin) / len(fin)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pool_mode(values):
|
|
44
|
+
"""Most frequent value in one pooling bin, for categorical cells
|
|
45
|
+
(a mean of category labels is meaningless). NaN folds into None —
|
|
46
|
+
both render as absent. Ties break to the first-seen value."""
|
|
47
|
+
counts = {}
|
|
48
|
+
for v in values:
|
|
49
|
+
if isinstance(v, float) and v != v:
|
|
50
|
+
v = None
|
|
51
|
+
counts[v] = counts.get(v, 0) + 1
|
|
52
|
+
return max(counts.items(), key=lambda kv: kv[1])[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pool_grid(matrix, out_h, out_w, pool):
|
|
56
|
+
"""Box-pool a `[row][col]` grid down to `out_h × out_w`. Bin edges by
|
|
57
|
+
integer rounding; every bin is non-empty because `out_* <= n_*`."""
|
|
58
|
+
nrows = len(matrix)
|
|
59
|
+
ncols = len(matrix[0])
|
|
60
|
+
re = [nrows * i // out_h for i in range(out_h + 1)]
|
|
61
|
+
ce = [ncols * j // out_w for j in range(out_w + 1)]
|
|
62
|
+
return [[pool([matrix[r][c]
|
|
63
|
+
for r in range(re[i], re[i + 1])
|
|
64
|
+
for c in range(ce[j], ce[j + 1])])
|
|
65
|
+
for j in range(out_w)]
|
|
66
|
+
for i in range(out_h)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rgb_buffer(grid, rgb_of):
|
|
70
|
+
"""Flatten a `[row][col]` value grid into a packed RGB byte buffer
|
|
71
|
+
via `rgb_of(value) -> (r, g, b)` — the input `image_png` expects."""
|
|
72
|
+
buf = bytearray()
|
|
73
|
+
for row in grid:
|
|
74
|
+
for v in row:
|
|
75
|
+
r, g, b = rgb_of(v)
|
|
76
|
+
buf.append(r); buf.append(g); buf.append(b)
|
|
77
|
+
return buf
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def minmax_grid(d, lo, hi):
|
|
81
|
+
"""NaN/None-ignoring min/max over a 2-D cell grid, for autoscale.
|
|
82
|
+
Fills only the unset bounds; returns `(lo, hi, found)` where `found`
|
|
83
|
+
says whether any finite cell existed (callers keep their empty-grid
|
|
84
|
+
defaults). Vectorized — min/max are comparisons, not arithmetic, so
|
|
85
|
+
the result is exact; a ±0.0 tie canonicalizes to +0.0 (`x + 0.0`)
|
|
86
|
+
so it can't depend on scan order. Non-numeric cells fall back to
|
|
87
|
+
the scalar scan."""
|
|
88
|
+
try:
|
|
89
|
+
a = np.asarray(d, dtype=np.float64) # None cells → NaN
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
flat = [v for row in d for v in row if v is not None and v == v]
|
|
92
|
+
if flat:
|
|
93
|
+
if lo is None: lo = min(flat)
|
|
94
|
+
if hi is None: hi = max(flat)
|
|
95
|
+
return lo, hi, bool(flat)
|
|
96
|
+
if bool(np.isnan(a).all()):
|
|
97
|
+
return lo, hi, False
|
|
98
|
+
if lo is None: lo = float(np.nanmin(a)) + 0.0
|
|
99
|
+
if hi is None: hi = float(np.nanmax(a)) + 0.0
|
|
100
|
+
return lo, hi, True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _pool_mean_np(a, out_h, out_w):
|
|
104
|
+
"""`pool_grid(..., pool_mean)` vectorized: box-pool a 2-D float
|
|
105
|
+
array to `out_h × out_w` by NaN-ignoring mean.
|
|
106
|
+
|
|
107
|
+
Bit-identical to the scalar path by construction: cells are added
|
|
108
|
+
in the same row-major order (vectorized *across* bins, sequential
|
|
109
|
+
*within* each bin via the offset loop), NaN adds 0.0 — exact, since
|
|
110
|
+
`x + 0.0 == x` — and one final division mirrors `sum(fin)/len(fin)`.
|
|
111
|
+
Only elementwise ops: `np.sum`/`np.mean` pairwise summation is a
|
|
112
|
+
numpy implementation detail that may change across versions, which
|
|
113
|
+
would break byte-identical SVG across machines."""
|
|
114
|
+
nrows, ncols = a.shape
|
|
115
|
+
re = np.array([nrows * i // out_h for i in range(out_h + 1)])
|
|
116
|
+
ce = np.array([ncols * j // out_w for j in range(out_w + 1)])
|
|
117
|
+
r0, rl = re[:-1], np.diff(re)
|
|
118
|
+
c0, cl = ce[:-1], np.diff(ce)
|
|
119
|
+
valid = ~np.isnan(a)
|
|
120
|
+
vals = np.where(valid, a, 0.0)
|
|
121
|
+
acc = np.zeros((out_h, out_w))
|
|
122
|
+
cnt = np.zeros((out_h, out_w), dtype=np.int64)
|
|
123
|
+
for dr in range(int(rl.max())):
|
|
124
|
+
ri = np.nonzero(dr < rl)[0]
|
|
125
|
+
row_v = vals[r0[ri] + dr] # gather rows once per offset
|
|
126
|
+
row_m = valid[r0[ri] + dr]
|
|
127
|
+
for dc in range(int(cl.max())):
|
|
128
|
+
ci = np.nonzero(dc < cl)[0]
|
|
129
|
+
dst = np.ix_(ri, ci)
|
|
130
|
+
acc[dst] += row_v[:, c0[ci] + dc]
|
|
131
|
+
cnt[dst] += row_m[:, c0[ci] + dc]
|
|
132
|
+
return np.divide(acc, cnt, out=np.full((out_h, out_w), np.nan),
|
|
133
|
+
where=cnt > 0)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def png_value_cells(rows, out_h, out_w, norm, lut, absent_rgb):
|
|
137
|
+
"""Vectorized pool + colormap for a numeric cell grid → `(rgb_bytes,
|
|
138
|
+
downsampled)`, or None when the norm only has a scalar form (log) —
|
|
139
|
+
the caller then falls back to `pool_grid` + `rgb_buffer`. `rows` is
|
|
140
|
+
a nested list (None/NaN cells → absent color) or a 2-D ndarray."""
|
|
141
|
+
if norm.kind == "log":
|
|
142
|
+
return None
|
|
143
|
+
a = np.asarray(rows, dtype=np.float64) # None cells → NaN
|
|
144
|
+
downsampled = (out_h, out_w) != a.shape
|
|
145
|
+
if downsampled:
|
|
146
|
+
a = _pool_mean_np(a, out_h, out_w)
|
|
147
|
+
u = norm.to_unit_array(a)
|
|
148
|
+
nan_mask = np.isnan(u)
|
|
149
|
+
# Same rounding as the scalar `int(u * 255 + 0.5)`: values are
|
|
150
|
+
# >= 0 after clipping, so trunc-toward-zero == floor.
|
|
151
|
+
idx = (np.where(nan_mask, 0.0, u) * 255.0 + 0.5).astype(np.int64)
|
|
152
|
+
rgb = np.frombuffer(bytes(lut), dtype=np.uint8).reshape(256, 3)[idx]
|
|
153
|
+
if nan_mask.any():
|
|
154
|
+
rgb[nan_mask] = absent_rgb
|
|
155
|
+
return rgb.tobytes(), downsampled
|
|
156
|
+
|
|
157
|
+
|
|
13
158
|
# Used by long-form expansion for `linestyle=` and `alpha=` column splits.
|
|
14
159
|
LINESTYLE_CYCLE = (None, "--", ":", "-.")
|
|
15
160
|
DEFAULT_ALPHA_RANGE = (0.3, 1.0)
|
|
@@ -104,6 +249,11 @@ def expand_xy_long_form(kind, data, x_col, y_col,
|
|
|
104
249
|
elif alpha_value is not None:
|
|
105
250
|
opts["alpha"] = alpha_value
|
|
106
251
|
records.append({"type": kind, "xs": xs_g, "ys": ys_g, "opts": opts})
|
|
252
|
+
# The raster/simplify thresholds gate on the artist call's total mark
|
|
253
|
+
# count, not each fan-out slice — ten 10k-point groups strain the
|
|
254
|
+
# renderer exactly like one 100k-point series.
|
|
255
|
+
for rec in records:
|
|
256
|
+
rec["opts"]["_fanout_n"] = n
|
|
107
257
|
return records
|
|
108
258
|
|
|
109
259
|
|