glplot 0.1.4__tar.gz → 0.1.5__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 (441) hide show
  1. {glplot-0.1.4 → glplot-0.1.5}/PKG-INFO +16 -1
  2. {glplot-0.1.4 → glplot-0.1.5}/README.md +15 -0
  3. {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/layers.rst +17 -0
  4. {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/plotting.rst +11 -0
  5. glplot-0.1.5/examples/dataflow.png +0 -0
  6. glplot-0.1.5/examples/ex_function_plot.py +151 -0
  7. glplot-0.1.5/examples/ex_mandelbrot.py +160 -0
  8. glplot-0.1.5/examples/free_energy_demo/compose.py +101 -0
  9. glplot-0.1.5/examples/free_energy_demo/data.py +273 -0
  10. glplot-0.1.5/examples/free_energy_demo/glplot_version.py +608 -0
  11. glplot-0.1.5/examples/free_energy_demo/mpl_version.py +398 -0
  12. glplot-0.1.5/examples/free_energy_demo/results/glplot_free_energy.png +0 -0
  13. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/a_inset.png +0 -0
  14. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/a_main.png +0 -0
  15. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/b_inset.png +0 -0
  16. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/b_main.png +0 -0
  17. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/c_inset.png +0 -0
  18. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/c_main.png +0 -0
  19. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/d_main.png +0 -0
  20. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/e_main.png +0 -0
  21. glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/f_main.png +0 -0
  22. glplot-0.1.5/examples/free_energy_demo/results/matplotlib_free_energy.png +0 -0
  23. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/a_inset.png +0 -0
  24. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/a_main.png +0 -0
  25. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/b_inset.png +0 -0
  26. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/b_main.png +0 -0
  27. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/c_inset.png +0 -0
  28. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/c_main.png +0 -0
  29. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/d_main.png +0 -0
  30. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/e_main.png +0 -0
  31. glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/f_main.png +0 -0
  32. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/02_scatter_fill.py +2 -2
  33. glplot-0.1.5/examples/gallery/20_categorical_bar_and_spines.py +21 -0
  34. glplot-0.1.5/examples/gallery/21_date_axis_and_text_bbox.py +37 -0
  35. glplot-0.1.5/examples/gallery/22_log_scale_plots.py +21 -0
  36. glplot-0.1.5/examples/gallery/23_symlog_scale.py +23 -0
  37. glplot-0.1.5/examples/gallery/24_log_scale_bar_chart.py +18 -0
  38. glplot-0.1.5/examples/gallery/25_log_scale_contourf.py +21 -0
  39. glplot-0.1.5/examples/gallery/26_logit_scale.py +21 -0
  40. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/README.md +7 -0
  41. glplot-0.1.5/examples/gallery/results/01_line_plot.png +0 -0
  42. glplot-0.1.5/examples/gallery/results/02_scatter_fill.png +0 -0
  43. glplot-0.1.5/examples/gallery/results/07_projected_3d_cloud.png +0 -0
  44. glplot-0.1.5/examples/gallery/results/12_surface_wireframe_bar3d.png +0 -0
  45. glplot-0.1.5/examples/gallery/results/13_volumetric_nebula.png +0 -0
  46. glplot-0.1.5/examples/gallery/results/15_vector_field_3d.png +0 -0
  47. glplot-0.1.5/examples/gallery/results/16_ssao_comparison.png +0 -0
  48. glplot-0.1.5/examples/gallery/results/20_categorical_bar_and_spines.png +0 -0
  49. glplot-0.1.5/examples/gallery/results/21_date_axis_and_text_bbox.png +0 -0
  50. glplot-0.1.5/examples/gallery/results/22_log_scale_plots.png +0 -0
  51. glplot-0.1.5/examples/gallery/results/23_symlog_scale.png +0 -0
  52. glplot-0.1.5/examples/gallery/results/24_log_scale_bar_chart.png +0 -0
  53. glplot-0.1.5/examples/gallery/results/25_log_scale_contourf.png +0 -0
  54. glplot-0.1.5/examples/gallery/results/26_logit_scale.png +0 -0
  55. glplot-0.1.5/glplot/anim/__init__.py +46 -0
  56. glplot-0.1.5/glplot/anim/applier.py +832 -0
  57. glplot-0.1.5/glplot/anim/primitives.py +1418 -0
  58. glplot-0.1.5/glplot/anim/processes.py +2279 -0
  59. glplot-0.1.5/glplot/animation.py +1784 -0
  60. glplot-0.1.5/glplot/core/camera3d.py +969 -0
  61. {glplot-0.1.4 → glplot-0.1.5}/glplot/core/context.py +18 -0
  62. glplot-0.1.5/glplot/core/layers.py +655 -0
  63. glplot-0.1.5/glplot/core/layout.py +222 -0
  64. {glplot-0.1.4 → glplot-0.1.5}/glplot/core/legacy.py +7 -1
  65. glplot-0.1.5/glplot/core/panel.py +156 -0
  66. glplot-0.1.5/glplot/core/timeline.py +784 -0
  67. {glplot-0.1.4 → glplot-0.1.5}/glplot/engine.py +1517 -258
  68. glplot-0.1.5/glplot/gui/dataio.py +836 -0
  69. glplot-0.1.5/glplot/gui/dynamics.py +742 -0
  70. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/expressions.py +124 -18
  71. glplot-0.1.5/glplot/gui/generators3d.py +2297 -0
  72. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/icons.py +368 -0
  73. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/layerops.py +392 -30
  74. glplot-0.1.5/glplot/gui/layerops3d.py +978 -0
  75. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/mathops.py +55 -0
  76. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/data_editor.py +338 -6
  77. glplot-0.1.5/glplot/gui/panels/dynamics.py +687 -0
  78. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/functions.py +198 -9
  79. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/help.py +204 -0
  80. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/mathlab.py +242 -18
  81. glplot-0.1.5/glplot/gui/panels/objects3d.py +744 -0
  82. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/palette.py +7 -4
  83. glplot-0.1.5/glplot/gui/panels/presentation.py +604 -0
  84. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/scene.py +41 -8
  85. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/style.py +1155 -24
  86. glplot-0.1.5/glplot/gui/panels/timeline.py +2052 -0
  87. glplot-0.1.5/glplot/gui/panels/view3d.py +521 -0
  88. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/styles.py +738 -42
  89. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/widgets.py +547 -6
  90. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/workspace.py +650 -7
  91. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/axis.py +187 -3
  92. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/effects.py +25 -2
  93. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/hud.py +19 -6
  94. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/renderer_manager.py +67 -4
  95. {glplot-0.1.4 → glplot-0.1.5}/glplot/options.py +18 -0
  96. {glplot-0.1.4 → glplot-0.1.5}/glplot/pyplot.py +5054 -550
  97. glplot-0.1.5/glplot/renderers/axes3d.py +1171 -0
  98. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/axis.py +95 -7
  99. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/base.py +4 -0
  100. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/density.py +85 -16
  101. glplot-0.1.5/glplot/renderers/fractal.py +213 -0
  102. glplot-0.1.5/glplot/renderers/geometry3d.py +848 -0
  103. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/legend.py +14 -3
  104. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/line_family.py +123 -4
  105. glplot-0.1.5/glplot/renderers/patch.py +358 -0
  106. glplot-0.1.5/glplot/renderers/polyline.py +475 -0
  107. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/scatter.py +249 -19
  108. glplot-0.1.5/glplot/utils/anim_export.py +786 -0
  109. glplot-0.1.5/glplot/utils/blending.py +84 -0
  110. glplot-0.1.5/glplot/utils/export.py +214 -0
  111. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/preview.py +211 -18
  112. glplot-0.1.5/glplot/utils/scale.py +115 -0
  113. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/shaders.py +414 -31
  114. glplot-0.1.5/plot_1784808407.png +0 -0
  115. {glplot-0.1.4 → glplot-0.1.5}/pyproject.toml +2 -2
  116. glplot-0.1.5/tests/conftest.py +79 -0
  117. glplot-0.1.5/tests/test_anim_applier.py +704 -0
  118. glplot-0.1.5/tests/test_anim_export.py +373 -0
  119. glplot-0.1.5/tests/test_anim_primitives.py +801 -0
  120. glplot-0.1.5/tests/test_anim_processes.py +1299 -0
  121. glplot-0.1.5/tests/test_animation_api.py +836 -0
  122. glplot-0.1.5/tests/test_animation_integration.py +295 -0
  123. glplot-0.1.5/tests/test_axes3d.py +666 -0
  124. {glplot-0.1.4 → glplot-0.1.5}/tests/test_axis_margins.py +29 -1
  125. glplot-0.1.5/tests/test_blending_3d.py +439 -0
  126. glplot-0.1.5/tests/test_camera3d.py +465 -0
  127. glplot-0.1.5/tests/test_compositing_3d.py +326 -0
  128. glplot-0.1.5/tests/test_compositing_api.py +110 -0
  129. glplot-0.1.5/tests/test_dataio.py +592 -0
  130. glplot-0.1.5/tests/test_density_tint.py +170 -0
  131. glplot-0.1.5/tests/test_encodings.py +196 -0
  132. {glplot-0.1.4 → glplot-0.1.5}/tests/test_export.py +14 -3
  133. glplot-0.1.5/tests/test_fractal.py +250 -0
  134. glplot-0.1.5/tests/test_function_layer.py +710 -0
  135. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gallery_integration.py +8 -1
  136. glplot-0.1.5/tests/test_generators3d.py +203 -0
  137. glplot-0.1.5/tests/test_generators3d_extra.py +528 -0
  138. glplot-0.1.5/tests/test_gui_3d_panels.py +628 -0
  139. glplot-0.1.5/tests/test_gui_dynamics.py +335 -0
  140. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_layer_kinds.py +18 -7
  141. glplot-0.1.5/tests/test_gui_layer_params.py +129 -0
  142. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_mathlab.py +131 -0
  143. glplot-0.1.5/tests/test_gui_style_layer_editor.py +619 -0
  144. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_styles.py +32 -3
  145. glplot-0.1.5/tests/test_gui_styles_extra.py +722 -0
  146. glplot-0.1.5/tests/test_gui_timeline_panel.py +1737 -0
  147. glplot-0.1.5/tests/test_impostor_scatter_fidelity.py +229 -0
  148. glplot-0.1.5/tests/test_layerops3d.py +435 -0
  149. {glplot-0.1.4 → glplot-0.1.5}/tests/test_layers.py +57 -0
  150. {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_compat.py +889 -71
  151. glplot-0.1.5/tests/test_mpl_compat_3d.py +867 -0
  152. glplot-0.1.5/tests/test_mpl_parity_surface.py +911 -0
  153. glplot-0.1.5/tests/test_objects3d_preview.py +692 -0
  154. glplot-0.1.5/tests/test_outline.py +545 -0
  155. glplot-0.1.5/tests/test_outline_2d.py +1007 -0
  156. glplot-0.1.5/tests/test_panels.py +396 -0
  157. glplot-0.1.5/tests/test_plot_style_api.py +100 -0
  158. glplot-0.1.5/tests/test_polyline_lod.py +119 -0
  159. glplot-0.1.5/tests/test_pyplot_3d.py +387 -0
  160. glplot-0.1.5/tests/test_timeline.py +474 -0
  161. glplot-0.1.4/examples/dataflow.png +0 -0
  162. glplot-0.1.4/examples/gallery/results/01_line_plot.png +0 -0
  163. glplot-0.1.4/examples/gallery/results/02_scatter_fill.png +0 -0
  164. glplot-0.1.4/examples/gallery/results/07_projected_3d_cloud.png +0 -0
  165. glplot-0.1.4/examples/gallery/results/12_surface_wireframe_bar3d.png +0 -0
  166. glplot-0.1.4/examples/gallery/results/13_volumetric_nebula.png +0 -0
  167. glplot-0.1.4/examples/gallery/results/15_vector_field_3d.png +0 -0
  168. glplot-0.1.4/examples/gallery/results/16_ssao_comparison.png +0 -0
  169. glplot-0.1.4/glplot/core/layers.py +0 -285
  170. glplot-0.1.4/glplot/renderers/geometry3d.py +0 -223
  171. glplot-0.1.4/glplot/renderers/patch.py +0 -192
  172. glplot-0.1.4/glplot/renderers/polyline.py +0 -245
  173. glplot-0.1.4/glplot/utils/export.py +0 -161
  174. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-api-analyzer.md +0 -0
  175. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-bug-reporter.md +0 -0
  176. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-documentation-auditor.md +0 -0
  177. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-performance-profiler.md +0 -0
  178. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-release-manager.md +0 -0
  179. {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-test-runner.md +0 -0
  180. {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/build.yml +0 -0
  181. {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/lint.yml +0 -0
  182. {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/tests.yml +0 -0
  183. {glplot-0.1.4 → glplot-0.1.5}/.gitignore +0 -0
  184. {glplot-0.1.4 → glplot-0.1.5}/.gitlab-ci.yml +0 -0
  185. {glplot-0.1.4 → glplot-0.1.5}/.readthedocs.yml +0 -0
  186. {glplot-0.1.4 → glplot-0.1.5}/CHANGELOG.md +0 -0
  187. {glplot-0.1.4 → glplot-0.1.5}/CITATION.cff +0 -0
  188. {glplot-0.1.4 → glplot-0.1.5}/CODE_OF_CONDUCT.md +0 -0
  189. {glplot-0.1.4 → glplot-0.1.5}/CONTRIBUTING.md +0 -0
  190. {glplot-0.1.4 → glplot-0.1.5}/DOCUMENTATION_SETUP.md +0 -0
  191. {glplot-0.1.4 → glplot-0.1.5}/GLPlot_Architecture_and_Mathematical_Formulation.md +0 -0
  192. {glplot-0.1.4 → glplot-0.1.5}/JOSS_READINESS.md +0 -0
  193. {glplot-0.1.4 → glplot-0.1.5}/LICENSE +0 -0
  194. {glplot-0.1.4 → glplot-0.1.5}/PUBLICATION_CHECKLIST.md +0 -0
  195. {glplot-0.1.4 → glplot-0.1.5}/PUBLICATION_WORKFLOW.md +0 -0
  196. {glplot-0.1.4 → glplot-0.1.5}/READY_FOR_PUBLICATION.md +0 -0
  197. {glplot-0.1.4 → glplot-0.1.5}/build_and_upload.sh +0 -0
  198. {glplot-0.1.4 → glplot-0.1.5}/docs/Makefile +0 -0
  199. {glplot-0.1.4 → glplot-0.1.5}/docs/README.md +0 -0
  200. {glplot-0.1.4 → glplot-0.1.5}/docs/requirements.txt +0 -0
  201. {glplot-0.1.4 → glplot-0.1.5}/docs/source/_static/custom.css +0 -0
  202. {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/core.rst +0 -0
  203. {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/managers.rst +0 -0
  204. {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/utilities.rst +0 -0
  205. {glplot-0.1.4 → glplot-0.1.5}/docs/source/conf.py +0 -0
  206. {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/architecture.rst +0 -0
  207. {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/contributing.rst +0 -0
  208. {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/index.rst +0 -0
  209. {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/testing.rst +0 -0
  210. {glplot-0.1.4 → glplot-0.1.5}/docs/source/gallery/gallery.rst +0 -0
  211. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/2d-plotting.rst +0 -0
  212. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/3d-visualization.rst +0 -0
  213. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/advanced-features.rst +0 -0
  214. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/basic-plotting.rst +0 -0
  215. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/installation.rst +0 -0
  216. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/performance-tips.rst +0 -0
  217. {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/quickstart.rst +0 -0
  218. {glplot-0.1.4 → glplot-0.1.5}/docs/source/index.rst +0 -0
  219. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/01_line_density.py +0 -0
  220. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/02_gpu_comparison.py +0 -0
  221. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/03_density_quality_comparison.py +0 -0
  222. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/04_hvplot_datashader_interactive.py +0 -0
  223. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/05_pipeline_diagram.py +0 -0
  224. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_1000.png +0 -0
  225. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_10000.png +0 -0
  226. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_100000.png +0 -0
  227. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_1000000.png +0 -0
  228. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_10000000.png +0 -0
  229. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_comparison.png +0 -0
  230. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_fastplotlib.png +0 -0
  231. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_glplot.png +0 -0
  232. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_vispy.png +0 -0
  233. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10.png +0 -0
  234. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_100.png +0 -0
  235. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_1000.png +0 -0
  236. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10000.png +0 -0
  237. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_100000.png +0 -0
  238. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_1000000.png +0 -0
  239. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10000000.png +0 -0
  240. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_10.png +0 -0
  241. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_100.png +0 -0
  242. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_1000.png +0 -0
  243. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_10000.png +0 -0
  244. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_100000.png +0 -0
  245. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_1000000.png +0 -0
  246. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10.png +0 -0
  247. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100.png +0 -0
  248. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_1000.png +0 -0
  249. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10000.png +0 -0
  250. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100000.png +0 -0
  251. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_1000000.png +0 -0
  252. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10000000.png +0 -0
  253. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100000000.png +0 -0
  254. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10.png +0 -0
  255. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100.png +0 -0
  256. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_1000.png +0 -0
  257. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10000.png +0 -0
  258. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100000.png +0 -0
  259. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_1000000.png +0 -0
  260. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10000000.png +0 -0
  261. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100000000.png +0 -0
  262. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_pipeline_diagram.png +0 -0
  263. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/gpu_comparison_benchmark_results.png +0 -0
  264. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_1000.png +0 -0
  265. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_10000.png +0 -0
  266. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_100000.png +0 -0
  267. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_1000000.png +0 -0
  268. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_10000000.png +0 -0
  269. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_1000.png +0 -0
  270. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_10000.png +0 -0
  271. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_100000.png +0 -0
  272. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/line_density_benchmark_results.png +0 -0
  273. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_10.png +0 -0
  274. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_100.png +0 -0
  275. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_1000.png +0 -0
  276. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_10000.png +0 -0
  277. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_100000.png +0 -0
  278. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_1000000.png +0 -0
  279. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_10.png +0 -0
  280. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_100.png +0 -0
  281. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_1000.png +0 -0
  282. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_10000.png +0 -0
  283. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_100000.png +0 -0
  284. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10.png +0 -0
  285. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_100.png +0 -0
  286. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_1000.png +0 -0
  287. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10000.png +0 -0
  288. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_100000.png +0 -0
  289. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_1000000.png +0 -0
  290. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10000000.png +0 -0
  291. {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/vispy_interactive.py +0 -0
  292. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_aspect_ratio_fix.py +0 -0
  293. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_density.py +0 -0
  294. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_density_gain.py +0 -0
  295. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_expert_performance.py +0 -0
  296. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_full_showcase.py +0 -0
  297. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_mpl_bridge.py +0 -0
  298. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_read_density.py +0 -0
  299. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_scatter.py +0 -0
  300. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_simple.py +0 -0
  301. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_v2_layers.py +0 -0
  302. {glplot-0.1.4 → glplot-0.1.5}/examples/ex_viewports.py +0 -0
  303. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/.gitignore +0 -0
  304. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/01_line_plot.py +0 -0
  305. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/03_bar_hist.py +0 -0
  306. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/04_line_family_density.py +0 -0
  307. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/05_guides_and_colormap.py +0 -0
  308. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/06_signal_tools.py +0 -0
  309. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/07_projected_3d_cloud.py +0 -0
  310. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/08_vector_field_quiver.py +0 -0
  311. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/09_large_matrix_heatmap.py +0 -0
  312. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/10_massive_hist2d_density.py +0 -0
  313. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/11_contour_pcolormesh_field.py +0 -0
  314. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/12_surface_wireframe_bar3d.py +0 -0
  315. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/13_volumetric_nebula.py +0 -0
  316. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/14_bar3d_hex_box_city.py +0 -0
  317. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/15_vector_field_3d.py +0 -0
  318. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/16_ssao_comparison.py +0 -0
  319. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/17_square_bars3d.py +0 -0
  320. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/18_hex_bars3d.py +0 -0
  321. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/19_turbulent_vector_field_3d.py +0 -0
  322. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/_preview.py +0 -0
  323. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/matplotlib/fontlist-v330.json +0 -0
  324. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/00d5189c1d5ced4af9bb23cbaa9b1e94-le64.cache-9 +0 -0
  325. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/617a9c25f653a4b32dbf39ee041b0318-le64.cache-9 +0 -0
  326. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/84c0f976e30e948e99073af70f4ae876-le64.cache-9 +0 -0
  327. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/CACHEDIR.TAG +0 -0
  328. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/b0a71e6bf6a8a1a908413a823d76e21f-le64.cache-9 +0 -0
  329. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/c6cafe6b1a56ed8b739253d6df90ca6a-le64.cache-9 +0 -0
  330. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.gitignore +0 -0
  331. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/03_bar_hist.png +0 -0
  332. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/04_line_family_density.png +0 -0
  333. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/05_guides_and_colormap.png +0 -0
  334. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/06_signal_tools.png +0 -0
  335. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/08_vector_field_quiver.png +0 -0
  336. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/09_large_matrix_heatmap.png +0 -0
  337. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/10_massive_hist2d_density.png +0 -0
  338. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/11_contour_pcolormesh_field.png +0 -0
  339. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/14_bar3d_hex_box_city.png +0 -0
  340. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/17_square_bars3d.png +0 -0
  341. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/18_hex_bars3d.png +0 -0
  342. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/19_turbulent_vector_field_3d.png +0 -0
  343. {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/run_gallery.py +0 -0
  344. {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/01_colorful_particles_2d.py +0 -0
  345. {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/02_mandelbrot_zoom_2d.py +0 -0
  346. {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/03_spinning_torus_3d.py +0 -0
  347. {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/04_cosmic_sphere_3d.py +0 -0
  348. {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/README.md +0 -0
  349. {glplot-0.1.4 → glplot-0.1.5}/examples/verify_polyline_cmap.py +0 -0
  350. {glplot-0.1.4 → glplot-0.1.5}/glplot/__init__.py +0 -0
  351. {glplot-0.1.4 → glplot-0.1.5}/glplot/__main__.py +0 -0
  352. {glplot-0.1.4 → glplot-0.1.5}/glplot/backend.py +0 -0
  353. {glplot-0.1.4 → glplot-0.1.5}/glplot/controllers.py +0 -0
  354. {glplot-0.1.4 → glplot-0.1.5}/glplot/core/__init__.py +0 -0
  355. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/__init__.py +0 -0
  356. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/actions.py +0 -0
  357. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/app.py +0 -0
  358. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/clipboard.py +0 -0
  359. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/commands.py +0 -0
  360. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/datasets.py +0 -0
  361. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/fuzzy.py +0 -0
  362. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/history.py +0 -0
  363. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/keys.py +0 -0
  364. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/__init__.py +0 -0
  365. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/annotate.py +0 -0
  366. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/base.py +0 -0
  367. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/pipeline.py +0 -0
  368. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/selection.py +0 -0
  369. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/pipeline.py +0 -0
  370. {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/theme.py +0 -0
  371. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/__init__.py +0 -0
  372. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/hud_state.py +0 -0
  373. {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/picking.py +0 -0
  374. {glplot-0.1.4 → glplot-0.1.5}/glplot/policy.py +0 -0
  375. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/__init__.py +0 -0
  376. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/exact.py +0 -0
  377. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/interaction.py +0 -0
  378. {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/text.py +0 -0
  379. {glplot-0.1.4 → glplot-0.1.5}/glplot/scratch/__init__.py +0 -0
  380. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/__init__.py +0 -0
  381. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/gl_utils.py +0 -0
  382. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/mpl_bridge.py +0 -0
  383. {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/mpl_process.py +0 -0
  384. {glplot-0.1.4 → glplot-0.1.5}/paper.md +0 -0
  385. {glplot-0.1.4 → glplot-0.1.5}/scratch/check_gl_limits.py +0 -0
  386. {glplot-0.1.4 → glplot-0.1.5}/scratch/check_runtime_math.py +0 -0
  387. {glplot-0.1.4 → glplot-0.1.5}/scratch/diag_view.py +0 -0
  388. {glplot-0.1.4 → glplot-0.1.5}/scratch/smoke_test.py +0 -0
  389. {glplot-0.1.4 → glplot-0.1.5}/scratch/test_asymmetric_projection.py +0 -0
  390. {glplot-0.1.4 → glplot-0.1.5}/scratch/test_autoscale_all.py +0 -0
  391. {glplot-0.1.4 → glplot-0.1.5}/scratch/test_modular_export.py +0 -0
  392. {glplot-0.1.4 → glplot-0.1.5}/tests/test_api_consistency.py +0 -0
  393. {glplot-0.1.4 → glplot-0.1.5}/tests/test_axes.py +0 -0
  394. {glplot-0.1.4 → glplot-0.1.5}/tests/test_camera_anisotropy.py +0 -0
  395. {glplot-0.1.4 → glplot-0.1.5}/tests/test_camera_projections.py +0 -0
  396. {glplot-0.1.4 → glplot-0.1.5}/tests/test_edge_cases.py +0 -0
  397. {glplot-0.1.4 → glplot-0.1.5}/tests/test_effects_post.py +0 -0
  398. {glplot-0.1.4 → glplot-0.1.5}/tests/test_engine.py +0 -0
  399. {glplot-0.1.4 → glplot-0.1.5}/tests/test_geometry3d.py +0 -0
  400. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_actions.py +0 -0
  401. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_annotate.py +0 -0
  402. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_clipboard.py +0 -0
  403. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_commands.py +0 -0
  404. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_data_editor.py +0 -0
  405. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_datasets.py +0 -0
  406. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_engine_integration.py +0 -0
  407. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_expressions.py +0 -0
  408. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_fields.py +0 -0
  409. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_fuzzy.py +0 -0
  410. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_history.py +0 -0
  411. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_import_safety.py +0 -0
  412. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_impostor_alignment.py +0 -0
  413. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_keyboard_capture.py +0 -0
  414. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_keys.py +0 -0
  415. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_layer_colormaps.py +0 -0
  416. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_legend.py +0 -0
  417. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_mathops.py +0 -0
  418. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_pipeline.py +0 -0
  419. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_regression_r2.py +0 -0
  420. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_scene_delete.py +0 -0
  421. {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_scene_order.py +0 -0
  422. {glplot-0.1.4 → glplot-0.1.5}/tests/test_helpers.py +0 -0
  423. {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_bridge.py +0 -0
  424. {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_process.py +0 -0
  425. {glplot-0.1.4 → glplot-0.1.5}/tests/test_options.py +0 -0
  426. {glplot-0.1.4 → glplot-0.1.5}/tests/test_performance_benchmarks.py +0 -0
  427. {glplot-0.1.4 → glplot-0.1.5}/tests/test_pyplot.py +0 -0
  428. {glplot-0.1.4 → glplot-0.1.5}/tests/test_pyplot_integration.py +0 -0
  429. {glplot-0.1.4 → glplot-0.1.5}/tests/test_regression.py +0 -0
  430. {glplot-0.1.4 → glplot-0.1.5}/tests/test_robustness.py +0 -0
  431. {glplot-0.1.4 → glplot-0.1.5}/tests/test_scatter_renderer.py +0 -0
  432. {glplot-0.1.4 → glplot-0.1.5}/tests/test_selection_model.py +0 -0
  433. {glplot-0.1.4 → glplot-0.1.5}/tests/verify_blending_extension.py +0 -0
  434. {glplot-0.1.4 → glplot-0.1.5}/tests/verify_density.py +0 -0
  435. {glplot-0.1.4 → glplot-0.1.5}/tests/verify_headless.py +0 -0
  436. {glplot-0.1.4 → glplot-0.1.5}/tests/verify_phase4.py +0 -0
  437. {glplot-0.1.4 → glplot-0.1.5}/tests/verify_phase5_density.py +0 -0
  438. {glplot-0.1.4 → glplot-0.1.5}/tools/README.md +0 -0
  439. {glplot-0.1.4 → glplot-0.1.5}/tools/check_gl_capabilities.py +0 -0
  440. {glplot-0.1.4 → glplot-0.1.5}/tools/diagnose_camera_state.py +0 -0
  441. {glplot-0.1.4 → glplot-0.1.5}/tools/validate_runtime_math.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glplot
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: High-performance OpenGL plotting library for Python
5
5
  Project-URL: Homepage, https://github.com/AkarisDimitry/GLPlot
6
6
  Project-URL: Repository, https://github.com/AkarisDimitry/GLPlot
@@ -251,6 +251,21 @@ gplt.plot_lines(a, b, x_range=(-2, 2))
251
251
  gplt.show(density=True)
252
252
  ```
253
253
 
254
+ ## Rendering Architecture
255
+
256
+ GLPlot runs two parallel rendering pipelines — one for 2D primitives (lines, scatter, density) and one for 3D geometry (bars, surfaces, wireframes, `scatter3d`). The diagram below traces both from input data to saved figure, with the vertical rail marking where each stage runs: **CPU** setup/transfer, **GPU** shading, and the **export** readback.
257
+
258
+ ![GLPlot rendering pipeline](examples/dataflow.png)
259
+
260
+ A few things worth noting from the diagram:
261
+
262
+ - **2D density path (steps 7R–9R).** Overlapping primitives are additively accumulated into a single-channel `R32F` texture (`DENSITY_ACCUM_FS`), then resolved in one pass with a log-normalized colormap, `t = log(1 + D) / log(1 + Dmax)`, `RGB = apply_heatmap(t)` (`DENSITY_RESOLVE_FS`). `Dmax` is read back to the CPU to normalize the map.
263
+ - **3D depth cues (step 9).** Cavity/rim shading (`ao = (1 − cavity)·rim`) is computed *inline* in `GEOMETRY3D_FS` from the per-vertex normalized depth `v_z_norm` — an SSAO-style effect, not a separate screen-space pass.
264
+ - **Post-processing (2D step 10 / 3D step 11).** The scene renders into an `RGBA16F` framebuffer and passes through background → bloom → tone-map → grain before the HUD and export.
265
+ - **CPU ↔ GPU boundary.** Only staging/upload (`glBufferData`) and the final `glReadPixels` readback touch the CPU; everything between runs on the GPU.
266
+
267
+ See [GLPlot_Architecture_and_Mathematical_Formulation.md](GLPlot_Architecture_and_Mathematical_Formulation.md) for the full derivation of each stage.
268
+
254
269
  ## Example Gallery
255
270
 
256
271
  The ordered gallery lives in `examples/gallery` and writes rendered output into `examples/gallery/results`:
@@ -209,6 +209,21 @@ gplt.plot_lines(a, b, x_range=(-2, 2))
209
209
  gplt.show(density=True)
210
210
  ```
211
211
 
212
+ ## Rendering Architecture
213
+
214
+ GLPlot runs two parallel rendering pipelines — one for 2D primitives (lines, scatter, density) and one for 3D geometry (bars, surfaces, wireframes, `scatter3d`). The diagram below traces both from input data to saved figure, with the vertical rail marking where each stage runs: **CPU** setup/transfer, **GPU** shading, and the **export** readback.
215
+
216
+ ![GLPlot rendering pipeline](examples/dataflow.png)
217
+
218
+ A few things worth noting from the diagram:
219
+
220
+ - **2D density path (steps 7R–9R).** Overlapping primitives are additively accumulated into a single-channel `R32F` texture (`DENSITY_ACCUM_FS`), then resolved in one pass with a log-normalized colormap, `t = log(1 + D) / log(1 + Dmax)`, `RGB = apply_heatmap(t)` (`DENSITY_RESOLVE_FS`). `Dmax` is read back to the CPU to normalize the map.
221
+ - **3D depth cues (step 9).** Cavity/rim shading (`ao = (1 − cavity)·rim`) is computed *inline* in `GEOMETRY3D_FS` from the per-vertex normalized depth `v_z_norm` — an SSAO-style effect, not a separate screen-space pass.
222
+ - **Post-processing (2D step 10 / 3D step 11).** The scene renders into an `RGBA16F` framebuffer and passes through background → bloom → tone-map → grain before the HUD and export.
223
+ - **CPU ↔ GPU boundary.** Only staging/upload (`glBufferData`) and the final `glReadPixels` readback touch the CPU; everything between runs on the GPU.
224
+
225
+ See [GLPlot_Architecture_and_Mathematical_Formulation.md](GLPlot_Architecture_and_Mathematical_Formulation.md) for the full derivation of each stage.
226
+
212
227
  ## Example Gallery
213
228
 
214
229
  The ordered gallery lives in `examples/gallery` and writes rendered output into `examples/gallery/results`:
@@ -11,6 +11,8 @@ Internal layer abstraction for representing geometric primitives on the GPU.
11
11
  glplot.core.layers.ScatterLayer
12
12
  glplot.core.layers.PolylineLayer
13
13
  glplot.core.layers.PatchLayer
14
+ glplot.core.layers.FunctionLayer
15
+ glplot.core.layers.FractalLayer
14
16
  glplot.core.layers.TextLayer
15
17
  glplot.core.layers.Layer3D
16
18
 
@@ -41,6 +43,21 @@ Base Layer
41
43
  :members:
42
44
  :show-inheritance:
43
45
 
46
+ View-Driven Layers
47
+ ------------------
48
+
49
+ Layers whose contents are a function of the **current view** and are recomputed when it
50
+ changes, rather than sampled once at creation. See :func:`glplot.pyplot.function` and
51
+ :func:`glplot.pyplot.mandelbrot`.
52
+
53
+ .. autoclass:: glplot.core.layers.FunctionLayer
54
+ :members:
55
+ :show-inheritance:
56
+
57
+ .. autoclass:: glplot.core.layers.FractalLayer
58
+ :members:
59
+ :show-inheritance:
60
+
44
61
  3D & Text Layers
45
62
  -----------------
46
63
 
@@ -21,6 +21,17 @@ High-level pyplot-style plotting interface, compatible with Matplotlib conventio
21
21
  .. autofunction:: glplot.pyplot.vlines
22
22
  .. autofunction:: glplot.pyplot.step
23
23
 
24
+ View-Driven Plots
25
+ -----------------
26
+
27
+ Plots that are **recomputed from the current view** rather than stored as fixed samples,
28
+ so zooming reveals detail that was never computed before. These have no matplotlib
29
+ equivalent — matplotlib's zoom magnifies what it was given.
30
+
31
+ .. autofunction:: glplot.pyplot.function
32
+ .. autofunction:: glplot.pyplot.mandelbrot
33
+ .. autofunction:: glplot.pyplot.julia
34
+
24
35
  Matrix & Grid Visualization
25
36
  -----------------------------
26
37
 
Binary file
@@ -0,0 +1,151 @@
1
+ """Screen-sampled function plots — zoom in and the curve is recomputed, not magnified.
2
+
3
+ The idea
4
+ --------
5
+ ``plot(x, y)`` stores a table. Zoom into it and you magnify the samples it was given: a
6
+ thousand points over (-1, 1) hold *one* sample in the window (1e-3, 2e-3), so a deep zoom
7
+ shows a straight line where the function actually oscillates eighty times. Zoom *out* and
8
+ the opposite waste happens — a million stored points crammed into a thousand pixels.
9
+
10
+ ``function(f, xlim)`` stores the **function** and samples it against the **screen**: about
11
+ one sample per pixel column of whatever x range is currently visible, re-evaluated every
12
+ time the view moves. Three consequences, and they are the whole point:
13
+
14
+ * **constant resolution** — always exactly what the display can show;
15
+ * **constant cost** — ~2000 evaluations per frame at any zoom depth;
16
+ * **unbounded detail** — features finer than any fixed sampling resolve as you approach.
17
+
18
+ Compare with the live GPU fractal (``ex_mandelbrot.py --gpu``), the other view-driven layer
19
+ here. Both recompute from the view, but the fractal evaluates per *pixel of area*, so it
20
+ gets more expensive the deeper you go. A curve is one-dimensional: one sample per pixel
21
+ *column* is enough, so it stays cheap forever. Different geometry, different budget.
22
+
23
+ matplotlib has no equivalent — its zoom never re-samples, because there is nothing to
24
+ re-sample from. Desmos and GeoGebra work this way, and Datashader re-aggregates per view
25
+ for data, but within the scientific-Python plotting stack this is new.
26
+
27
+ When *not* to use it: measured data. There is no ``f`` to re-evaluate, and resampling
28
+ measurements would be inventing values that were never observed. ``plot`` is right there.
29
+
30
+ Run it::
31
+
32
+ python examples/ex_function_plot.py # sin(1/x) — scroll into x = 0
33
+ python examples/ex_function_plot.py --compare # the same curve, stored vs live
34
+ python examples/ex_function_plot.py --zoo # a page of hard functions
35
+ python examples/ex_function_plot.py --poles # tan and 1/x: gaps, not spikes
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import sys
41
+
42
+ import numpy as np
43
+
44
+ import glplot.pyplot as plt
45
+
46
+
47
+ def topologist_sine() -> None:
48
+ """``sin(1/x)`` — the standard test for whether a plotter is sampling or storing.
49
+
50
+ Every zoom towards the origin brings out oscillations that were not in the previous
51
+ frame, because they were never computed until you asked to look there.
52
+ """
53
+ plt.figure("sin(1/x) — scroll into the origin", figsize=(10, 6))
54
+ plt.function(lambda x: np.sin(1.0 / x), (-1.0, 1.0), ylim=(-1.3, 1.3), label="sin(1/x)")
55
+ plt.title("sin(1/x) — scroll towards x = 0; the detail is computed on arrival")
56
+ plt.xlabel("x")
57
+ plt.ylabel("y")
58
+ plt.legend()
59
+ plt.show()
60
+
61
+
62
+ def compare_stored_and_live() -> None:
63
+ """The same function, both ways, in one figure. Zoom in and they diverge.
64
+
65
+ The stored curve is a 2000-point table — a generous one, more than the live layer uses
66
+ per frame. It looks identical at the full view. Scroll towards the origin and it turns
67
+ into straight segments while the live one keeps resolving, because the table has
68
+ nothing left to show and the function always does.
69
+ """
70
+ xs = np.linspace(-1.0, 1.0, 2000)
71
+ with np.errstate(divide="ignore", invalid="ignore"):
72
+ ys = np.sin(1.0 / xs)
73
+
74
+ plt.figure("stored table vs live function", figsize=(10, 6))
75
+ plt.plot(
76
+ xs, ys, color=(0.85, 0.30, 0.25, 0.9), linewidth=3.0, label="plot() — 2000 stored points"
77
+ )
78
+ plt.function(
79
+ lambda x: np.sin(1.0 / x),
80
+ (-1.0, 1.0),
81
+ ylim=(-1.3, 1.3),
82
+ color=(0.15, 0.45, 0.75, 1.0),
83
+ linewidth=1.5,
84
+ label="function() — resampled per view",
85
+ )
86
+ plt.title("identical at this zoom — scroll into x = 0 and watch the red one give up")
87
+ plt.legend()
88
+ plt.show()
89
+
90
+
91
+ def zoo() -> None:
92
+ """Functions that punish a fixed sampling, each in its own panel."""
93
+ _fig, axes = plt.subplots(2, 2, figsize=(12, 8))
94
+
95
+ specs = [
96
+ ("sin(1/x)", lambda x: np.sin(1.0 / x), (-0.5, 0.5), (-1.3, 1.3)),
97
+ (
98
+ "x·sin(1/x) — the squeeze",
99
+ lambda x: x * np.sin(1.0 / x),
100
+ (-0.3, 0.3),
101
+ (-0.32, 0.32),
102
+ ),
103
+ (
104
+ "Weierstrass — continuous, nowhere differentiable",
105
+ lambda x: sum(0.5**k * np.cos(3.0**k * np.pi * x) for k in range(12)),
106
+ (-1.0, 1.0),
107
+ (-2.2, 2.2),
108
+ ),
109
+ (
110
+ "a chirp — frequency grows without bound",
111
+ lambda x: np.sin(x * x * 20.0),
112
+ (-4.0, 4.0),
113
+ (-1.3, 1.3),
114
+ ),
115
+ ]
116
+
117
+ for ax, (title, f, xlim, ylim) in zip(np.ravel(axes), specs):
118
+ plt.sca(ax)
119
+ plt.function(f, xlim, ylim=ylim, label=title)
120
+ plt.title(title)
121
+
122
+ plt.suptitle("each panel resamples its own function against its own view — zoom any of them")
123
+ plt.show()
124
+
125
+
126
+ def poles() -> None:
127
+ """Non-finite values become gaps, which is what makes an asymptote readable.
128
+
129
+ ``1/x`` is infinite at zero. A stored table would either miss the pole entirely or draw
130
+ a vertical line across the whole plot joining +inf to -inf. Here the infinity is turned
131
+ into a gap, so the two branches stay separate — and because ``sqrt`` is only real on
132
+ the non-negatives, ``domain=`` stops it being evaluated where it is not defined at all.
133
+ """
134
+ plt.figure("poles and domains", figsize=(10, 6))
135
+ plt.function(lambda x: 1.0 / x, (-3.0, 3.0), ylim=(-6.0, 6.0), label="1/x")
136
+ plt.function(np.tan, (-3.0, 3.0), label="tan(x)")
137
+ plt.function(np.sqrt, (-3.0, 3.0), domain=(0.0, np.inf), label="sqrt(x) — domain x ≥ 0")
138
+ plt.title("1/x breaks at the pole; sqrt is simply not evaluated below zero")
139
+ plt.legend()
140
+ plt.show()
141
+
142
+
143
+ if __name__ == "__main__":
144
+ if "--compare" in sys.argv:
145
+ compare_stored_and_live()
146
+ elif "--zoo" in sys.argv:
147
+ zoo()
148
+ elif "--poles" in sys.argv:
149
+ poles()
150
+ else:
151
+ topologist_sine()
@@ -0,0 +1,160 @@
1
+ """Mandelbrot set — the recursive escape-time fractal, plotted and animated.
2
+
3
+ The Mandelbrot set is the set of complex numbers ``c`` for which the recursion
4
+
5
+ z_{n+1} = z_n^2 + c, z_0 = 0
6
+
7
+ stays bounded forever. In practice a point is "in" the set if ``|z_n|`` has not exceeded 2
8
+ after some iteration budget, and the *colour* of a point outside the set is how many
9
+ iterations it took to escape — the escape time. That is the recursion the title asks for:
10
+ one line, applied over and over, per point.
11
+
12
+ How each point could be computed on the GPU
13
+ --------------------------------------------
14
+ The escape loop is embarrassingly parallel: every pixel is independent, so the natural GPU
15
+ form is a **fragment shader** that runs the whole ``z = z*z + c`` loop for its own pixel
16
+ and writes the escape count as the colour. That is how a real-time Mandelbrot explorer
17
+ works — millions of pixels iterate at once, and a deep zoom stays interactive.
18
+
19
+ GLPlot does not ship that shader yet, so this example computes the escape counts on the
20
+ **CPU, vectorised with numpy** — every pixel of the grid iterated together as one array
21
+ operation, which is the same data-parallel idea one step short of the GPU — and then hands
22
+ the finished grid to GLPlot's GPU renderer as an image. The maths is identical; only *where
23
+ the loop runs* differs. The building block for a GPU port already lives in
24
+ ``glplot.anim.processes`` as the ``mandelbrot_zoom`` process, whose ``mandelbrot_frame(t)``
25
+ is exactly the per-frame escape grid a fragment shader would produce.
26
+
27
+ There are now **two** ways to draw a Mandelbrot in GLPlot, and this file shows both:
28
+
29
+ * **CPU-baked** (``plot_still`` / ``animate_zoom`` below): compute the escape grid with
30
+ numpy and hand the finished image to the renderer. Simple, portable, and what you do for
31
+ any field you can only express in Python — but the pixels are fixed once computed, so
32
+ zooming just magnifies them.
33
+ * **Live on the GPU** (``--gpu``): ``gplt.mandelbrot()`` / ``gplt.julia()`` draw a
34
+ ``fractal`` layer whose fragment shader runs the escape loop per screen pixel every
35
+ frame. Scroll to zoom and the boundary refines to the new scale on its own — this is the
36
+ "recompute on zoom for more precision" version, and it is real-time. It is the GPU port
37
+ the "how could each be computed on the GPU?" question is about.
38
+
39
+ Run it::
40
+
41
+ python examples/ex_mandelbrot.py # CPU-baked still, coloured by escape time
42
+ python examples/ex_mandelbrot.py --zoom # CPU-baked animated zoom into a seahorse
43
+ python examples/ex_mandelbrot.py --gpu # LIVE GPU set — scroll to zoom, it refines
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import sys
49
+
50
+ import numpy as np
51
+
52
+ import glplot.pyplot as plt
53
+
54
+
55
+ def mandelbrot(width: int, height: int, centre: complex, span: float, max_iter: int):
56
+ """The escape-time grid over a window of the complex plane.
57
+
58
+ Vectorised: the whole grid iterates as one numpy operation per step, so the Python
59
+ loop runs ``max_iter`` times total, not ``max_iter * width * height`` times. A GPU
60
+ fragment shader would drop even the ``max_iter`` loop onto the hardware and run every
61
+ pixel's iterations concurrently — this is the CPU shadow of that.
62
+
63
+ Returns ``(extent, smooth_counts)``: the ``(x0, x1, y0, y1)`` the window covers and a
64
+ ``(height, width)`` array of smoothed escape values (continuous, so the colour bands do
65
+ not stair-step).
66
+ """
67
+ aspect = width / height
68
+ re = np.linspace(centre.real - span * aspect, centre.real + span * aspect, width)
69
+ im = np.linspace(centre.imag - span, centre.imag + span, height)
70
+ c = re[None, :] + 1j * im[:, None]
71
+
72
+ z = np.zeros_like(c)
73
+ count = np.zeros(c.shape, dtype=np.float64)
74
+ alive = np.ones(c.shape, dtype=bool)
75
+
76
+ for i in range(max_iter):
77
+ z[alive] = z[alive] * z[alive] + c[alive]
78
+ escaped = alive & (z.real * z.real + z.imag * z.imag > 4.0)
79
+ # Smooth (fractional) escape count: the standard renormalisation that turns the
80
+ # integer iteration bands into a continuous field, so the colormap does not
81
+ # stair-step. ``i`` is how many times the recursion has been applied so far.
82
+ zabs = np.abs(z[escaped])
83
+ count[escaped] = (i + 1) - np.log2(np.log(zabs + 1e-12))
84
+ alive[escaped] = False
85
+ if not alive.any():
86
+ break
87
+
88
+ count[alive] = float(max_iter) # points still bounded: treat as "in the set"
89
+ extent = (re[0], re[-1], im[0], im[-1])
90
+ return extent, count
91
+
92
+
93
+ def plot_still() -> None:
94
+ """A single high-detail Mandelbrot, coloured by escape time."""
95
+ extent, counts = mandelbrot(900, 700, centre=-0.6 + 0.0j, span=1.25, max_iter=200)
96
+ plt.figure("Mandelbrot set", figsize=(9, 7))
97
+ # log1p compresses the huge dynamic range near the boundary so the filaments show.
98
+ plt.imshow(
99
+ np.log1p(counts),
100
+ extent=extent,
101
+ origin="lower",
102
+ cmap="magma",
103
+ aspect="equal",
104
+ )
105
+ plt.title("Mandelbrot set — coloured by escape time")
106
+ plt.xlabel("Re(c)")
107
+ plt.ylabel("Im(c)")
108
+ plt.show()
109
+
110
+
111
+ def animate_zoom() -> None:
112
+ """Zoom into a seahorse-valley point, one frame per escape grid.
113
+
114
+ Uses ``glplot.animation`` (the matplotlib-compatible surface): a ``FuncAnimation`` whose
115
+ update recomputes the grid at a smaller span each frame. Every frame is a fresh
116
+ escape-time computation — the discrete progression the animation model calls a
117
+ *step* rather than a blend, because there is no meaningful "half a Mandelbrot frame".
118
+ """
119
+ import glplot.animation as animation
120
+
121
+ target = -0.743643887037151 + 0.13182590420533j # a classic seahorse spiral
122
+ fig = plt.figure("Mandelbrot zoom", figsize=(8, 8))
123
+ frames = 120
124
+
125
+ def update(frame: int):
126
+ span = 1.4 * (0.94**frame) # geometric zoom: constant magnification per frame
127
+ max_iter = int(120 + frame * 6) # deeper zoom needs more iterations to resolve
128
+ extent, counts = mandelbrot(500, 500, centre=target, span=span, max_iter=max_iter)
129
+ plt.cla()
130
+ plt.imshow(np.log1p(counts), extent=extent, origin="lower", cmap="turbo", aspect="equal")
131
+ plt.title(f"zoom {1.4 / span:,.0f}x ({max_iter} iterations)")
132
+ return []
133
+
134
+ ani = animation.FuncAnimation(fig, update, frames=frames, interval=40)
135
+ # ani.save("mandelbrot_zoom.mp4", fps=25) # needs imageio-ffmpeg or ffmpeg on PATH
136
+ plt.show()
137
+
138
+
139
+ def show_gpu() -> None:
140
+ """The live, GPU-computed set. Scroll to zoom — it refines to the new scale in real time.
141
+
142
+ ``gplt.mandelbrot()`` adds a ``fractal`` layer; the escape loop runs in a fragment
143
+ shader, once per screen pixel, so there is no grid and no recompute to trigger. The
144
+ renderer raises the iteration budget as you zoom in, so the boundary keeps resolving.
145
+ """
146
+ plt.figure("Mandelbrot (live GPU)", figsize=(9, 7))
147
+ plt.mandelbrot(center=(-0.6, 0.0), span=1.3, cmap="magma", gain=1.2)
148
+ plt.title("Mandelbrot — live on the GPU (scroll to zoom)")
149
+ plt.xlabel("Re(c)")
150
+ plt.ylabel("Im(c)")
151
+ plt.show()
152
+
153
+
154
+ if __name__ == "__main__":
155
+ if "--gpu" in sys.argv:
156
+ show_gpu()
157
+ elif "--zoom" in sys.argv:
158
+ animate_zoom()
159
+ else:
160
+ plot_still()
@@ -0,0 +1,101 @@
1
+ """Panel-grid compositing shared by mpl_version.py and glplot_version.py.
2
+
3
+ Both scripts render each panel (and inset) as its own single-panel image and
4
+ hand the file paths here -- using the exact same code to assemble the final
5
+ grid and draw the title is what guarantees the two composites are laid out
6
+ and captioned identically, rather than relying on two independent
7
+ implementations to happen to agree.
8
+
9
+ Why per-panel images at all, instead of one `plt.subplots(3, 2)` figure for
10
+ mpl_version.py too: GLPlot's headless `savefig()` can only ever render one
11
+ panel per figure (see glplot_version.py's module docstring), so matching
12
+ mpl_version.py to that -- one figure per panel, composited afterward -- is
13
+ what makes the two pipelines comparable at every stage, not just in the
14
+ final pixels.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ from PIL import Image, ImageDraw, ImageFont
22
+
23
+ #: Both scripts build every panel at this exact physical size and DPI. GLPlot's
24
+ #: headless preview (glplot/utils/preview.py) hardcodes dpi=120 for the
25
+ #: matplotlib figure it reconstructs -- not configurable from the calling
26
+ #: script -- so mpl_version.py matches that number rather than the other way
27
+ #: around. With the same figsize *and* the same DPI, a "9pt" font or a
28
+ #: "1.5pt" line ends up the same pixel size in both outputs; matching only one
29
+ #: of the two would still leave a scale mismatch.
30
+ PANEL_FIGSIZE = (6.4, 5.6)
31
+ PANEL_DPI = 120
32
+ #: render_preview also floors the *inset* figure at 4.0x3.0in
33
+ #: (``max(engine.width/100, 4.0)``, ``max(engine.height/100, 3.0)`` in
34
+ #: glplot/utils/preview.py) regardless of the figsize passed to
35
+ #: ``gplt.figure()`` -- requesting anything smaller (as this used to, at
36
+ #: 2.6x2.3) silently got the floor size instead, so GLPlot's inset PNG came
37
+ #: back at a different pixel size *and* a different aspect ratio (4:3 vs
38
+ #: 2.6:2.3) than mpl_version.py's, which does honor a smaller figsize exactly.
39
+ #: Matching that floor here, instead of fighting it, is what makes the two
40
+ #: inset images the same shape before ``composite_inset`` resizes either of
41
+ #: them into the parent panel's inset box.
42
+ INSET_FIGSIZE = (4.0, 3.0)
43
+
44
+ TITLE_FONT_PT = 15
45
+ TITLE_BAR_PX = int(round(TITLE_FONT_PT * PANEL_DPI / 72.0 * 2.2))
46
+ GAP_PX = 18
47
+
48
+
49
+ def load_font(size_px):
50
+ for candidate in (
51
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
52
+ "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
53
+ ):
54
+ if Path(candidate).exists():
55
+ return ImageFont.truetype(candidate, size_px)
56
+ return ImageFont.load_default()
57
+
58
+
59
+ def composite_inset(parent_path, inset_path, frac_box):
60
+ """Paste ``inset_path`` onto ``parent_path`` at a fraction-of-panel box, with a border."""
61
+ parent = Image.open(parent_path).convert("RGB")
62
+ inset = Image.open(inset_path).convert("RGB")
63
+ pw, ph = parent.size
64
+ x0, y0, w, h = frac_box
65
+ box_w, box_h = int(pw * w), int(ph * h)
66
+ inset = inset.resize((box_w - 6, box_h - 6))
67
+ bordered = Image.new("RGB", (box_w, box_h), "black")
68
+ bordered.paste(inset, (3, 3))
69
+ parent.paste(bordered, (int(pw * x0), int(ph * y0)))
70
+ parent.save(parent_path)
71
+
72
+
73
+ def assemble_grid(paths_grid, out_path, title):
74
+ """Lay ``paths_grid`` (a list of rows of image paths) into one canvas with a centered title."""
75
+ images = [[Image.open(p).convert("RGB") for p in row] for row in paths_grid]
76
+ cell_w = max(im.width for row in images for im in row)
77
+ cell_h = max(im.height for row in images for im in row)
78
+ n_rows, n_cols = len(images), len(images[0])
79
+ canvas_w = n_cols * cell_w + (n_cols + 1) * GAP_PX
80
+ canvas_h = TITLE_BAR_PX + n_rows * cell_h + (n_rows + 1) * GAP_PX
81
+ canvas = Image.new("RGB", (canvas_w, canvas_h), "white")
82
+ draw = ImageDraw.Draw(canvas)
83
+
84
+ font = load_font(int(round(TITLE_FONT_PT * PANEL_DPI / 72.0)))
85
+ bbox = draw.textbbox((0, 0), title, font=font)
86
+ text_w = bbox[2] - bbox[0]
87
+ text_h = bbox[3] - bbox[1]
88
+ draw.text(
89
+ ((canvas_w - text_w) / 2, (TITLE_BAR_PX - text_h) / 2 - bbox[1]),
90
+ title,
91
+ fill="black",
92
+ font=font,
93
+ )
94
+
95
+ for r, row in enumerate(images):
96
+ for c, im in enumerate(row):
97
+ x = GAP_PX + c * (cell_w + GAP_PX)
98
+ y = TITLE_BAR_PX + GAP_PX + r * (cell_h + GAP_PX)
99
+ canvas.paste(im, (x, y))
100
+ canvas.save(out_path)
101
+ print(f"wrote {out_path}")