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.
Files changed (285) hide show
  1. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/PKG-INFO +3 -1
  2. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/SOURCES.txt +9 -0
  3. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/PKG-INFO +3 -1
  4. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/README.md +2 -0
  5. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/debug_config.py +7 -0
  6. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/design_runtime.py +4 -0
  7. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/figure_types.py +10 -1
  8. jarvisplot-2.1.9/jarvisplot/Figure/forest_debug.py +35 -0
  9. jarvisplot-2.1.9/jarvisplot/Figure/forest_legend.py +88 -0
  10. jarvisplot-2.1.9/jarvisplot/Figure/forest_runtime.py +294 -0
  11. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/legend_runtime.py +3 -3
  12. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/capabilities.py +7 -0
  13. jarvisplot-2.1.9/jarvisplot/cards/forest/swiss_column.json +196 -0
  14. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/style_preference.json +3 -0
  15. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/column_demand.py +6 -0
  16. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core.py +7 -1
  17. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core_runtime.py +14 -0
  18. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader.py +9 -1
  19. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_runtime.py +9 -4
  20. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/dryrun_runtime.py +30 -0
  21. jarvisplot-2.1.9/jarvisplot/forest_contract.py +436 -0
  22. jarvisplot-2.1.9/jarvisplot/forest_text.py +86 -0
  23. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_render_agent.py +7 -0
  24. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/manifest.yaml +2 -0
  25. jarvisplot-2.1.9/jarvisplot/manual_cards/type-forest.yaml +244 -0
  26. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/figure.json +297 -3
  27. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/manifest.json +5 -2
  28. jarvisplot-2.1.9/jarvisplot/schema/types/forest.json +824 -0
  29. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/templates_catalog.py +18 -0
  30. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/validation.py +21 -0
  31. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/config_cmd.py +9 -2
  32. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/doctor.py +1 -0
  33. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/pyproject.toml +1 -1
  34. jarvisplot-2.1.9/tests/test_forest.py +619 -0
  35. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
  36. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/entry_points.txt +0 -0
  37. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/requires.txt +0 -0
  38. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/JarvisPLOT.egg-info/top_level.txt +0 -0
  39. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/__init__.py +0 -0
  40. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adapters_rect.py +0 -0
  41. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adapters_ternary.py +0 -0
  42. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/adaptive_cdf_runtime.py +0 -0
  43. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/bin_stat_runtime.py +0 -0
  44. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
  45. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/colorbar_runtime.py +0 -0
  46. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/config_runtime.py +0 -0
  47. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_layout.py +0 -0
  48. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_layout_diamond.py +0 -0
  49. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corr_order.py +0 -0
  50. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/correlation_runtime.py +0 -0
  51. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/corrplot_runtime.py +0 -0
  52. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/data_pipelines.py +0 -0
  53. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/density_cell_runtime.py +0 -0
  54. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/distribution_1d_runtime.py +0 -0
  55. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/dynesty_runtime.py +0 -0
  56. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/figure.py +0 -0
  57. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/helper.py +0 -0
  58. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_2d_runtime.py +0 -0
  59. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_natural_neighbor.py +0 -0
  60. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/interp_natural_neighbor_exact.py +0 -0
  61. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/layer_runtime.py +0 -0
  62. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/layout_runtime.py +0 -0
  63. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/method_registry.py +0 -0
  64. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_density_runtime.py +0 -0
  65. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_hpd.py +0 -0
  66. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/posterior_mesh.py +0 -0
  67. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/preprocessor.py +0 -0
  68. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/preprocessor_runtime.py +0 -0
  69. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/profile_runtime.py +0 -0
  70. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/significance_runtime.py +0 -0
  71. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/Figure/style_runtime.py +0 -0
  72. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/__init__.py +0 -0
  73. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/agent_digest.py +0 -0
  74. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/agent_io.py +0 -0
  75. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cache_registry.py +0 -0
  76. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cache_store.py +0 -0
  77. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
  78. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/dynesty_runplot.json +0 -0
  79. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
  80. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
  81. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
  82. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rect_marginal.json +0 -0
  83. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/rectratio.json +0 -0
  84. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
  85. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
  86. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
  87. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
  88. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
  89. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
  90. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/args.json +0 -0
  91. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/colors/colormaps.json +0 -0
  92. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/corrplot/diamond.json +0 -0
  93. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/corrplot/matrix.json +0 -0
  94. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/default/output.json +0 -0
  95. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/flowchart/default.json +0 -0
  96. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
  97. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
  98. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
  99. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
  100. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
  101. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/JarvisHEP.png +0 -0
  102. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/calculator.png +0 -0
  103. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/inputfile.png +0 -0
  104. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/opera.png +0 -0
  105. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/outputfile.png +0 -0
  106. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/flowchart/sampler.png +0 -0
  107. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/gambit.png +0 -0
  108. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/icons/gambit_small.png +0 -0
  109. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/compact/legend-card.json +0 -0
  110. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/default/legend-card.json +0 -0
  111. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/paper/legend-card.json +0 -0
  112. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cards/legends/presentation/legend-card.json +0 -0
  113. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cli.py +0 -0
  114. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/cli_help.py +0 -0
  115. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/client.py +0 -0
  116. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/column_probe.py +0 -0
  117. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/config.py +0 -0
  118. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/config_address.py +0 -0
  119. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/core_assets.py +0 -0
  120. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_access.py +0 -0
  121. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_generation_contracts.py +0 -0
  122. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_hdf5.py +0 -0
  123. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/data_loader_summary.py +0 -0
  124. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/dataset_types.py +0 -0
  125. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/diagnostic_guidance.py +0 -0
  126. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/diagnostics.py +0 -0
  127. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/distribution_1d_config.py +0 -0
  128. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/expr_names.py +0 -0
  129. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/fix_apply.py +0 -0
  130. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/flowchart.py +0 -0
  131. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/generated_data.py +0 -0
  132. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/inner_func.py +0 -0
  133. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/legend_cards.py +0 -0
  134. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_catalog.py +0 -0
  135. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_methods.py +0 -0
  136. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_render_human.py +0 -0
  137. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/man_transforms.py +0 -0
  138. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/agent-output.yaml +0 -0
  139. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/cache.yaml +0 -0
  140. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/cli-map.yaml +0 -0
  141. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/data-columns.yaml +0 -0
  142. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/debug-overlay.yaml +0 -0
  143. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/diagnostics.yaml +0 -0
  144. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/dryrun-viz.yaml +0 -0
  145. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/field-interp.yaml +0 -0
  146. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/generated-data.yaml +0 -0
  147. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/layer-method.yaml +0 -0
  148. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/legend.yaml +0 -0
  149. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/method_notes.yaml +0 -0
  150. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/style-axes.yaml +0 -0
  151. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-correlation-matrix.yaml +0 -0
  152. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-posterior-2d.yaml +0 -0
  153. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/type-profile-2d.yaml +0 -0
  154. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/validate.yaml +0 -0
  155. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/workflow.yaml +0 -0
  156. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/manual_cards/yaml-root.yaml +0 -0
  157. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/memtrace.py +0 -0
  158. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/method_contracts.py +0 -0
  159. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/render_health.py +0 -0
  160. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/common.json +0 -0
  161. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/dataset.json +0 -0
  162. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/distribution-1d.json +0 -0
  163. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/generated-data.json +0 -0
  164. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/layer.json +0 -0
  165. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/legend-card.json +0 -0
  166. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/legend.json +0 -0
  167. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/core/transform.json +0 -0
  168. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/bar.json +0 -0
  169. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/barh.json +0 -0
  170. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/contour.json +0 -0
  171. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/contourf.json +0 -0
  172. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/corrplot.json +0 -0
  173. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/dynesty_runplot.json +0 -0
  174. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/errorbar.json +0 -0
  175. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill.json +0 -0
  176. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill_between.json +0 -0
  177. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/fill_betweenx.json +0 -0
  178. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/hist.json +0 -0
  179. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/hist2d.json +0 -0
  180. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/imshow.json +0 -0
  181. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpcontour.json +0 -0
  182. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpcontourf.json +0 -0
  183. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/jpfield.json +0 -0
  184. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/pcolor.json +0 -0
  185. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/pcolormesh.json +0 -0
  186. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/plot.json +0 -0
  187. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/quiver.json +0 -0
  188. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/scatter.json +0 -0
  189. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/stairs.json +0 -0
  190. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/step.json +0 -0
  191. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tricontour.json +0 -0
  192. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tricontourf.json +0 -0
  193. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tripcolor.json +0 -0
  194. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/tripcolor_axes.json +0 -0
  195. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/triplot.json +0 -0
  196. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/voronoi.json +0 -0
  197. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/methods/voronoif.json +0 -0
  198. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema/plot-config.json +0 -0
  199. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema_catalog.py +0 -0
  200. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/schema_diagnostics.py +0 -0
  201. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/transform_contracts.py +0 -0
  202. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/__init__.py +0 -0
  203. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/cmaps.py +0 -0
  204. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/dataframes.py +0 -0
  205. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/expression.py +0 -0
  206. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/interpolator.py +0 -0
  207. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/utils/pathing.py +0 -0
  208. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/__init__.py +0 -0
  209. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/cache.py +0 -0
  210. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/cap.py +0 -0
  211. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/data.py +0 -0
  212. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/dryrun.py +0 -0
  213. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/explain.py +0 -0
  214. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/man.py +0 -0
  215. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/suggest.py +0 -0
  216. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/template.py +0 -0
  217. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/verbs/validate.py +0 -0
  218. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/jarvisplot/yaml_io.py +0 -0
  219. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/setup.cfg +0 -0
  220. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_adaptive_cdf.py +0 -0
  221. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_add_column.py +0 -0
  222. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_agent_digest.py +0 -0
  223. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_agent_envelope.py +0 -0
  224. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_axis_title.py +0 -0
  225. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cache_cleanup.py +0 -0
  226. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cache_registry.py +0 -0
  227. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_capabilities.py +0 -0
  228. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_card_debug_shape.py +0 -0
  229. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cli_help.py +0 -0
  230. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cli_verbs.py +0 -0
  231. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_cmaps.py +0 -0
  232. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_column_existence.py +0 -0
  233. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_column_pruning.py +0 -0
  234. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_addressing.py +0 -0
  235. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_edit.py +0 -0
  236. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_config_expand.py +0 -0
  237. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_correlation_runtime.py +0 -0
  238. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_corrplot.py +0 -0
  239. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_access.py +0 -0
  240. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_describe.py +0 -0
  241. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_head_eval.py +0 -0
  242. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_loader_contracts.py +0 -0
  243. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_data_pipelines.py +0 -0
  244. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_config.py +0 -0
  245. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_switches.py +0 -0
  246. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_debug_yaml_override.py +0 -0
  247. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_density_cell_transform.py +0 -0
  248. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_design_runtime.py +0 -0
  249. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_diagnostic_guidance.py +0 -0
  250. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_distribution_1d_transform.py +0 -0
  251. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_docs_status_labels.py +0 -0
  252. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_doctor_partial.py +0 -0
  253. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_examples_validate.py +0 -0
  254. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_explain.py +0 -0
  255. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_expression_semantics.py +0 -0
  256. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_figure_contracts.py +0 -0
  257. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_figure_type_expander.py +0 -0
  258. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_fix_application.py +0 -0
  259. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_flowchart_renderer.py +0 -0
  260. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_generated_data.py +0 -0
  261. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_imshow_adapter.py +0 -0
  262. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interp_2d_transform.py +0 -0
  263. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interp_natural_neighbor.py +0 -0
  264. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_interpolator_contracts.py +0 -0
  265. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_jpcontour_methods.py +0 -0
  266. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_layer_legend.py +0 -0
  267. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_legend_cards.py +0 -0
  268. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_legend_runtime.py +0 -0
  269. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_man_cli.py +0 -0
  270. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_method_contracts.py +0 -0
  271. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_parse_data_metadata.py +0 -0
  272. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_pathing_contracts.py +0 -0
  273. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_posterior_hpd.py +0 -0
  274. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_profile_runtime.py +0 -0
  275. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_render_health.py +0 -0
  276. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_render_lifecycle.py +0 -0
  277. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_row_ledger.py +0 -0
  278. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_schema_catalog.py +0 -0
  279. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_suggest.py +0 -0
  280. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_suggest_axes.py +0 -0
  281. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_template_catalog.py +0 -0
  282. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_template_json.py +0 -0
  283. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_ternary_debug.py +0 -0
  284. {jarvisplot-2.1.8 → jarvisplot-2.1.9}/tests/test_transform_contracts.py +0 -0
  285. {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.8
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.8
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
- KNOWN_FIGURE_TYPES = frozenset({"posterior_2d", "profile_2d", "correlation_matrix"})
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