plotlet 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. plotlet-0.7.0/PKG-INFO +73 -0
  2. plotlet-0.7.0/README.md +35 -0
  3. {plotlet-0.6.0 → plotlet-0.7.0}/pyproject.toml +15 -2
  4. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/__init__.py +24 -10
  5. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_coord_registry.py +13 -8
  6. plotlet-0.7.0/src/plotlet/_datasets/earth.npz +0 -0
  7. plotlet-0.7.0/src/plotlet/_json_layer.py +227 -0
  8. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_regions.py +37 -2
  9. plotlet-0.7.0/src/plotlet/_spec.py +187 -0
  10. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_tree.py +1 -1
  11. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/__init__.py +9 -3
  12. plotlet-0.7.0/src/plotlet/artists/_shared.py +389 -0
  13. plotlet-0.7.0/src/plotlet/artists/annotation_strip.py +742 -0
  14. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/bar.py +258 -161
  15. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/boxplot.py +92 -62
  16. {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.7.0/src/plotlet/artists}/chord_links.py +9 -36
  17. {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.7.0/src/plotlet/artists}/chord_ribbon.py +21 -44
  18. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/contour.py +2 -2
  19. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/dendrogram.py +43 -12
  20. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/density_1d.py +4 -4
  21. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/ecdf.py +30 -6
  22. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/errorbar.py +33 -27
  23. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/fills.py +13 -11
  24. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/freqpoly.py +4 -4
  25. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/heatmap.py +154 -79
  26. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/hexbin.py +6 -6
  27. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/hist.py +37 -26
  28. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/hist2d.py +4 -4
  29. plotlet-0.7.0/src/plotlet/artists/image_cmap.py +323 -0
  30. plotlet-0.7.0/src/plotlet/artists/image_rgba.py +236 -0
  31. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/kde_2d.py +12 -10
  32. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/line.py +56 -32
  33. {plotlet-0.6.0/src/plotlet/extensions → plotlet-0.7.0/src/plotlet/artists}/numeric_bar.py +13 -37
  34. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/pointplot.py +53 -22
  35. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/qq.py +53 -17
  36. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/references.py +23 -19
  37. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/regression.py +17 -14
  38. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/ridge.py +11 -9
  39. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/rug.py +50 -18
  40. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/scatter.py +166 -73
  41. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/shapes.py +28 -9
  42. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/strip.py +70 -44
  43. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/swarm.py +67 -42
  44. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/text.py +18 -14
  45. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/violin.py +23 -12
  46. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/cluster.py +19 -4
  47. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/datasets.py +21 -9
  48. plotlet-0.7.0/src/plotlet/describe.py +122 -0
  49. plotlet-0.7.0/src/plotlet/docs/AI_ATTRS.md +170 -0
  50. plotlet-0.7.0/src/plotlet/docs/API.md +501 -0
  51. plotlet-0.7.0/src/plotlet/docs/ARCHITECTURE.md +202 -0
  52. plotlet-0.7.0/src/plotlet/docs/COORDINATES.md +203 -0
  53. plotlet-0.7.0/src/plotlet/docs/EXTENDING.md +287 -0
  54. plotlet-0.7.0/src/plotlet/docs/PHILOSOPHY.md +125 -0
  55. plotlet-0.7.0/src/plotlet/docs/SUBPLOTS.md +149 -0
  56. plotlet-0.7.0/src/plotlet/docs/THEMES.md +195 -0
  57. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/__init__.py +11 -4
  58. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/_png.py +44 -1
  59. plotlet-0.7.0/src/plotlet/draw/_raster.py +259 -0
  60. plotlet-0.7.0/src/plotlet/draw/_simplify.py +100 -0
  61. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/colormaps.py +30 -4
  62. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/colors.py +2 -2
  63. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/font.py +30 -11
  64. plotlet-0.7.0/src/plotlet/draw/linestyles.py +58 -0
  65. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/primitives.py +56 -20
  66. plotlet-0.7.0/src/plotlet/examples/01_scatter.py +24 -0
  67. plotlet-0.7.0/src/plotlet/examples/02_line.py +16 -0
  68. plotlet-0.7.0/src/plotlet/examples/03_bar.py +20 -0
  69. plotlet-0.7.0/src/plotlet/examples/04_violin.py +21 -0
  70. plotlet-0.7.0/src/plotlet/examples/05_anscombe.py +24 -0
  71. plotlet-0.7.0/src/plotlet/examples/06_heatmap.py +22 -0
  72. plotlet-0.7.0/src/plotlet/examples/07_annotated_heatmap.py +39 -0
  73. plotlet-0.7.0/src/plotlet/examples/08_tracks.py +41 -0
  74. plotlet-0.7.0/src/plotlet/examples/09_circular.py +17 -0
  75. plotlet-0.7.0/src/plotlet/examples/10_chord.py +30 -0
  76. plotlet-0.7.0/src/plotlet/examples/12_hist.py +19 -0
  77. plotlet-0.7.0/src/plotlet/examples/13_ecdf.py +14 -0
  78. plotlet-0.7.0/src/plotlet/examples/14_box_strip.py +17 -0
  79. plotlet-0.7.0/src/plotlet/examples/15_swarm.py +15 -0
  80. plotlet-0.7.0/src/plotlet/examples/16_pointplot.py +26 -0
  81. plotlet-0.7.0/src/plotlet/examples/17_ridge.py +23 -0
  82. plotlet-0.7.0/src/plotlet/examples/18_qq.py +20 -0
  83. plotlet-0.7.0/src/plotlet/examples/19_hexbin.py +20 -0
  84. plotlet-0.7.0/src/plotlet/examples/20_kde_2d.py +23 -0
  85. plotlet-0.7.0/src/plotlet/examples/21_image_cmap.py +19 -0
  86. plotlet-0.7.0/src/plotlet/examples/22_facets.py +21 -0
  87. plotlet-0.7.0/src/plotlet/examples/23_ring_tree.py +43 -0
  88. plotlet-0.7.0/src/plotlet/examples/24_image_rgba.py +19 -0
  89. plotlet-0.7.0/src/plotlet/examples/json/01_scatter.json +42 -0
  90. plotlet-0.7.0/src/plotlet/examples/json/02_line.json +32 -0
  91. plotlet-0.7.0/src/plotlet/examples/json/03_bar.json +21 -0
  92. plotlet-0.7.0/src/plotlet/examples/json/04_violin.json +42 -0
  93. plotlet-0.7.0/src/plotlet/examples/json/05_anscombe.json +82 -0
  94. plotlet-0.7.0/src/plotlet/examples/json/06_heatmap.json +63 -0
  95. plotlet-0.7.0/src/plotlet/examples/json/07_annotated_heatmap.json +99 -0
  96. plotlet-0.7.0/src/plotlet/examples/json/08_tracks.json +81 -0
  97. plotlet-0.7.0/src/plotlet/examples/json/09_circular.json +36 -0
  98. plotlet-0.7.0/src/plotlet/examples/json/10_chord.json +99 -0
  99. plotlet-0.7.0/src/plotlet/examples/json/12_hist.json +37 -0
  100. plotlet-0.7.0/src/plotlet/examples/json/13_ecdf.json +53 -0
  101. plotlet-0.7.0/src/plotlet/examples/json/14_box_strip.json +59 -0
  102. plotlet-0.7.0/src/plotlet/examples/json/15_swarm.json +58 -0
  103. plotlet-0.7.0/src/plotlet/examples/json/16_pointplot.json +46 -0
  104. plotlet-0.7.0/src/plotlet/examples/json/17_ridge.json +34 -0
  105. plotlet-0.7.0/src/plotlet/examples/json/18_qq.json +29 -0
  106. plotlet-0.7.0/src/plotlet/examples/json/19_hexbin.json +62 -0
  107. plotlet-0.7.0/src/plotlet/examples/json/20_kde_2d.json +38 -0
  108. plotlet-0.7.0/src/plotlet/examples/json/22_facets.json +52 -0
  109. plotlet-0.7.0/src/plotlet/examples/json/23_ring_tree.json +94 -0
  110. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/layout_diagram.py +3 -3
  111. plotlet-0.7.0/src/plotlet/lint.py +263 -0
  112. plotlet-0.7.0/src/plotlet/op_help.py +508 -0
  113. plotlet-0.7.0/src/plotlet/record/__init__.py +12 -0
  114. {plotlet-0.6.0/src/plotlet → plotlet-0.7.0/src/plotlet/record}/chart.py +412 -104
  115. plotlet-0.7.0/src/plotlet/record/codegen.py +550 -0
  116. {plotlet-0.6.0/src/plotlet → plotlet-0.7.0/src/plotlet/record}/facet.py +14 -6
  117. plotlet-0.6.0/src/plotlet/_ir.py → plotlet-0.7.0/src/plotlet/record/figure_ir.py +74 -34
  118. plotlet-0.6.0/src/plotlet/_journal.py → plotlet-0.7.0/src/plotlet/record/journal.py +200 -49
  119. plotlet-0.7.0/src/plotlet/record/journal_lite.py +225 -0
  120. {plotlet-0.6.0/src/plotlet → plotlet-0.7.0/src/plotlet/record}/legend.py +1 -1
  121. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/registry.py +43 -19
  122. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/__init__.py +18 -12
  123. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_attachments.py +6 -6
  124. plotlet-0.7.0/src/plotlet/render/_chrome_bands.py +402 -0
  125. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_chrome_circular.py +81 -6
  126. plotlet-0.7.0/src/plotlet/render/_chrome_emit.py +788 -0
  127. plotlet-0.7.0/src/plotlet/render/_chrome_visibility.py +97 -0
  128. plotlet-0.6.0/src/plotlet/render/coordinates.py → plotlet-0.7.0/src/plotlet/render/_coord_circular.py +17 -343
  129. plotlet-0.7.0/src/plotlet/render/_layout_circular.py +324 -0
  130. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_layout_engine.py +206 -145
  131. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_legend.py +304 -118
  132. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_nodes.py +84 -57
  133. plotlet-0.7.0/src/plotlet/render/_resolution.py +1713 -0
  134. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/render/_validate.py +54 -19
  135. plotlet-0.7.0/src/plotlet/render/emit.py +723 -0
  136. plotlet-0.6.0/src/plotlet/render/resolved.py → plotlet-0.7.0/src/plotlet/render/resolved_ir.py +145 -106
  137. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/scales.py +137 -78
  138. plotlet-0.7.0/src/plotlet/schemas/journal_lite.schema.json +823 -0
  139. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/sectors.py +1 -1
  140. plotlet-0.7.0/src/plotlet/skill.py +40 -0
  141. plotlet-0.7.0/src/plotlet/skills/developers.md +143 -0
  142. plotlet-0.7.0/src/plotlet/skills/json.md +183 -0
  143. plotlet-0.7.0/src/plotlet/skills/users.md +220 -0
  144. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/spec.json +40 -48
  145. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/themes/dark.json +4 -6
  146. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/themes/minimal.json +6 -7
  147. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/themes/void.json +2 -1
  148. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/themes.py +6 -1
  149. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/utils.py +215 -17
  150. plotlet-0.7.0/src/plotlet.egg-info/PKG-INFO +73 -0
  151. plotlet-0.7.0/src/plotlet.egg-info/SOURCES.txt +245 -0
  152. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet.egg-info/requires.txt +1 -0
  153. plotlet-0.7.0/tests/test_aes.py +137 -0
  154. plotlet-0.7.0/tests/test_authoring_errors.py +69 -0
  155. plotlet-0.7.0/tests/test_chart_annotation_strip.py +200 -0
  156. plotlet-0.7.0/tests/test_chart_axis.py +518 -0
  157. plotlet-0.7.0/tests/test_chart_bar.py +353 -0
  158. plotlet-0.7.0/tests/test_chart_boxplot.py +105 -0
  159. plotlet-0.7.0/tests/test_chart_chord_links.py +68 -0
  160. plotlet-0.7.0/tests/test_chart_chord_ribbon.py +56 -0
  161. plotlet-0.7.0/tests/test_chart_contour.py +84 -0
  162. plotlet-0.7.0/tests/test_chart_dendrogram.py +186 -0
  163. plotlet-0.7.0/tests/test_chart_density_1d.py +59 -0
  164. plotlet-0.7.0/tests/test_chart_ecdf.py +40 -0
  165. plotlet-0.7.0/tests/test_chart_errorbar.py +57 -0
  166. plotlet-0.7.0/tests/test_chart_facets.py +237 -0
  167. plotlet-0.7.0/tests/test_chart_fills.py +119 -0
  168. plotlet-0.7.0/tests/test_chart_freqpoly.py +40 -0
  169. plotlet-0.7.0/tests/test_chart_heatmap.py +378 -0
  170. plotlet-0.7.0/tests/test_chart_hexbin.py +50 -0
  171. plotlet-0.7.0/tests/test_chart_hist.py +136 -0
  172. plotlet-0.7.0/tests/test_chart_hist2d.py +108 -0
  173. plotlet-0.7.0/tests/test_chart_image_cmap.py +284 -0
  174. plotlet-0.7.0/tests/test_chart_image_rgba.py +214 -0
  175. plotlet-0.7.0/tests/test_chart_kde_2d.py +77 -0
  176. plotlet-0.7.0/tests/test_chart_labels.py +233 -0
  177. plotlet-0.7.0/tests/test_chart_line.py +240 -0
  178. plotlet-0.7.0/tests/test_chart_numeric_bar.py +50 -0
  179. plotlet-0.7.0/tests/test_chart_pointplot.py +90 -0
  180. plotlet-0.7.0/tests/test_chart_qq.py +74 -0
  181. plotlet-0.7.0/tests/test_chart_references.py +68 -0
  182. plotlet-0.7.0/tests/test_chart_regression.py +159 -0
  183. plotlet-0.7.0/tests/test_chart_ridge.py +72 -0
  184. plotlet-0.7.0/tests/test_chart_rug.py +36 -0
  185. plotlet-0.7.0/tests/test_chart_scatter.py +200 -0
  186. plotlet-0.7.0/tests/test_chart_shapes.py +189 -0
  187. plotlet-0.7.0/tests/test_chart_spines.py +52 -0
  188. plotlet-0.7.0/tests/test_chart_strip.py +100 -0
  189. plotlet-0.7.0/tests/test_chart_swarm.py +58 -0
  190. plotlet-0.7.0/tests/test_chart_text.py +89 -0
  191. plotlet-0.7.0/tests/test_chart_violin.py +64 -0
  192. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_colors.py +32 -3
  193. plotlet-0.6.0/tests/test_circular_coordinate.py → plotlet-0.7.0/tests/test_coord_circular.py +302 -116
  194. plotlet-0.6.0/tests/test_sectors.py → plotlet-0.7.0/tests/test_coord_sectors.py +108 -101
  195. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_datasets.py +7 -1
  196. plotlet-0.7.0/tests/test_describe.py +54 -0
  197. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_fonts.py +80 -41
  198. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_formatters.py +5 -2
  199. plotlet-0.7.0/tests/test_help.py +66 -0
  200. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_import_boundary.py +120 -13
  201. plotlet-0.7.0/tests/test_io.py +188 -0
  202. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_ir.py +43 -37
  203. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_ir_resolved.py +109 -56
  204. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_ir_validate.py +60 -20
  205. plotlet-0.7.0/tests/test_journal_lite.py +188 -0
  206. plotlet-0.7.0/tests/test_journal_roundtrip.py +366 -0
  207. plotlet-0.7.0/tests/test_json_examples.py +53 -0
  208. plotlet-0.7.0/tests/test_json_schema.py +66 -0
  209. plotlet-0.6.0/tests/test_attachments.py → plotlet-0.7.0/tests/test_layout_attachments.py +152 -79
  210. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_layout_diagram.py +38 -19
  211. plotlet-0.7.0/tests/test_layout_legend.py +580 -0
  212. plotlet-0.6.0/tests/test_subplots.py → plotlet-0.7.0/tests/test_layout_subplots.py +221 -110
  213. plotlet-0.7.0/tests/test_lint.py +212 -0
  214. plotlet-0.7.0/tests/test_op_help.py +191 -0
  215. plotlet-0.7.0/tests/test_orientation.py +175 -0
  216. plotlet-0.7.0/tests/test_record_signatures.py +317 -0
  217. plotlet-0.7.0/tests/test_scale_sweep.py +189 -0
  218. plotlet-0.7.0/tests/test_skill.py +44 -0
  219. plotlet-0.7.0/tests/test_spec_ownership.py +113 -0
  220. plotlet-0.7.0/tests/test_themes.py +320 -0
  221. plotlet-0.7.0/tests/test_to_python.py +196 -0
  222. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_units.py +10 -5
  223. plotlet-0.6.0/PKG-INFO +0 -95
  224. plotlet-0.6.0/README.md +0 -58
  225. plotlet-0.6.0/src/plotlet/_json_layer.py +0 -144
  226. plotlet-0.6.0/src/plotlet/_spec.py +0 -147
  227. plotlet-0.6.0/src/plotlet/artists/_shared.py +0 -204
  228. plotlet-0.6.0/src/plotlet/artists/imshow.py +0 -237
  229. plotlet-0.6.0/src/plotlet/draw/linestyles.py +0 -27
  230. plotlet-0.6.0/src/plotlet/extensions/annotation_strip.py +0 -675
  231. plotlet-0.6.0/src/plotlet/extensions/curved_tree.py +0 -244
  232. plotlet-0.6.0/src/plotlet/lint.py +0 -130
  233. plotlet-0.6.0/src/plotlet/render/_chrome.py +0 -953
  234. plotlet-0.6.0/src/plotlet/render/core.py +0 -2100
  235. plotlet-0.6.0/src/plotlet.egg-info/PKG-INFO +0 -95
  236. plotlet-0.6.0/src/plotlet.egg-info/SOURCES.txt +0 -129
  237. plotlet-0.6.0/tests/test_chart.py +0 -3594
  238. plotlet-0.6.0/tests/test_extensions.py +0 -40
  239. plotlet-0.6.0/tests/test_help.py +0 -25
  240. plotlet-0.6.0/tests/test_io.py +0 -61
  241. plotlet-0.6.0/tests/test_journal_roundtrip.py +0 -127
  242. plotlet-0.6.0/tests/test_legend.py +0 -288
  243. plotlet-0.6.0/tests/test_lint.py +0 -23
  244. plotlet-0.6.0/tests/test_record_signatures.py +0 -75
  245. plotlet-0.6.0/tests/test_themes.py +0 -89
  246. {plotlet-0.6.0 → plotlet-0.7.0}/LICENSE +0 -0
  247. {plotlet-0.6.0 → plotlet-0.7.0}/setup.cfg +0 -0
  248. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_datasets/anscombe.csv +0 -0
  249. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_datasets/flights.csv +0 -0
  250. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_datasets/penguins.csv +0 -0
  251. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_datasets/tips.csv +0 -0
  252. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/_splits.py +0 -0
  253. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/artists/_marching.py +0 -0
  254. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/_cm_data.py +0 -0
  255. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/_css_colors.py +0 -0
  256. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Bold.ttf +0 -0
  257. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-BoldItalic.ttf +0 -0
  258. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Italic.ttf +0 -0
  259. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/Arimo-Regular.ttf +0 -0
  260. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-Bold.ttf +0 -0
  261. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-BoldOblique.ttf +0 -0
  262. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans-Oblique.ttf +0 -0
  263. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/DejaVuSans.ttf +0 -0
  264. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/LICENSE-Arimo.txt +0 -0
  265. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/fonts/LICENSE-DejaVuSans.txt +0 -0
  266. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/draw/format.py +0 -0
  267. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet/formatters.py +0 -0
  268. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet.egg-info/dependency_links.txt +0 -0
  269. {plotlet-0.6.0 → plotlet-0.7.0}/src/plotlet.egg-info/top_level.txt +0 -0
  270. {plotlet-0.6.0 → plotlet-0.7.0}/tests/test_cluster.py +0 -0
