plotlet 0.6.4__tar.gz → 0.7.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.6.4/src/plotlet.egg-info → plotlet-0.7.0}/PKG-INFO +2 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/pyproject.toml +11 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/__init__.py +11 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_json_layer.py +48 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_regions.py +31 -0
- plotlet-0.7.0/src/plotlet/_spec.py +187 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/_shared.py +55 -20
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/bar.py +42 -20
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/boxplot.py +15 -7
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/chord_ribbon.py +15 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/dendrogram.py +6 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/errorbar.py +12 -9
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/fills.py +8 -7
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/heatmap.py +11 -11
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/hexbin.py +3 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/hist.py +28 -18
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/hist2d.py +3 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/image_cmap.py +9 -9
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/image_rgba.py +5 -5
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/kde_2d.py +7 -5
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/line.py +3 -15
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/numeric_bar.py +3 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/pointplot.py +47 -18
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/qq.py +7 -5
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/references.py +13 -13
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/regression.py +7 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/ridge.py +5 -4
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/scatter.py +67 -38
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/shapes.py +23 -7
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/strip.py +48 -44
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/swarm.py +45 -41
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/text.py +10 -10
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/violin.py +15 -7
- plotlet-0.7.0/src/plotlet/describe.py +122 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/AI_ATTRS.md +15 -15
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/API.md +64 -18
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/ARCHITECTURE.md +2 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/EXTENDING.md +25 -9
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/SUBPLOTS.md +14 -1
- plotlet-0.7.0/src/plotlet/docs/THEMES.md +195 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/__init__.py +5 -4
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/_raster.py +72 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/_simplify.py +3 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/colormaps.py +2 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/font.py +30 -11
- plotlet-0.7.0/src/plotlet/draw/linestyles.py +58 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/primitives.py +55 -19
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/06_heatmap.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/07_annotated_heatmap.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/15_swarm.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/23_ring_tree.py +1 -1
- plotlet-0.7.0/src/plotlet/examples/json/01_scatter.json +42 -0
- plotlet-0.7.0/src/plotlet/examples/json/02_line.json +32 -0
- plotlet-0.7.0/src/plotlet/examples/json/03_bar.json +21 -0
- plotlet-0.7.0/src/plotlet/examples/json/04_violin.json +42 -0
- plotlet-0.7.0/src/plotlet/examples/json/05_anscombe.json +82 -0
- plotlet-0.7.0/src/plotlet/examples/json/06_heatmap.json +63 -0
- plotlet-0.7.0/src/plotlet/examples/json/07_annotated_heatmap.json +99 -0
- plotlet-0.7.0/src/plotlet/examples/json/08_tracks.json +81 -0
- plotlet-0.7.0/src/plotlet/examples/json/09_circular.json +36 -0
- plotlet-0.7.0/src/plotlet/examples/json/10_chord.json +99 -0
- plotlet-0.7.0/src/plotlet/examples/json/12_hist.json +37 -0
- plotlet-0.7.0/src/plotlet/examples/json/13_ecdf.json +53 -0
- plotlet-0.7.0/src/plotlet/examples/json/14_box_strip.json +59 -0
- plotlet-0.7.0/src/plotlet/examples/json/15_swarm.json +58 -0
- plotlet-0.7.0/src/plotlet/examples/json/16_pointplot.json +46 -0
- plotlet-0.7.0/src/plotlet/examples/json/17_ridge.json +34 -0
- plotlet-0.7.0/src/plotlet/examples/json/18_qq.json +29 -0
- plotlet-0.7.0/src/plotlet/examples/json/19_hexbin.json +62 -0
- plotlet-0.7.0/src/plotlet/examples/json/20_kde_2d.json +38 -0
- plotlet-0.7.0/src/plotlet/examples/json/22_facets.json +52 -0
- plotlet-0.7.0/src/plotlet/examples/json/23_ring_tree.json +94 -0
- plotlet-0.7.0/src/plotlet/lint.py +263 -0
- plotlet-0.7.0/src/plotlet/op_help.py +508 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/chart.py +299 -85
- plotlet-0.7.0/src/plotlet/record/codegen.py +550 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/facet.py +11 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/figure_ir.py +36 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/journal.py +94 -16
- plotlet-0.7.0/src/plotlet/record/journal_lite.py +225 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/registry.py +14 -8
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_chrome_bands.py +16 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_chrome_emit.py +93 -69
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_layout_engine.py +86 -25
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_legend.py +292 -109
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_nodes.py +66 -19
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_resolution.py +271 -203
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_validate.py +11 -4
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/emit.py +26 -16
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/resolved_ir.py +50 -19
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/scales.py +137 -78
- plotlet-0.7.0/src/plotlet/schemas/journal_lite.schema.json +823 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/sectors.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/skill.py +4 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/skills/developers.md +7 -5
- plotlet-0.7.0/src/plotlet/skills/json.md +183 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/skills/users.md +47 -9
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/spec.json +37 -50
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/themes/dark.json +4 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/themes/minimal.json +4 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/themes.py +6 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/utils.py +69 -6
- {plotlet-0.6.4 → plotlet-0.7.0/src/plotlet.egg-info}/PKG-INFO +2 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet.egg-info/SOURCES.txt +37 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet.egg-info/requires.txt +1 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_aes.py +11 -10
- plotlet-0.7.0/tests/test_authoring_errors.py +69 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_annotation_strip.py +4 -3
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_axis.py +45 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_bar.py +6 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_boxplot.py +2 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_dendrogram.py +2 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_facets.py +1 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_heatmap.py +23 -23
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_image_cmap.py +4 -4
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_labels.py +98 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_scatter.py +54 -0
- plotlet-0.7.0/tests/test_chart_strip.py +100 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_swarm.py +2 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_text.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_violin.py +18 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_colors.py +27 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_coord_circular.py +6 -6
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_coord_sectors.py +3 -2
- plotlet-0.7.0/tests/test_describe.py +54 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_fonts.py +61 -31
- plotlet-0.7.0/tests/test_help.py +66 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_import_boundary.py +1 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_io.py +2 -1
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_ir.py +2 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_ir_resolved.py +3 -2
- plotlet-0.7.0/tests/test_journal_lite.py +188 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_journal_roundtrip.py +145 -15
- plotlet-0.7.0/tests/test_json_examples.py +53 -0
- plotlet-0.7.0/tests/test_json_schema.py +66 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_layout_attachments.py +5 -2
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_layout_legend.py +228 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_layout_subplots.py +3 -1
- plotlet-0.7.0/tests/test_lint.py +212 -0
- plotlet-0.7.0/tests/test_op_help.py +191 -0
- plotlet-0.7.0/tests/test_orientation.py +175 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_record_signatures.py +101 -9
- plotlet-0.7.0/tests/test_scale_sweep.py +189 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_skill.py +9 -0
- plotlet-0.7.0/tests/test_spec_ownership.py +113 -0
- plotlet-0.7.0/tests/test_themes.py +320 -0
- plotlet-0.7.0/tests/test_to_python.py +196 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_units.py +1 -1
- plotlet-0.6.4/src/plotlet/_spec.py +0 -147
- plotlet-0.6.4/src/plotlet/docs/THEMES.md +0 -124
- plotlet-0.6.4/src/plotlet/draw/linestyles.py +0 -27
- plotlet-0.6.4/src/plotlet/lint.py +0 -130
- plotlet-0.6.4/tests/test_chart_strip.py +0 -46
- plotlet-0.6.4/tests/test_help.py +0 -19
- plotlet-0.6.4/tests/test_lint.py +0 -26
- plotlet-0.6.4/tests/test_themes.py +0 -99
- {plotlet-0.6.4 → plotlet-0.7.0}/LICENSE +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/README.md +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/setup.cfg +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_coord_registry.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_datasets/anscombe.csv +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_datasets/earth.npz +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_datasets/flights.csv +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_datasets/penguins.csv +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_datasets/tips.csv +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_splits.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/_tree.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/__init__.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/_marching.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/annotation_strip.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/chord_links.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/contour.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/density_1d.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/ecdf.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/freqpoly.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/artists/rug.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/cluster.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/datasets.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/COORDINATES.md +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/docs/PHILOSOPHY.md +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/_cm_data.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/_css_colors.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/_png.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/colors.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Bold.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-BoldItalic.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Italic.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Regular.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-Bold.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-Oblique.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans.ttf +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/LICENSE-Arimo.txt +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/fonts/LICENSE-DejaVuSans.txt +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/draw/format.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/01_scatter.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/02_line.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/03_bar.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/04_violin.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/05_anscombe.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/08_tracks.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/09_circular.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/10_chord.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/12_hist.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/13_ecdf.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/14_box_strip.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/16_pointplot.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/17_ridge.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/18_qq.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/19_hexbin.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/20_kde_2d.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/21_image_cmap.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/22_facets.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/examples/24_image_rgba.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/formatters.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/layout_diagram.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/__init__.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/record/legend.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/__init__.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_attachments.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_chrome_circular.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_chrome_visibility.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_coord_circular.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/render/_layout_circular.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet/themes/void.json +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet.egg-info/dependency_links.txt +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/src/plotlet.egg-info/top_level.txt +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_chord_links.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_chord_ribbon.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_contour.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_density_1d.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_ecdf.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_errorbar.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_fills.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_freqpoly.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_hexbin.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_hist.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_hist2d.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_image_rgba.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_kde_2d.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_line.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_numeric_bar.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_pointplot.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_qq.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_references.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_regression.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_ridge.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_rug.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_shapes.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_chart_spines.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_cluster.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_datasets.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_formatters.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_ir_validate.py +0 -0
- {plotlet-0.6.4 → plotlet-0.7.0}/tests/test_layout_diagram.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plotlet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
5
|
Author: gitbamboo42
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,7 @@ Requires-Dist: nbconvert; extra == "dev"
|
|
|
31
31
|
Provides-Extra: test
|
|
32
32
|
Requires-Dist: pytest>=7; extra == "test"
|
|
33
33
|
Requires-Dist: pandas; extra == "test"
|
|
34
|
+
Requires-Dist: jsonschema; extra == "test"
|
|
34
35
|
Provides-Extra: pdf
|
|
35
36
|
Requires-Dist: cairosvg; extra == "pdf"
|
|
36
37
|
Dynamic: license-file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plotlet"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
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"
|
|
@@ -48,6 +48,7 @@ dev = [
|
|
|
48
48
|
test = [
|
|
49
49
|
"pytest>=7",
|
|
50
50
|
"pandas",
|
|
51
|
+
"jsonschema",
|
|
51
52
|
]
|
|
52
53
|
pdf = [
|
|
53
54
|
"cairosvg",
|
|
@@ -74,4 +75,12 @@ plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "draw/fonts/*.txt",
|
|
|
74
75
|
# AI onboarding: pt.skill() reads skills/, and the guides point
|
|
75
76
|
# at the shipped docs/ and examples/ so an installed package is
|
|
76
77
|
# self-sufficient — no repo clone, no web access needed.
|
|
77
|
-
"skills/*.md", "docs/*.md", "examples/*.py"
|
|
78
|
+
"skills/*.md", "docs/*.md", "examples/*.py",
|
|
79
|
+
# JSON twins of the examples, for authoring plots as JSON
|
|
80
|
+
# journals (skills/json.md). Generated by
|
|
81
|
+
# tests/gen_json_examples.py — never edited by hand.
|
|
82
|
+
"examples/json/*.json",
|
|
83
|
+
# JSON Schema for journal-lite blobs, for editor/server-side
|
|
84
|
+
# validation. Generated by tests/gen_json_schema.py from
|
|
85
|
+
# pt.op_schema() — never edited by hand.
|
|
86
|
+
"schemas/*.json"]
|
|
@@ -34,8 +34,16 @@ from .formatters import (register_formatter, list_formatters,
|
|
|
34
34
|
from ._coord_registry import register_coord_codec
|
|
35
35
|
from .record.journal import (to_journal, from_journal, to_json, from_json,
|
|
36
36
|
JournalNode, Journal)
|
|
37
|
+
from .record.journal_lite import to_json_lite, from_json_lite
|
|
38
|
+
from .record.codegen import to_python
|
|
37
39
|
from .record.figure_ir import to_ir, from_ir, FigureIR, IRNode
|
|
38
40
|
from .skill import skill
|
|
41
|
+
# Shadows the `plotlet.lint` submodule attribute with the function —
|
|
42
|
+
# the os.path-style pattern: `pt.lint(c)` calls it, and
|
|
43
|
+
# `from plotlet.lint import lint` still resolves via sys.modules.
|
|
44
|
+
from .lint import lint
|
|
45
|
+
from .describe import describe
|
|
46
|
+
from .op_help import op_help, op_schema # same shadowing pattern as lint
|
|
39
47
|
from . import draw, utils
|
|
40
48
|
|
|
41
49
|
|
|
@@ -62,10 +70,11 @@ __all__ = ["chart", "Chart", "Layout", "SPEC", "TAB10",
|
|
|
62
70
|
"linkage", "linkage_split", "SplitTree",
|
|
63
71
|
"register_formatter", "list_formatters",
|
|
64
72
|
"superscript", "subscript",
|
|
65
|
-
"to_json", "from_json", "
|
|
73
|
+
"to_json", "from_json", "to_json_lite", "from_json_lite",
|
|
74
|
+
"to_python", "register_coord_codec",
|
|
66
75
|
"to_journal", "from_journal", "JournalNode", "Journal",
|
|
67
76
|
"to_ir", "from_ir", "FigureIR", "IRNode",
|
|
68
|
-
"skill",
|
|
77
|
+
"skill", "lint", "describe", "op_help", "op_schema",
|
|
69
78
|
"draw", "utils"]
|
|
70
79
|
|
|
71
80
|
# Single source of truth: pyproject.toml. importlib.metadata reads it at
|
|
@@ -36,6 +36,7 @@ both halves need it (the render tree's hydrator and the front half's
|
|
|
36
36
|
facet expansion) and it resolves only against shared vocabulary.
|
|
37
37
|
"""
|
|
38
38
|
from __future__ import annotations
|
|
39
|
+
import json
|
|
39
40
|
from typing import Any
|
|
40
41
|
|
|
41
42
|
import numpy as np
|
|
@@ -136,6 +137,53 @@ def json_hydrate(value: Any) -> Any:
|
|
|
136
137
|
return value
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
_PRETTY_WIDTH = 100 # target line width for pretty_dumps
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def pretty_dumps(value: Any) -> str:
|
|
144
|
+
"""Canonical JSON text form — what `pt.to_json` returns and the
|
|
145
|
+
shipped example twins use. Objects and arrays stay on one line when
|
|
146
|
+
they fit in `_PRETTY_WIDTH` columns; a too-long dict or mixed list
|
|
147
|
+
expands one element per line (1-space indent steps); a too-long
|
|
148
|
+
list of primitives wraps at the width, continuation lines aligned
|
|
149
|
+
under its opening bracket — so journal entries read structurally
|
|
150
|
+
while data columns stay a few dense lines. Deterministic (same
|
|
151
|
+
value → same bytes) and whitespace-only: `json.loads` of the
|
|
152
|
+
result equals `json.loads(json.dumps(value))`."""
|
|
153
|
+
return _pretty(value, 0, "")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _pretty(value: Any, col: int, pad: str) -> str:
|
|
157
|
+
"""Render `value` starting at column `col`; `pad` is the
|
|
158
|
+
indentation of the enclosing construct."""
|
|
159
|
+
compact = json.dumps(value)
|
|
160
|
+
if col + len(compact) <= _PRETTY_WIDTH \
|
|
161
|
+
or not isinstance(value, (dict, list)):
|
|
162
|
+
return compact
|
|
163
|
+
inner = pad + " "
|
|
164
|
+
if isinstance(value, dict):
|
|
165
|
+
lines = []
|
|
166
|
+
for k, v in value.items():
|
|
167
|
+
key = f"{inner}{json.dumps(k)}: "
|
|
168
|
+
lines.append(key + _pretty(v, len(key), inner))
|
|
169
|
+
return "{\n" + ",\n".join(lines) + "\n" + pad + "}"
|
|
170
|
+
if all(not isinstance(v, (dict, list)) for v in value):
|
|
171
|
+
room = max(_PRETTY_WIDTH - col - 1, 20) # width left after "["
|
|
172
|
+
parts, lines, used = [json.dumps(v) for v in value], [], -2
|
|
173
|
+
for part in parts:
|
|
174
|
+
if used < 0 or used + 2 + len(part) > room:
|
|
175
|
+
lines.append([part])
|
|
176
|
+
used = len(part)
|
|
177
|
+
else:
|
|
178
|
+
lines[-1].append(part)
|
|
179
|
+
used += 2 + len(part)
|
|
180
|
+
joined = (",\n" + " " * (col + 1)).join(
|
|
181
|
+
", ".join(line) for line in lines)
|
|
182
|
+
return "[" + joined + "]"
|
|
183
|
+
lines = [inner + _pretty(v, len(inner), inner) for v in value]
|
|
184
|
+
return "[\n" + ",\n".join(lines) + "\n" + pad + "]"
|
|
185
|
+
|
|
186
|
+
|
|
139
187
|
def _decode(value: Any, nid_to_node: dict, data_table: dict) -> Any:
|
|
140
188
|
"""Resolve plotlet's *reference* envelopes back to live objects —
|
|
141
189
|
`{"$node"}` via `nid_to_node`, `{"$coord"}` via the coord registry,
|
|
@@ -58,6 +58,11 @@ class _Sink:
|
|
|
58
58
|
# offset to recorded bboxes so consumers receive outer-SVG coords
|
|
59
59
|
# regardless of whether the chart is single- or multi-panel.
|
|
60
60
|
translate_stack: list[tuple[float, float]] = field(default_factory=list)
|
|
61
|
+
# Depth of active `structural()` blocks — chrome recorded inside
|
|
62
|
+
# one lives inside the panel rect by design (coordinate-owned
|
|
63
|
+
# chrome, an inside-positioned legend), so overlap lint can tell
|
|
64
|
+
# it from misplaced chrome.
|
|
65
|
+
structural_depth: int = 0
|
|
61
66
|
|
|
62
67
|
def _offset(self) -> tuple[float, float]:
|
|
63
68
|
dx = dy = 0.0
|
|
@@ -75,6 +80,12 @@ class _Sink:
|
|
|
75
80
|
# the bbox in outer-SVG coords.
|
|
76
81
|
if "polygon" in meta:
|
|
77
82
|
meta["polygon"] = [(px + dx, py + dy) for px, py in meta["polygon"]]
|
|
83
|
+
# Multi-line text ships one quad per inked line under `lines`.
|
|
84
|
+
if "lines" in meta:
|
|
85
|
+
meta["lines"] = [[(px + dx, py + dy) for px, py in quad]
|
|
86
|
+
for quad in meta["lines"]]
|
|
87
|
+
if self.structural_depth:
|
|
88
|
+
meta.setdefault("structural", True)
|
|
78
89
|
self.regions.append(Region(kind=kind, bbox=(x + dx, y + dy, w, h),
|
|
79
90
|
name=name, meta=meta))
|
|
80
91
|
|
|
@@ -128,6 +139,26 @@ def suppressed():
|
|
|
128
139
|
_CURRENT.reset(token)
|
|
129
140
|
|
|
130
141
|
|
|
142
|
+
@contextmanager
|
|
143
|
+
def structural():
|
|
144
|
+
"""Mark every region recorded in the block as `structural=True` —
|
|
145
|
+
chrome that lives inside the panel rect *by design*, so its overlap
|
|
146
|
+
with the panel/spines is rendering geometry, not a layout bug.
|
|
147
|
+
Wrapped around coordinate-owned chrome emission (a circular chart's
|
|
148
|
+
angular tick labels and sector walls all sit inside the data ring)
|
|
149
|
+
and an inside-positioned legend's body. Overlap lint skips
|
|
150
|
+
structural-vs-container pairs. No-op when no sink is active."""
|
|
151
|
+
sink = _CURRENT.get()
|
|
152
|
+
if sink is None:
|
|
153
|
+
yield
|
|
154
|
+
return
|
|
155
|
+
sink.structural_depth += 1
|
|
156
|
+
try:
|
|
157
|
+
yield
|
|
158
|
+
finally:
|
|
159
|
+
sink.structural_depth -= 1
|
|
160
|
+
|
|
161
|
+
|
|
131
162
|
@contextmanager
|
|
132
163
|
def translate(dx: float, dy: float):
|
|
133
164
|
"""Push a translate offset onto the sink's transform stack for the
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Visual spec — loaded from `spec.json` at import time. Internal.
|
|
2
|
+
|
|
3
|
+
The spec is the locked visual contract: colors, fonts, sizes, default alphas,
|
|
4
|
+
legend dimensions. Submodules read from here to avoid hardcoding literals.
|
|
5
|
+
|
|
6
|
+
`SPEC` and the per-section handles (`_FRAME`, `_GRIDSPEC`, …) are *live dicts*:
|
|
7
|
+
when a chart's `theme=` is applied, [`active_theme(name)`](#active_theme)
|
|
8
|
+
mutates the inner contents of these dicts so every module sees the
|
|
9
|
+
override without an import-time refactor. The previous contents are
|
|
10
|
+
restored when the context exits.
|
|
11
|
+
|
|
12
|
+
Decision note — module-level mutation vs the no-global-state policy
|
|
13
|
+
-------------------------------------------------------------------
|
|
14
|
+
The policy ("no global state: themes per-chart, deterministic") is about
|
|
15
|
+
what a *user* can observe: no chart's theme leaks into another, and the
|
|
16
|
+
same script renders byte-identical SVG everywhere. This module satisfies
|
|
17
|
+
that with scoped mutation, not with parameter threading:
|
|
18
|
+
|
|
19
|
+
- `active_theme` wraps each leaf's render and measurement walk in
|
|
20
|
+
`_layout_engine`; restore runs in `try/finally`, so an exception
|
|
21
|
+
inside a render still restores. `load_theme(name)` is evaluated
|
|
22
|
+
before any mutation, so an unknown theme raises without touching the
|
|
23
|
+
spec.
|
|
24
|
+
- Rendering is synchronous and single-threaded, so the swap is
|
|
25
|
+
invisible from outside a render call. The known limit: rendering two
|
|
26
|
+
charts concurrently from *user* threads would interleave spec
|
|
27
|
+
mutations. Accepted deliberately — threading a spec object through
|
|
28
|
+
every draw/measure call in every render module buys thread safety
|
|
29
|
+
nobody has asked for at the cost of noise on every signature. Revisit
|
|
30
|
+
if a concurrent-render use case actually appears.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import contextlib
|
|
35
|
+
import copy
|
|
36
|
+
import difflib
|
|
37
|
+
|
|
38
|
+
from .themes import load_theme, _deep_merge
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _install(target, source) -> None:
|
|
42
|
+
"""Replace `target`'s contents with `source`'s, preserving identity of
|
|
43
|
+
nested mutable containers (dicts AND lists). Used to swap themes
|
|
44
|
+
without breaking references that other modules captured at import
|
|
45
|
+
time via `from ._spec import _FRAME`. Nested dicts
|
|
46
|
+
and lists that exist on both sides are recursed into so their
|
|
47
|
+
identity survives; mismatched-type or absent-on-target values are
|
|
48
|
+
deep-copied wholesale.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(target, dict) and isinstance(source, dict):
|
|
51
|
+
for k in list(target.keys()):
|
|
52
|
+
if k not in source:
|
|
53
|
+
del target[k]
|
|
54
|
+
for k, v in source.items():
|
|
55
|
+
existing = target.get(k)
|
|
56
|
+
if isinstance(v, dict) and isinstance(existing, dict):
|
|
57
|
+
_install(existing, v)
|
|
58
|
+
elif isinstance(v, list) and isinstance(existing, list):
|
|
59
|
+
_install(existing, v)
|
|
60
|
+
else:
|
|
61
|
+
target[k] = copy.deepcopy(v)
|
|
62
|
+
elif isinstance(target, list) and isinstance(source, list):
|
|
63
|
+
target.clear()
|
|
64
|
+
target.extend(copy.deepcopy(item) for item in source)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Initialize from the locked default theme. Every module that imports
|
|
68
|
+
# `_FRAME`, `_FONTSPEC`, … gets a reference to one of these dicts; their
|
|
69
|
+
# contents change when `active_theme(name)` is in effect.
|
|
70
|
+
SPEC: dict = {}
|
|
71
|
+
_install(SPEC, load_theme("classic"))
|
|
72
|
+
|
|
73
|
+
_DASH = SPEC["linestyles"]
|
|
74
|
+
_SCALESPEC = SPEC["scale"]
|
|
75
|
+
_RASTERSPEC = SPEC["raster"]
|
|
76
|
+
_FRAME = SPEC["frame"]
|
|
77
|
+
_GRIDSPEC = SPEC["grid"]
|
|
78
|
+
_FONTSPEC = SPEC["font"]
|
|
79
|
+
_LEGSPEC = SPEC["legend"]
|
|
80
|
+
_PADSPEC = SPEC["pad"]
|
|
81
|
+
_SIZESPEC = SPEC["size"]
|
|
82
|
+
_MARGIN_FLOOR = SPEC["size"]["margin_floor"]
|
|
83
|
+
_OUTER_MARGIN = SPEC["size"]["outer_margin"]
|
|
84
|
+
_LAYOUTSPEC = SPEC["layout"]
|
|
85
|
+
_FIGSPEC = SPEC["figure"]
|
|
86
|
+
|
|
87
|
+
_CURRENT_THEME: str = "classic"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def current_theme() -> str:
|
|
91
|
+
"""Name of the theme currently installed in the spec dicts."""
|
|
92
|
+
return _CURRENT_THEME
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _swap_to(name: str | None, overrides: dict | None = None) -> dict:
|
|
96
|
+
"""Install theme `name` (deep-merged with per-chart `overrides`, if
|
|
97
|
+
any) into the live spec dicts; return the previous full SPEC
|
|
98
|
+
snapshot for later restoration. `_install` recurses into nested
|
|
99
|
+
dicts and lists, so module-level handles at any depth (`_FRAME`,
|
|
100
|
+
`_MARGIN_FLOOR`, …) keep their identity. The merge never
|
|
101
|
+
touches `load_theme`'s cache — `_deep_merge` copies its base."""
|
|
102
|
+
global _CURRENT_THEME
|
|
103
|
+
snapshot = copy.deepcopy(SPEC)
|
|
104
|
+
prev_theme = _CURRENT_THEME
|
|
105
|
+
resolved = load_theme(name)
|
|
106
|
+
if overrides:
|
|
107
|
+
resolved = _deep_merge(resolved, overrides)
|
|
108
|
+
_install(SPEC, resolved)
|
|
109
|
+
_CURRENT_THEME = name if name is not None else "classic"
|
|
110
|
+
return {"spec": snapshot, "theme": prev_theme}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _restore(snapshot: dict) -> None:
|
|
114
|
+
global _CURRENT_THEME
|
|
115
|
+
_install(SPEC, snapshot["spec"])
|
|
116
|
+
_CURRENT_THEME = snapshot["theme"]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _bad_key(key: str, candidates, where: str) -> ValueError:
|
|
120
|
+
hint = difflib.get_close_matches(key, list(candidates), n=1)
|
|
121
|
+
did_you_mean = f"; did you mean {hint[0]!r}?" if hint else ""
|
|
122
|
+
return ValueError(
|
|
123
|
+
f"spec override: no key {key!r} in {where}{did_you_mean} "
|
|
124
|
+
f"Available: {sorted(candidates)}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_overrides(overrides: dict) -> None:
|
|
129
|
+
"""Check `c.spec(...)` overrides against the spec's shape: every
|
|
130
|
+
kwarg must be a root spec key — a section/block (`font`, `grid`,
|
|
131
|
+
`scatter`, …) holding a dict, or one of the shared scalar keys
|
|
132
|
+
(`linewidth`, `cmap`, …) — and every key inside a dict must exist
|
|
133
|
+
at that path in the classic spec; the deep merge would silently
|
|
134
|
+
absorb a typo like `titlesize` otherwise. Validation runs against
|
|
135
|
+
the classic spec (`load_theme("classic")`), never the live dicts
|
|
136
|
+
above: those are mutated mid-render by `active_theme`, while
|
|
137
|
+
classic is the stable shape authority. Values are not type-checked
|
|
138
|
+
beyond the dict/scalar split; the spec's leaves are too
|
|
139
|
+
heterogeneous for that to pay its way."""
|
|
140
|
+
spec = load_theme("classic")
|
|
141
|
+
valid = {k: v for k, v in spec.items() if k != "version"}
|
|
142
|
+
for key, value in overrides.items():
|
|
143
|
+
if key not in valid:
|
|
144
|
+
raise _bad_key(key, valid, "the spec")
|
|
145
|
+
if isinstance(valid[key], dict):
|
|
146
|
+
if not isinstance(value, dict):
|
|
147
|
+
raise TypeError(
|
|
148
|
+
f"spec override {key!r} must be a dict of "
|
|
149
|
+
f"{key}.* keys, got {type(value).__name__}"
|
|
150
|
+
)
|
|
151
|
+
_validate_section(value, valid[key], key)
|
|
152
|
+
elif isinstance(value, dict):
|
|
153
|
+
raise TypeError(
|
|
154
|
+
f"spec override {key!r} takes a single value, "
|
|
155
|
+
"got a dict"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_section(override: dict, base: dict, path: str) -> None:
|
|
160
|
+
for k, v in override.items():
|
|
161
|
+
if k not in base:
|
|
162
|
+
raise _bad_key(k, base.keys(), f"spec section {path!r}")
|
|
163
|
+
if isinstance(base[k], dict) and isinstance(v, dict):
|
|
164
|
+
_validate_section(v, base[k], f"{path}.{k}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@contextlib.contextmanager
|
|
168
|
+
def active_theme(name: str | None, overrides: dict | None = None):
|
|
169
|
+
"""Apply theme `name` to the live spec dicts for the duration of the
|
|
170
|
+
`with` block. `None` is a passthrough — used by code paths that
|
|
171
|
+
haven't decided a theme yet (e.g. parent rendering before walking
|
|
172
|
+
into a leaf that may set its own).
|
|
173
|
+
|
|
174
|
+
`overrides` carries a chart's accumulated `c.spec(...)` values,
|
|
175
|
+
deep-merged over the named theme. With overrides present the swap
|
|
176
|
+
always runs — even `name=None` means "classic + overrides", not a
|
|
177
|
+
passthrough — and never short-circuits on the current-theme name,
|
|
178
|
+
since the merged spec differs from the plain named one.
|
|
179
|
+
"""
|
|
180
|
+
if not overrides and (name is None or name == _CURRENT_THEME):
|
|
181
|
+
yield
|
|
182
|
+
return
|
|
183
|
+
snapshot = _swap_to(name, overrides)
|
|
184
|
+
try:
|
|
185
|
+
yield
|
|
186
|
+
finally:
|
|
187
|
+
_restore(snapshot)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
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
|
-
Paint logic is a nested closure;
|
|
5
|
+
Paint logic is a nested closure; the only shared swatch helpers are the
|
|
6
|
+
two size clamps (`swatch_radius` / `swatch_stroke`).
|
|
6
7
|
|
|
7
8
|
The `pool_*` / `rgb_buffer` helpers back the PNG-embedded raster paths of
|
|
8
9
|
image_cmap / image_rgba and heatmap: a grid denser than the display can show is box-pooled
|
|
@@ -14,20 +15,65 @@ import math
|
|
|
14
15
|
|
|
15
16
|
import numpy as np
|
|
16
17
|
|
|
17
|
-
from .._spec import _LEGSPEC,
|
|
18
|
-
from ..draw import marker, segment, rect
|
|
18
|
+
from .._spec import _LEGSPEC, SPEC, _RASTERSPEC
|
|
19
|
+
from ..draw import marker, segment, rect, resolve_color
|
|
19
20
|
from ..draw import TAB10
|
|
20
21
|
from ..utils import to_list, resolve_aes, palette_color
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def edge_px(scale, v):
|
|
25
|
+
"""Map `v` through `scale`, clamping an unmappable value to the
|
|
26
|
+
domain-lo panel edge. On a log axis a value at or below zero has no
|
|
27
|
+
pixel; connected geometry (bands, baselines, fitted lines, KDE
|
|
28
|
+
tails) draws it at the axis edge — where the panel clip cuts it
|
|
29
|
+
off — instead of printing a non-finite coordinate into the path.
|
|
30
|
+
Point markers should keep masking (skip the point): a clamped
|
|
31
|
+
marker would invent a datum at the edge."""
|
|
32
|
+
p = scale(v)
|
|
33
|
+
if math.isfinite(p):
|
|
34
|
+
return p
|
|
35
|
+
return scale.r0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def point_outline(opts):
|
|
39
|
+
"""Outline (stroke, width) for a point-cloud artist (strip/swarm),
|
|
40
|
+
per the `draw.marker` rule: `edgecolor=` turns the outline on,
|
|
41
|
+
`linewidth=` only sizes it (spec marker stroke width when omitted).
|
|
42
|
+
No edgecolor → no outline, whatever the width. Returns stroke None
|
|
43
|
+
when hidden."""
|
|
44
|
+
stroke = resolve_color(opts.get("edgecolor"))
|
|
45
|
+
if stroke is None:
|
|
46
|
+
return None, 0
|
|
47
|
+
lw = opts.get("linewidth")
|
|
48
|
+
lw = SPEC["marker_stroke_width"] if lw is None else float(lw)
|
|
49
|
+
if lw <= 0:
|
|
50
|
+
return None, 0
|
|
51
|
+
return stroke, lw
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def swatch_radius(r):
|
|
55
|
+
"""Marker radius for a legend key: the plotted radius, capped so the
|
|
56
|
+
glyph stays inside its legend row. A key mirrors the mark's styling,
|
|
57
|
+
but a `size=40` dot would spill across the whole legend box and out
|
|
58
|
+
the other side — past the cap the exact radius stops carrying
|
|
59
|
+
information anyway."""
|
|
60
|
+
return min(float(r), _LEGSPEC["max_swatch_radius"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def swatch_stroke(w):
|
|
64
|
+
"""Stroke width for a legend key, capped the same way — half of a
|
|
65
|
+
wide stroke sits outside the shape it outlines."""
|
|
66
|
+
return min(float(w), _LEGSPEC["max_swatch_stroke"])
|
|
67
|
+
|
|
68
|
+
|
|
23
69
|
def pool_target(n_cells, px):
|
|
24
70
|
"""Output-pixel count along one raster axis: full data resolution
|
|
25
|
-
until cells outnumber `
|
|
71
|
+
until cells outnumber `raster.oversample` cells per display pixel, then
|
|
26
72
|
capped at that. The oversample headroom keeps hi-dpi rendering and
|
|
27
73
|
moderate zoom sharp; past it the viewer can't show the extra cells
|
|
28
74
|
anyway, and its nearest-neighbour downscale would *subsample* them
|
|
29
75
|
(drop cells) where pooling summarizes them."""
|
|
30
|
-
cap = max(1, math.ceil(px *
|
|
76
|
+
cap = max(1, math.ceil(px * _RASTERSPEC["oversample"]))
|
|
31
77
|
return n_cells if n_cells <= cap else cap
|
|
32
78
|
|
|
33
79
|
|
|
@@ -309,11 +355,11 @@ def _line_legend_entries(a):
|
|
|
309
355
|
sw = _LEGSPEC["swatch_width"]
|
|
310
356
|
out = segment(x0, y_mid, x0 + sw, y_mid,
|
|
311
357
|
color=a["_color"],
|
|
312
|
-
width=a["opts"].get("linewidth", ctx.
|
|
358
|
+
width=a["opts"].get("linewidth", ctx.spec["linewidth"]),
|
|
313
359
|
dash=a["opts"].get("linestyle"))
|
|
314
360
|
if a["opts"].get("marker"):
|
|
315
361
|
out += marker(a["opts"]["marker"], x0 + sw / 2, y_mid,
|
|
316
|
-
a["opts"].get("markersize", ctx.
|
|
362
|
+
a["opts"].get("markersize", ctx.spec["marker_size"]),
|
|
317
363
|
a["_color"], 1)
|
|
318
364
|
return out
|
|
319
365
|
return [{"label": label, "color": a.get("_color"), "paint": paint}]
|
|
@@ -327,22 +373,11 @@ def _refline_legend_entries(a):
|
|
|
327
373
|
sw = _LEGSPEC["swatch_width"]
|
|
328
374
|
return segment(x0, y_mid, x0 + sw, y_mid,
|
|
329
375
|
color=a["_color"],
|
|
330
|
-
width=a["opts"].get("linewidth", ctx.
|
|
376
|
+
width=a["opts"].get("linewidth", ctx.spec["refline"]["linewidth"]),
|
|
331
377
|
dash=a["opts"].get("linestyle"))
|
|
332
378
|
return [{"label": label, "color": a.get("_color"), "paint": paint}]
|
|
333
379
|
|
|
334
380
|
|
|
335
|
-
def _bar_legend_entries(a):
|
|
336
|
-
label = a["opts"].get("label")
|
|
337
|
-
if not label:
|
|
338
|
-
return []
|
|
339
|
-
def paint(a, ctx, x0, y_mid):
|
|
340
|
-
sw = _LEGSPEC["swatch_width"]
|
|
341
|
-
return rect(x0, y_mid - 5, sw, 10, fill=a["_color"],
|
|
342
|
-
alpha=a["opts"].get("alpha", 1))
|
|
343
|
-
return [{"label": label, "color": a.get("_color"), "paint": paint}]
|
|
344
|
-
|
|
345
|
-
|
|
346
381
|
def _refspan_legend_entries(a):
|
|
347
382
|
label = a["opts"].get("label")
|
|
348
383
|
if not label:
|
|
@@ -350,5 +385,5 @@ def _refspan_legend_entries(a):
|
|
|
350
385
|
def paint(a, ctx, x0, y_mid):
|
|
351
386
|
sw = _LEGSPEC["swatch_width"]
|
|
352
387
|
return rect(x0, y_mid - 5, sw, 10, fill=a["_color"],
|
|
353
|
-
alpha=a["opts"].get("alpha", ctx.
|
|
388
|
+
alpha=a["opts"].get("alpha", ctx.spec["refspan"]["alpha"]))
|
|
354
389
|
return [{"label": label, "color": a.get("_color"), "paint": paint}]
|