jarvisplot 2.1.12__tar.gz → 2.1.14__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 (335) hide show
  1. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/CHANGELOG.md +25 -0
  2. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/PKG-INFO +5 -2
  3. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/SOURCES.txt +2 -0
  4. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/requires.txt +4 -0
  5. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/MANIFEST.in +0 -1
  6. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/PKG-INFO +5 -2
  7. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/README.md +1 -1
  8. jarvisplot-2.1.14/VERSION +1 -0
  9. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/adapters_rect.py +17 -6
  10. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/forest_legend.py +11 -0
  11. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/forest_runtime.py +6 -3
  12. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/layer_runtime.py +1 -1
  13. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/preprocessor_runtime.py +1 -1
  14. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/1x1/ternary.json +1 -1
  15. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/ternary.json +1 -1
  16. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +1 -1
  17. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +1 -1
  18. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/gambit/2x1/rect_cmap.json +1 -1
  19. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_loader.py +30 -17
  20. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_loader_runtime.py +2 -0
  21. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/diagnostic_guidance.py +1 -1
  22. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/dryrun_runtime.py +38 -23
  23. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/flowchart.py +39 -5
  24. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/render_health.py +3 -0
  25. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/transform_contracts.py +1 -4
  26. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/expression.py +9 -1
  27. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/validation.py +33 -2
  28. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/doctor.py +3 -1
  29. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/pyproject.toml +4 -1
  30. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_card_debug_shape.py +21 -0
  31. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_flowchart_renderer.py +76 -1
  32. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_forest.py +7 -0
  33. jarvisplot-2.1.14/tests/test_forest_legend.py +39 -0
  34. jarvisplot-2.1.14/tests/test_review_regressions.py +163 -0
  35. jarvisplot-2.1.12/VERSION +0 -1
  36. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/README.md +0 -0
  37. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/a4paper_2x1.yaml +0 -0
  38. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/a4paper_4x1_rectcmap.yaml +0 -0
  39. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/compact_legend.yaml +0 -0
  40. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/correlation_demo.csv +0 -0
  41. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/correlation_demo_wide.csv +0 -0
  42. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/corrplot_diamond.yaml +0 -0
  43. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/corrplot_matrix.yaml +0 -0
  44. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/corrplot_scales.yaml +0 -0
  45. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/README.md +0 -0
  46. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/box.yaml +0 -0
  47. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/box.csv +0 -0
  48. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/forest-points.csv +0 -0
  49. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/forest-rows.csv +0 -0
  50. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/interval.csv +0 -0
  51. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/points.csv +0 -0
  52. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/summary-intervals.csv +0 -0
  53. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/summary-labels.csv +0 -0
  54. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/summary-points.csv +0 -0
  55. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/data/summary-rows.csv +0 -0
  56. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/interval.yaml +0 -0
  57. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/intervals-only.yaml +0 -0
  58. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/multi-interval.yaml +0 -0
  59. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/real-demo-debug.yaml +0 -0
  60. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/forest/real-demo.yaml +0 -0
  61. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/gambit_2x1.yaml +0 -0
  62. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/generated_data.yaml +0 -0
  63. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/legend_cards.yaml +0 -0
  64. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/pdf_cdf_1d.yaml +0 -0
  65. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/Example/ternary_1x1.yaml +0 -0
  66. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
  67. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/entry_points.txt +0 -0
  68. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/JarvisPLOT.egg-info/top_level.txt +0 -0
  69. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/__init__.py +0 -0
  70. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/adapters_ternary.py +0 -0
  71. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/adaptive_cdf_runtime.py +0 -0
  72. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/bin_stat_runtime.py +0 -0
  73. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
  74. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/colorbar_runtime.py +0 -0
  75. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/config_runtime.py +0 -0
  76. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/corr_layout.py +0 -0
  77. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/corr_layout_diamond.py +0 -0
  78. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/corr_order.py +0 -0
  79. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/correlation_runtime.py +0 -0
  80. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/corrplot_runtime.py +0 -0
  81. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/data_pipelines.py +0 -0
  82. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/debug_config.py +0 -0
  83. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/density_cell_runtime.py +0 -0
  84. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/design_runtime.py +0 -0
  85. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/distribution_1d_runtime.py +0 -0
  86. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/dynesty_runtime.py +0 -0
  87. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/figure.py +0 -0
  88. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/figure_types.py +0 -0
  89. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/forest_debug.py +0 -0
  90. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/helper.py +0 -0
  91. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/interp_2d_runtime.py +0 -0
  92. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/interp_natural_neighbor.py +0 -0
  93. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/interp_natural_neighbor_exact.py +0 -0
  94. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/layout_runtime.py +0 -0
  95. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/legend_runtime.py +0 -0
  96. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/method_registry.py +0 -0
  97. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/posterior_density_runtime.py +0 -0
  98. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/posterior_hpd.py +0 -0
  99. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/posterior_mesh.py +0 -0
  100. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/preprocessor.py +0 -0
  101. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/profile_runtime.py +0 -0
  102. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/significance_runtime.py +0 -0
  103. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/Figure/style_runtime.py +0 -0
  104. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/__init__.py +0 -0
  105. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/agent_digest.py +0 -0
  106. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/agent_io.py +0 -0
  107. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cache_registry.py +0 -0
  108. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cache_store.py +0 -0
  109. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/capabilities.py +0 -0
  110. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/dynesty_runplot.json +0 -0
  111. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
  112. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
  113. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
  114. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/rect_marginal.json +0 -0
  115. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/2x1/rectratio.json +0 -0
  116. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
  117. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
  118. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
  119. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/args.json +0 -0
  120. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/colors/colormaps.json +0 -0
  121. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/corrplot/diamond.json +0 -0
  122. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/corrplot/matrix.json +0 -0
  123. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/default/output.json +0 -0
  124. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/flowchart/default.json +0 -0
  125. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/fonts/default.json +0 -0
  126. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/forest/swiss_column.json +0 -0
  127. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
  128. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
  129. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
  130. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
  131. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/JarvisHEP.png +0 -0
  132. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/calculator.png +0 -0
  133. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/inputfile.png +0 -0
  134. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/opera.png +0 -0
  135. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/outputfile.png +0 -0
  136. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/flowchart/sampler.png +0 -0
  137. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/gambit.png +0 -0
  138. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/icons/gambit_small.png +0 -0
  139. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/legends/compact/legend-card.json +0 -0
  140. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/legends/default/legend-card.json +0 -0
  141. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/legends/paper/legend-card.json +0 -0
  142. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/legends/presentation/legend-card.json +0 -0
  143. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cards/style_preference.json +0 -0
  144. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cli.py +0 -0
  145. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/cli_help.py +0 -0
  146. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/client.py +0 -0
  147. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/column_demand.py +0 -0
  148. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/column_probe.py +0 -0
  149. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/config.py +0 -0
  150. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/config_address.py +0 -0
  151. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/core.py +0 -0
  152. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/core_assets.py +0 -0
  153. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/core_runtime.py +0 -0
  154. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_access.py +0 -0
  155. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_generation_contracts.py +0 -0
  156. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_loader_hdf5.py +0 -0
  157. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/data_loader_summary.py +0 -0
  158. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/dataset_types.py +0 -0
  159. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/diagnostics.py +0 -0
  160. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/distribution_1d_config.py +0 -0
  161. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/expr_names.py +0 -0
  162. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/font_runtime.py +0 -0
  163. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/fonts.py +0 -0
  164. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/forest_contract.py +0 -0
  165. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/forest_text.py +0 -0
  166. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/generated_data.py +0 -0
  167. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/inner_func.py +0 -0
  168. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/legend_cards.py +0 -0
  169. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/man_catalog.py +0 -0
  170. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/man_methods.py +0 -0
  171. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/man_render_agent.py +0 -0
  172. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/man_render_human.py +0 -0
  173. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/man_transforms.py +0 -0
  174. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/agent-output.yaml +0 -0
  175. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/agent-readme.yaml +0 -0
  176. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/cache.yaml +0 -0
  177. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/cli-map.yaml +0 -0
  178. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/data-columns.yaml +0 -0
  179. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/debug-overlay.yaml +0 -0
  180. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/diagnostics.yaml +0 -0
  181. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/doctor.yaml +0 -0
  182. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/dryrun-viz.yaml +0 -0
  183. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/field-interp.yaml +0 -0
  184. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/fonts.yaml +0 -0
  185. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/generated-data.yaml +0 -0
  186. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/layer-method.yaml +0 -0
  187. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/legend.yaml +0 -0
  188. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/manifest.yaml +0 -0
  189. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/method_notes.yaml +0 -0
  190. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/style-axes.yaml +0 -0
  191. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/type-correlation-matrix.yaml +0 -0
  192. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/type-forest.yaml +0 -0
  193. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/type-posterior-2d.yaml +0 -0
  194. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/type-profile-2d.yaml +0 -0
  195. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/workflow.yaml +0 -0
  196. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/manual_cards/yaml-root.yaml +0 -0
  197. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/memtrace.py +0 -0
  198. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/method_contracts.py +0 -0
  199. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/common.json +0 -0
  200. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/dataset.json +0 -0
  201. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/distribution-1d.json +0 -0
  202. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/figure.json +0 -0
  203. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/generated-data.json +0 -0
  204. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/layer.json +0 -0
  205. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/legend-card.json +0 -0
  206. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/legend.json +0 -0
  207. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/core/transform.json +0 -0
  208. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/manifest.json +0 -0
  209. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/bar.json +0 -0
  210. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/barh.json +0 -0
  211. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/contour.json +0 -0
  212. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/contourf.json +0 -0
  213. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/corrplot.json +0 -0
  214. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/dynesty_runplot.json +0 -0
  215. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/errorbar.json +0 -0
  216. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/fill.json +0 -0
  217. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/fill_between.json +0 -0
  218. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/fill_betweenx.json +0 -0
  219. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/hist.json +0 -0
  220. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/hist2d.json +0 -0
  221. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/imshow.json +0 -0
  222. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/jpcontour.json +0 -0
  223. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/jpcontourf.json +0 -0
  224. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/jpfield.json +0 -0
  225. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/pcolor.json +0 -0
  226. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/pcolormesh.json +0 -0
  227. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/plot.json +0 -0
  228. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/quiver.json +0 -0
  229. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/scatter.json +0 -0
  230. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/stairs.json +0 -0
  231. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/step.json +0 -0
  232. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/tricontour.json +0 -0
  233. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/tricontourf.json +0 -0
  234. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/tripcolor.json +0 -0
  235. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/tripcolor_axes.json +0 -0
  236. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/triplot.json +0 -0
  237. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/voronoi.json +0 -0
  238. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/methods/voronoif.json +0 -0
  239. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/plot-config.json +0 -0
  240. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema/types/forest.json +0 -0
  241. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema_catalog.py +0 -0
  242. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/schema_diagnostics.py +0 -0
  243. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/templates_catalog.py +0 -0
  244. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/__init__.py +0 -0
  245. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/cmaps.py +0 -0
  246. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/dataframes.py +0 -0
  247. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/interpolator.py +0 -0
  248. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/utils/pathing.py +0 -0
  249. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/__init__.py +0 -0
  250. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/cache.py +0 -0
  251. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/cap.py +0 -0
  252. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/config_cmd.py +0 -0
  253. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/data.py +0 -0
  254. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/dryrun.py +0 -0
  255. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/explain.py +0 -0
  256. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/fonts.py +0 -0
  257. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/man.py +0 -0
  258. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/suggest.py +0 -0
  259. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/verbs/template.py +0 -0
  260. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/version.py +0 -0
  261. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/jarvisplot/yaml_io.py +0 -0
  262. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/setup.cfg +0 -0
  263. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/conftest.py +0 -0
  264. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_adaptive_cdf.py +0 -0
  265. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_add_column.py +0 -0
  266. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_agent_digest.py +0 -0
  267. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_agent_envelope.py +0 -0
  268. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_axis_title.py +0 -0
  269. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cache_cleanup.py +0 -0
  270. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cache_policy.py +0 -0
  271. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cache_registry.py +0 -0
  272. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_capabilities.py +0 -0
  273. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cli_help.py +0 -0
  274. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cli_verbs.py +0 -0
  275. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_cmaps.py +0 -0
  276. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_column_existence.py +0 -0
  277. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_column_pruning.py +0 -0
  278. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_config_addressing.py +0 -0
  279. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_config_edit.py +0 -0
  280. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_config_expand.py +0 -0
  281. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_correlation_runtime.py +0 -0
  282. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_corrplot.py +0 -0
  283. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_data_access.py +0 -0
  284. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_data_describe.py +0 -0
  285. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_data_head_eval.py +0 -0
  286. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_data_loader_contracts.py +0 -0
  287. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_data_pipelines.py +0 -0
  288. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_debug_config.py +0 -0
  289. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_debug_switches.py +0 -0
  290. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_debug_yaml_override.py +0 -0
  291. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_density_cell_transform.py +0 -0
  292. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_design_runtime.py +0 -0
  293. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_diagnostic_guidance.py +0 -0
  294. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_distribution_1d_transform.py +0 -0
  295. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_doctor_partial.py +0 -0
  296. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_examples_validate.py +0 -0
  297. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_explain.py +0 -0
  298. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_expression_semantics.py +0 -0
  299. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_figure_contracts.py +0 -0
  300. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_figure_type_expander.py +0 -0
  301. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_font_runtime.py +0 -0
  302. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_fonts.py +0 -0
  303. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_fonts_cli.py +0 -0
  304. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_forest_examples.py +0 -0
  305. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_forest_text.py +0 -0
  306. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_generated_data.py +0 -0
  307. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_imshow_adapter.py +0 -0
  308. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_interp_2d_transform.py +0 -0
  309. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_interp_natural_neighbor.py +0 -0
  310. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_interpolator_contracts.py +0 -0
  311. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_jpcontour_methods.py +0 -0
  312. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_layer_legend.py +0 -0
  313. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_legend_cards.py +0 -0
  314. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_legend_runtime.py +0 -0
  315. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_man_cli.py +0 -0
  316. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_method_contracts.py +0 -0
  317. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_parse_data_metadata.py +0 -0
  318. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_pathing_contracts.py +0 -0
  319. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_posterior_hpd.py +0 -0
  320. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_profile_runtime.py +0 -0
  321. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_release_contract.py +0 -0
  322. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_render_defects.py +0 -0
  323. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_render_health.py +0 -0
  324. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_render_lifecycle.py +0 -0
  325. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_row_ledger.py +0 -0
  326. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_savefig_provenance.py +0 -0
  327. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_schema_catalog.py +0 -0
  328. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_suggest.py +0 -0
  329. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_suggest_axes.py +0 -0
  330. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_template_catalog.py +0 -0
  331. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_ternary_debug.py +0 -0
  332. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_transform_contracts.py +0 -0
  333. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_validation_imports.py +0 -0
  334. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tests/test_version.py +0 -0
  335. {jarvisplot-2.1.12 → jarvisplot-2.1.14}/tools/check_release.py +0 -0