plotlet-0.7.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: plotlet
3
+ Version: 0.7.0
4
+ Summary: Python library for reproducible, multi-panel scientific figures, with byte-identical output across machines and an extension API for custom plot types.
5
+ Author: gitbamboo42
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gitbamboo42/plotlet
8
+ Project-URL: Repository, https://github.com/gitbamboo42/plotlet
9
+ Project-URL: Issues, https://github.com/gitbamboo42/plotlet/issues
10
+ Keywords: plot,svg,scientific,visualization,jupyter,reproducible
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Classifier: Topic :: Multimedia :: Graphics
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: fonttools>=4.0
26
+ Requires-Dist: scipy>=1.10
27
+ Requires-Dist: resvg_py>=0.3
28
+ Provides-Extra: dev
29
+ Requires-Dist: jupyter; extra == "dev"
30
+ Requires-Dist: nbconvert; extra == "dev"
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7; extra == "test"
33
+ Requires-Dist: pandas; extra == "test"
34
+ Requires-Dist: jsonschema; extra == "test"
35
+ Provides-Extra: pdf
36
+ Requires-Dist: cairosvg; extra == "pdf"
37
+ Dynamic: license-file
38
+
39
+ # plotlet
40
+
41
+ plotlet is a Python library for reproducible, multi-panel scientific figures, built for AI authorship: figures are written, inspected, and verified as data, not pixels. Byte-identical output across machines, a standard plotting vocabulary, built-in Cartesian and circular coordinate systems, and an easy way to add your own plot types.
42
+
43
+ ## Documentation
44
+
45
+ Online documentation is available at [gitbamboo42.github.io/plotlet](https://gitbamboo42.github.io/plotlet/).
46
+
47
+ The docs include a [tutorial](https://gitbamboo42.github.io/plotlet/tutorial.html), a [plot-type reference](https://gitbamboo42.github.io/plotlet/reference.html), a [cookbook](https://gitbamboo42.github.io/plotlet/cookbook.html), an [extensions gallery](https://gitbamboo42.github.io/plotlet/extensions.html), [deep-dive guides](https://gitbamboo42.github.io/plotlet/docs-api.html), and a [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html).
48
+
49
+ ## Dependencies
50
+
51
+ plotlet supports Python 3.10+.
52
+
53
+ Required: `fonttools`, `scipy`, `resvg_py`. numpy / pandas / polars inputs work transparently.
54
+
55
+ Optional: `cairosvg` for PDF export (`pip install plotlet[pdf]`).
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install plotlet
61
+ ```
62
+
63
+ ## For AI assistants
64
+
65
+ Tell your assistant to run plotlet's `skill()` and follow it. The guides, docs, and worked examples ship inside the installed package; the [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html) shows the full workflow.
66
+
67
+ ## Development
68
+
69
+ Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,35 @@
1
+ # plotlet
2
+
3
+ plotlet is a Python library for reproducible, multi-panel scientific figures, built for AI authorship: figures are written, inspected, and verified as data, not pixels. Byte-identical output across machines, a standard plotting vocabulary, built-in Cartesian and circular coordinate systems, and an easy way to add your own plot types.
4
+
5
+ ## Documentation
6
+
7
+ Online documentation is available at [gitbamboo42.github.io/plotlet](https://gitbamboo42.github.io/plotlet/).
8
+
9
+ The docs include a [tutorial](https://gitbamboo42.github.io/plotlet/tutorial.html), a [plot-type reference](https://gitbamboo42.github.io/plotlet/reference.html), a [cookbook](https://gitbamboo42.github.io/plotlet/cookbook.html), an [extensions gallery](https://gitbamboo42.github.io/plotlet/extensions.html), [deep-dive guides](https://gitbamboo42.github.io/plotlet/docs-api.html), and a [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html).
10
+
11
+ ## Dependencies
12
+
13
+ plotlet supports Python 3.10+.
14
+
15
+ Required: `fonttools`, `scipy`, `resvg_py`. numpy / pandas / polars inputs work transparently.
16
+
17
+ Optional: `cairosvg` for PDF export (`pip install plotlet[pdf]`).
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install plotlet
23
+ ```
24
+
25
+ ## For AI assistants
26
+
27
+ Tell your assistant to run plotlet's `skill()` and follow it. The guides, docs, and worked examples ship inside the installed package; the [page for AI agents](https://gitbamboo42.github.io/plotlet/agents.html) shows the full workflow.
28
+
29
+ ## Development
30
+
31
+ Development takes place on GitHub. Please submit bugs to the issue tracker with a reproducible example.
32
+
33
+ ## License
34
+
35
+ MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "plotlet"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Python library for reproducible, multi-panel scientific figures, with byte-identical output across machines and an extension API for custom plot types."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -48,6 +48,7 @@ dev = [
48
48
  test = [
49
49
  "pytest>=7",
50
50
  "pandas",
51
+ "jsonschema",
51
52
  ]
52
53
  pdf = [
53
54
  "cairosvg",
@@ -70,4 +71,16 @@ namespaces = true
70
71
  # draw/fonts/*.txt are the font licenses — DejaVu (Bitstream Vera–derived)
71
72
  # and Arimo (SIL OFL 1.1) both require the license text to accompany
72
73
  # redistribution, so they must ship in the wheel next to the TTFs.
73
- plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "draw/fonts/*.txt", "_datasets/*.csv"]
74
+ plotlet = ["spec.json", "themes/*.json", "draw/fonts/*.ttf", "draw/fonts/*.txt", "_datasets/*.csv", "_datasets/*.npz",
75
+ # AI onboarding: pt.skill() reads skills/, and the guides point
76
+ # at the shipped docs/ and examples/ so an installed package is
77
+ # self-sufficient — no repo clone, no web access needed.
78
+ "skills/*.md", "docs/*.md", "examples/*.py",
79
+ # JSON twins of the examples, for authoring plots as JSON
80
+ # journals (skills/json.md). Generated by
81
+ # tests/gen_json_examples.py — never edited by hand.
82
+ "examples/json/*.json",
83
+ # JSON Schema for journal-lite blobs, for editor/server-side
84
+ # validation. Generated by tests/gen_json_schema.py from
85
+ # pt.op_schema() — never edited by hand.
86
+ "schemas/*.json"]
@@ -1,15 +1,17 @@
1
1
  """plotlet — pure-Python deferred-rendering SVG plot library.
2
2
 
3
3
  import plotlet as pt
4
+ from plotlet import aes
5
+
4
6
  c = pt.chart(df, title="...", xlabel="x", ylabel="y", legend=True, gridlines=True)
5
- c.line(x="time", y="value", color="series")
7
+ c.add_line(aes(x="time", y="value", color="series"))
6
8
  c # auto-renders in Jupyter
7
9
 
8
10
  Chart methods chain for incremental composition:
9
11
 
10
12
  df = {"x": [1, 2, 3], "y": [1, 4, 9]}
11
13
  c = pt.chart()
12
- c.line(df, x="x", y="y", label="squares")
14
+ c.add_line(df, aes(x="x", y="y"), label="squares")
13
15
  c.title("Hello").legend().gridlines(True)
14
16
  c
15
17
  """
@@ -17,21 +19,31 @@ from ._spec import SPEC
17
19
  from .draw import TAB10, palette, list_palettes
18
20
  from .draw import colormap, list_colormaps, register_colormap
19
21
  from . import artists # noqa: F401 — registers built-in artists on import
20
- from .chart import Chart, Layout, chart, grid
21
- from .legend import legend
22
+ from .record.chart import Chart, Layout, chart, grid
23
+ from .utils import aes
24
+ from .record.legend import legend
22
25
  from .registry import ArtistSpec, add_artist, artist_table, declare_coord_support
23
26
  from .sectors import Sectors
24
27
  from .layout_diagram import layout_diagram
25
28
  from .themes import load_theme, list_themes, register_theme
26
- from .facet import facet, FacetGrid
29
+ from .record.facet import facet, FacetGrid
27
30
  from .datasets import load_dataset, list_datasets
28
31
  from .cluster import linkage, linkage_split, SplitTree
29
32
  from .formatters import (register_formatter, list_formatters,
30
33
  superscript, subscript)
31
34
  from ._coord_registry import register_coord_codec
32
- from ._journal import (to_journal, from_journal, to_json, from_json,
35
+ from .record.journal import (to_journal, from_journal, to_json, from_json,
33
36
  JournalNode, Journal)
34
- from ._ir import to_ir, from_ir, resolve_ir, FigureIR, IRNode
37
+ from .record.journal_lite import to_json_lite, from_json_lite
38
+ from .record.codegen import to_python
39
+ from .record.figure_ir import to_ir, from_ir, FigureIR, IRNode
40
+ from .skill import skill
41
+ # Shadows the `plotlet.lint` submodule attribute with the function —
42
+ # the os.path-style pattern: `pt.lint(c)` calls it, and
43
+ # `from plotlet.lint import lint` still resolves via sys.modules.
44
+ from .lint import lint
45
+ from .describe import describe
46
+ from .op_help import op_help, op_schema # same shadowing pattern as lint
35
47
  from . import draw, utils
36
48
 
37
49
 
@@ -41,7 +53,7 @@ def __getattr__(name):
41
53
  # the render half loads on first render (or first touch of this
42
54
  # name). Enforced by tests/test_import_boundary.py.
43
55
  if name == "CircularCoordinate":
44
- from .render.coordinates import CircularCoordinate
56
+ from .render._coord_circular import CircularCoordinate
45
57
  return CircularCoordinate
46
58
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
47
59
 
@@ -58,9 +70,11 @@ __all__ = ["chart", "Chart", "Layout", "SPEC", "TAB10",
58
70
  "linkage", "linkage_split", "SplitTree",
59
71
  "register_formatter", "list_formatters",
60
72
  "superscript", "subscript",
61
- "to_json", "from_json", "register_coord_codec",
73
+ "to_json", "from_json", "to_json_lite", "from_json_lite",
74
+ "to_python", "register_coord_codec",
62
75
  "to_journal", "from_journal", "JournalNode", "Journal",
63
- "to_ir", "from_ir", "FigureIR", "IRNode", "resolve_ir",
76
+ "to_ir", "from_ir", "FigureIR", "IRNode",
77
+ "skill", "lint", "describe", "op_help", "op_schema",
64
78
  "draw", "utils"]
65
79
 
66
80
  # Single source of truth: pyproject.toml. importlib.metadata reads it at
@@ -1,11 +1,16 @@
1
- """Coord codec registry — a neutral home for the `class_name → class` map
2
- that both `serialize.py` and `_journal.py` need for round-tripping coord
3
- objects like `CircularCoordinate`.
1
+ """Coord codec registry — the `class_name → class` map behind
2
+ `{"$coord": ...}` envelopes, for coord objects like
3
+ `CircularCoordinate`.
4
4
 
5
- Lives here rather than in either consumer so that removing one of them
6
- later (e.g. retiring the JSON tree serializer once the journal covers
7
- the same ground) doesn't leave the registry orphaned. The public API
8
- `plotlet.register_coord_codec` re-exports from this module directly.
5
+ `record/journal.py` encodes a coord object to its class name;
6
+ `_json_layer._decode` turns the name back into the class via
7
+ `resolve_coord`, which imports the built-in coord module on the first
8
+ miss so cold processes decode without a prior render. The render half
9
+ reads the map too (`render/_validate.py` cross-checks the `container`
10
+ flag, `render/resolved_ir.py` rehydrates coords), so the registry sits
11
+ at the package root, where `record/` and `render/` may both import it.
12
+ The public API `plotlet.register_coord_codec` re-exports from this
13
+ module directly.
9
14
  """
10
15
  from __future__ import annotations
11
16
 
@@ -38,7 +43,7 @@ def resolve_coord(name: str) -> type:
38
43
  return _COORD_REGISTRY[name]
39
44
  except KeyError:
40
45
  pass
41
- from .render import coordinates # noqa: F401 — registers built-ins
46
+ from .render import _coord_circular # noqa: F401 — registers built-ins
42
47
  try:
43
48
  return _COORD_REGISTRY[name]
44
49
  except KeyError:
@@ -0,0 +1,227 @@
1
+ """JSON envelope layer — shared by `record/` and `render/`.
2
+
3
+ Envelopes Python values that aren't JSON-native (tuple, set, date,
4
+ datetime, dicts with non-string keys, DataFrameLite) so they can be
5
+ dumped through `json.dumps` and rehydrated. Four consumers: the
6
+ journal's JSON form (`record/journal.py`), the FigureIR wire format
7
+ (`record/figure_ir.py`), value-envelope decoding at render hydration
8
+ (`_decode`, `render/_nodes.py`), and the resolved IR's debug view
9
+ (`render/resolved_ir.py`). It sits at the package root because both
10
+ `record/` and `render/` need it and neither may import the other
11
+ (`tests/test_import_boundary.py`). It also keeps JSON support out of
12
+ `journal.py` itself — the journal stays a plain event log.
13
+
14
+ DataFrame-shaped inputs never reach this layer: they're normalized to
15
+ `DataFrameLite` / plain lists at the recorder boundary in
16
+ `record/chart.py` (via `utils._normalize_data`), so it never imports
17
+ pandas or grows a branch per data library. numpy is the one exception:
18
+ a matrix artist's data rides through record as a 2-D ndarray (numpy is
19
+ a guaranteed dependency, via scipy), and `json_safe` lowers it to
20
+ nested lists — the JSON wire format itself carries no array types.
21
+
22
+ Envelope keys used here:
23
+ $dataframe utils.DataFrameLite (canonical DataFrame form)
24
+ $tuple tuple (JSON has no tuple type; without this every
25
+ tuple would silently degrade to list, breaking
26
+ isinstance dispatch inside artist code)
27
+ $set set
28
+ $date datetime.date
29
+ $datetime datetime.datetime
30
+ $dict_pairs dict whose keys aren't all JSON-native strings
31
+
32
+ `_decode` at the bottom handles the other envelope family — the
33
+ *reference* envelopes ($node / $coord / $sectors / $data) that journals
34
+ and IRs carry whether or not they ever touch JSON. It lives here because
35
+ both halves need it (the render tree's hydrator and the front half's
36
+ facet expansion) and it resolves only against shared vocabulary.
37
+ """
38
+ from __future__ import annotations
39
+ import json
40
+ from typing import Any
41
+
42
+ import numpy as np
43
+
44
+ from .utils import DataFrameLite, all_primitive
45
+
46
+
47
+ def json_safe(value: Any) -> Any:
48
+ """Walk `value`, replace non-JSON types with envelopes. Plotlet
49
+ envelopes ($node / $coord / $sectors) already added at `to_journal`
50
+ time pass through as regular dicts — their inner values still get
51
+ recursed."""
52
+ if value is None or isinstance(value, (bool, int, float, str)):
53
+ return value
54
+ # datetime.datetime is a subclass of datetime.date — check the more
55
+ # specific type first so datetimes get $datetime, not $date.
56
+ import datetime as _dt
57
+ if isinstance(value, _dt.datetime):
58
+ return {"$datetime": value.isoformat()}
59
+ if isinstance(value, _dt.date):
60
+ return {"$date": value.isoformat()}
61
+ if isinstance(value, DataFrameLite):
62
+ # index and cell values recurse — a date/datetime column must
63
+ # wire as `$date` / `$datetime` cells, same as anywhere else.
64
+ return {"$dataframe": {
65
+ "columns": value.columns,
66
+ "index": [json_safe(v) for v in value.index],
67
+ "values": [[json_safe(v) for v in row] for row in value.values],
68
+ }}
69
+ from .sectors import Sectors
70
+ if isinstance(value, Sectors):
71
+ # Live instance → the same envelope `to_journal` emits and
72
+ # `_decode` reconstructs (`Sectors._from_dict`). Reached by
73
+ # values that never passed through the journal emitter, e.g. a
74
+ # resolved-IR state dict.
75
+ return {"$sectors": json_safe(value._to_dict())}
76
+ if isinstance(value, np.ndarray):
77
+ # A matrix artist's data section entry. The wire format stays
78
+ # plain nested lists — identical JSON whether the matrix came
79
+ # in as an array or as lists. Recursing catches the rare
80
+ # object-dtype cell.
81
+ return json_safe(value.tolist())
82
+ if isinstance(value, tuple):
83
+ return {"$tuple": [json_safe(v) for v in value]}
84
+ if isinstance(value, (set, frozenset)):
85
+ # Iteration order is hash order, which varies across processes
86
+ # (PYTHONHASHSEED) — sort the serialized elements so $set
87
+ # payloads are byte-stable.
88
+ return {"$set": sorted((json_safe(v) for v in value), key=repr)}
89
+ if isinstance(value, dict):
90
+ if all(isinstance(k, str) for k in value):
91
+ return {k: json_safe(v) for k, v in value.items()}
92
+ # Non-string key means the whole dict can't be a JSON object;
93
+ # emit as a list of [key, value] pairs.
94
+ return {"$dict_pairs": [[json_safe(k), json_safe(v)]
95
+ for k, v in value.items()]}
96
+ if isinstance(value, list):
97
+ # Bulk numeric rows are already JSON-native — one flat scan
98
+ # instead of per-cell recursion (same bail-out in json_hydrate,
99
+ # _decode, and the journal/IR walkers).
100
+ if all_primitive(value):
101
+ return list(value)
102
+ return [json_safe(v) for v in value]
103
+ return value
104
+
105
+
106
+ def json_hydrate(value: Any) -> Any:
107
+ """Inverse of `json_safe`. Plotlet envelopes ($node / $coord /
108
+ $sectors) are left as dicts — the journal's own `_decode` handles
109
+ them at replay time."""
110
+ if isinstance(value, dict):
111
+ if "$dataframe" in value:
112
+ d = value["$dataframe"]
113
+ return DataFrameLite(
114
+ values=[[json_hydrate(v) for v in row]
115
+ for row in d["values"]],
116
+ columns=d["columns"],
117
+ index=[json_hydrate(v) for v in d["index"]],
118
+ )
119
+ if "$tuple" in value:
120
+ return tuple(json_hydrate(v) for v in value["$tuple"])
121
+ if "$set" in value:
122
+ return {json_hydrate(v) for v in value["$set"]}
123
+ if "$date" in value:
124
+ import datetime as _dt
125
+ return _dt.date.fromisoformat(value["$date"])
126
+ if "$datetime" in value:
127
+ import datetime as _dt
128
+ return _dt.datetime.fromisoformat(value["$datetime"])
129
+ if "$dict_pairs" in value:
130
+ return {json_hydrate(k): json_hydrate(v)
131
+ for k, v in value["$dict_pairs"]}
132
+ return {k: json_hydrate(v) for k, v in value.items()}
133
+ if isinstance(value, list):
134
+ if all_primitive(value):
135
+ return list(value)
136
+ return [json_hydrate(v) for v in value]
137
+ return value
138
+
139
+
140
+ _PRETTY_WIDTH = 100 # target line width for pretty_dumps
141
+
142
+
143
+ def pretty_dumps(value: Any) -> str:
144
+ """Canonical JSON text form — what `pt.to_json` returns and the
145
+ shipped example twins use. Objects and arrays stay on one line when
146
+ they fit in `_PRETTY_WIDTH` columns; a too-long dict or mixed list
147
+ expands one element per line (1-space indent steps); a too-long
148
+ list of primitives wraps at the width, continuation lines aligned
149
+ under its opening bracket — so journal entries read structurally
150
+ while data columns stay a few dense lines. Deterministic (same
151
+ value → same bytes) and whitespace-only: `json.loads` of the
152
+ result equals `json.loads(json.dumps(value))`."""
153
+ return _pretty(value, 0, "")
154
+
155
+
156
+ def _pretty(value: Any, col: int, pad: str) -> str:
157
+ """Render `value` starting at column `col`; `pad` is the
158
+ indentation of the enclosing construct."""
159
+ compact = json.dumps(value)
160
+ if col + len(compact) <= _PRETTY_WIDTH \
161
+ or not isinstance(value, (dict, list)):
162
+ return compact
163
+ inner = pad + " "
164
+ if isinstance(value, dict):
165
+ lines = []
166
+ for k, v in value.items():
167
+ key = f"{inner}{json.dumps(k)}: "
168
+ lines.append(key + _pretty(v, len(key), inner))
169
+ return "{\n" + ",\n".join(lines) + "\n" + pad + "}"
170
+ if all(not isinstance(v, (dict, list)) for v in value):
171
+ room = max(_PRETTY_WIDTH - col - 1, 20) # width left after "["
172
+ parts, lines, used = [json.dumps(v) for v in value], [], -2
173
+ for part in parts:
174
+ if used < 0 or used + 2 + len(part) > room:
175
+ lines.append([part])
176
+ used = len(part)
177
+ else:
178
+ lines[-1].append(part)
179
+ used += 2 + len(part)
180
+ joined = (",\n" + " " * (col + 1)).join(
181
+ ", ".join(line) for line in lines)
182
+ return "[" + joined + "]"
183
+ lines = [inner + _pretty(v, len(inner), inner) for v in value]
184
+ return "[\n" + ",\n".join(lines) + "\n" + pad + "]"
185
+
186
+
187
+ def _decode(value: Any, nid_to_node: dict, data_table: dict) -> Any:
188
+ """Resolve plotlet's *reference* envelopes back to live objects —
189
+ `{"$node"}` via `nid_to_node`, `{"$coord"}` via the coord registry,
190
+ `{"$sectors"}` via `Sectors`, `{"$data"}` via `data_table` (the
191
+ Journal's / FigureIR's data section). Containers recurse; everything
192
+ else passes through.
193
+
194
+ Distinct from `json_hydrate` above: that undoes the JSON-native
195
+ envelopes at the JSON boundary, while these reference envelopes live
196
+ in journals and IRs whether or not they ever touch JSON, and decode
197
+ at hydration time. Shared vocabulary — used by the render tree's
198
+ hydrator (`render.hydrate`) and by the facet expansion in
199
+ `record/figure_ir.py`."""
200
+ if isinstance(value, dict):
201
+ if "$node" in value and len(value) == 1:
202
+ return nid_to_node[value["$node"]]
203
+ if "$data" in value and len(value) == 1:
204
+ return data_table[value["$data"]]
205
+ if "$coord" in value:
206
+ from ._coord_registry import resolve_coord
207
+ cls = resolve_coord(value["$coord"])
208
+ return cls._from_dict(
209
+ _decode(value.get("kwargs", {}), nid_to_node, data_table))
210
+ if "$sectors" in value:
211
+ from .sectors import Sectors
212
+ return Sectors._from_dict(
213
+ _decode(value["$sectors"], nid_to_node, data_table))
214
+ if "$aes" in value and len(value) == 1:
215
+ from .utils import Aes
216
+ return Aes(value["$aes"])
217
+ return {k: _decode(v, nid_to_node, data_table)
218
+ for k, v in value.items()}
219
+ if isinstance(value, list):
220
+ if all_primitive(value):
221
+ return list(value)
222
+ return [_decode(v, nid_to_node, data_table) for v in value]
223
+ if isinstance(value, tuple):
224
+ if all_primitive(value):
225
+ return value
226
+ return tuple(_decode(v, nid_to_node, data_table) for v in value)
227
+ return value
@@ -15,7 +15,9 @@ The mechanism enforces this:
15
15
  When set, the primitive records its bbox under that name. When
16
16
  unset (the default), nothing is recorded — so artist `draw()`
17
17
  bodies, which never pass `tag=`, produce no regions for free.
18
- * Central code (`core.py`, `legend.py`) passes `tag="title"` /
18
+ * Central code (`render/_chrome_emit.py`, `render/emit.py`,
19
+ `render/_legend.py`)
20
+ passes `tag="title"` /
19
21
  `"spine"` / `"tick-x"` / `"legend-text"` etc. at the chrome
20
22
  emission sites. The tag is right there in the call, grep-able,
21
23
  no contextvar magic.
@@ -56,6 +58,11 @@ class _Sink:
56
58
  # offset to recorded bboxes so consumers receive outer-SVG coords
57
59
  # regardless of whether the chart is single- or multi-panel.
58
60
  translate_stack: list[tuple[float, float]] = field(default_factory=list)
61
+ # Depth of active `structural()` blocks — chrome recorded inside
62
+ # one lives inside the panel rect by design (coordinate-owned
63
+ # chrome, an inside-positioned legend), so overlap lint can tell
64
+ # it from misplaced chrome.
65
+ structural_depth: int = 0
59
66
 
60
67
  def _offset(self) -> tuple[float, float]:
61
68
  dx = dy = 0.0
@@ -73,6 +80,12 @@ class _Sink:
73
80
  # the bbox in outer-SVG coords.
74
81
  if "polygon" in meta:
75
82
  meta["polygon"] = [(px + dx, py + dy) for px, py in meta["polygon"]]
83
+ # Multi-line text ships one quad per inked line under `lines`.
84
+ if "lines" in meta:
85
+ meta["lines"] = [[(px + dx, py + dy) for px, py in quad]
86
+ for quad in meta["lines"]]
87
+ if self.structural_depth:
88
+ meta.setdefault("structural", True)
76
89
  self.regions.append(Region(kind=kind, bbox=(x + dx, y + dy, w, h),
77
90
  name=name, meta=meta))
78
91
 
@@ -85,7 +98,9 @@ _CURRENT: contextvars.ContextVar = contextvars.ContextVar(
85
98
  def record(kind: str, bbox, *, name: str, **meta) -> None:
86
99
  """Record a region into the active sink, if any. Called by
87
100
  `draw.*` primitives when they receive a `tag=` kwarg, and by
88
- central code (`core.py`, `legend.py`) for chrome that has no
101
+ central code (`render/_chrome_emit.py`, `render/emit.py`,
102
+ `render/_legend.py`)
103
+ for chrome that has no
89
104
  primitive call to wrap (panel boundary, canonical legend swatch).
90
105
  No-op when no sink is active — the common render path."""
91
106
  sink = _CURRENT.get()
@@ -124,6 +139,26 @@ def suppressed():
124
139
  _CURRENT.reset(token)
125
140
 
126
141
 
142
+ @contextmanager
143
+ def structural():
144
+ """Mark every region recorded in the block as `structural=True` —
145
+ chrome that lives inside the panel rect *by design*, so its overlap
146
+ with the panel/spines is rendering geometry, not a layout bug.
147
+ Wrapped around coordinate-owned chrome emission (a circular chart's
148
+ angular tick labels and sector walls all sit inside the data ring)
149
+ and an inside-positioned legend's body. Overlap lint skips
150
+ structural-vs-container pairs. No-op when no sink is active."""
151
+ sink = _CURRENT.get()
152
+ if sink is None:
153
+ yield
154
+ return
155
+ sink.structural_depth += 1
156
+ try:
157
+ yield
158
+ finally:
159
+ sink.structural_depth -= 1
160
+
161
+
127
162
  @contextmanager
128
163
  def translate(dx: float, dy: float):
129
164
  """Push a translate offset onto the sink's transform stack for the