jarvisplot 2.1.8__tar.gz → 2.1.9__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.
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/PKG-INFO +3 -1
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/SOURCES.txt +9 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/PKG-INFO +3 -1
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/README.md +2 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/debug_config.py +7 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/design_runtime.py +4 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/figure_types.py +10 -1
- jarvisplot-2.1.9/jarvisplot/Figure/forest_debug.py +35 -0
- jarvisplot-2.1.9/jarvisplot/Figure/forest_legend.py +88 -0
- jarvisplot-2.1.9/jarvisplot/Figure/forest_runtime.py +294 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/legend_runtime.py +3 -3
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/capabilities.py +7 -0
- jarvisplot-2.1.9/jarvisplot/cards/forest/swiss_column.json +196 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/style_preference.json +3 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/column_demand.py +6 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core.py +7 -1
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core_runtime.py +14 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader.py +9 -1
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_runtime.py +9 -4
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/dryrun_runtime.py +30 -0
- jarvisplot-2.1.9/jarvisplot/forest_contract.py +436 -0
- jarvisplot-2.1.9/jarvisplot/forest_text.py +86 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_render_agent.py +7 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/manifest.yaml +2 -0
- jarvisplot-2.1.9/jarvisplot/manual_cards/type-forest.yaml +244 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/figure.json +297 -3
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/manifest.json +5 -2
- jarvisplot-2.1.9/jarvisplot/schema/types/forest.json +824 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/templates_catalog.py +18 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/validation.py +21 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/config_cmd.py +9 -2
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/doctor.py +1 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/pyproject.toml +1 -1
- jarvisplot-2.1.9/tests/test_forest.py +619 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/entry_points.txt +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/requires.txt +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/top_level.txt +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/__init__.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adapters_rect.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adapters_ternary.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adaptive_cdf_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/bin_stat_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/colorbar_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/config_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_layout.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_layout_diamond.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_order.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/correlation_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corrplot_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/data_pipelines.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/density_cell_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/distribution_1d_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/dynesty_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/figure.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/helper.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_2d_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_natural_neighbor.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_natural_neighbor_exact.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/layer_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/layout_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/method_registry.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_density_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_hpd.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_mesh.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/preprocessor.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/preprocessor_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/profile_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/significance_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/style_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/__init__.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/agent_digest.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/agent_io.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cache_registry.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cache_store.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/dynesty_runplot.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect_marginal.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rectratio.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/args.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/colors/colormaps.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/corrplot/diamond.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/corrplot/matrix.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/default/output.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/flowchart/default.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/JarvisHEP.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/calculator.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/inputfile.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/opera.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/outputfile.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/sampler.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/gambit.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/gambit_small.png +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/compact/legend-card.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/default/legend-card.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/paper/legend-card.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/presentation/legend-card.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cli.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cli_help.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/client.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/column_probe.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/config.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/config_address.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core_assets.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_access.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_generation_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_hdf5.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_summary.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/dataset_types.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/diagnostic_guidance.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/diagnostics.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/distribution_1d_config.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/expr_names.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/fix_apply.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/flowchart.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/generated_data.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/inner_func.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/legend_cards.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_catalog.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_methods.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_render_human.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_transforms.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/agent-output.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/cache.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/cli-map.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/data-columns.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/debug-overlay.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/diagnostics.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/dryrun-viz.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/field-interp.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/generated-data.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/layer-method.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/legend.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/method_notes.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/style-axes.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-correlation-matrix.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-posterior-2d.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-profile-2d.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/validate.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/workflow.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/yaml-root.yaml +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/memtrace.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/method_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/render_health.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/common.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/dataset.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/distribution-1d.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/generated-data.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/layer.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/legend-card.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/legend.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/transform.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/bar.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/barh.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/contour.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/contourf.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/corrplot.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/dynesty_runplot.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/errorbar.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill_between.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill_betweenx.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/hist.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/hist2d.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/imshow.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpcontour.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpcontourf.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpfield.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/pcolor.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/pcolormesh.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/plot.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/quiver.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/scatter.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/stairs.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/step.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tricontour.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tricontourf.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tripcolor.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tripcolor_axes.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/triplot.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/voronoi.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/voronoif.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/plot-config.json +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema_catalog.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema_diagnostics.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/transform_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/__init__.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/cmaps.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/dataframes.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/expression.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/interpolator.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/pathing.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/__init__.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/cache.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/cap.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/data.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/dryrun.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/explain.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/man.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/suggest.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/template.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/validate.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/yaml_io.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/setup.cfg +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_adaptive_cdf.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_add_column.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_agent_digest.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_agent_envelope.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_axis_title.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cache_cleanup.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cache_registry.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_capabilities.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_card_debug_shape.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cli_help.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cli_verbs.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cmaps.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_column_existence.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_column_pruning.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_addressing.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_edit.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_expand.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_correlation_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_corrplot.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_access.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_describe.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_head_eval.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_loader_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_pipelines.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_config.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_switches.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_yaml_override.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_density_cell_transform.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_design_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_diagnostic_guidance.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_distribution_1d_transform.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_docs_status_labels.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_doctor_partial.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_examples_validate.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_explain.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_expression_semantics.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_figure_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_figure_type_expander.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_fix_application.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_flowchart_renderer.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_generated_data.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_imshow_adapter.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interp_2d_transform.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interp_natural_neighbor.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interpolator_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_jpcontour_methods.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_layer_legend.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_legend_cards.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_legend_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_man_cli.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_method_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_parse_data_metadata.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_pathing_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_posterior_hpd.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_profile_runtime.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_render_health.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_render_lifecycle.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_row_ledger.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_schema_catalog.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_suggest.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_suggest_axes.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_template_catalog.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_template_json.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_ternary_debug.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_transform_contracts.py +0 -0
- {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_validate_no_matplotlib.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: JarvisPLOT
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.9
|
|
4
4
|
Summary: Jarvis-PLOT: YAML-driven plotting engine
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -350,3 +350,5 @@ Read the context docs and roadmap before changing parsing, transforms, rendering
|
|
|
350
350
|
## License
|
|
351
351
|
|
|
352
352
|
MIT License
|
|
353
|
+
|
|
354
|
+
Create a continuous forest figure with `type: forest`, a generic row table and optional point table. The default Swiss-column card uses 82.5 mm width and 5.5 mm row pitch; every row shares one numerical x domain. See [the forest contract](docs/specs/FOREST.md), [native examples](Example/forest/README.md), or `jplot man forest --json`.
|
|
@@ -36,6 +36,8 @@ jarvisplot/dryrun_runtime.py
|
|
|
36
36
|
jarvisplot/expr_names.py
|
|
37
37
|
jarvisplot/fix_apply.py
|
|
38
38
|
jarvisplot/flowchart.py
|
|
39
|
+
jarvisplot/forest_contract.py
|
|
40
|
+
jarvisplot/forest_text.py
|
|
39
41
|
jarvisplot/generated_data.py
|
|
40
42
|
jarvisplot/inner_func.py
|
|
41
43
|
jarvisplot/legend_cards.py
|
|
@@ -73,6 +75,9 @@ jarvisplot/Figure/distribution_1d_runtime.py
|
|
|
73
75
|
jarvisplot/Figure/dynesty_runtime.py
|
|
74
76
|
jarvisplot/Figure/figure.py
|
|
75
77
|
jarvisplot/Figure/figure_types.py
|
|
78
|
+
jarvisplot/Figure/forest_debug.py
|
|
79
|
+
jarvisplot/Figure/forest_legend.py
|
|
80
|
+
jarvisplot/Figure/forest_runtime.py
|
|
76
81
|
jarvisplot/Figure/helper.py
|
|
77
82
|
jarvisplot/Figure/interp_2d_runtime.py
|
|
78
83
|
jarvisplot/Figure/interp_natural_neighbor.py
|
|
@@ -110,6 +115,7 @@ jarvisplot/cards/corrplot/diamond.json
|
|
|
110
115
|
jarvisplot/cards/corrplot/matrix.json
|
|
111
116
|
jarvisplot/cards/default/output.json
|
|
112
117
|
jarvisplot/cards/flowchart/default.json
|
|
118
|
+
jarvisplot/cards/forest/swiss_column.json
|
|
113
119
|
jarvisplot/cards/gambit/1x1/ternary.json
|
|
114
120
|
jarvisplot/cards/gambit/2x1/rect_cmap.json
|
|
115
121
|
jarvisplot/cards/gambit/2x1/ternary.json
|
|
@@ -142,6 +148,7 @@ jarvisplot/manual_cards/manifest.yaml
|
|
|
142
148
|
jarvisplot/manual_cards/method_notes.yaml
|
|
143
149
|
jarvisplot/manual_cards/style-axes.yaml
|
|
144
150
|
jarvisplot/manual_cards/type-correlation-matrix.yaml
|
|
151
|
+
jarvisplot/manual_cards/type-forest.yaml
|
|
145
152
|
jarvisplot/manual_cards/type-posterior-2d.yaml
|
|
146
153
|
jarvisplot/manual_cards/type-profile-2d.yaml
|
|
147
154
|
jarvisplot/manual_cards/validate.yaml
|
|
@@ -188,6 +195,7 @@ jarvisplot/schema/methods/tripcolor_axes.json
|
|
|
188
195
|
jarvisplot/schema/methods/triplot.json
|
|
189
196
|
jarvisplot/schema/methods/voronoi.json
|
|
190
197
|
jarvisplot/schema/methods/voronoif.json
|
|
198
|
+
jarvisplot/schema/types/forest.json
|
|
191
199
|
jarvisplot/utils/__init__.py
|
|
192
200
|
jarvisplot/utils/cmaps.py
|
|
193
201
|
jarvisplot/utils/dataframes.py
|
|
@@ -246,6 +254,7 @@ tests/test_figure_contracts.py
|
|
|
246
254
|
tests/test_figure_type_expander.py
|
|
247
255
|
tests/test_fix_application.py
|
|
248
256
|
tests/test_flowchart_renderer.py
|
|
257
|
+
tests/test_forest.py
|
|
249
258
|
tests/test_generated_data.py
|
|
250
259
|
tests/test_imshow_adapter.py
|
|
251
260
|
tests/test_interp_2d_transform.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: JarvisPLOT
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.9
|
|
4
4
|
Summary: Jarvis-PLOT: YAML-driven plotting engine
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -350,3 +350,5 @@ Read the context docs and roadmap before changing parsing, transforms, rendering
|
|
|
350
350
|
## License
|
|
351
351
|
|
|
352
352
|
MIT License
|
|
353
|
+
|
|
354
|
+
Create a continuous forest figure with `type: forest`, a generic row table and optional point table. The default Swiss-column card uses 82.5 mm width and 5.5 mm row pitch; every row shares one numerical x domain. See [the forest contract](docs/specs/FOREST.md), [native examples](Example/forest/README.md), or `jplot man forest --json`.
|
|
@@ -328,3 +328,5 @@ Read the context docs and roadmap before changing parsing, transforms, rendering
|
|
|
328
328
|
## License
|
|
329
329
|
|
|
330
330
|
MIT License
|
|
331
|
+
|
|
332
|
+
Create a continuous forest figure with `type: forest`, a generic row table and optional point table. The default Swiss-column card uses 82.5 mm width and 5.5 mm row pitch; every row shares one numerical x domain. See [the forest contract](docs/specs/FOREST.md), [native examples](Example/forest/README.md), or `jplot man forest --json`.
|
|
@@ -71,6 +71,13 @@ _LABEL_FONT = {
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
DEFAULT_DEBUG = {
|
|
74
|
+
"forest_grid": {
|
|
75
|
+
"show": True,
|
|
76
|
+
"columns": {"show": True, "plot": {"color": "#DA291C", "linewidth": 0.3, "linestyle": ":", "alpha": 0.7}},
|
|
77
|
+
"baselines": {"show": True, "plot": {"color": "#DA291C", "linewidth": 0.2, "linestyle": "--", "alpha": 0.5}},
|
|
78
|
+
"bands": {"show": True, "Rectangle": {"edgecolor": "#DA291C", "linewidth": 0.25, "fill": False, "alpha": 0.5}},
|
|
79
|
+
"labels": {"show": True, "text": {"color": "#DA291C", "fontsize": 4, "ha": "left", "va": "top"}},
|
|
80
|
+
},
|
|
74
81
|
# Which axes gets fully dimensioned and hosts the info panel. First match wins.
|
|
75
82
|
"primary_order": ["ax", "ax0", "ax1", "axtri"],
|
|
76
83
|
"units": {"cm_per_inch": 2.54, "pt_per_inch": 72.0},
|
|
@@ -185,6 +185,10 @@ def draw_design_reference(fig) -> None:
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
def _draw(fig) -> None:
|
|
188
|
+
if hasattr(fig, 'forest_layout'):
|
|
189
|
+
from .forest_debug import draw_forest_reference
|
|
190
|
+
draw_forest_reference(fig, _debug_config(fig)['forest_grid'])
|
|
191
|
+
return
|
|
188
192
|
F = fig.fig
|
|
189
193
|
w_in, h_in = (float(v) for v in F.get_size_inches())
|
|
190
194
|
debug = _debug_config(fig)
|
|
@@ -752,7 +752,8 @@ def expand_correlation_matrix(info: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
752
752
|
|
|
753
753
|
# Types that expand_figure_type can lower to layers. Keep in sync with
|
|
754
754
|
# expand_figure_type dispatch and capabilities.figure_types.
|
|
755
|
-
|
|
755
|
+
NATIVE_FIGURE_TYPES = frozenset({"forest"})
|
|
756
|
+
KNOWN_FIGURE_TYPES = frozenset({"posterior_2d", "profile_2d", "correlation_matrix"}) | NATIVE_FIGURE_TYPES
|
|
756
757
|
|
|
757
758
|
|
|
758
759
|
def expand_figure_type(info: Any) -> Any:
|
|
@@ -849,6 +850,7 @@ def expand_typed_figures(
|
|
|
849
850
|
raise KeyError(f"figure(s) not found: {', '.join(missing)}")
|
|
850
851
|
|
|
851
852
|
expanded_names: list[str] = []
|
|
853
|
+
native_found = False
|
|
852
854
|
for index, fig in enumerate(figures):
|
|
853
855
|
if not isinstance(fig, Mapping):
|
|
854
856
|
continue
|
|
@@ -876,6 +878,12 @@ def expand_typed_figures(
|
|
|
876
878
|
_note(logger, msg)
|
|
877
879
|
continue
|
|
878
880
|
|
|
881
|
+
if fig_type in NATIVE_FIGURE_TYPES:
|
|
882
|
+
# Native table-driven formats keep their public contract. Their
|
|
883
|
+
# physical solve is inspectable, not a list of hand-authored layers.
|
|
884
|
+
native_found = True
|
|
885
|
+
continue
|
|
886
|
+
|
|
879
887
|
try:
|
|
880
888
|
out = expand_figure_type(fig)
|
|
881
889
|
except Exception as exc:
|
|
@@ -897,6 +905,7 @@ def expand_typed_figures(
|
|
|
897
905
|
|
|
898
906
|
if (
|
|
899
907
|
not expanded_names
|
|
908
|
+
and not native_found
|
|
900
909
|
and not allow_noop
|
|
901
910
|
and raise_on_error
|
|
902
911
|
and wanted is not None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Read-only Swiss grid and physical band references on a separate overlay."""
|
|
2
|
+
from matplotlib.patches import Rectangle
|
|
3
|
+
from ..forest_text import font_properties
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def draw_forest_reference(fig, cfg):
|
|
7
|
+
if not cfg['show'] or any(ax.get_label() == 'forest-design-reference' for ax in fig.fig.axes):
|
|
8
|
+
return
|
|
9
|
+
scene = fig.forest_layout
|
|
10
|
+
g = scene['geometry']
|
|
11
|
+
ax = fig.fig.add_axes([0, 0, 1, 1], label='forest-design-reference', zorder=10000)
|
|
12
|
+
ax.set(xlim=(0, scene['width_mm']), ylim=(scene['height_mm'], 0)); ax.axis('off')
|
|
13
|
+
ax.patch.set_alpha(0)
|
|
14
|
+
if cfg['columns']['show']:
|
|
15
|
+
for index, x in enumerate(g['grid_lines_mm']):
|
|
16
|
+
line, = ax.plot([x]*2, [0, scene['height_mm']], **cfg['columns']['plot'])
|
|
17
|
+
line.set_gid(f'forest-grid-{index:02d}')
|
|
18
|
+
if cfg['labels']['show']:
|
|
19
|
+
opts = dict(cfg['labels']['text'])
|
|
20
|
+
size = opts.pop('fontsize')
|
|
21
|
+
for index, x in enumerate(g['grid_lines_mm'][:-1]):
|
|
22
|
+
ax.text(x, g['top_axis_y_mm']+.35, f'{index+1:02d}', fontproperties=font_properties(size, scene['typography']['family']), **opts)
|
|
23
|
+
if cfg['baselines']['show']:
|
|
24
|
+
ys = [g[key] for key in ('meta_y_mm','title_y_mm','description_y_mm','x_label_y_mm','column_header_y_mm','top_tick_y_mm')]
|
|
25
|
+
ys += [r['top_mm']+g[key] for r in scene['rows'] for key in ('cell_y_mm','plot_y_mm')]
|
|
26
|
+
for index, y in enumerate(ys):
|
|
27
|
+
line, = ax.plot([g['plot_left_mm'], g['plot_right_mm']], [y]*2, **cfg['baselines']['plot'])
|
|
28
|
+
line.set_gid(f'forest-baseline-{index:03d}')
|
|
29
|
+
if cfg['bands']['show']:
|
|
30
|
+
for index, row in enumerate(scene['rows']):
|
|
31
|
+
for name, y, height in (('text', row['top_mm'], g['plot_y_mm']-g['point_band_mm']/2),
|
|
32
|
+
('marks', row['top_mm']+g['plot_y_mm']-g['point_band_mm']/2, g['point_band_mm'])):
|
|
33
|
+
box = Rectangle((g['plot_left_mm'], y), scene['plot_width_mm'], height, **cfg['bands']['Rectangle'])
|
|
34
|
+
box.set_gid(f'forest-band-{name}-{index:03d}')
|
|
35
|
+
ax.add_patch(box)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""A native legend handler for the actual forest glyph artist container."""
|
|
2
|
+
from matplotlib.container import Container
|
|
3
|
+
from matplotlib.legend_handler import HandlerBase, HandlerPathCollection
|
|
4
|
+
from matplotlib.lines import Line2D
|
|
5
|
+
from matplotlib.patches import Rectangle
|
|
6
|
+
from matplotlib.offsetbox import VPacker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PhysicalLegendColumn(VPacker):
|
|
10
|
+
"""Recompute physical column width for each raster/vector renderer."""
|
|
11
|
+
def __init__(self, width_mm, **kwargs):
|
|
12
|
+
super().__init__(**kwargs)
|
|
13
|
+
self.width_pt = width_mm*72/25.4
|
|
14
|
+
|
|
15
|
+
def _get_bbox_and_child_offsets(self, renderer):
|
|
16
|
+
self.width = self.width_pt*renderer.points_to_pixels(1)
|
|
17
|
+
return super()._get_bbox_and_child_offsets(renderer)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ForestGlyph(Container):
|
|
21
|
+
def __init__(self, artists, *, glyph):
|
|
22
|
+
super().__init__(artists)
|
|
23
|
+
self.glyph = glyph
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HandlerForestPoint(HandlerPathCollection):
|
|
27
|
+
"""Place one point at the left of its compact native key."""
|
|
28
|
+
def __init__(self, key_width_pt):
|
|
29
|
+
super().__init__(numpoints=1)
|
|
30
|
+
self.key_width_pt = key_width_pt
|
|
31
|
+
|
|
32
|
+
def get_xdata(self, legend, xdescent, ydescent, width, height, fontsize):
|
|
33
|
+
x = -xdescent+self.key_width_pt/2
|
|
34
|
+
return [x, x], [x]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def group_glyph_and_points(legend, config, layers, glyph_object, *, base_dir=None):
|
|
38
|
+
"""Use the canonical native items, with glyphs left and point classes right.
|
|
39
|
+
|
|
40
|
+
Matplotlib normally balances item counts across columns, which would place
|
|
41
|
+
some point classes beside the box sample. Repack its existing item boxes
|
|
42
|
+
without recreating handles, labels, or their handlers.
|
|
43
|
+
"""
|
|
44
|
+
from matplotlib.offsetbox import DrawingArea
|
|
45
|
+
from ..legend_cards import resolve_axes_legend
|
|
46
|
+
kwargs, entries = resolve_axes_legend(config, layers, base_dir=base_dir)
|
|
47
|
+
if kwargs.get('ncols', kwargs.get('ncol', 1)) != 2:
|
|
48
|
+
return
|
|
49
|
+
legend._ncols = 2
|
|
50
|
+
if kwargs.get('reverse', False):
|
|
51
|
+
entries = list(reversed(entries))
|
|
52
|
+
objects = {id(text): entry['object'] for text, entry in zip(legend.get_texts(), entries)}
|
|
53
|
+
columns = legend._legend_handle_box.get_children()
|
|
54
|
+
groups = [[], []]
|
|
55
|
+
for column in columns:
|
|
56
|
+
for item in column.get_children():
|
|
57
|
+
label = item.get_children()[-1]._text
|
|
58
|
+
groups[0 if objects[id(label)] == glyph_object else 1].append(item)
|
|
59
|
+
# Preserve the empty semantic column when only one group is present.
|
|
60
|
+
legend._legend_handle_box._children = [
|
|
61
|
+
VPacker(pad=0, sep=legend.labelspacing*legend._fontsize,
|
|
62
|
+
align='left', children=items or [DrawingArea(0, 0)])
|
|
63
|
+
for items in groups
|
|
64
|
+
]
|
|
65
|
+
legend._legend_handle_box.align = 'top'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class HandlerForestGlyph(HandlerBase):
|
|
69
|
+
def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans):
|
|
70
|
+
whisker = orig_handle[0]
|
|
71
|
+
left, right = -xdescent, width-xdescent
|
|
72
|
+
yc = (height-ydescent)/2
|
|
73
|
+
half = height*.32
|
|
74
|
+
stroke = {'color': whisker.get_color(), 'linewidth': whisker.get_linewidth(), 'transform': trans}
|
|
75
|
+
artists = [Line2D([left, right], [yc, yc], **stroke)]
|
|
76
|
+
for xx in (left, right):
|
|
77
|
+
artists.append(Line2D([xx, xx], [yc-half, yc+half], **stroke))
|
|
78
|
+
if orig_handle.glyph == 'box':
|
|
79
|
+
box, median = orig_handle[1:]
|
|
80
|
+
artists.append(Rectangle((left+width*.25, yc-half), width*.5, half*2,
|
|
81
|
+
facecolor=box.get_facecolor(), edgecolor=box.get_edgecolor(),
|
|
82
|
+
linewidth=box.get_linewidth(), alpha=box.get_alpha(), transform=trans))
|
|
83
|
+
artists.append(Line2D([(left+right)/2]*2, [yc-half, yc+half], color=median.get_color(), linewidth=median.get_linewidth(), transform=trans))
|
|
84
|
+
else:
|
|
85
|
+
point = orig_handle[1]
|
|
86
|
+
artists.append(Line2D([(left+right)/2], [yc], linestyle='', marker=point.get_marker(), color=point.get_color(),
|
|
87
|
+
markersize=point.get_markersize(), markeredgewidth=point.get_markeredgewidth(), transform=trans))
|
|
88
|
+
return artists
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Native physical forest renderer on one continuous canvas."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .figure import Figure
|
|
8
|
+
from ..forest_contract import solve_forest, x_to_mm, MM_PER_PT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ForestFigure(Figure):
|
|
12
|
+
def from_dict(self, info):
|
|
13
|
+
self.name = info['name']
|
|
14
|
+
if info.get('enable', True) is False:
|
|
15
|
+
self._setup_status = 'disabled'
|
|
16
|
+
return False
|
|
17
|
+
from ..schema_catalog import config_validator
|
|
18
|
+
errors = list(config_validator().iter_errors({'DataSet': [], 'Figures': [info]}))
|
|
19
|
+
if errors:
|
|
20
|
+
raise ValueError(f'figure {self.name!r}: {errors[0].message}')
|
|
21
|
+
self.info = info
|
|
22
|
+
self.style = info.get('style', ['forest', 'swiss_column'])
|
|
23
|
+
self.debug = info.get('debug', False)
|
|
24
|
+
sources = [info['data'], *([info['points']] if info.get('points') else [])]
|
|
25
|
+
frames = {name: self.context.get(name) for name in sources}
|
|
26
|
+
self.forest_layout = solve_forest(info, frames)
|
|
27
|
+
for source in sources:
|
|
28
|
+
self.context.consume(source)
|
|
29
|
+
g = self.forest_layout
|
|
30
|
+
from ..forest_contract import merge
|
|
31
|
+
logo = self._frame.get('axlogo', {})
|
|
32
|
+
logo_override = info.get('frame', {}).get('axlogo', True)
|
|
33
|
+
if isinstance(logo_override, dict):
|
|
34
|
+
logo = merge(logo, logo_override)
|
|
35
|
+
self._frame = {'figure': {'figsize': [g['width_mm']/25.4, g['height_mm']/25.4]},
|
|
36
|
+
'axes': {'ax': {'rect': [0, 0, 1, 1]}}}
|
|
37
|
+
if logo_override is not False and not self.print:
|
|
38
|
+
self._frame['axlogo'] = logo
|
|
39
|
+
size = g['geometry']['logo_size_mm']
|
|
40
|
+
self._frame['axes']['axlogo'] = {'rect': [g['geometry']['margin_left_mm']/g['width_mm'], g['geometry']['logo_bottom_mm']/g['height_mm'], g['geometry']['grid_unit_mm']/g['width_mm'], size/g['height_mm']], 'frameon': False, 'xticks': [], 'yticks': []}
|
|
41
|
+
import matplotlib.pyplot as plt
|
|
42
|
+
self.fig = plt.figure(figsize=self._frame['figure']['figsize'], dpi=self.dpi, facecolor=g['style']['rows']['background'])
|
|
43
|
+
self.fig.set_size_inches(*self._frame['figure']['figsize'], forward=False)
|
|
44
|
+
ax = self.fig.add_axes([0, 0, 1, 1])
|
|
45
|
+
ax.set(xlim=(0, g['width_mm']), ylim=(g['height_mm'], 0))
|
|
46
|
+
ax.axis('off')
|
|
47
|
+
self.axes = {'ax': ax}
|
|
48
|
+
if 'axlogo' in self._frame['axes']:
|
|
49
|
+
self.axlogo = self._frame['axes']['axlogo']
|
|
50
|
+
from ..forest_text import font_properties
|
|
51
|
+
for artist in self.axlogo.texts:
|
|
52
|
+
artist.set_fontproperties(font_properties(artist.get_fontsize(), artist.get_fontfamily()[0], artist.get_fontweight()))
|
|
53
|
+
artist.set_parse_math(False)
|
|
54
|
+
self.forest_layout['canvas_axes'] = len(self.axes)
|
|
55
|
+
self._setup_status = 'ok'
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def render(self):
|
|
59
|
+
import matplotlib as mpl
|
|
60
|
+
# Unhinted glyphs agree with physical font measurements and avoid
|
|
61
|
+
# native-hinting failures for small Helvetica ticks at low DPI.
|
|
62
|
+
with mpl.rc_context({'text.hinting': 'none'}):
|
|
63
|
+
self._render()
|
|
64
|
+
|
|
65
|
+
def _render(self):
|
|
66
|
+
from matplotlib.patches import Rectangle
|
|
67
|
+
from .legend_runtime import apply_legend
|
|
68
|
+
from .forest_legend import ForestGlyph, HandlerForestGlyph, HandlerForestPoint, PhysicalLegendColumn, group_glyph_and_points
|
|
69
|
+
from matplotlib.collections import PathCollection
|
|
70
|
+
info, layout, ax = self.info, self.forest_layout, self.axes['ax']
|
|
71
|
+
g, fonts, style = layout['geometry'], layout['typography'], layout['style']
|
|
72
|
+
width, height = layout['width_mm'], layout['height_mm']
|
|
73
|
+
domain, ticks = layout['shared_domain'], layout['shared_ticks']
|
|
74
|
+
to_x = lambda value: x_to_mm(value, domain, g)
|
|
75
|
+
checks = []
|
|
76
|
+
annotations = []
|
|
77
|
+
from ..forest_text import font_properties, wrap_text
|
|
78
|
+
|
|
79
|
+
def text(x, y, value, size, *, slot=None, row=None, **kwargs):
|
|
80
|
+
if not value:
|
|
81
|
+
return None
|
|
82
|
+
weight = kwargs.pop('fontweight', 'normal')
|
|
83
|
+
kwargs.setdefault('ha', 'left')
|
|
84
|
+
artist = ax.text(x, y, value, fontproperties=font_properties(size, fonts['family'], weight), va='center', parse_math=False, **kwargs)
|
|
85
|
+
checks.append((artist, slot, row))
|
|
86
|
+
return artist
|
|
87
|
+
|
|
88
|
+
title = info.get('title', {})
|
|
89
|
+
origin = g['margin_left_mm']
|
|
90
|
+
title_size = title.get('fontsize', fonts['title_pt'])
|
|
91
|
+
title_weight = title.get('fontweight', 'bold')
|
|
92
|
+
title_right = origin+g['title_columns']*g['grid_unit_mm']
|
|
93
|
+
for i, line in enumerate(wrap_text(title.get('text', ''), title_size, fonts['family'], title_weight, title_right-origin)):
|
|
94
|
+
text(origin, g['title_y_mm']+i*g['title_line_pitch_mm'], line, title_size, fontweight=title_weight, color=title.get('color', style['rows']['text_color']), slot=(origin, title_right, g['meta_y_mm']+1, g['x_label_y_mm']-1.5))
|
|
95
|
+
meta = info.get('meta', {})
|
|
96
|
+
for key, column in (('number', 0), ('year', 9), ('place', 10)):
|
|
97
|
+
text(g['grid_lines_mm'][column], g['meta_y_mm'], meta.get(key, ''), fonts['meta_pt'], color=style['rows']['accent'] if key == 'number' else style['rows']['text_color'], slot=(g['grid_lines_mm'][column], g['grid_lines_mm'][10 if key=='year' else 12], 0, g['title_y_mm']-1.5))
|
|
98
|
+
xlabel = info.get('x', {}).get('label', '')
|
|
99
|
+
text(origin, g['x_label_y_mm'], xlabel, fonts['header_pt'], slot=(origin, g['plot_right_mm'], g['x_label_y_mm']-1.5, g['column_header_y_mm']-1.5))
|
|
100
|
+
handles = {}
|
|
101
|
+
class_index = {layer['legend']['object']: i for i, layer in enumerate(layout['legend_layers'])}
|
|
102
|
+
def cell_x(column):
|
|
103
|
+
return column['left_mm'] + (column['width_mm']-column['text_right_padding_mm'] if column['align']=='right' else column['text_left_padding_mm'])
|
|
104
|
+
for column in layout['columns']:
|
|
105
|
+
text(cell_x(column), g['column_header_y_mm'], column['label'], fonts['header_pt'], fontweight='bold', ha=column['align'], slot=(column['left_mm'], column['left_mm']+column['width_mm'], g['column_header_y_mm']-1.5, g['top_tick_y_mm']-1))
|
|
106
|
+
tick_artists = []
|
|
107
|
+
def tick_labels(y):
|
|
108
|
+
for value in ticks:
|
|
109
|
+
# Numerical annotations follow the exact tick coordinate, just
|
|
110
|
+
# like the data marks; the modular grid governs layout edges.
|
|
111
|
+
artist = text(to_x(value), y, f"{value:g}{style['axis']['tick_suffix']}", fonts['tick_pt'], ha=style['axis']['tick_align'], color=style['rows']['secondary_color'], slot=(0, width, y-1, y+1))
|
|
112
|
+
tick_artists.append(artist)
|
|
113
|
+
tick_labels(g['top_tick_y_mm'])
|
|
114
|
+
base = g['header_mm']+layout['row_count']*g['row_pitch_mm']
|
|
115
|
+
axis = style['axis']
|
|
116
|
+
line, = ax.plot([g['plot_left_mm'],g['plot_right_mm']], [g['top_axis_y_mm']]*2, color=axis['color'], linewidth=axis['linewidth_pt'])
|
|
117
|
+
line.set_gid('forest-top-axis')
|
|
118
|
+
grid = style['grid']
|
|
119
|
+
grid_values = sorted(set(ticks + [domain[0]+i*(domain[1]-domain[0])/grid['divisions'] for i in range(grid['divisions']+1)]))
|
|
120
|
+
for index, value in enumerate(grid_values):
|
|
121
|
+
ax.plot([to_x(value)]*2, [g['top_axis_y_mm']-.5,g['top_axis_y_mm']], color=axis['color'], linewidth=axis['linewidth_pt'])
|
|
122
|
+
if grid['show']:
|
|
123
|
+
line, = ax.plot([to_x(value)]*2, [g['top_axis_y_mm'],base], color=grid['color'], linewidth=grid['linewidth_pt'], alpha=grid['alpha'], linestyle=grid['linestyle'], zorder=1)
|
|
124
|
+
line.set_gid(f'forest-numerical-grid-{index:02d}')
|
|
125
|
+
if 'reference' in info.get('x', {}):
|
|
126
|
+
ref = style['reference']
|
|
127
|
+
line, = ax.plot([to_x(info['x']['reference'])]*2, [g['top_axis_y_mm'],base], color=ref['color'], linewidth=ref['linewidth_pt'], linestyle=ref['linestyle'], zorder=2)
|
|
128
|
+
line.set_gid('forest-reference')
|
|
129
|
+
for index, row in enumerate(layout['rows']):
|
|
130
|
+
top = row['top_mm']
|
|
131
|
+
rows = style['rows']
|
|
132
|
+
row_fill = rows['fills'][index % len(rows['fills'])]
|
|
133
|
+
from matplotlib.colors import to_rgba
|
|
134
|
+
if to_rgba(row_fill)[3]:
|
|
135
|
+
ax.add_patch(Rectangle((0, top), width, g['row_band_mm'], facecolor=row_fill, edgecolor='none', zorder=0))
|
|
136
|
+
if rows['separators']:
|
|
137
|
+
line, = ax.plot([origin, g['plot_right_mm']], [top+g['row_band_mm']]*2, color=rows['separator_color'], linewidth=rows['separator_linewidth_pt'])
|
|
138
|
+
line.set_gid(f'forest-row-separator-{index:03d}')
|
|
139
|
+
trio = [text(cell_x(column), top+g['cell_y_mm'], value, column['fontsize'],
|
|
140
|
+
slot=(column['left_mm'], column['left_mm']+column['width_mm'], top, top+g['plot_y_mm']-g['point_band_mm']/2), row=row['row'],
|
|
141
|
+
fontweight=column['fontweight'], ha=column['align'], color=rows['text_color'] if column['role'] != 'left' else rows['secondary_color'],
|
|
142
|
+
bbox={'facecolor':row_fill if to_rgba(row_fill)[3] else rows['background'], 'edgecolor':'none', 'boxstyle':'square,pad=0.04'})
|
|
143
|
+
for column, value in zip(layout['columns'], row['cells'])]
|
|
144
|
+
annotations.append((row['row'], [a for a in trio if a is not None]))
|
|
145
|
+
yc = top+g['plot_y_mm']
|
|
146
|
+
c = row['coordinates']
|
|
147
|
+
if c['lower'] is not None:
|
|
148
|
+
s = style['interval']
|
|
149
|
+
whisker, = ax.plot([to_x(c['lower']), to_x(c['upper'])], [yc]*2, color=s['color'], linewidth=s['linewidth_pt'], zorder=3)
|
|
150
|
+
for value in (c['lower'], c['upper']):
|
|
151
|
+
ax.plot([to_x(value)]*2, [yc-s['cap_height_mm']/2, yc+s['cap_height_mm']/2], color=s['color'], linewidth=s['linewidth_pt'], zorder=3)
|
|
152
|
+
if info.get('glyph', 'interval') == 'box':
|
|
153
|
+
b = style['box']
|
|
154
|
+
box = Rectangle((to_x(c['q1']), yc-b['height_mm']/2), to_x(c['q3'])-to_x(c['q1']), b['height_mm'], facecolor=b['facecolor'], edgecolor=b['edgecolor'], alpha=b['alpha'], linewidth=b['linewidth_pt'], zorder=3)
|
|
155
|
+
ax.add_patch(box)
|
|
156
|
+
median, = ax.plot([to_x(c['median'])]*2, [yc-b['height_mm']/2, yc+b['height_mm']/2], color=b['median_color'], linewidth=b['linewidth_pt'], zorder=4)
|
|
157
|
+
glyph = ForestGlyph((whisker, box, median), glyph='box')
|
|
158
|
+
extent = b['height_mm']/2+b['linewidth_pt']*MM_PER_PT/2
|
|
159
|
+
else:
|
|
160
|
+
point, = ax.plot([to_x(c['estimate'])], [yc], linestyle='', marker=s['marker'], color=s['color'], markersize=s['diameter_mm']/MM_PER_PT, markeredgewidth=s['linewidth_pt'], zorder=4)
|
|
161
|
+
glyph = ForestGlyph((whisker, point), glyph='interval')
|
|
162
|
+
extent = (s['diameter_mm']+s['linewidth_pt']*MM_PER_PT)/2
|
|
163
|
+
extent = max(extent, (s['cap_height_mm']+s['linewidth_pt']*MM_PER_PT)/2)
|
|
164
|
+
if extent > g['point_band_mm']/2:
|
|
165
|
+
raise ValueError(f"row {row['row']!r}: glyph exceeds reserved plot band; increase forest.geometry.point_band_mm and row band/pitch")
|
|
166
|
+
handles.setdefault(f"@layer:{class_index[layout['glyph_object']]}", glyph)
|
|
167
|
+
for p in row['points']:
|
|
168
|
+
s = p['style']
|
|
169
|
+
colors = {'color': s['color']} if s['marker'] == 'x' else {'facecolors': s['color'] if s['fill'] else 'none', 'edgecolors': s.get('edgecolor', s['color'])}
|
|
170
|
+
mark = ax.scatter([p['x_mm']], [yc+p['offset_mm']], marker=s['marker'], s=(s['diameter_mm']/MM_PER_PT)**2, **colors, linewidths=s['linewidth_pt'], zorder=5)
|
|
171
|
+
handles.setdefault(f"@layer:{class_index[p['class']]}", mark)
|
|
172
|
+
legend_config = dict(layout['legend']) if isinstance(layout['legend'], dict) else layout['legend']
|
|
173
|
+
base = g['header_mm']+layout['row_count']*g['row_pitch_mm']
|
|
174
|
+
description = info.get('description', {})
|
|
175
|
+
desc_origin = g['grid_lines_mm'][g['description_start_column']-1]
|
|
176
|
+
desc_right = desc_origin+g['description_columns']*g['grid_unit_mm']
|
|
177
|
+
desc_size, desc_weight = description.get('fontsize', fonts['footer_pt']), description.get('fontweight', 'normal')
|
|
178
|
+
description_artists = []
|
|
179
|
+
for i, line in enumerate(wrap_text(description.get('text', '').strip(), desc_size, fonts['family'], desc_weight, desc_right-desc_origin)):
|
|
180
|
+
description_artists.append(text(desc_origin, base+g['description_y_mm']+i*g['text_line_pitch_mm'], line, desc_size,
|
|
181
|
+
fontweight=desc_weight, color=description.get('color', style['rows']['secondary_color']),
|
|
182
|
+
slot=(desc_origin, desc_right, base+g['legend_y_mm'], height-1)))
|
|
183
|
+
grouped = isinstance(legend_config, dict) and legend_config.get('ncols') == 2
|
|
184
|
+
legend_origin = g['grid_lines_mm'][g['legend_start_columns'][0]-1] if grouped else origin
|
|
185
|
+
text(legend_origin, base+g['legend_heading_y_mm'], style['rows']['legend_heading'], fonts['meta_pt'], fontweight='bold', color=style['rows']['accent'], slot=(legend_origin, g['plot_right_mm'], base+3.5, base+g['legend_y_mm']-1))
|
|
186
|
+
if isinstance(legend_config, dict):
|
|
187
|
+
legend_config.setdefault('bbox_to_anchor', (legend_origin/width, 1-(base+g['legend_y_mm'])/height))
|
|
188
|
+
legend_config.setdefault('borderpad', 0)
|
|
189
|
+
legend_config['handlelength'] = g['grid_unit_mm']/MM_PER_PT/legend_config.get('fontsize', 5)-legend_config.get('handletextpad', .6)
|
|
190
|
+
legend_config['handler_map'] = {**legend_config.get('handler_map', {}), ForestGlyph: HandlerForestGlyph(), PathCollection: HandlerForestPoint(g['legend_point_key_mm']/MM_PER_PT)}
|
|
191
|
+
legend = apply_legend(ax, legend_config, handles, layer_specs=layout['legend_layers'], base_dir=self._yaml_dir)
|
|
192
|
+
if legend:
|
|
193
|
+
group_glyph_and_points(legend, legend_config, layout['legend_layers'], layout['glyph_object'], base_dir=self._yaml_dir)
|
|
194
|
+
for artist in legend.get_texts():
|
|
195
|
+
artist.set_fontproperties(font_properties(artist.get_fontsize(), fonts['family']))
|
|
196
|
+
artist.set_color(style['rows']['text_color'])
|
|
197
|
+
artist.set_ha('left')
|
|
198
|
+
artist.set_parse_math(False)
|
|
199
|
+
# Keep canonical items, with keys at declared reference columns.
|
|
200
|
+
# Labels start one reference column after their keys.
|
|
201
|
+
ncols = legend._ncols
|
|
202
|
+
if g['grid_columns'] % ncols:
|
|
203
|
+
raise ValueError('Swiss forest legend ncols must divide the 12-column grid')
|
|
204
|
+
legend._legend_handle_box.sep = 0
|
|
205
|
+
spans = ([g['legend_start_columns'][1]-g['legend_start_columns'][0],
|
|
206
|
+
g['description_start_column']-g['legend_start_columns'][1]] if ncols == 2
|
|
207
|
+
else [g['grid_columns']/ncols]*ncols)
|
|
208
|
+
physical_columns = []
|
|
209
|
+
for column, span in zip(legend._legend_handle_box.get_children(), spans):
|
|
210
|
+
column = PhysicalLegendColumn(span*g['grid_unit_mm'], pad=column.pad, sep=column.sep,
|
|
211
|
+
align='left', children=column.get_children())
|
|
212
|
+
physical_columns.append(column)
|
|
213
|
+
span_px = span*g['grid_unit_mm']/MM_PER_PT*self.fig.canvas.get_renderer().points_to_pixels(1)
|
|
214
|
+
for item in column.get_children():
|
|
215
|
+
if item.get_children():
|
|
216
|
+
key = item.get_children()[0]
|
|
217
|
+
if any(isinstance(a, PathCollection) for a in key.get_children()):
|
|
218
|
+
key.width = g['legend_point_key_mm']/MM_PER_PT
|
|
219
|
+
if ncols == 2 and item.get_children():
|
|
220
|
+
label = item.get_children()[-1]._text
|
|
221
|
+
key_and_gap = (key.width+item.sep)*MM_PER_PT
|
|
222
|
+
label.set_text('\n'.join(wrap_text(label.get_text(), label.get_fontsize(), fonts['family'], 'normal', span*g['grid_unit_mm']-key_and_gap)))
|
|
223
|
+
if item.get_bbox(self.fig.canvas.get_renderer()).width > span_px:
|
|
224
|
+
raise ValueError(f'Swiss forest legend item exceeds its modular span; reduce frame.ax.legend.ncols ({ncols}) or fontsize, or shorten labels')
|
|
225
|
+
legend._legend_handle_box._children = physical_columns
|
|
226
|
+
checks.append((legend, (0, width, base+3.5, height-1), None))
|
|
227
|
+
tick_labels(base+2)
|
|
228
|
+
if 'axlogo' in self.axes:
|
|
229
|
+
for artist in self.axlogo.get_children():
|
|
230
|
+
from matplotlib.text import Text
|
|
231
|
+
if isinstance(artist, Text) and artist.get_text():
|
|
232
|
+
checks.append((artist, (0, width, height-g['logo_bottom_mm']-g['logo_size_mm']-1, height), None))
|
|
233
|
+
self.fig.canvas.draw()
|
|
234
|
+
renderer = self.fig.canvas.get_renderer()
|
|
235
|
+
def mm_bounds(artist):
|
|
236
|
+
b = artist.get_window_extent(renderer)
|
|
237
|
+
# Text's window bbox reserves the font's unused ascender/descender
|
|
238
|
+
# space (e.g. 'lp' even for B001). Measure actual literal glyph ink
|
|
239
|
+
# against the physical slot; do not shrink type to that padding.
|
|
240
|
+
from matplotlib.text import Text
|
|
241
|
+
if isinstance(artist, Text):
|
|
242
|
+
from matplotlib.textpath import TextToPath
|
|
243
|
+
from matplotlib.path import Path as MplPath
|
|
244
|
+
from matplotlib.transforms import Bbox
|
|
245
|
+
anchor = artist.get_transform().transform(artist.get_position())
|
|
246
|
+
_, lines, _ = artist._get_layout(renderer)
|
|
247
|
+
bounds = []
|
|
248
|
+
for line in lines:
|
|
249
|
+
label = line[0]
|
|
250
|
+
ox, oy = line[2] if len(line) == 3 else line[2:4]
|
|
251
|
+
vertices, codes = TextToPath().get_text_path(artist.get_fontproperties(), label, ismath=False)
|
|
252
|
+
if vertices:
|
|
253
|
+
ink = MplPath(vertices, codes).get_extents()
|
|
254
|
+
scale = artist.get_fontsize()/100*self.fig.dpi/72
|
|
255
|
+
bounds.append(Bbox.from_extents(anchor[0]+ox+ink.x0*scale, anchor[1]+oy+ink.y0*scale,
|
|
256
|
+
anchor[0]+ox+ink.x1*scale, anchor[1]+oy+ink.y1*scale))
|
|
257
|
+
if bounds:
|
|
258
|
+
b = Bbox.union(bounds)
|
|
259
|
+
return (b.x0/self.fig.dpi*25.4, b.x1/self.fig.dpi*25.4,
|
|
260
|
+
height-b.y1/self.fig.dpi*25.4, height-b.y0/self.fig.dpi*25.4)
|
|
261
|
+
for artist, slot, row in checks:
|
|
262
|
+
bounds = mm_bounds(artist)
|
|
263
|
+
tolerance = max(0.02, 25.4/self.fig.dpi/2) # half a raster pixel for font hinting
|
|
264
|
+
if slot and (bounds[0] < slot[0]-tolerance or bounds[1] > slot[1]+tolerance or bounds[2] < slot[2]-tolerance or bounds[3] > slot[3]+tolerance):
|
|
265
|
+
label = artist.get_text() if hasattr(artist, 'get_text') else 'legend'
|
|
266
|
+
raise ValueError(f"figure {self.name!r}, row {row!r}: text/layout overflow for {label!r}; measured mm {bounds}, slot {slot}. Adjust annotation/typography/geometry or frame.ax.legend.")
|
|
267
|
+
if legend:
|
|
268
|
+
from matplotlib.transforms import Bbox
|
|
269
|
+
def physical_box(artist):
|
|
270
|
+
b = mm_bounds(artist)
|
|
271
|
+
return Bbox.from_extents(b[0], b[2], b[1], b[3])
|
|
272
|
+
badge_boxes = ([physical_box(a) for a in [*self.axlogo.images, *self.axlogo.texts]] if 'axlogo' in self.axes else [])
|
|
273
|
+
description_boxes = [physical_box(a) for a in description_artists]
|
|
274
|
+
for column in legend._legend_handle_box.get_children():
|
|
275
|
+
for item in column.get_children():
|
|
276
|
+
if any(physical_box(item).overlaps(b) for b in badge_boxes):
|
|
277
|
+
raise ValueError('forest footer legend overlaps axlogo; adjust forest.geometry footer/legend/logo positions or frame.ax.legend layout')
|
|
278
|
+
if any(physical_box(item).overlaps(b) for b in description_boxes):
|
|
279
|
+
raise ValueError('forest footer legend overlaps description; adjust forest.geometry column starts or frame.ax.legend layout')
|
|
280
|
+
for row, artists in annotations:
|
|
281
|
+
from matplotlib.transforms import Bbox
|
|
282
|
+
bounds = [mm_bounds(a) for a in artists]
|
|
283
|
+
boxes = [Bbox.from_extents(b[0], b[2], b[1], b[3]) for b in bounds]
|
|
284
|
+
for i, b in enumerate(boxes):
|
|
285
|
+
if any(b.overlaps(other) for other in boxes[i+1:]):
|
|
286
|
+
raise ValueError(f'row {row!r}: annotation slots collide; enlarge forest.geometry slots or change literal annotations')
|
|
287
|
+
layout['verification'] = {'text_and_legend_bounds': 'passed', 'all_points_once': True, 'packing': 'deterministic_physical_mm', 'numeric_x_preserved': True, 'tick_label_count': len(tick_artists)}
|
|
288
|
+
|
|
289
|
+
def savefig(self):
|
|
290
|
+
import matplotlib as mpl
|
|
291
|
+
with mpl.rc_context({'svg.fonttype': 'none', 'text.hinting': 'none'}):
|
|
292
|
+
super().savefig()
|
|
293
|
+
path = Path(self.dir) / f'{self.name}.layout.json'
|
|
294
|
+
path.write_text(json.dumps(self.forest_layout, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
|
@@ -41,12 +41,12 @@ class HandlerStack(HandlerBase):
|
|
|
41
41
|
return artists
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def representative_handle(rendered):
|
|
44
|
+
def representative_handle(rendered, handler_map=None):
|
|
45
45
|
"""Require one supported artist/container; multiple series are ambiguous."""
|
|
46
46
|
from matplotlib.artist import Artist
|
|
47
47
|
from matplotlib.container import Container
|
|
48
48
|
|
|
49
|
-
handler_map = Legend.get_default_handler_map()
|
|
49
|
+
handler_map = {**Legend.get_default_handler_map(), **(handler_map or {})}
|
|
50
50
|
def collect(value):
|
|
51
51
|
if isinstance(value, (Artist, Container)):
|
|
52
52
|
return [value] if Legend.get_legend_handler(handler_map, value) is not None else []
|
|
@@ -74,7 +74,7 @@ def apply_legend(ax, config, layer_handles=None, *, base_dir=None, layer_specs=N
|
|
|
74
74
|
for name in names:
|
|
75
75
|
if name not in layer_handles:
|
|
76
76
|
raise ValueError(f"Legend object {entry['object']!r} is missing its rendered component {name!r}")
|
|
77
|
-
handle = representative_handle(layer_handles[name])
|
|
77
|
+
handle = representative_handle(layer_handles[name], kw.get('handler_map'))
|
|
78
78
|
if handle is None:
|
|
79
79
|
raise ValueError(f"Legend object {entry['object']!r} component {name!r} has no supported legend handle")
|
|
80
80
|
parts.append(handle)
|
|
@@ -200,6 +200,13 @@ def _types() -> list[dict[str, Any]]:
|
|
|
200
200
|
else f"jplot explain {name}"
|
|
201
201
|
),
|
|
202
202
|
}
|
|
203
|
+
if name == 'forest':
|
|
204
|
+
from .forest_contract import ROW_FIELDS, POINT_FIELDS
|
|
205
|
+
entry.update(expands_to='native', styles=[['forest', 'swiss_column']], axes=['ax', 'axlogo'],
|
|
206
|
+
required=['data'], optional=['points', 'glyph', 'x', 'title', 'description', 'columns', 'glyph_label', 'fields', 'point_fields', 'annotations', 'forest', 'frame', 'debug'],
|
|
207
|
+
glyphs=['interval', 'box'], default_glyph='interval', row_fields=list(ROW_FIELDS), point_fields=list(POINT_FIELDS),
|
|
208
|
+
geometry='physical_mm', x_scale='shared', tick_labels='top_and_bottom',
|
|
209
|
+
schema='https://jarvis-plot.org/schema/v2/types/forest.json', man='jplot man forest')
|
|
203
210
|
if name == "correlation_matrix":
|
|
204
211
|
matching = [
|
|
205
212
|
item
|