forgegraph 0.1.0__tar.gz → 0.2.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 (232) hide show
  1. {forgegraph-0.1.0 → forgegraph-0.2.0}/CHANGELOG.md +15 -0
  2. {forgegraph-0.1.0 → forgegraph-0.2.0}/CONTRIBUTING.md +4 -2
  3. {forgegraph-0.1.0 → forgegraph-0.2.0}/PKG-INFO +5 -5
  4. {forgegraph-0.1.0 → forgegraph-0.2.0}/README.md +4 -4
  5. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/architecture.md +2 -2
  6. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/decisions.md +4 -4
  7. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/base_theme.md +1 -1
  8. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/graphspec/polarization.md +3 -0
  9. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/forgegraph_base.md +1 -1
  10. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/theme/base_theme.md +4 -2
  11. {forgegraph-0.1.0 → forgegraph-0.2.0}/pyproject.toml +4 -2
  12. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/build_recreations.py +1 -1
  13. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/run_skill_evals.py +1 -1
  14. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/visual_changelog.py +23 -6
  15. forgegraph-0.2.0/src/forgegraph/__init__.py +58 -0
  16. forgegraph-0.1.0/src/forgegraph/blank.py → forgegraph-0.2.0/src/forgegraph/canvas.py +4 -4
  17. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/examples.py +2 -2
  18. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/cli/__init__.py +2 -2
  19. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/axes.py +4 -4
  20. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/colorbar.py +4 -4
  21. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/export.py +3 -3
  22. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/figure.py +5 -5
  23. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/key.py +4 -4
  24. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/labels.py +4 -4
  25. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/layers.py +4 -4
  26. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/lint.py +3 -3
  27. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/marks.py +8 -2
  28. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/spacing.py +4 -4
  29. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/xy.py +10 -10
  30. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/graph.py +5 -5
  31. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/jobs/run.py +6 -6
  32. {forgegraph-0.1.0/src/forgegraph/layout → forgegraph-0.2.0/src/forgegraph/layouts}/__init__.py +19 -19
  33. {forgegraph-0.1.0/src/forgegraph/layout → forgegraph-0.2.0/src/forgegraph/layouts}/arrange.py +3 -3
  34. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/catalogs/themes/base_theme.yaml +1 -1
  35. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/graphspecs/polarization.yaml +3 -0
  36. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/pack.yaml +1 -1
  37. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/themes/base_theme.yaml +3 -1
  38. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/pack.yaml +1 -1
  39. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/references/themes_formats_configs.md +1 -1
  40. forgegraph-0.2.0/tests/conftest.py +30 -0
  41. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/recreations.py +1 -1
  42. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/render_helpers.py +2 -2
  43. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_axes.py +1 -0
  44. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_blank.py +1 -1
  45. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_catalog.py +3 -1
  46. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_cli.py +35 -0
  47. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_jobs.py +6 -2
  48. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_key_position.py +2 -2
  49. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_layers.py +50 -3
  50. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_layout.py +17 -2
  51. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_lint.py +2 -2
  52. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_marks.py +3 -3
  53. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_markup.py +1 -1
  54. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_packs.py +3 -2
  55. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_perturbation.py +2 -0
  56. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_regression.py +3 -1
  57. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_render.py +14 -8
  58. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_text.py +5 -5
  59. {forgegraph-0.1.0 → forgegraph-0.2.0}/uv.lock +25 -1
  60. forgegraph-0.1.0/src/forgegraph/__init__.py +0 -27
  61. forgegraph-0.1.0/tests/conftest.py +0 -14
  62. {forgegraph-0.1.0 → forgegraph-0.2.0}/.gitignore +0 -0
  63. {forgegraph-0.1.0 → forgegraph-0.2.0}/.pre-commit-config.yaml +0 -0
  64. {forgegraph-0.1.0 → forgegraph-0.2.0}/LICENSE +0 -0
  65. {forgegraph-0.1.0 → forgegraph-0.2.0}/TRADEMARKS.md +0 -0
  66. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/build-plan.md +0 -0
  67. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/forgemark-repro-reattach.md +0 -0
  68. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/forgemark-request.md +0 -0
  69. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/initial specification.md +0 -0
  70. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/base_format.md +0 -0
  71. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/graph_type_xy.md +0 -0
  72. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/layer_error_bar.md +0 -0
  73. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/layer_note.md +0 -0
  74. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/catalog/layer_reference_line.md +0 -0
  75. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/config/config.md +0 -0
  76. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/format/base_format.md +0 -0
  77. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/format/base_layout_format.md +0 -0
  78. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/graph_type/xy.md +0 -0
  79. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/layer/error_bar.md +0 -0
  80. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/layer/note.md +0 -0
  81. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/layer/reference_line.md +0 -0
  82. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/layout/base_layout.md +0 -0
  83. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/formats/slide.md +0 -0
  84. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/graphspecs/polarization-curve.md +0 -0
  85. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/layouts/dashboard.md +0 -0
  86. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/pack.md +0 -0
  87. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/schedules/workday.md +0 -0
  88. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/pack/acme/themes/acme.md +0 -0
  89. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/provenance/provenance.md +0 -0
  90. {forgegraph-0.1.0 → forgegraph-0.2.0}/docs/specs/schedule/base_schedule.md +0 -0
  91. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/formats/slide.yaml +0 -0
  92. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/graphspecs/polarization-curve.yaml +0 -0
  93. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/layouts/dashboard.yaml +0 -0
  94. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/pack.yaml +0 -0
  95. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/references/house.md +0 -0
  96. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/schedules/workday.yaml +0 -0
  97. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/packs/acme/themes/acme.yaml +0 -0
  98. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/README.md +0 -0
  99. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/air_passengers.csv +0 -0
  100. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/air_passengers_by_month.csv +0 -0
  101. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/anscombe.csv +0 -0
  102. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/assay_summary.csv +0 -0
  103. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/cell_durability.csv +0 -0
  104. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/cell_polarization.csv +0 -0
  105. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/co2_monthly.csv +0 -0
  106. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/diamonds.csv +0 -0
  107. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/dose_response.csv +0 -0
  108. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/economics.csv +0 -0
  109. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/enzyme_kinetics.csv +0 -0
  110. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/fmri.csv +0 -0
  111. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/gapminder.csv +0 -0
  112. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/geyser.csv +0 -0
  113. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/growth.csv +0 -0
  114. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/iris.csv +0 -0
  115. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/lab_export.xlsx +0 -0
  116. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/lab_runs.xlsx +0 -0
  117. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/penguins.csv +0 -0
  118. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/seaice.csv +0 -0
  119. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/sensor_log_eu.csv +0 -0
  120. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/stocks.csv +0 -0
  121. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/tips.csv +0 -0
  122. {forgegraph-0.1.0 → forgegraph-0.2.0}/examples/sample-data/weather.xlsx +0 -0
  123. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/build_api_notebook.py +0 -0
  124. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/build_bundled_skill.py +0 -0
  125. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/build_example_data.py +0 -0
  126. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/build_sample_data.py +0 -0
  127. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/check_denylist.py +0 -0
  128. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/forgemark.py +0 -0
  129. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/mirror_specs.py +0 -0
  130. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/report_skill_evals.py +0 -0
  131. {forgegraph-0.1.0 → forgegraph-0.2.0}/scripts/smoke_installed.py +0 -0
  132. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/__main__.py +0 -0
  133. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/__init__.py +0 -0
  134. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/build.py +0 -0
  135. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/files.py +0 -0
  136. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/forgemark.py +0 -0
  137. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalog/page.py +0 -0
  138. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalogs/graph_types/xy.yaml +0 -0
  139. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalogs/layers/error_bar.yaml +0 -0
  140. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalogs/layers/note.yaml +0 -0
  141. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/catalogs/layers/reference_line.yaml +0 -0
  142. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/__init__.py +0 -0
  143. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/markup.py +0 -0
  144. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/page.py +0 -0
  145. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/style.py +0 -0
  146. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/text.py +0 -0
  147. {forgegraph-0.1.0/src/forgegraph/render → forgegraph-0.2.0/src/forgegraph/drawing}/ticks.py +0 -0
  148. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/IBMPlexSerif-Bold.ttf +0 -0
  149. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/IBMPlexSerif-Light.ttf +0 -0
  150. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/IBMPlexSerif-Medium.ttf +0 -0
  151. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/IBMPlexSerif-Regular.ttf +0 -0
  152. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
  153. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/OFL-IBMPlexSerif.txt +0 -0
  154. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/OFL-Roboto.txt +0 -0
  155. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/OFL-SourceSans3.md +0 -0
  156. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-Bold.ttf +0 -0
  157. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-Italic.ttf +0 -0
  158. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-Light.ttf +0 -0
  159. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-Medium.ttf +0 -0
  160. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-Regular.ttf +0 -0
  161. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/Roboto-SemiBold.ttf +0 -0
  162. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-Bold.ttf +0 -0
  163. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-It.ttf +0 -0
  164. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-Light.ttf +0 -0
  165. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-Medium.ttf +0 -0
  166. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-Regular.ttf +0 -0
  167. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/SourceSans3-Semibold.ttf +0 -0
  168. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/fonts/__init__.py +0 -0
  169. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/graph_types/__init__.py +0 -0
  170. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/graph_types/xy.yaml +0 -0
  171. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/jobs/__init__.py +0 -0
  172. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/jobs/status.py +0 -0
  173. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/layers/error_bar.yaml +0 -0
  174. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/layers/note.yaml +0 -0
  175. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/layers/reference_line.yaml +0 -0
  176. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/__init__.py +0 -0
  177. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/config.toml +0 -0
  178. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/catalogs/formats/base_format.yaml +0 -0
  179. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/catalogs/formats/base_layout_format.yaml +0 -0
  180. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/formats/base_format.yaml +0 -0
  181. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/formats/base_layout_format.yaml +0 -0
  182. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/graphspecs/dated.parquet +0 -0
  183. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/graphspecs/fanned.parquet +0 -0
  184. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/graphspecs/polarization.parquet +0 -0
  185. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/graphspecs/reinforced.parquet +0 -0
  186. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/layouts/base_layout.yaml +0 -0
  187. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base/schedules/base_schedule.yaml +0 -0
  188. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/catalogs/formats/compact_format.yaml +0 -0
  189. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/catalogs/themes/base_theme.yaml +0 -0
  190. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/formats/compact_format.yaml +0 -0
  191. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/graphspecs/dated.parquet +0 -0
  192. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/graphspecs/example.yaml +0 -0
  193. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/graphspecs/fanned.parquet +0 -0
  194. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/graphspecs/polarization.parquet +0 -0
  195. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/graphspecs/reinforced.parquet +0 -0
  196. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/packs/forgegraph_base_2/themes/base_theme.yaml +0 -0
  197. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/__init__.py +0 -0
  198. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/SKILL.md +0 -0
  199. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/bars.yaml +0 -0
  200. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/figure.layout.yaml +0 -0
  201. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/figure.schedule.yaml +0 -0
  202. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/layers.yaml +0 -0
  203. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/xy.parquet +0 -0
  204. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/examples/xy.yaml +0 -0
  205. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/skill/forgegraph/references/requests.md +0 -0
  206. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/__init__.py +0 -0
  207. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/common.py +0 -0
  208. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/documents.py +0 -0
  209. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/layout.py +0 -0
  210. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/parameters.py +0 -0
  211. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/provenance.py +0 -0
  212. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/provenance.yaml +0 -0
  213. {forgegraph-0.1.0 → forgegraph-0.2.0}/src/forgegraph/spec/schedule.py +0 -0
  214. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/crowded_runs.png +0 -0
  215. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/flow_sweep_wide.png +0 -0
  216. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/fuel_by_size.png +0 -0
  217. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/layout_anode_layers.png +0 -0
  218. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/layout_screening_and_rate.png +0 -0
  219. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/polarization.png +0 -0
  220. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/polarization_narrow.png +0 -0
  221. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/precipitation_loglog.png +0 -0
  222. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/replicates.png +0 -0
  223. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/screening_scatter.png +0 -0
  224. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/baselines/separator_colorbar.png +0 -0
  225. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/fixtures/catalog_comments.html +0 -0
  226. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/pack_helpers.py +0 -0
  227. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/spec_mirror.py +0 -0
  228. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_denylist.py +0 -0
  229. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_schedule.py +0 -0
  230. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_smoke.py +0 -0
  231. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_spec_mirrors.py +0 -0
  232. {forgegraph-0.1.0 → forgegraph-0.2.0}/tests/test_specs.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  ForgeGraph follows [semantic versioning](https://semver.org). Before 1.0, a minor version may change how graphs look or how specs are written. From 0.1.0 on, each release's entry links its visual changelog: before and after images of every catalog card and regression graph that changed, made with `scripts/visual_changelog.py`.
4
4
 
5
+ ## 0.2.0 (2026-10-24)
6
+
7
+ Images that changed: the [visual changelog](review/visual-changelog/0.2.0.html).
8
+
9
+ ### Changed
10
+
11
+ - Error bars are black by default in both built-in themes, and their color (`layer.error_bar.color`) is a variable parameter: a GraphSpec may set `series` or any hex color.
12
+ - Three internal modules are renamed so none shares a name with the API: `forgegraph.render` is `forgegraph.drawing`, `forgegraph.layout` is `forgegraph.layouts` and `forgegraph.blank` is `forgegraph.canvas`. `import forgegraph.layouts as m` now gives the module, not `fg.layout`. The API is unchanged.
13
+
14
+ ### Fixed
15
+
16
+ - `forgegraph --version` and `--help` no longer load matplotlib and pandas, so they answer at once, even on a new install. The API loads on first use.
17
+ - The README and the skill's reference say when the main config is written: the first time a command reads the configs, such as `catalog` or `render`.
18
+ - Error bars on side-by-side bars (`series.offset: side_by_side`) are drawn on their own bars. Before, every series but the middle one had its error bars drawn where they were first placed, before layout, usually off the axes.
19
+
5
20
  ## 0.1.0 (2026-10-24)
6
21
 
7
22
  The first public release.
@@ -9,12 +9,14 @@ uv sync
9
9
  uv run pre-commit install
10
10
  ```
11
11
 
12
- The checks a pull request must pass:
12
+ While you work, `uv run pytest` runs the development suite: every test not marked slow, in parallel. The full suite adds the slow ones (every catalog card drawn, the image baselines, the catalog pages, and any test over a second). CI runs it on every pull request, and it's the one to run before a release:
13
13
 
14
14
  ```bash
15
- uv run pytest && uv run ruff check . && uv run pyright
15
+ uv run pytest --full && uv run ruff check . && uv run pyright
16
16
  ```
17
17
 
18
+ Mark a new test `@pytest.mark.slow` if it takes over a second (`uv run pytest --durations=20` lists the slowest).
19
+
18
20
  ## After changing a definition or how graphs are drawn
19
21
 
20
22
  A new or changed parameter needs its card in the matching catalog file under `src/forgegraph/catalogs/`; the build refuses a catalog that misses one.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forgegraph
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Consistent, high-quality graphs from pandas DataFrames, for people and AI agents. Early development.
5
5
  Project-URL: Repository, https://github.com/sskaye/forgegraph
6
6
  Author: Steven Kaye
@@ -123,7 +123,7 @@ Every named theme, format, GraphSpec, layout and schedule comes from a **pack**,
123
123
 
124
124
  A **config** lists the packs that may be used:
125
125
 
126
- - The main config, `~/.config/forgegraph/config.toml` (or the file `FORGEGRAPH_CONFIG` names), applies to all work. ForgeGraph writes it the first time it runs, listing the built-in packs.
126
+ - The main config, `~/.config/forgegraph/config.toml` (or the file `FORGEGRAPH_CONFIG` names), applies to all work. ForgeGraph writes it, listing the built-in packs, the first time a command reads the configs, such as `catalog` or `render`.
127
127
  - A folder config, `forgegraph.toml`, applies to the files in its folder and every folder below it. A file may use only the packs it lists.
128
128
  - A pack that a folder config lists but the main config doesn't still works, but it's non-standard: graphs drawn with it are watermarked, and ForgeGraph's commands say so. That way you can make and try a pack without changing an organization's main config.
129
129
 
@@ -169,16 +169,16 @@ uv sync && uv run pre-commit install
169
169
  ```
170
170
 
171
171
  ```bash
172
- uv run pytest && uv run ruff check . && uv run pyright
172
+ uv run pytest --full && uv run ruff check . && uv run pyright
173
173
  ```
174
174
 
175
175
  ## Repository map
176
176
 
177
177
  - **src/forgegraph/**: the package.
178
- - `graph.py`: the Python API (`fg.xy`, `fg.render`, `fg.graphspec`, `fg.load`) and the `Graph` object; `blank.py`: the themed canvas.
178
+ - `graph.py`: the Python API (`fg.xy`, `fg.render`, `fg.graphspec`, `fg.load`) and the `Graph` object; `canvas.py`: the themed canvas.
179
179
  - **cli/**: the `forgegraph` command.
180
180
  - **spec/**: reading, checking and resolving specs, and the provenance record.
181
- - **render/**: drawing, layout and lint. **layout/**: layouts. **jobs/**: scheduled renders.
181
+ - **drawing/**: drawing, layout and lint. **layouts/**: layouts. **jobs/**: scheduled renders.
182
182
  - **packs/**: packs and configs, and the built-in packs `forgegraph_base` and `forgegraph_base_2`. **fonts/**: bundled fonts and their licences.
183
183
  - **graph_types/**, **layers/**: the graph type and layer type definitions. **catalog/**, **catalogs/**: the catalog builder, and each definition's catalog file.
184
184
  - **skill/forgegraph/**: the agent skill.
@@ -98,7 +98,7 @@ Every named theme, format, GraphSpec, layout and schedule comes from a **pack**,
98
98
 
99
99
  A **config** lists the packs that may be used:
100
100
 
101
- - The main config, `~/.config/forgegraph/config.toml` (or the file `FORGEGRAPH_CONFIG` names), applies to all work. ForgeGraph writes it the first time it runs, listing the built-in packs.
101
+ - The main config, `~/.config/forgegraph/config.toml` (or the file `FORGEGRAPH_CONFIG` names), applies to all work. ForgeGraph writes it, listing the built-in packs, the first time a command reads the configs, such as `catalog` or `render`.
102
102
  - A folder config, `forgegraph.toml`, applies to the files in its folder and every folder below it. A file may use only the packs it lists.
103
103
  - A pack that a folder config lists but the main config doesn't still works, but it's non-standard: graphs drawn with it are watermarked, and ForgeGraph's commands say so. That way you can make and try a pack without changing an organization's main config.
104
104
 
@@ -144,16 +144,16 @@ uv sync && uv run pre-commit install
144
144
  ```
145
145
 
146
146
  ```bash
147
- uv run pytest && uv run ruff check . && uv run pyright
147
+ uv run pytest --full && uv run ruff check . && uv run pyright
148
148
  ```
149
149
 
150
150
  ## Repository map
151
151
 
152
152
  - **src/forgegraph/**: the package.
153
- - `graph.py`: the Python API (`fg.xy`, `fg.render`, `fg.graphspec`, `fg.load`) and the `Graph` object; `blank.py`: the themed canvas.
153
+ - `graph.py`: the Python API (`fg.xy`, `fg.render`, `fg.graphspec`, `fg.load`) and the `Graph` object; `canvas.py`: the themed canvas.
154
154
  - **cli/**: the `forgegraph` command.
155
155
  - **spec/**: reading, checking and resolving specs, and the provenance record.
156
- - **render/**: drawing, layout and lint. **layout/**: layouts. **jobs/**: scheduled renders.
156
+ - **drawing/**: drawing, layout and lint. **layouts/**: layouts. **jobs/**: scheduled renders.
157
157
  - **packs/**: packs and configs, and the built-in packs `forgegraph_base` and `forgegraph_base_2`. **fonts/**: bundled fonts and their licences.
158
158
  - **graph_types/**, **layers/**: the graph type and layer type definitions. **catalog/**, **catalogs/**: the catalog builder, and each definition's catalog file.
159
159
  - **skill/forgegraph/**: the agent skill.
@@ -502,8 +502,8 @@ forgegraph/
502
502
  │ ├── graph_types/ # xy/, bar/ — graph_type.yaml + render.py + samples/
503
503
  │ ├── layers/ # reference_line/, note/, … — layer.yaml + render.py
504
504
  │ ├── fonts/ # bundled fonts
505
- │ ├── render/ # matplotlib engine, text markup, lint, export, provenance
506
- │ ├── layout/ # multi-panel figures and dashboards
505
+ │ ├── drawing/ # matplotlib engine, text markup, lint, export, provenance
506
+ │ ├── layouts/ # multi-panel figures and dashboards
507
507
  │ ├── jobs/ # running a schedule, status files
508
508
  │ ├── packs/ # finding packs and configs; the built-in packs forgegraph_base/ and forgegraph_base_2/
509
509
  │ ├── catalog/ # HTML catalog, Forgemark carry-over
@@ -73,11 +73,11 @@ Decisions made while building that refine or depart from [architecture.md](archi
73
73
 
74
74
  **Default look, round 1 (Steven).** The default format's height is auto (the plot box keeps plot\_box.aspect) and its base font 18 pt. Tick labels: a y label's digits are centred on its tick, and the new theme parameter tick.label\_gap\_mult sets the gap from a tick to its label's digits on both axes (matplotlib measured both from a text box that reaches well above the digits). A colour bar is exactly the plot box's height, so an auto figure size can't stretch it. Tests draw at their own fixed size (150 × 140 mm, 12 pt), so tuning the defaults doesn't change what they check.
75
75
 
76
- **Text is measured to its letters (Steven, phase 3).** Matplotlib spaces text by a box sized for the font's tallest letters and deepest descenders, so its gaps aren't the gaps you see. Following the CSS `text-box: trim-both cap alphabetic` convention, Figma's vertical trim and D3's axis offsets, a text's edges run from the font's capital height to its baseline and grow only where its letters reach further (superscripts, descenders). Rows of text share one set of edges; collision checks use the letters themselves. forgegraph/render/text.py measures; render/spacing.py sets the tick-label, axis-title and title gaps from it; render/figure.py lays the figure out itself (no matplotlib layout engine), placing the plot box so what's drawn sits a fixed margin from each edge; render/key.py draws legends and style keys. New theme parameters: title.gap\_mult, axis\_title.gap\_mult; legend title\_align, frame\_width\_mult, padding\_mult (one value for every side), title\_gap\_mult, row\_gap\_mult, handle\_length\_mult, handle\_gap\_mult, inset\_mult; color\_bar width\_mult and gap\_mult (its ticks and title use the axes' settings); watermark weight, angle and span (it is centred on, and fitted to, the plot box); tick.minor\_num, minor\_length\_mult and minor\_width\_mult.
76
+ **Text is measured to its letters (Steven, phase 3).** Matplotlib spaces text by a box sized for the font's tallest letters and deepest descenders, so its gaps aren't the gaps you see. Following the CSS `text-box: trim-both cap alphabetic` convention, Figma's vertical trim and D3's axis offsets, a text's edges run from the font's capital height to its baseline and grow only where its letters reach further (superscripts, descenders). Rows of text share one set of edges; collision checks use the letters themselves. forgegraph/drawing/text.py measures; drawing/spacing.py sets the tick-label, axis-title and title gaps from it; drawing/figure.py lays the figure out itself (no matplotlib layout engine), placing the plot box so what's drawn sits a fixed margin from each edge; drawing/key.py draws legends and style keys. New theme parameters: title.gap\_mult, axis\_title.gap\_mult; legend title\_align, frame\_width\_mult, padding\_mult (one value for every side), title\_gap\_mult, row\_gap\_mult, handle\_length\_mult, handle\_gap\_mult, inset\_mult; color\_bar width\_mult and gap\_mult (its ticks and title use the axes' settings); watermark weight, angle and span (it is centred on, and fitted to, the plot box); tick.minor\_num, minor\_length\_mult and minor\_width\_mult.
77
77
 
78
78
  **Default look, round 2 (Steven).** Legend rows and the title-to-first-entry gap are about half what they were, the sample-to-name gap 20% less, and the legend title is left-aligned. series.marker\_edge\_width\_mult is 0.06. interior\_label.max\_axis\_extension is a limit: the x-axis grows by the least that fits the labels. The example GraphSpec titles its color bar by column.
79
79
 
80
- **Default look, round 3 (Steven).** Roboto (SIL Open Font License) is the default font, chosen from a comparison of 13 open fonts. Major ticks on linear axes come from a published algorithm chosen by tick.algorithm (render/ticks.py): extended\_wilkinson (Talbot, Lin & Hanrahan 2010, the default), wilkinson, heckbert, nelder (AS 96), r\_pretty or matplotlib; each gets a target label count from the axis length. Text is drawn without font hinting, so it has the same width at every resolution and what's laid out is what's saved; the box lines' half-widths count in the layout's visible extent, so the figure's margins are exact.
80
+ **Default look, round 3 (Steven).** Roboto (SIL Open Font License) is the default font, chosen from a comparison of 13 open fonts. Major ticks on linear axes come from a published algorithm chosen by tick.algorithm (drawing/ticks.py): extended\_wilkinson (Talbot, Lin & Hanrahan 2010, the default), wilkinson, heckbert, nelder (AS 96), r\_pretty or matplotlib; each gets a target label count from the axis length. Text is drawn without font hinting, so it has the same width at every resolution and what's laid out is what's saved; the box lines' half-widths count in the layout's visible extent, so the figure's margins are exact.
81
81
 
82
82
  **Parameter names and the catalog page, round 4 (Steven).** Parameters nest in categories and sub-categories to any depth (a mapping inside a category is a sub-category); no path is a prefix of another, across kinds. No parameter name says \_mult: every size, width, length and gap is × figure.base\_size. The scheme: theme font, background, title, axis (title, tick, tick.label, tick.minor), plot\_area, grid, series (line, marker, benchmark), legend (label, direct, key, color\_bar), error\_bar, watermark; format figure, output, provenance; graph type axis (x, y), encoding (color, line\_style, marker\_shape, marker\_fill, detail, order), benchmark, mark (line, marker), legend (method, key.title.text, color\_bar.title.text). Where a standard term exists it follows Vega-Lite (encoding, mark, detail), keeping ForgeGraph's nesting, snake\_case and units; Vega-Lite's names weren't adopted wholesale, since a third of the parameters have no equivalent there. "Legend" is the umbrella for every way of naming what colors and styles mean (Vega-Lite's usage); the legend methods are direct, key and none, chosen by legend.method within the theme's legend.methods. series.colors is a named palette (color\_blind, Tableau's Color Blind palette; tableau\_10; okabe\_ito; tol\_bright; tol\_muted) or a list of hex colors. legend.color\_bar.layout places a color bar's title: title\_above (the default), title\_along (reading downward, its baseline facing the bar) or below (a horizontal bar under the x-axis title). The catalog page writes allowed values tersely (float > 0, int ≥ 0, float, 0-1, hex color code, alternatives separated by |, None last, lists in []), has no baseline-value column, groups each table by category, and shows cards only for variable parameters; the perturbation test still draws every card in the catalog files. The default format saves at 600 dpi, and its output.file\_types is variable (default [png]).
83
83
 
@@ -111,13 +111,13 @@ Decisions made while building that refine or depart from [architecture.md](archi
111
111
 
112
112
  **The layout always fits its tick labels.** The layout lays out, measures the room needed around the plot box, and repeats until a pass changes the figure's size by under 0.001 in and moves the box by under 0.0001 of the figure. Tick labels can keep it from settling: a taller box gets more, wider labels, which make it shorter again. After 6 unsettled passes it lays out once more with, on each side, the most room either of the last two passes needed: an alternation has both its states there, so whichever labels the final draw has fit. Earlier passes, made while the figure was far from its size, don't count; including them left too little room on a graph that was settling slowly. Ticks are cleaned of float noise by rounding to decimal places that suit their step, not to whole multiples of it, which merged ticks that start off a multiple (1950, 1970… became 1960, 1960…).
113
113
 
114
- **Log-axis ticks come from published algorithms (Steven, phase 4).** The theme's axis.tick.log\_algorithm picks one: breaks\_log (the default), a port of R's scales::breaks\_log (ggplot2's default for log axes; MIT licence, notice in render/ticks.py), or matplotlib, matplotlib's LogLocator (powers of ten only). breaks\_log's target number of breaks is how many ticks the theme's linear algorithm places across the axis's log10 range; where it gives up (an axis within one power of ten), the linear algorithm places the ticks. On either, the labels must fit: while the drawn labels would overlap or come within the tick-label gap of each other, the algorithm is asked for one tick fewer. This replaced a rule of ForgeGraph's own (1-2-5 steps within three decades), whose labels ran together on short axes in the skill evals.
114
+ **Log-axis ticks come from published algorithms (Steven, phase 4).** The theme's axis.tick.log\_algorithm picks one: breaks\_log (the default), a port of R's scales::breaks\_log (ggplot2's default for log axes; MIT licence, notice in drawing/ticks.py), or matplotlib, matplotlib's LogLocator (powers of ten only). breaks\_log's target number of breaks is how many ticks the theme's linear algorithm places across the axis's log10 range; where it gives up (an axis within one power of ten), the linear algorithm places the ticks. On either, the labels must fit: while the drawn labels would overlap or come within the tick-label gap of each other, the algorithm is asked for one tick fewer. This replaced a rule of ForgeGraph's own (1-2-5 steps within three decades), whose labels ran together on short axes in the skill evals.
115
115
 
116
116
  ## After phase 4
117
117
 
118
118
  **The watermark belongs to the theme's author (Steven).** A GraphSpec can't change the watermark, since its parameters are fixed in the theme; a theme's author can restyle it however they like, even out of sight. An organisation that wants it required keeps those parameters fixed in its own theme. Its text must have at least one visible character, since it's drawn from its letters' outlines; a theme whose text has none is an error.
119
119
 
120
- **The figure checks itself.** A ForgeGraph figure is a GraphFigure on a GraphCanvas (render/figure.py). Whatever draws or saves it, ForgeGraph or matplotlib directly (`figure.savefig`, a notebook showing the figure), first checks whether it's still as rendering left it: the same artists, drawing the same pixels (a hash of the figure drawn at its own size and resolution). A graph found changed stays non-standard, and every drawing of it carries the watermark, which sizes itself to the plot box each time it's drawn. Every save embeds the provenance record, through one function per file type's carrier (render/export.py), unless the caller sets the carrier's own key, as the catalog does to keep its images free of metadata.
120
+ **The figure checks itself.** A ForgeGraph figure is a GraphFigure on a GraphCanvas (drawing/figure.py). Whatever draws or saves it, ForgeGraph or matplotlib directly (`figure.savefig`, a notebook showing the figure), first checks whether it's still as rendering left it: the same artists, drawing the same pixels (a hash of the figure drawn at its own size and resolution). A graph found changed stays non-standard, and every drawing of it carries the watermark, which sizes itself to the plot box each time it's drawn. Every save embeds the provenance record, through one function per file type's carrier (drawing/export.py), unless the caller sets the carrier's own key, as the catalog does to keep its images free of metadata.
121
121
 
122
122
  **provenance: off keeps the non-standard flag.** With off, a standard graph's files carry no record; a non-standard graph's carry `level: off`, the ForgeGraph version and `nonstandard: true`, so they can still be told apart. `forgegraph load` reports such a record and can't recover a GraphSpec from it.
123
123
 
@@ -196,7 +196,7 @@ variations:          *# Every parameter of the theme, by category: its
196
196
      layer:\
197
197
          error_bar:\
198
198
              cap_size: {value: 0.8, from: error_bars}          *# \[positive float\]; Cap width, × base size.*\
199
-             color: {value: "#000000", from: error_bars}          *# \[series, hex color\]; Error bar color. series: the series' color.*\
199
+             color: {value: series, from: error_bars}          *# \[series, hex color\]; Error bar color. series: the series' color.*\
200
200
              line_width: {value: 0.25, from: error_bars}          *# \[positive float\]; Line width, × base size.*\
201
201
          reference_line:\
202
202
              line_style: {value: dotted, from: target}          *# \[solid, dashed, dotted, dashdot\]; Line style.*\
@@ -127,6 +127,9 @@ variable_parameters:          *# List of variable parameters for the G
127
127
          file_types: \[png\]          *# \[png, jpg, tiff, svg, pdf, html\]; File types saved.*\
128
128
      provenance:\
129
129
          level: full          *# \[full, minimal, off\]; Which entries of the provenance record (docs/specs/provenance) each saved file carries. off: none.*\
130
+     layer:\
131
+         error_bar:\
132
+             color: "#000000"          *# \[series, hex color\]; Error bar color. series: the series' color.*\
130
133
      layers:          *# Layers drawn on the graph, each under a name of the GraphSpec's choosing, drawn in this order.*\
131
134
          target:\
132
135
              type: reference_line          *# \[layer type\]; The layer's type: one of the graph type's layer.types.*\
@@ -3,7 +3,7 @@
3
3
  This page mirrors src/forgegraph/packs/forgegraph_base/pack.yaml, which is the specification. A test fails if they ever differ.
4
4
 
5
5
  name: forgegraph_base          *# \[text\]; The pack's name, either a folder where the pack is stored or a pip pack's entry point.*\
6
- version: 0.1.0          *# \[text\]; The pack's release version.*\
6
+ version: 0.2.0          *# \[text\]; The pack's release version.*\
7
7
  description: ForgeGraph's base specs, complete, for other specs to extend or to use as they are.          *# \[text\]; One line, describing what the pack is.*\
8
8
  issues: https://github.com/sskaye/forgegraph/issues          *# \[text\]; Where to file feature requests and bug reports about the pack's specs.*\
9
9
  requires: None          *# \[None, version specifier\]; The ForgeGraph versions the pack works with, as pip writes them, e.g. ">=0.6, <1". None: any.*
@@ -99,7 +99,6 @@ fixed_parameters:          *# List of fixed parameters for the theme.*
99
99
          error_bar:\
100
100
              cap_size: 0.25          *# \[positive float\]; Cap width, × base size.*\
101
101
              line_width: 0.08          *# \[positive float\]; Line width, × base size.*\
102
-             color: series          *# \[series, hex color\]; Error bar color. series: the series' color.*\
103
102
          reference_line:\
104
103
              color: "#808080"          *# \[hex color\]; Line and label color, unless drawn per series.*\
105
104
              line_style: dashed          *# \[solid, dashed, dotted, dashdot\]; Line style.*\
@@ -156,4 +155,7 @@ variable_parameters:          *# List of variable parameters for the t
156
155
              max_relative_distance: 0.8          *# \[0-1\]; Most a label's distance to its series may be, ÷ its distance to the next nearest.*\
157
156
              max_axis_extension: 0.15          *# \[0-1\]; Most the x-axis may grow to fit labels, × its range.*\
158
157
          color_bar:\
159
-             colors: viridis          *# \[viridis, cividis, plasma, inferno, magma, greys, blues, red_blue, hex color, two hex colors\]; Colors for a numeric encoding.color: a named scale, one hex color (light to it) or two (first to second).*
158
+             colors: viridis          *# \[viridis, cividis, plasma, inferno, magma, greys, blues, red_blue, hex color, two hex colors\]; Colors for a numeric encoding.color: a named scale, one hex color (light to it) or two (first to second).*\
159
+     layer:\
160
+         error_bar:\
161
+             color: "#000000"          *# \[series, hex color\]; Error bar color. series: the series' color.*
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgegraph"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Consistent, high-quality graphs from pandas DataFrames, for people and AI agents. Early development."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -36,6 +36,7 @@ forgegraph = "forgegraph.cli:app"
36
36
  dev = [
37
37
  "pytest>=8",
38
38
  "pytest-cov>=5",
39
+ "pytest-xdist>=3.6", # tests run in parallel
39
40
  "ruff>=0.6",
40
41
  "pyright>=1.1.380",
41
42
  "pre-commit>=3.8",
@@ -74,7 +75,8 @@ typeCheckingMode = "standard"
74
75
 
75
76
  [tool.pytest.ini_options]
76
77
  testpaths = ["tests"]
77
- addopts = "--strict-markers"
78
+ addopts = "--strict-markers -n auto" # -n 0 runs them one at a time
79
+ markers = ["slow: over a second, or drawing every catalog card; run only with --full"]
78
80
 
79
81
  [tool.coverage.run]
80
82
  source = ["forgegraph"]
@@ -24,7 +24,7 @@ import tempfile # noqa: E402
24
24
  from recreations import LAYOUT_RECREATIONS, RECREATIONS # noqa: E402
25
25
 
26
26
  from forgegraph.catalog.build import write # noqa: E402
27
- from forgegraph.render.style import isolated # noqa: E402
27
+ from forgegraph.drawing.style import isolated # noqa: E402
28
28
 
29
29
  BASELINE_DPI = 72
30
30
  CSS_DPI = 96 # a CSS inch, so images show at the figure's real size
@@ -292,7 +292,7 @@ def _layout_checks(want: dict[str, Any], ws: Path, allowed: list[str]) -> list[C
292
292
  """The final layout: it draws, its tabs and tiles, a spanning tile, a blank cell, its
293
293
  column widths, and its lint."""
294
294
  import forgegraph as fg
295
- from forgegraph.layout import _load
295
+ from forgegraph.layouts import _load
296
296
  from forgegraph.spec.layout import BLANK, tabs
297
297
 
298
298
  layouts = _named(ws, ".layout.yaml")
@@ -4,8 +4,8 @@ uv run python scripts/visual_changelog.py main # main against the wor
4
4
  uv run python scripts/visual_changelog.py v0.1.0 v0.2.0 # between two commits
5
5
  [--out review/visual-changelog.html]
6
6
 
7
- Compares the regression baselines (tests/baselines/*.png) and the catalog's figures (by id),
8
- and writes one page showing each changed, added or removed image.
7
+ Compares the regression baselines (tests/baselines/*.png) and each catalog page's figures (by
8
+ id), and writes one page showing each changed, added or removed image.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  ROOT = Path(__file__).parents[1]
21
21
  BASELINES = "tests/baselines"
22
22
  CATALOG = "catalog/forgegraph_base.html"
23
- EARLIER_CATALOG = "catalog/catalog.html" # its name before phase 6
23
+ EARLIER_CATALOG = "catalog/catalog.html" # forgegraph_base's page before phase 6
24
24
  FIGURE = re.compile(r'<figure id="([^"]+)"><img [^>]*src="data:image/png;base64,([^"]+)"')
25
25
 
26
26
 
@@ -37,9 +37,18 @@ def _baselines(ref: str | None) -> dict[str, bytes]:
37
37
  }
38
38
 
39
39
 
40
- def _catalog(ref: str | None) -> dict[str, bytes]:
40
+ def _pages(ref: str | None) -> set[str]:
41
+ """The catalog pages at a commit, or in the working tree, by path."""
42
+ if ref is None:
43
+ return {f"catalog/{p.name}" for p in (ROOT / "catalog").glob("*.html")}
44
+ names = _git("ls-tree", "--name-only", f"{ref}:catalog").decode().split()
45
+ pages = {f"catalog/{n}" for n in names if n.endswith(".html")}
46
+ return {CATALOG if p == EARLIER_CATALOG else p for p in pages}
47
+
48
+
49
+ def _catalog(ref: str | None, page: str) -> dict[str, bytes]:
41
50
  text = None
42
- for name in (CATALOG, EARLIER_CATALOG):
51
+ for name in (page, EARLIER_CATALOG) if page == CATALOG else (page,):
43
52
  try:
44
53
  text = (
45
54
  (ROOT / name).read_text(encoding="utf-8")
@@ -94,7 +103,15 @@ def page(before: str, after: str | None) -> str:
94
103
  body = "\n".join(
95
104
  [
96
105
  _section("Regression baselines", "baselines", _baselines(before), _baselines(after)),
97
- _section("Catalog", "catalog", _catalog(before), _catalog(after)),
106
+ *(
107
+ _section(
108
+ f"Catalog: {Path(p).stem}",
109
+ Path(p).stem,
110
+ _catalog(before, p),
111
+ _catalog(after, p),
112
+ )
113
+ for p in sorted(_pages(before) | _pages(after))
114
+ ),
98
115
  ]
99
116
  )
100
117
  return (
@@ -0,0 +1,58 @@
1
+ """ForgeGraph: consistent, high-quality graphs from a declarative GraphSpec.
2
+
3
+ The API loads on first use, so `forgegraph --version` and `--help` don't load matplotlib and
4
+ pandas.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from importlib.metadata import PackageNotFoundError, version
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ try:
14
+ __version__ = version("forgegraph")
15
+ except PackageNotFoundError: # running from a source tree without installation
16
+ __version__ = "0.0.0"
17
+
18
+ if TYPE_CHECKING:
19
+ from forgegraph.canvas import Blank, blank
20
+ from forgegraph.graph import Graph, SpecError, graphspec, load, render, xy
21
+ from forgegraph.layouts import Layout
22
+ from forgegraph.layouts import render_layout as layout
23
+
24
+ _API = { # name: (module, attribute)
25
+ "Blank": ("forgegraph.canvas", "Blank"),
26
+ "blank": ("forgegraph.canvas", "blank"),
27
+ "Graph": ("forgegraph.graph", "Graph"),
28
+ "SpecError": ("forgegraph.graph", "SpecError"),
29
+ "graphspec": ("forgegraph.graph", "graphspec"),
30
+ "load": ("forgegraph.graph", "load"),
31
+ "render": ("forgegraph.graph", "render"),
32
+ "xy": ("forgegraph.graph", "xy"),
33
+ "Layout": ("forgegraph.layouts", "Layout"),
34
+ "layout": ("forgegraph.layouts", "render_layout"),
35
+ }
36
+
37
+
38
+ def __getattr__(name: str) -> Any:
39
+ if name not in _API:
40
+ raise AttributeError(f"module 'forgegraph' has no attribute {name!r}")
41
+ module, attribute = _API[name]
42
+ value = globals()[name] = getattr(import_module(module), attribute)
43
+ return value
44
+
45
+
46
+ __all__ = [
47
+ "Blank",
48
+ "Graph",
49
+ "Layout",
50
+ "SpecError",
51
+ "__version__",
52
+ "blank",
53
+ "graphspec",
54
+ "layout",
55
+ "load",
56
+ "render",
57
+ "xy",
58
+ ]
@@ -19,9 +19,8 @@ from typing import Any
19
19
  from matplotlib.axes import Axes
20
20
 
21
21
  from forgegraph import __version__
22
- from forgegraph.graph import SpecError, _created, _plain, spec_record
23
- from forgegraph.render import export
24
- from forgegraph.render.figure import (
22
+ from forgegraph.drawing import export
23
+ from forgegraph.drawing.figure import (
25
24
  GraphFigure,
26
25
  LayoutError,
27
26
  Watermark,
@@ -29,7 +28,8 @@ from forgegraph.render.figure import (
29
28
  layout,
30
29
  new_figure,
31
30
  )
32
- from forgegraph.render.style import PALETTES, Style, isolated
31
+ from forgegraph.drawing.style import PALETTES, Style, isolated
32
+ from forgegraph.graph import SpecError, _created, _plain, spec_record
33
33
  from forgegraph.spec.documents import (
34
34
  Registry,
35
35
  name_problems,
@@ -11,9 +11,9 @@ from dataclasses import dataclass
11
11
  from pathlib import Path
12
12
  from typing import Any
13
13
 
14
+ from forgegraph.drawing import export
15
+ from forgegraph.drawing.style import isolated
14
16
  from forgegraph.graph import Graph, data_path, render
15
- from forgegraph.render import export
16
- from forgegraph.render.style import isolated
17
17
  from forgegraph.spec.documents import Registry, builtin, resolve, resolve_layers
18
18
  from forgegraph.spec.parameters import NONE, Parameter, definitions, tree_has, tree_set
19
19
 
@@ -88,7 +88,7 @@ def render(
88
88
  several tabs saves a file per tab (name-<tab>.png), a PDF with a page per tab, and an
89
89
  HTML page with every tab."""
90
90
  import forgegraph as fg
91
- from forgegraph.layout import is_layout, stem
91
+ from forgegraph.layouts import is_layout, stem
92
92
  from forgegraph.spec.documents import nonstandard_note
93
93
 
94
94
  drawn: Any
@@ -138,7 +138,7 @@ def explain(
138
138
  """Show every value a graph is drawn with, whether it's fixed, and which file set it. For
139
139
  a layout: its own values, then each tile's, under tabs.<tab>.tiles.<tile>."""
140
140
  import forgegraph as fg
141
- from forgegraph.layout import explain_layout, is_layout, nonstandard_packs_of
141
+ from forgegraph.layouts import explain_layout, is_layout, nonstandard_packs_of
142
142
  from forgegraph.spec.documents import explain as explain_values
143
143
  from forgegraph.spec.documents import load as load_yaml
144
144
  from forgegraph.spec.documents import nonstandard_note, packs_used, registry_for, validate
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Type** (data.type): decimal, integer, datetime or categorical. auto takes it from the
4
4
  column: dates and times, then numbers (integer for an integer column), otherwise
5
- categorical. An integer axis's automatic ticks are whole numbers (forgegraph.render.ticks).
5
+ categorical. An integer axis's automatic ticks are whole numbers (forgegraph.drawing.ticks).
6
6
  - **Categories** sit at positions 0, 1, 2 … in data.order: as in the data (a pandas
7
7
  Categorical's own order), alphanumeric (natural: Run 2 before Run 10), or by the other
8
8
  axis's value (the total at each category), ascending or descending. Every value present is
@@ -43,9 +43,9 @@ from matplotlib.dates import date2num, num2date
43
43
  from matplotlib.text import Text
44
44
  from matplotlib.ticker import FixedLocator, Formatter, Locator, MultipleLocator
45
45
 
46
- from forgegraph.render.markup import literal
47
- from forgegraph.render.style import Style
48
- from forgegraph.render.ticks import LogTicks, NiceTicks, target
46
+ from forgegraph.drawing.markup import literal
47
+ from forgegraph.drawing.style import Style
48
+ from forgegraph.drawing.ticks import LogTicks, NiceTicks, target
49
49
 
50
50
 
51
51
  class DrawError(ValueError):
@@ -21,10 +21,10 @@ from matplotlib.cm import ScalarMappable
21
21
  from matplotlib.colors import Colormap, Normalize
22
22
  from matplotlib.transforms import Affine2D, Bbox, ScaledTranslation
23
23
 
24
- from forgegraph.render import spacing, text
25
- from forgegraph.render.figure import WATERMARK, texts_of, tick_marks
26
- from forgegraph.render.style import Style
27
- from forgegraph.render.ticks import NiceTicks
24
+ from forgegraph.drawing import spacing, text
25
+ from forgegraph.drawing.figure import WATERMARK, texts_of, tick_marks
26
+ from forgegraph.drawing.style import Style
27
+ from forgegraph.drawing.ticks import NiceTicks
28
28
 
29
29
  TITLE_ABOVE, TITLE_ALONG, BELOW = "title_above", "title_along", "below"
30
30
 
@@ -7,7 +7,7 @@ The record is one JSON document, stored invisibly in each file type:
7
7
  - pdf: the document information dictionary, key /ForgeGraph;
8
8
  - jpg: a JPEG comment segment;
9
9
  - tiff: the ImageDescription tag;
10
- - html: a JSON script in the page's head (render/page.py), with each figure's SVG carrying
10
+ - html: a JSON script in the page's head (drawing/page.py), with each figure's SVG carrying
11
11
  its own record too.
12
12
 
13
13
  A layout's PDF has a page per tab, and the document's record covers them all.
@@ -31,7 +31,7 @@ from matplotlib.backends.backend_pdf import PdfPages
31
31
  from matplotlib.figure import Figure
32
32
  from PIL import Image
33
33
 
34
- from forgegraph.render import page
34
+ from forgegraph.drawing import page
35
35
 
36
36
  KEY = "ForgeGraph"
37
37
  FILE_TYPES = ("png", "jpg", "tiff", "svg", "pdf", "html")
@@ -77,7 +77,7 @@ def with_record(kind: str, kwargs: dict[str, Any], record: dict[str, Any] | None
77
77
 
78
78
  def save(fig: Figure, path: Path, dpi: int) -> Path:
79
79
  """Save fig to path (its suffix is the file type). A ForgeGraph figure's canvas adds the
80
- provenance record (forgegraph.render.figure.GraphCanvas)."""
80
+ provenance record (forgegraph.drawing.figure.GraphCanvas)."""
81
81
  kind = kind_of(path)
82
82
  if kind not in IMAGES:
83
83
  raise ValueError(f"can't save as .{kind}; file types are {', '.join(FILE_TYPES)}")
@@ -1,7 +1,7 @@
1
1
  """Figures sized by the format, laid out by what's visible.
2
2
 
3
3
  Width and height are in mm. ForgeGraph lays the figure out itself rather than with
4
- matplotlib's layout engines, which space text by its box (see forgegraph.render.text): the
4
+ matplotlib's layout engines, which space text by its box (see forgegraph.drawing.text): the
5
5
  plot box is placed so that everything drawn, measured to its letters and lines, sits MARGIN
6
6
  from each edge of the figure. With both sizes given, the plot box takes the space left; with
7
7
  one auto, the plot box keeps the theme's plot_area.aspect and the figure is fitted around it.
@@ -26,10 +26,10 @@ from matplotlib.text import Text
26
26
  from matplotlib.textpath import TextPath
27
27
  from matplotlib.transforms import Affine2D, Bbox, ScaledTranslation
28
28
 
29
- from forgegraph.render import export, spacing, text
30
- from forgegraph.render.key import keys
31
- from forgegraph.render.markup import literal
32
- from forgegraph.render.style import MM_PER_INCH, Style, isolated
29
+ from forgegraph.drawing import export, spacing, text
30
+ from forgegraph.drawing.key import keys
31
+ from forgegraph.drawing.markup import literal
32
+ from forgegraph.drawing.style import MM_PER_INCH, Style, isolated
33
33
 
34
34
  MARGIN_IN = 0.04 # space kept between what's drawn and the figure's edges
35
35
  WATERMARK = "forgegraph-watermark"
@@ -1,7 +1,7 @@
1
1
  """Keys: a legend naming the series, or a style key explaining line styles and markers.
2
2
 
3
3
  ForgeGraph lays a key out itself, with every gap measured to the letters (see
4
- forgegraph.render.text): an optional title, then one row per entry, a line sample then the
4
+ forgegraph.drawing.text): an optional title, then one row per entry, a line sample then the
5
5
  entry's name. The theme's legend parameters set the frame, its padding (the same on every
6
6
  side), the gaps between title and rows and between rows, the sample's length and its gap to
7
7
  the name, and the key's inset from the plot box.
@@ -24,9 +24,9 @@ from matplotlib.patches import Rectangle
24
24
  from matplotlib.text import Text
25
25
  from matplotlib.transforms import Affine2D, Bbox, ScaledTranslation, Transform
26
26
 
27
- from forgegraph.render import text
28
- from forgegraph.render.marks import Bars
29
- from forgegraph.render.style import Style
27
+ from forgegraph.drawing import text
28
+ from forgegraph.drawing.marks import Bars
29
+ from forgegraph.drawing.style import Style
30
30
 
31
31
  # Each position: where it is on the plot box (axes fraction), and which way the key extends
32
32
  # from it: right (1), left (-1) or centred (0); down (-1), up (1) or centred (0).
@@ -29,10 +29,10 @@ from matplotlib.lines import Line2D
29
29
  from matplotlib.ticker import FixedLocator
30
30
  from matplotlib.transforms import Bbox
31
31
 
32
- from forgegraph.render import text
33
- from forgegraph.render.key import KEY, Entry, Key
34
- from forgegraph.render.marks import Bars
35
- from forgegraph.render.style import Style
32
+ from forgegraph.drawing import text
33
+ from forgegraph.drawing.key import KEY, Entry, Key
34
+ from forgegraph.drawing.marks import Bars
35
+ from forgegraph.drawing.style import Style
36
36
 
37
37
  DIRECTIONS = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (1, -1), (-1, -1)]
38
38
  SERIES_LABEL = "forgegraph-series-label"
@@ -33,10 +33,10 @@ from matplotlib.axes import Axes
33
33
  from matplotlib.text import Annotation
34
34
  from matplotlib.transforms import Bbox
35
35
 
36
- from forgegraph.render import marks
37
- from forgegraph.render.axes import DrawError
38
- from forgegraph.render.markup import markup
39
- from forgegraph.render.style import LINE_STYLES, Style, unset
36
+ from forgegraph.drawing import marks
37
+ from forgegraph.drawing.axes import DrawError
38
+ from forgegraph.drawing.markup import markup
39
+ from forgegraph.drawing.style import LINE_STYLES, Style, unset
40
40
 
41
41
  NOTE = "forgegraph-note"
42
42
  REFERENCE = "forgegraph-reference-line"
@@ -37,9 +37,9 @@ from matplotlib.lines import Line2D
37
37
  from matplotlib.text import Text
38
38
  from matplotlib.transforms import Bbox
39
39
 
40
- from forgegraph.render import text
41
- from forgegraph.render.figure import WATERMARK, texts_of
42
- from forgegraph.render.key import KEY, keys
40
+ from forgegraph.drawing import text
41
+ from forgegraph.drawing.figure import WATERMARK, texts_of
42
+ from forgegraph.drawing.key import KEY, keys
43
43
 
44
44
  MIN_TEXT_PT = 6.0 # the smallest text journals accept in a printed figure
45
45
  OVERLAP_PX = 0.5 # letters closer than this count as touching, not overlapping