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.
- {glplot-0.1.4 → glplot-0.1.5}/PKG-INFO +16 -1
- {glplot-0.1.4 → glplot-0.1.5}/README.md +15 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/layers.rst +17 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/plotting.rst +11 -0
- glplot-0.1.5/examples/dataflow.png +0 -0
- glplot-0.1.5/examples/ex_function_plot.py +151 -0
- glplot-0.1.5/examples/ex_mandelbrot.py +160 -0
- glplot-0.1.5/examples/free_energy_demo/compose.py +101 -0
- glplot-0.1.5/examples/free_energy_demo/data.py +273 -0
- glplot-0.1.5/examples/free_energy_demo/glplot_version.py +608 -0
- glplot-0.1.5/examples/free_energy_demo/mpl_version.py +398 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_free_energy.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/a_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/a_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/b_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/b_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/c_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/c_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/d_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/e_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/glplot_panels/f_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/matplotlib_free_energy.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/a_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/a_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/b_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/b_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/c_inset.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/c_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/d_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/e_main.png +0 -0
- glplot-0.1.5/examples/free_energy_demo/results/mpl_panels/f_main.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/02_scatter_fill.py +2 -2
- glplot-0.1.5/examples/gallery/20_categorical_bar_and_spines.py +21 -0
- glplot-0.1.5/examples/gallery/21_date_axis_and_text_bbox.py +37 -0
- glplot-0.1.5/examples/gallery/22_log_scale_plots.py +21 -0
- glplot-0.1.5/examples/gallery/23_symlog_scale.py +23 -0
- glplot-0.1.5/examples/gallery/24_log_scale_bar_chart.py +18 -0
- glplot-0.1.5/examples/gallery/25_log_scale_contourf.py +21 -0
- glplot-0.1.5/examples/gallery/26_logit_scale.py +21 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/README.md +7 -0
- glplot-0.1.5/examples/gallery/results/01_line_plot.png +0 -0
- glplot-0.1.5/examples/gallery/results/02_scatter_fill.png +0 -0
- glplot-0.1.5/examples/gallery/results/07_projected_3d_cloud.png +0 -0
- glplot-0.1.5/examples/gallery/results/12_surface_wireframe_bar3d.png +0 -0
- glplot-0.1.5/examples/gallery/results/13_volumetric_nebula.png +0 -0
- glplot-0.1.5/examples/gallery/results/15_vector_field_3d.png +0 -0
- glplot-0.1.5/examples/gallery/results/16_ssao_comparison.png +0 -0
- glplot-0.1.5/examples/gallery/results/20_categorical_bar_and_spines.png +0 -0
- glplot-0.1.5/examples/gallery/results/21_date_axis_and_text_bbox.png +0 -0
- glplot-0.1.5/examples/gallery/results/22_log_scale_plots.png +0 -0
- glplot-0.1.5/examples/gallery/results/23_symlog_scale.png +0 -0
- glplot-0.1.5/examples/gallery/results/24_log_scale_bar_chart.png +0 -0
- glplot-0.1.5/examples/gallery/results/25_log_scale_contourf.png +0 -0
- glplot-0.1.5/examples/gallery/results/26_logit_scale.png +0 -0
- glplot-0.1.5/glplot/anim/__init__.py +46 -0
- glplot-0.1.5/glplot/anim/applier.py +832 -0
- glplot-0.1.5/glplot/anim/primitives.py +1418 -0
- glplot-0.1.5/glplot/anim/processes.py +2279 -0
- glplot-0.1.5/glplot/animation.py +1784 -0
- glplot-0.1.5/glplot/core/camera3d.py +969 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/core/context.py +18 -0
- glplot-0.1.5/glplot/core/layers.py +655 -0
- glplot-0.1.5/glplot/core/layout.py +222 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/core/legacy.py +7 -1
- glplot-0.1.5/glplot/core/panel.py +156 -0
- glplot-0.1.5/glplot/core/timeline.py +784 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/engine.py +1517 -258
- glplot-0.1.5/glplot/gui/dataio.py +836 -0
- glplot-0.1.5/glplot/gui/dynamics.py +742 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/expressions.py +124 -18
- glplot-0.1.5/glplot/gui/generators3d.py +2297 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/icons.py +368 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/layerops.py +392 -30
- glplot-0.1.5/glplot/gui/layerops3d.py +978 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/mathops.py +55 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/data_editor.py +338 -6
- glplot-0.1.5/glplot/gui/panels/dynamics.py +687 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/functions.py +198 -9
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/help.py +204 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/mathlab.py +242 -18
- glplot-0.1.5/glplot/gui/panels/objects3d.py +744 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/palette.py +7 -4
- glplot-0.1.5/glplot/gui/panels/presentation.py +604 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/scene.py +41 -8
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/style.py +1155 -24
- glplot-0.1.5/glplot/gui/panels/timeline.py +2052 -0
- glplot-0.1.5/glplot/gui/panels/view3d.py +521 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/styles.py +738 -42
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/widgets.py +547 -6
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/workspace.py +650 -7
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/axis.py +187 -3
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/effects.py +25 -2
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/hud.py +19 -6
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/renderer_manager.py +67 -4
- {glplot-0.1.4 → glplot-0.1.5}/glplot/options.py +18 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/pyplot.py +5054 -550
- glplot-0.1.5/glplot/renderers/axes3d.py +1171 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/axis.py +95 -7
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/base.py +4 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/density.py +85 -16
- glplot-0.1.5/glplot/renderers/fractal.py +213 -0
- glplot-0.1.5/glplot/renderers/geometry3d.py +848 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/legend.py +14 -3
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/line_family.py +123 -4
- glplot-0.1.5/glplot/renderers/patch.py +358 -0
- glplot-0.1.5/glplot/renderers/polyline.py +475 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/scatter.py +249 -19
- glplot-0.1.5/glplot/utils/anim_export.py +786 -0
- glplot-0.1.5/glplot/utils/blending.py +84 -0
- glplot-0.1.5/glplot/utils/export.py +214 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/preview.py +211 -18
- glplot-0.1.5/glplot/utils/scale.py +115 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/shaders.py +414 -31
- glplot-0.1.5/plot_1784808407.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/pyproject.toml +2 -2
- glplot-0.1.5/tests/conftest.py +79 -0
- glplot-0.1.5/tests/test_anim_applier.py +704 -0
- glplot-0.1.5/tests/test_anim_export.py +373 -0
- glplot-0.1.5/tests/test_anim_primitives.py +801 -0
- glplot-0.1.5/tests/test_anim_processes.py +1299 -0
- glplot-0.1.5/tests/test_animation_api.py +836 -0
- glplot-0.1.5/tests/test_animation_integration.py +295 -0
- glplot-0.1.5/tests/test_axes3d.py +666 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_axis_margins.py +29 -1
- glplot-0.1.5/tests/test_blending_3d.py +439 -0
- glplot-0.1.5/tests/test_camera3d.py +465 -0
- glplot-0.1.5/tests/test_compositing_3d.py +326 -0
- glplot-0.1.5/tests/test_compositing_api.py +110 -0
- glplot-0.1.5/tests/test_dataio.py +592 -0
- glplot-0.1.5/tests/test_density_tint.py +170 -0
- glplot-0.1.5/tests/test_encodings.py +196 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_export.py +14 -3
- glplot-0.1.5/tests/test_fractal.py +250 -0
- glplot-0.1.5/tests/test_function_layer.py +710 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gallery_integration.py +8 -1
- glplot-0.1.5/tests/test_generators3d.py +203 -0
- glplot-0.1.5/tests/test_generators3d_extra.py +528 -0
- glplot-0.1.5/tests/test_gui_3d_panels.py +628 -0
- glplot-0.1.5/tests/test_gui_dynamics.py +335 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_layer_kinds.py +18 -7
- glplot-0.1.5/tests/test_gui_layer_params.py +129 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_mathlab.py +131 -0
- glplot-0.1.5/tests/test_gui_style_layer_editor.py +619 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_styles.py +32 -3
- glplot-0.1.5/tests/test_gui_styles_extra.py +722 -0
- glplot-0.1.5/tests/test_gui_timeline_panel.py +1737 -0
- glplot-0.1.5/tests/test_impostor_scatter_fidelity.py +229 -0
- glplot-0.1.5/tests/test_layerops3d.py +435 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_layers.py +57 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_compat.py +889 -71
- glplot-0.1.5/tests/test_mpl_compat_3d.py +867 -0
- glplot-0.1.5/tests/test_mpl_parity_surface.py +911 -0
- glplot-0.1.5/tests/test_objects3d_preview.py +692 -0
- glplot-0.1.5/tests/test_outline.py +545 -0
- glplot-0.1.5/tests/test_outline_2d.py +1007 -0
- glplot-0.1.5/tests/test_panels.py +396 -0
- glplot-0.1.5/tests/test_plot_style_api.py +100 -0
- glplot-0.1.5/tests/test_polyline_lod.py +119 -0
- glplot-0.1.5/tests/test_pyplot_3d.py +387 -0
- glplot-0.1.5/tests/test_timeline.py +474 -0
- glplot-0.1.4/examples/dataflow.png +0 -0
- glplot-0.1.4/examples/gallery/results/01_line_plot.png +0 -0
- glplot-0.1.4/examples/gallery/results/02_scatter_fill.png +0 -0
- glplot-0.1.4/examples/gallery/results/07_projected_3d_cloud.png +0 -0
- glplot-0.1.4/examples/gallery/results/12_surface_wireframe_bar3d.png +0 -0
- glplot-0.1.4/examples/gallery/results/13_volumetric_nebula.png +0 -0
- glplot-0.1.4/examples/gallery/results/15_vector_field_3d.png +0 -0
- glplot-0.1.4/examples/gallery/results/16_ssao_comparison.png +0 -0
- glplot-0.1.4/glplot/core/layers.py +0 -285
- glplot-0.1.4/glplot/renderers/geometry3d.py +0 -223
- glplot-0.1.4/glplot/renderers/patch.py +0 -192
- glplot-0.1.4/glplot/renderers/polyline.py +0 -245
- glplot-0.1.4/glplot/utils/export.py +0 -161
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-api-analyzer.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-bug-reporter.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-documentation-auditor.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-performance-profiler.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-release-manager.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.claude/agents/glplot-test-runner.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/build.yml +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/lint.yml +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.github/workflows/tests.yml +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.gitignore +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.gitlab-ci.yml +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/.readthedocs.yml +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/CHANGELOG.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/CITATION.cff +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/CODE_OF_CONDUCT.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/CONTRIBUTING.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/DOCUMENTATION_SETUP.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/GLPlot_Architecture_and_Mathematical_Formulation.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/JOSS_READINESS.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/LICENSE +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/PUBLICATION_CHECKLIST.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/PUBLICATION_WORKFLOW.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/READY_FOR_PUBLICATION.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/build_and_upload.sh +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/Makefile +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/README.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/requirements.txt +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/_static/custom.css +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/core.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/managers.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/api/utilities.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/conf.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/architecture.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/contributing.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/index.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/dev/testing.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/gallery/gallery.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/2d-plotting.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/3d-visualization.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/advanced-features.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/basic-plotting.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/installation.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/performance-tips.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/guide/quickstart.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/docs/source/index.rst +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/01_line_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/02_gpu_comparison.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/03_density_quality_comparison.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/04_hvplot_datashader_interactive.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/05_pipeline_diagram.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/datashader_density_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_comparison.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_fastplotlib.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_glplot.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/density_quality_vispy.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/fastplotlib_gpu_lines_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_density_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_density_100000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_gpu_exact_100000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/glplot_pipeline_diagram.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/gpu_comparison_benchmark_results.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_density_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/hvplot_lines_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/line_density_benchmark_results.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_density_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/matplotlib_lines_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_100.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_1000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_100000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_1000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/results/vispy_gpu_lines_10000000.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/benchmark/vispy_interactive.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_aspect_ratio_fix.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_density_gain.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_expert_performance.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_full_showcase.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_mpl_bridge.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_read_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_scatter.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_simple.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_v2_layers.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/ex_viewports.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/.gitignore +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/01_line_plot.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/03_bar_hist.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/04_line_family_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/05_guides_and_colormap.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/06_signal_tools.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/07_projected_3d_cloud.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/08_vector_field_quiver.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/09_large_matrix_heatmap.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/10_massive_hist2d_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/11_contour_pcolormesh_field.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/12_surface_wireframe_bar3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/13_volumetric_nebula.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/14_bar3d_hex_box_city.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/15_vector_field_3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/16_ssao_comparison.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/17_square_bars3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/18_hex_bars3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/19_turbulent_vector_field_3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/_preview.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/matplotlib/fontlist-v330.json +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/00d5189c1d5ced4af9bb23cbaa9b1e94-le64.cache-9 +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/617a9c25f653a4b32dbf39ee041b0318-le64.cache-9 +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/84c0f976e30e948e99073af70f4ae876-le64.cache-9 +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/CACHEDIR.TAG +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/b0a71e6bf6a8a1a908413a823d76e21f-le64.cache-9 +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.cache/xdg/fontconfig/c6cafe6b1a56ed8b739253d6df90ca6a-le64.cache-9 +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/.gitignore +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/03_bar_hist.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/04_line_family_density.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/05_guides_and_colormap.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/06_signal_tools.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/08_vector_field_quiver.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/09_large_matrix_heatmap.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/10_massive_hist2d_density.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/11_contour_pcolormesh_field.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/14_bar3d_hex_box_city.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/17_square_bars3d.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/18_hex_bars3d.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/results/19_turbulent_vector_field_3d.png +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/gallery/run_gallery.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/01_colorful_particles_2d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/02_mandelbrot_zoom_2d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/03_spinning_torus_3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/04_cosmic_sphere_3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/showcase/README.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/examples/verify_polyline_cmap.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/__main__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/backend.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/controllers.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/core/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/actions.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/app.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/clipboard.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/commands.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/datasets.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/fuzzy.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/history.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/keys.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/annotate.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/base.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/pipeline.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/panels/selection.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/pipeline.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/gui/theme.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/hud_state.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/managers/picking.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/policy.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/exact.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/interaction.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/renderers/text.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/scratch/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/__init__.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/gl_utils.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/mpl_bridge.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/glplot/utils/mpl_process.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/paper.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/check_gl_limits.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/check_runtime_math.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/diag_view.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/smoke_test.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/test_asymmetric_projection.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/test_autoscale_all.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/scratch/test_modular_export.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_api_consistency.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_axes.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_camera_anisotropy.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_camera_projections.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_edge_cases.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_effects_post.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_engine.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_geometry3d.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_actions.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_annotate.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_clipboard.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_commands.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_data_editor.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_datasets.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_engine_integration.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_expressions.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_fields.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_fuzzy.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_history.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_import_safety.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_impostor_alignment.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_keyboard_capture.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_keys.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_layer_colormaps.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_legend.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_mathops.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_pipeline.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_regression_r2.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_scene_delete.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_gui_scene_order.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_helpers.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_bridge.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_mpl_process.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_options.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_performance_benchmarks.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_pyplot.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_pyplot_integration.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_regression.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_robustness.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_scatter_renderer.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/test_selection_model.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/verify_blending_extension.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/verify_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/verify_headless.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/verify_phase4.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tests/verify_phase5_density.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tools/README.md +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tools/check_gl_capabilities.py +0 -0
- {glplot-0.1.4 → glplot-0.1.5}/tools/diagnose_camera_state.py +0 -0
- {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.
|
|
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
|
+

|
|
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
|
+

|
|
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}")
|