plotlet 0.6.4__tar.gz → 0.7.0__tar.gz

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