@@ -4,6 +4,31 @@ Release history of the `JarvisPLOT` distribution (import package `jarvisplot`,
4
4
  CLI `jplot`). User guides and reference documentation are available in
5
5
  [Jarvis-Books `Docs/Jarvis-PLOT/`](https://github.com/Pengxuan-Zhu-Phys/Jarvis-Books/blob/main/Docs/Jarvis-PLOT/README.md).
6
6
 
7
+ ## 2.1.14
8
+
9
+ ### Fixed
10
+
11
+ - Render `pcolor` from named coordinates and keep Voronoi boundary fills from
12
+ acquiring Matplotlib's default cycle colour.
13
+ - Load CSV path lists and apply `add_column.fillna` consistently across backends.
14
+ - Allow unambiguous HDF5 leaf names in expressions while retaining full paths.
15
+ - Make doctor honor HDF5 renaming and validity masks, load Functions, count literal
16
+ coordinates, detect missing style axes and avoid duplicate path diagnostics.
17
+ - Advertise only supported `make_interp_2d` methods in capability metadata.
18
+
19
+ ## 2.1.13 — 2026-10-29
20
+ ### Fixed
21
+
22
+ - Branch flowchart connections from a module to its output files, and give each
23
+ file's output variables a separate, spaced group centered on its file icon.
24
+ - Correct A4 ternary badge image paths on case-sensitive filesystems, including
25
+ Linux, for the full-width, standard and colorbar cards.
26
+ - Keep the GAMBIT `rectcmap` colorbar label inside the page with Matplotlib 3.10
27
+ while preserving its separation from tick labels.
28
+ - Allow forest footer legends to meet the badge at their shared grid boundary
29
+ with macOS Helvetica and Matplotlib 3.10.9. Actual overlap with the badge or
30
+ description remains an error.
31
+
7
32
  ## 2.1.12 — 2026-10-19
8
33
 
9
34
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 2.1.12
3
+ Version: 2.1.14
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -19,6 +19,9 @@ Requires-Dist: scipy
19
19
  Requires-Dist: shapely
20
20
  Requires-Dist: deepmerge
21
21
  Requires-Dist: rich
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Requires-Dist: pyarrow; extra == "test"
22
25
 
23
26
  # Jarvis-PLOT
24
27
 
@@ -27,7 +30,7 @@ but it can also be used as a **standalone scientific plotting tool**.
27
30
 
28
31
  It provides a simple command-line interface (CLI) to generate publication-quality figures from YAML configuration files, with most layout and style decisions handled by predefined profiles and style cards.
29
32
 
30
- Current source candidate: **2.1.12** (`jplot -v` reports the installed version).
33
+ Current source candidate: **2.1.14** (`jplot -v` reports the installed version).
31
34
  Release history: [CHANGELOG.md](https://github.com/Pengxuan-Zhu-Phys/Jarvis-PLOT/blob/main/CHANGELOG.md). Design documents and specifications: [Jarvis-Books `Docs/Jarvis-PLOT/`](https://github.com/Pengxuan-Zhu-Phys/Jarvis-Books/blob/main/Docs/Jarvis-PLOT/README.md).
32
35
 
33
36
  ---
@@ -297,6 +297,7 @@ tests/test_fonts.py
297
297
  tests/test_fonts_cli.py
298
298
  tests/test_forest.py
299
299
  tests/test_forest_examples.py
300
+ tests/test_forest_legend.py
300
301
  tests/test_forest_text.py
301
302
  tests/test_generated_data.py
302
303
  tests/test_imshow_adapter.py
@@ -317,6 +318,7 @@ tests/test_release_contract.py
317
318
  tests/test_render_defects.py
318
319
  tests/test_render_health.py
319
320
  tests/test_render_lifecycle.py
321
+ tests/test_review_regressions.py
320
322
  tests/test_row_ledger.py
321
323
  tests/test_savefig_provenance.py
322
324
  tests/test_schema_catalog.py
@@ -13,3 +13,7 @@ scipy
13
13
  shapely
14
14
  deepmerge
15
15
  rich
16
+
17
+ [test]
18
+ pytest
19
+ pyarrow
@@ -8,7 +8,6 @@ include tests/conftest.py
8
8
  exclude jprel
9
9
  prune docs
10
10
  prune .github
11
- prune .claude
12
11
  prune .codex
13
12
  prune .agents
14
13
  prune .cursor
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 2.1.12
3
+ Version: 2.1.14
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -19,6 +19,9 @@ Requires-Dist: scipy
19
19
  Requires-Dist: shapely
20
20
  Requires-Dist: deepmerge
21
21
  Requires-Dist: rich
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Requires-Dist: pyarrow; extra == "test"
22
25
 
23
26
  # Jarvis-PLOT
24
27
 
@@ -27,7 +30,7 @@ but it can also be used as a **standalone scientific plotting tool**.
27
30
 
28
31
  It provides a simple command-line interface (CLI) to generate publication-quality figures from YAML configuration files, with most layout and style decisions handled by predefined profiles and style cards.
29
32
 
30
- Current source candidate: **2.1.12** (`jplot -v` reports the installed version).
33
+ Current source candidate: **2.1.14** (`jplot -v` reports the installed version).
31
34
  Release history: [CHANGELOG.md](https://github.com/Pengxuan-Zhu-Phys/Jarvis-PLOT/blob/main/CHANGELOG.md). Design documents and specifications: [Jarvis-Books `Docs/Jarvis-PLOT/`](https://github.com/Pengxuan-Zhu-Phys/Jarvis-Books/blob/main/Docs/Jarvis-PLOT/README.md).
32
35
 
33
36
  ---
@@ -5,7 +5,7 @@ but it can also be used as a **standalone scientific plotting tool**.
5
5
 
6
6
  It provides a simple command-line interface (CLI) to generate publication-quality figures from YAML configuration files, with most layout and style decisions handled by predefined profiles and style cards.
7
7
 
8
- Current source candidate: **2.1.12** (`jplot -v` reports the installed version).
8
+ Current source candidate: **2.1.14** (`jplot -v` reports the installed version).
9
9
  Release history: [CHANGELOG.md](https://github.com/Pengxuan-Zhu-Phys/Jarvis-PLOT/blob/main/CHANGELOG.md). Design documents and specifications: [Jarvis-Books `Docs/Jarvis-PLOT/`](https://github.com/Pengxuan-Zhu-Phys/Jarvis-Books/blob/main/Docs/Jarvis-PLOT/README.md).
10
10
 
11
11
  ---
@@ -0,0 +1 @@
1
+ 2.1.14
@@ -379,18 +379,24 @@ class StdAxesAdapter:
379
379
  artists = self.ax.scatter(x, y, **kw)
380
380
  return _auto_clip(artists, self.ax, self._clip_path)
381
381
 
382
+ def pcolor(self, *args, **kwargs):
383
+ return self._quadmesh("pcolor", args, kwargs)
384
+
382
385
  def pcolormesh(self, *args, **kwargs):
383
- """Render regular-grid fields, including compact tables with __grid_* metadata."""
386
+ return self._quadmesh("pcolormesh", args, kwargs)
387
+
388
+ def _quadmesh(self, method, args, kwargs):
389
+ """Render either quad-mesh method from coordinates or a compact grid table."""
384
390
  if args:
385
- kw = self._merge("pcolormesh", kwargs)
386
- artist = self.ax.pcolormesh(*args, **kw)
391
+ kw = self._merge(method, kwargs)
392
+ artist = getattr(self.ax, method)(*args, **kw)
387
393
  return _auto_clip(artist, self.ax, self._clip_path)
388
394
 
389
395
  x = np.asarray(kwargs.pop("x"), dtype=float)
390
396
  y = np.asarray(kwargs.pop("y"), dtype=float)
391
397
  z = np.asarray(kwargs.pop("z", np.zeros_like(x)), dtype=float)
392
398
  df = kwargs.pop("__df__", None)
393
- kw = self._merge("pcolormesh", kwargs)
399
+ kw = self._merge(method, kwargs)
394
400
 
395
401
  # Strip kwargs that are not used by pcolormesh rendering.
396
402
  for k in ("shading", "levels", "extend", "space", "marker", "s", "c"):
@@ -451,7 +457,7 @@ class StdAxesAdapter:
451
457
  kw.setdefault("zorder", zorder)
452
458
 
453
459
  if norm is not None:
454
- artist = self.ax.pcolormesh(
460
+ artist = getattr(self.ax, method)(
455
461
  x_edges,
456
462
  y_edges,
457
463
  grid,
@@ -460,7 +466,7 @@ class StdAxesAdapter:
460
466
  **kw,
461
467
  )
462
468
  else:
463
- artist = self.ax.pcolormesh(
469
+ artist = getattr(self.ax, method)(
464
470
  x_edges,
465
471
  y_edges,
466
472
  grid,
@@ -1251,6 +1257,11 @@ class StdAxesAdapter:
1251
1257
  kw_edge, kw_face, kw_rest = split_fill_kwargs(kw_all)
1252
1258
  kw_edge.update(kw_rest)
1253
1259
  kw_face.update(kw_rest)
1260
+ if "color" in kw_all:
1261
+ kw_edge.setdefault("facecolor", kw_all["color"])
1262
+ kw_face.setdefault("facecolor", kw_all["color"])
1263
+ elif "facecolor" not in kw_edge and "fc" not in kw_edge:
1264
+ kw_edge["facecolor"] = kw_all.get("edgecolor", kw_all.get("ec", "none"))
1254
1265
  # ---- inputs ----
1255
1266
  x = _np.asarray(x)
1256
1267
  y = _np.asarray(y)
@@ -6,6 +6,17 @@ from matplotlib.patches import Rectangle
6
6
  from matplotlib.offsetbox import VPacker
7
7
 
8
8
 
9
+ def boxes_overlap(first, second):
10
+ """Require positive overlap on both physical axes, measured in mm.
11
+
12
+ Footer columns and the badge share grid boundaries. Touching those edges
13
+ is valid; Bbox.overlaps also counts contact and floating-point roundoff.
14
+ """
15
+ width = min(first.xmax, second.xmax) - max(first.xmin, second.xmin)
16
+ height = min(first.ymax, second.ymax) - max(first.ymin, second.ymin)
17
+ return min(width, height) > 1e-9
18
+
19
+
9
20
  class PhysicalLegendColumn(VPacker):
10
21
  """Recompute physical column width for each raster/vector renderer."""
11
22
  def __init__(self, width_mm, **kwargs):
@@ -91,7 +91,10 @@ class ForestFigure(Figure):
91
91
  def _render(self):
92
92
  from matplotlib.patches import Rectangle
93
93
  from .legend_runtime import apply_legend
94
- from .forest_legend import ForestGlyph, HandlerForestGlyph, HandlerForestPoint, PhysicalLegendColumn, group_glyph_and_points
94
+ from .forest_legend import (
95
+ ForestGlyph, HandlerForestGlyph, HandlerForestPoint,
96
+ PhysicalLegendColumn, boxes_overlap, group_glyph_and_points,
97
+ )
95
98
  from matplotlib.collections import PathCollection
96
99
  info, layout, ax = self.info, self.forest_layout, self.axes['ax']
97
100
  g, fonts, style = layout['geometry'], layout['typography'], layout['style']
@@ -468,9 +471,9 @@ class ForestFigure(Figure):
468
471
  if legend:
469
472
  for column in legend._legend_handle_box.get_children():
470
473
  for item in column.get_children():
471
- if any(physical_box(item).overlaps(b) for b in badge_boxes):
474
+ if any(boxes_overlap(physical_box(item), b) for b in badge_boxes):
472
475
  raise ValueError('forest footer legend overlaps axlogo; adjust forest.geometry footer/legend/logo positions or frame.ax.legend layout')
473
- if any(physical_box(item).overlaps(b) for b in description_boxes):
476
+ if any(boxes_overlap(physical_box(item), b) for b in description_boxes):
474
477
  raise ValueError('forest footer legend overlaps description; adjust forest.geometry column starts or frame.ax.legend layout')
475
478
  for row, artists in annotations:
476
479
  boxes = [physical_box(artist) for artist in artists]
@@ -32,7 +32,7 @@ _MISSING_CLIP_PATH = object()
32
32
  #: coordinates: the grid methods read the ``__grid_*`` metadata off it, and
33
33
  #: corrplot reads the whole correlation table -- var names, indices, rho and
34
34
  #: the per-pair row count that significance is computed from.
35
- _GRID_TABLE_METHODS = frozenset({"pcolormesh", "imshow", "corrplot"})
35
+ _GRID_TABLE_METHODS = frozenset({"pcolor", "pcolormesh", "imshow", "corrplot"})
36
36
 
37
37
  def _normalize_layer_combine(value) -> str:
38
38
  """Return the canonical combine spelling while keeping v1 YAML usable."""
@@ -280,7 +280,7 @@ def add_column(df, adds, logger):
280
280
  return df
281
281
  name = str(adds.get("name")).strip()
282
282
  expr = adds.get("expr")
283
- value = eval_dataframe_expression(df, expr, logger=logger, allow_column=True)
283
+ value = eval_dataframe_expression(df, expr, logger=logger, allow_column=True, fillna=adds.get("fillna"))
284
284
  # The frame may be a table other blocks still read (a preprofile pass
285
285
  # works on the source itself); add to a copy, never into the caller's.
286
286
  df = df.copy(deep=False)
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "axlogo": {
139
139
  "clip": false,
140
- "file": "&JP/jarvisplot/cards/icons/jarvisHEP.png"
140
+ "file": "&JP/jarvisplot/cards/icons/JarvisHEP.png"
141
141
  }
142
142
  },
143
143
  "Debug": {
@@ -134,7 +134,7 @@
134
134
  },
135
135
  "axlogo": {
136
136
  "clip": false,
137
- "file": "&JP/jarvisplot/cards/icons/jarvisHEP.png"
137
+ "file": "&JP/jarvisplot/cards/icons/JarvisHEP.png"
138
138
  }
139
139
  },
140
140
  "Debug": {
@@ -178,7 +178,7 @@
178
178
  },
179
179
  "axlogo": {
180
180
  "clip": false,
181
- "file": "&JP/jarvisplot/cards/icons/jarvisHEP.png"
181
+ "file": "&JP/jarvisplot/cards/icons/JarvisHEP.png"
182
182
  }
183
183
  },
184
184
  "Debug": {
@@ -177,7 +177,7 @@
177
177
  },
178
178
  "axlogo": {
179
179
  "clip": false,
180
- "file": "&JP/jarvisplot/cards/icons/jarvisHEP.png"
180
+ "file": "&JP/jarvisplot/cards/icons/JarvisHEP.png"
181
181
  }
182
182
  },
183
183
  "Style": {
@@ -162,7 +162,7 @@
162
162
  "fontfamily": "STIXGeneral"
163
163
  },
164
164
  "ylabel_coords": {
165
- "x": 3.7,
165
+ "x": 3.4,
166
166
  "y": 0.5
167
167
  },
168
168
  "ticks": {
@@ -25,6 +25,7 @@ JP_ROW_IDX = "__jp_row_idx__"
25
25
 
26
26
  class DataSet():
27
27
  def __init__(self):
28
+ self.paths: list[str] = []
28
29
  self._file: Optional[str] = None
29
30
  self.path: Optional[str] = None
30
31
  self.rootpath: Optional[str] = None
@@ -62,9 +63,14 @@ class DataSet():
62
63
  # transforms (see the ``to_df`` / ``to_ds`` steps).
63
64
  self.file = None
64
65
  else:
65
- raw_path = str(dtinfo['path'])
66
- resolved = resolve_project_path(raw_path, base_dir=rootpath or ".")
67
- self.file = str(resolved)
66
+ raw_paths = dtinfo['path'] if isinstance(dtinfo['path'], list) else [dtinfo['path']]
67
+ paths = [str(resolve_project_path(path, base_dir=rootpath or ".")) for path in raw_paths]
68
+ if not paths:
69
+ raise ValueError("DataSet.path must contain at least one file")
70
+ if len(paths) > 1 and declared_type != "csv":
71
+ raise ValueError("DataSet.path lists currently require type: csv")
72
+ self.file = paths[0]
73
+ self.paths = paths
68
74
  self.name = dtinfo['name']
69
75
  self.type = declared_type
70
76
  self.transform = dtinfo.get("transform", None)
@@ -118,7 +124,8 @@ class DataSet():
118
124
  if not wanted:
119
125
  return None
120
126
  names = [str(c) for c in available]
121
- keep = [c for c in names if c in wanted]
127
+ keep = [c for c in names if c in wanted or
128
+ (self.type == "hdf5" and c.rsplit("/", 1)[-1].rsplit(":", 1)[-1] in wanted)]
122
129
  if not any(c != JP_ROW_IDX for c in keep) or len(keep) == len(names):
123
130
  return None
124
131
  return keep
@@ -134,8 +141,8 @@ class DataSet():
134
141
  """Fetch cheap metadata only; avoid full table load."""
135
142
  if self.type == "csv":
136
143
  try:
137
- head = pd.read_csv(self.path, nrows=0)
138
- self.keys = list(head.columns)
144
+ self.keys = list(dict.fromkeys(column for path in (self.paths or [self.path])
145
+ for column in pd.read_csv(path, nrows=0).columns))
139
146
  if self.logger:
140
147
  self.logger.debug(
141
148
  f"Dataset '{self.name}' registered in lazy mode (csv columns={len(self.keys)})."
@@ -182,6 +189,8 @@ class DataSet():
182
189
  "transform": self.transform,
183
190
  "columns": self.columns,
184
191
  "full_load": bool(getattr(self, "full_load", False)),
192
+ "sources": [(path, os.stat(path).st_size, os.stat(path).st_mtime_ns)
193
+ for path in self.paths if os.path.isfile(path)],
185
194
  }
186
195
  from .distribution_1d_config import distribution_kind
187
196
 
@@ -396,13 +405,15 @@ class DataSet():
396
405
 
397
406
  @file.setter
398
407
  def file(self, value: Optional[str]) -> None:
399
- if value is None:
408
+ if value is None:
409
+ self.paths = []
400
410
  self._file = None
401
411
  self.path = None
402
412
  self.base = None
403
413
  return
404
414
 
405
415
  p = Path(value).expanduser().resolve()
416
+ self.paths = [str(p)]
406
417
  self._file = str(p)
407
418
  self.path = os.path.abspath(p)
408
419
  self.base = os.path.basename(p)
@@ -426,21 +437,23 @@ class DataSet():
426
437
  # of the table: dropping them here would silently apply a policy
427
438
  # across every column before an explicit transform can decide how
428
439
  # required and optional fields should be handled.
429
- usecols = None
440
+ selected = None
430
441
  if self.required_columns:
431
- header = list(pd.read_csv(self.path, nrows=0).columns)
442
+ header = list(dict.fromkeys(column for path in (self.paths or [self.path])
443
+ for column in pd.read_csv(path, nrows=0).columns))
432
444
  planned = self.planned_columns(header)
433
445
  if planned is not None:
434
- # By position: the names above are pandas' own (duplicated
435
- # headers already read as x, x.1), not the raw header text.
436
- wanted = set(planned)
437
- usecols = [i for i, name in enumerate(header) if str(name) in wanted]
438
- self._log_column_plan(len(usecols), len(header))
439
- self.data = pd.read_csv(
440
- self.path, low_memory=False, usecols=usecols,
446
+ # Union headers allow each input to omit columns supplied
447
+ # by another file; pandas keeps its duplicate-name handling.
448
+ selected = set(planned)
449
+ self._log_column_plan(len(selected), len(header))
450
+ frames = [pd.read_csv(
451
+ path, low_memory=False,
452
+ usecols=(lambda name: name in selected) if selected is not None else None,
441
453
  dtype={name: str for name in self.literal_columns},
442
454
  keep_default_na=not self.preserve_empty_strings,
443
- )
455
+ ) for path in (self.paths or [self.path])]
456
+ self.data = pd.concat(frames, ignore_index=True, sort=False) if len(frames) > 1 else frames[0]
444
457
  self.keys = list(self.data.columns)
445
458
  runtime.apply_dataset_transform(self, stage="csv")
446
459
 
@@ -419,6 +419,8 @@ def _apply_dataset_transform_polars(
419
419
  lf = lf.with_columns(key.alias(skey)).sort(skey).drop(skey)
420
420
  elif "add_column" in trans:
421
421
  adds = trans.get("add_column", {})
422
+ if isinstance(adds, dict) and adds.get("fillna") is not None:
423
+ raise TypeError("add_column.fillna requires the shared pandas evaluator")
422
424
  if not isinstance(adds, dict):
423
425
  raise TypeError("add_column step must be dict")
424
426
  problem = add_column_spec_problem(adds)
@@ -103,7 +103,7 @@ _GUIDANCE_BY_CODE: dict[str, tuple[str, Optional[str]]] = {
103
103
  # Data / figures / refs
104
104
  "JP-DAT-004": (
105
105
  "Paths resolve relative to the config file's directory "
106
- "(or use the &JP/ prefix for package-relative paths).",
106
+ "(or use the &JP/ prefix for bundled resource paths).",
107
107
  "path: ./DATABASE/samples.csv",
108
108
  ),
109
109
  "JP-DAT-005": (
@@ -152,6 +152,11 @@ def _dryrun_config(
152
152
  "type_expanded": [],
153
153
  }, bag
154
154
 
155
+ from .core_assets import load_interpolators
156
+ try:
157
+ load_interpolators(work, yaml_dir=base_dir)
158
+ except Exception as exc:
159
+ bag.error("JP-VIZ-003", "$.Functions", f"could not load Functions: {exc}")
155
160
  datasets_meta, frames = _load_datasets(work, base_dir=base_dir, bag=bag)
156
161
  forest_layouts = []
157
162
  observations: list[LayerObservation] = []
@@ -356,29 +361,34 @@ def _load_datasets(
356
361
  meta[name] = {"path": None, "rows": 0, "in_memory": True}
357
362
  continue
358
363
  raw_path = entry.get("path")
359
- if not isinstance(raw_path, str):
360
- bag.warning(
361
- "JP-DAT-004",
362
- f"$.DataSet[{index}].path",
363
- f"dataset {name!r} has no single string path; dryrun skips it",
364
- )
364
+ paths = raw_path if isinstance(raw_path, list) else [raw_path]
365
+ if not paths or not all(isinstance(path, str) for path in paths):
366
+ bag.error("JP-DAT-004", f"$.DataSet[{index}].path", f"dataset {name!r} needs a path or path list")
365
367
  continue
366
- resolved = resolve_project_path(raw_path, base_dir)
367
- if not Path(resolved).exists():
368
- bag.error(
369
- "JP-DAT-004",
370
- f"$.DataSet[{index}].path",
371
- f"data file not found: {resolved}",
372
- )
373
- meta[name] = {"path": str(resolved), "rows": None, "error": "missing"}
368
+ resolved = [resolve_project_path(path, base_dir) for path in paths]
369
+ missing = [path for path in resolved if not path.exists()]
370
+ if missing:
371
+ bag.error("JP-DAT-004", f"$.DataSet[{index}].path", f"data file not found: {missing[0]}")
372
+ meta[name] = {"path": str(missing[0]), "rows": None, "error": "missing"}
373
+ continue
374
+ kind = str(entry.get("type") or detect_type(str(resolved[0]), "auto"))
375
+ display_path = str(resolved[0]) if len(resolved) == 1 else [str(path) for path in resolved]
376
+ if len(resolved) > 1 and kind != "csv":
377
+ bag.error("JP-DAT-006", f"$.DataSet[{index}].path", "DataSet.path lists currently require type: csv")
374
378
  continue
375
- kind = str(entry.get("type") or detect_type(str(resolved), "auto"))
376
379
  try:
377
- df = load_dataframe(
378
- str(resolved),
379
- kind=kind if kind in {"csv", "parquet", "hdf5"} else "csv",
380
- group=entry.get("dataset") if isinstance(entry.get("dataset"), str) else None,
381
- )
380
+ if kind == "hdf5":
381
+ import logging
382
+ from .data_loader import DataSet
383
+ loader = DataSet()
384
+ loader.logger = logging.getLogger("jarvisplot.dryrun.data")
385
+ spec = dict(entry)
386
+ spec.pop("transform", None)
387
+ loader.setinfo(spec, rootpath=base_dir, eager=True)
388
+ df = loader.get_data()
389
+ else:
390
+ tables = [load_dataframe(str(path), kind=kind) for path in resolved]
391
+ df = pd.concat(tables, ignore_index=True, sort=False) if len(tables) > 1 else tables[0]
382
392
  # dataset-level transform (simple steps only)
383
393
  _heavy = []
384
394
  ds_transform = entry.get("transform")
@@ -387,7 +397,7 @@ def _load_datasets(
387
397
  rows = int(len(df))
388
398
  frames[name] = df
389
399
  meta[name] = {
390
- "path": str(resolved),
400
+ "path": display_path,
391
401
  "type": kind,
392
402
  "rows": rows,
393
403
  "columns": [str(c) for c in df.columns],
@@ -399,7 +409,7 @@ def _load_datasets(
399
409
  f"$.DataSet[{index}]",
400
410
  f"could not load dataset {name!r}: {exc}",
401
411
  )
402
- meta[name] = {"path": str(resolved), "rows": None, "error": str(exc)}
412
+ meta[name] = {"path": display_path, "rows": None, "error": str(exc)}
403
413
  return meta, frames
404
414
 
405
415
 
@@ -598,6 +608,11 @@ def _observe_layer(
598
608
  incomplete_sources = incomplete_sources or set()
599
609
  data_blocks = layer.get("data") or []
600
610
  if not isinstance(data_blocks, list) or not data_blocks:
611
+ if layer.get("coordinates"):
612
+ import pandas as pd
613
+ obs = observe_layer_dataframe(figure=figure_name, layer=layer, df=pd.DataFrame(),
614
+ frame_cfg=frame_cfg, source="<literal>")
615
+ return obs, None, []
601
616
  return (
602
617
  LayerObservation(
603
618
  figure=figure_name,
@@ -690,7 +705,7 @@ def _observe_layer(
690
705
  source=", ".join(source_labels),
691
706
  n_points=0,
692
707
  notes=notes or ["no loadable data blocks"],
693
- incomplete=incomplete_source_seen,
708
+ incomplete=incomplete_source_seen or bool(heavy_skipped),
694
709
  ),
695
710
  None,
696
711
  heavy_skipped,
@@ -12,6 +12,8 @@ from .utils.pathing import resolve_project_path
12
12
 
13
13
 
14
14
  DEFAULT_FLOWCHART_CARD = "&JP/jarvisplot/cards/flowchart/default.json"
15
+ _OUTPUT_FILE_Y_OFFSET = -0.18
16
+ _OUTPUT_FILE_GROUP_GAP = 0.22
15
17
 
16
18
 
17
19
  def _deep_merge(base: dict, override: Mapping | None) -> dict:
@@ -314,6 +316,14 @@ class FlowchartRenderer:
314
316
  ax.text(x+0.12, y-0.01, "SEL", ha="center", va="center", fontsize=5, fontweight="extra bold", color="#008A34", zorder=121, path_effects=[pe.withStroke(linewidth=0.2, foreground="white")])
315
317
 
316
318
  def _draw_classic_edges(self, ax, graph) -> None:
319
+ output_files: dict[str, set[str]] = {}
320
+ for edge in graph.edges:
321
+ source_id = str(edge.get("source", {}).get("node", ""))
322
+ target_id = str(edge.get("target", {}).get("node", ""))
323
+ if (str(edge.get("role", "dataflow")).lower() == "fileflow"
324
+ and source_id in graph.mains and target_id in graph.files):
325
+ output_files.setdefault(source_id, set()).add(target_id)
326
+
317
327
  for edge in graph.edges:
318
328
  role = str(edge.get("role", "dataflow")).lower()
319
329
  source_id = str(edge.get("source", {}).get("node", ""))
@@ -336,7 +346,10 @@ class FlowchartRenderer:
336
346
  ax.plot(start[0] + 0.03, start[1], "s", color="gray", markersize=2.0, zorder=79)
337
347
  self._classic_curve(ax, start, end, color="#D45040", lw=2.2, alpha=0.95)
338
348
  elif role == "fileflow" and graph.is_main(source_id) and graph.is_file(target_id):
339
- start = (start[0], end[1])
349
+ if len(output_files.get(source_id, ())) <= 1:
350
+ start = (start[0], end[1])
351
+ else:
352
+ start = (start[0], start[1] + _OUTPUT_FILE_Y_OFFSET)
340
353
  self._classic_curve(ax, start, end, color="#3B4DC0", lw=2.2, alpha=0.95)
341
354
  elif role == "fileflow" and graph.is_file(source_id) and graph.is_var(target_id):
342
355
  self._classic_curve(ax, start, end, color="#3B4DC0", lw=0.75, alpha=0.7)
@@ -660,14 +673,35 @@ class _ClassicGraph:
660
673
  counts = []
661
674
  for file_id in self._files_for_main(main_id, "input"):
662
675
  counts.append(len(self._incoming_variables(file_id)))
663
- for file_id in self._files_for_main(main_id, "output"):
676
+ output_files = self._files_for_main(main_id, "output")
677
+ for file_id in output_files:
664
678
  counts.append(len(self._outgoing_variables(file_id)))
665
679
  counts.append(len(self._direct_input_variables(main_id)))
666
680
  counts.append(len(self._direct_output_variables(main_id)))
667
681
  if self.nodes[main_id].get("kind") == "source":
668
682
  counts.append(len(self._outgoing_variables(main_id)))
669
683
  max_count = max(counts or [1])
670
- return max(1.25, max_count * self.var_gap + 0.45)
684
+ height = max(1.25, max_count * self.var_gap + 0.45)
685
+ if len(output_files) > 1:
686
+ output_height = sum(self._output_file_band_height(file_id) for file_id in output_files)
687
+ output_height += (len(output_files) - 1) * _OUTPUT_FILE_GROUP_GAP
688
+ height = max(height, output_height + 2 * abs(_OUTPUT_FILE_Y_OFFSET) + 0.45)
689
+ return height
690
+
691
+ def _output_file_band_height(self, file_id: str) -> float:
692
+ return max(0.65, len(self._outgoing_variables(file_id)) * self.var_gap)
693
+
694
+ def _output_file_centers(self, file_ids: list[str], center_y: float) -> list[float]:
695
+ if len(file_ids) <= 1:
696
+ return self._spread_y(center_y, len(file_ids), 0.65)
697
+ heights = [self._output_file_band_height(file_id) for file_id in file_ids]
698
+ total_height = sum(heights) + (len(file_ids) - 1) * _OUTPUT_FILE_GROUP_GAP
699
+ top = center_y + total_height / 2.0
700
+ centers = []
701
+ for height in heights:
702
+ centers.append(top - height / 2.0)
703
+ top -= height + _OUTPUT_FILE_GROUP_GAP
704
+ return centers
671
705
 
672
706
  def _place_main_group(self, main_id: str, x: float, y: float, height: float) -> None:
673
707
  node = self.nodes[main_id]
@@ -688,14 +722,14 @@ class _ClassicGraph:
688
722
  input_files = self._files_for_main(main_id, "input")
689
723
  output_files = self._files_for_main(main_id, "output")
690
724
  input_anchor_y = y + 0.18
691
- output_anchor_y = y - 0.18
725
+ output_anchor_y = y + _OUTPUT_FILE_Y_OFFSET
692
726
  for file_id, fy in zip(input_files, self._spread_y(input_anchor_y, len(input_files), 0.65)):
693
727
  self._place_file(file_id, x - self.input_file_dx, fy, "input")
694
728
  incoming = self._incoming_variables(file_id)
695
729
  for var_id in incoming:
696
730
  if var_id not in self.variables and var_id not in self.bridges:
697
731
  self._place_variable(var_id, x - self.input_variable_dx, fy, align="right")
698
- for file_id, fy in zip(output_files, self._spread_y(output_anchor_y, len(output_files), 0.65)):
732
+ for file_id, fy in zip(output_files, self._output_file_centers(output_files, output_anchor_y)):
699
733
  self._place_file(file_id, x + self.output_file_dx, fy, "output")
700
734
  self._place_variables(self._outgoing_variables(file_id), x + self.output_variable_dx, fy, align="right")
701
735