glplot 0.1.8__tar.gz → 0.1.10__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.8 → glplot-0.1.10}/.gitignore +0 -2
- {glplot-0.1.8 → glplot-0.1.10}/CITATION.cff +2 -0
- {glplot-0.1.8 → glplot-0.1.10}/PKG-INFO +5 -3
- {glplot-0.1.8 → glplot-0.1.10}/README.md +2 -2
- {glplot-0.1.8 → glplot-0.1.10}/glplot/animation.py +215 -51
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/panel.py +81 -1
- {glplot-0.1.8 → glplot-0.1.10}/glplot/engine.py +112 -36
- glplot-0.1.10/glplot/gui/background.py +111 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/datasets.py +37 -9
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/expressions.py +17 -1
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/layerops.py +323 -38
- glplot-0.1.10/glplot/gui/mathadvise.py +397 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/mathops.py +1151 -75
- glplot-0.1.10/glplot/gui/mathops2d.py +786 -0
- glplot-0.1.10/glplot/gui/mathopsnd.py +405 -0
- glplot-0.1.10/glplot/gui/models.py +132 -0
- glplot-0.1.10/glplot/gui/notifications.py +73 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/base.py +6 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/data_editor.py +466 -65
- glplot-0.1.10/glplot/gui/panels/mathlab.py +6037 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/pipeline.py +202 -2
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/scene.py +6 -6
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/style.py +68 -9
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/styles.py +25 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/widgets.py +688 -34
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/workspace.py +153 -12
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/hud.py +8 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/options.py +7 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/pyplot.py +565 -84
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/axis.py +40 -13
- glplot-0.1.10/glplot/renderers/colorbar.py +476 -0
- glplot-0.1.10/glplot/renderers/contour_labels.py +276 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/scatter.py +33 -8
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/anim_export.py +11 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/preview.py +346 -27
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/shaders.py +86 -3
- {glplot-0.1.8 → glplot-0.1.10}/pyproject.toml +18 -3
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_animation_api.py +19 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_animation_integration.py +179 -0
- glplot-0.1.10/tests/test_contour_labels.py +132 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_encodings.py +19 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_export.py +112 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gallery_integration.py +34 -0
- glplot-0.1.10/tests/test_gui_background.py +170 -0
- glplot-0.1.10/tests/test_gui_data_editor.py +1844 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_datasets.py +48 -0
- glplot-0.1.10/tests/test_gui_layer_kinds.py +1130 -0
- glplot-0.1.10/tests/test_gui_mathadvise.py +295 -0
- glplot-0.1.10/tests/test_gui_mathlab.py +5921 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_mathops.py +1236 -0
- glplot-0.1.10/tests/test_gui_mathops2d.py +548 -0
- glplot-0.1.10/tests/test_gui_mathopsnd.py +297 -0
- glplot-0.1.10/tests/test_gui_models.py +87 -0
- glplot-0.1.10/tests/test_gui_notifications.py +102 -0
- glplot-0.1.10/tests/test_gui_pipeline.py +572 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_styles.py +50 -0
- glplot-0.1.10/tests/test_gui_widgets.py +538 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_mpl_compat.py +375 -33
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_outline_2d.py +1 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_plot_style_api.py +30 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_robustness.py +38 -1
- glplot-0.1.10/tests/test_scatter_default_size.py +163 -0
- glplot-0.1.8/glplot/gui/panels/mathlab.py +0 -2498
- glplot-0.1.8/tests/test_gui_data_editor.py +0 -837
- glplot-0.1.8/tests/test_gui_layer_kinds.py +0 -537
- glplot-0.1.8/tests/test_gui_mathlab.py +0 -1209
- glplot-0.1.8/tests/test_gui_pipeline.py +0 -268
- {glplot-0.1.8 → glplot-0.1.10}/CHANGELOG.md +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/LICENSE +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/__main__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/anim/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/anim/applier.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/anim/primitives.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/anim/processes.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/backend.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/controllers.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/camera3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/context.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/layers.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/layout.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/legacy.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/core/timeline.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/actions.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/app.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/clipboard.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/commands.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/dataio.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/dynamics.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/fuzzy.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/generators3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/history.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/icons.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/keys.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/layerops3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/annotate.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/dynamics.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/functions.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/help.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/objects3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/palette.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/presentation.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/selection.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/timeline.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/panels/view3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/pipeline.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/gui/theme.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/axis.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/effects.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/hud_state.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/picking.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/managers/renderer_manager.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/policy.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/axes3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/base.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/density.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/exact.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/fractal.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/geometry3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/interaction.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/legend.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/line_family.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/patch.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/polyline.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/renderers/text.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/__init__.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/blending.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/export.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/gl_utils.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/mpl_bridge.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/mpl_process.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/glplot/utils/scale.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/conftest.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_anim_applier.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_anim_export.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_anim_primitives.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_anim_processes.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_api_consistency.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_axes.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_axes3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_axis_margins.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_blending_3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_camera3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_camera_anisotropy.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_camera_projections.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_compositing_3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_compositing_api.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_dataio.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_density_tint.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_edge_cases.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_effects_post.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_engine.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_fractal.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_function_layer.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_generators3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_generators3d_extra.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_geometry3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_3d_panels.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_actions.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_annotate.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_clipboard.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_commands.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_dynamics.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_engine_integration.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_expressions.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_fields.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_fuzzy.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_history.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_import_safety.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_impostor_alignment.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_input_regressions.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_keyboard_capture.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_keys.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_layer_colormaps.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_layer_params.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_legend.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_regression_r2.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_scene_delete.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_scene_order.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_style_layer_editor.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_styles_extra.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_gui_timeline_panel.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_helpers.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_impostor_scatter_fidelity.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_layerops3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_layers.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_mpl_bridge.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_mpl_compat_3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_mpl_parity_surface.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_mpl_process.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_objects3d_preview.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_options.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_outline.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_panels.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_performance_benchmarks.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_polyline_lod.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_pyplot.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_pyplot_3d.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_pyplot_integration.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_regression.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_scatter_renderer.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_selection_model.py +0 -0
- {glplot-0.1.8 → glplot-0.1.10}/tests/test_timeline.py +0 -0
|
@@ -12,8 +12,10 @@ authors:
|
|
|
12
12
|
orcid: "https://orcid.org/0009-0002-3961-1426"
|
|
13
13
|
- family-names: Riccius
|
|
14
14
|
given-names: Felix
|
|
15
|
+
orcid: "https://orcid.org/0000-0002-1331-4469"
|
|
15
16
|
- family-names: Holland
|
|
16
17
|
given-names: Julian
|
|
18
|
+
orcid: "https://orcid.org/0000-0003-0134-1790"
|
|
17
19
|
- family-names: Ducci
|
|
18
20
|
given-names: Gianmarco
|
|
19
21
|
identifiers:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glplot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
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
|
|
@@ -32,11 +32,13 @@ Provides-Extra: dev
|
|
|
32
32
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
33
33
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
34
34
|
Requires-Dist: pytest-mock>=3.10; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-timeout>=2.1; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: twine>=4.0; extra == 'dev'
|
|
37
38
|
Provides-Extra: test
|
|
38
39
|
Requires-Dist: pytest-cov>=4.0; extra == 'test'
|
|
39
40
|
Requires-Dist: pytest-mock>=3.10; extra == 'test'
|
|
41
|
+
Requires-Dist: pytest-timeout>=2.1; extra == 'test'
|
|
40
42
|
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
|
|
@@ -545,7 +547,7 @@ interactive path never reads pixels back to the CPU.
|
|
|
545
547
|
|
|
546
548
|

|
|
547
549
|
|
|
548
|
-
See [
|
|
550
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
549
551
|
for the full derivation of each stage, including the density-accumulation math and the
|
|
550
552
|
viewport-relative projection that keeps zoom numerically stable.
|
|
551
553
|
|
|
@@ -566,7 +568,7 @@ for GPU/environment diagnostics.
|
|
|
566
568
|
## Documentation
|
|
567
569
|
|
|
568
570
|
- API reference: docstrings in `glplot.pyplot`, or the built docs — see [docs/README.md](docs/README.md)
|
|
569
|
-
- Architecture: [
|
|
571
|
+
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
570
572
|
- Dev tools: [tools/README.md](tools/README.md)
|
|
571
573
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) · Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
572
574
|
|
|
@@ -503,7 +503,7 @@ interactive path never reads pixels back to the CPU.
|
|
|
503
503
|
|
|
504
504
|

|
|
505
505
|
|
|
506
|
-
See [
|
|
506
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
507
507
|
for the full derivation of each stage, including the density-accumulation math and the
|
|
508
508
|
viewport-relative projection that keeps zoom numerically stable.
|
|
509
509
|
|
|
@@ -524,7 +524,7 @@ for GPU/environment diagnostics.
|
|
|
524
524
|
## Documentation
|
|
525
525
|
|
|
526
526
|
- API reference: docstrings in `glplot.pyplot`, or the built docs — see [docs/README.md](docs/README.md)
|
|
527
|
-
- Architecture: [
|
|
527
|
+
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
528
528
|
- Dev tools: [tools/README.md](tools/README.md)
|
|
529
529
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) · Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
530
530
|
|
|
@@ -33,18 +33,24 @@ How a frame is produced
|
|
|
33
33
|
:func:`figure_to_rgb` is the whole bridge, and it has two modes:
|
|
34
34
|
|
|
35
35
|
* **Live GL** — when ``fig.window`` exists, ``fig.export.savefig()`` renders the scene
|
|
36
|
-
offscreen through the real pipeline at full quality
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
offscreen through the real pipeline at full quality, to a temporary PNG that is read back
|
|
37
|
+
immediately after. That round trip is a deliberate trade: reimplementing
|
|
38
|
+
``ExportManager.savefig``'s ``glReadPixels`` here to skip the file would duplicate the
|
|
39
|
+
panel/scissor/projection logic and go stale the first time the render pipeline changed —
|
|
40
|
+
there is no "give me the pixels" entry point to call instead, and this is the one GL render
|
|
41
|
+
mode this module has, so paying that cost once here beats maintaining a second copy.
|
|
42
|
+
* **Headless** — when it does not,
|
|
43
|
+
:func:`glplot.utils.preview.render_preview_array` draws the same scene through
|
|
44
|
+
matplotlib's Agg backend and hands back the rendered pixels directly, with no file
|
|
45
|
+
involved. This is the path that makes ``ani.save(...)`` work in CI, in a notebook, and in
|
|
46
|
+
any script that never called ``show()`` — the common case for a saved animation, and the
|
|
47
|
+
one with hundreds of frames to pay a per-frame cost on. It used to round-trip through a
|
|
48
|
+
PNG the same way the GL path still does; profiling a real animation loop found PIL's PNG
|
|
49
|
+
encoder alone cost as much as matplotlib's entire draw, so that encode plus the disk
|
|
50
|
+
write/read it required were pure overhead for a caller (this one) that only ever wanted
|
|
51
|
+
the array. :func:`glplot.utils.preview.render_preview` (the file-writing function a plain
|
|
52
|
+
``savefig()`` call uses) is unchanged; :func:`render_preview_array` is a second entry
|
|
53
|
+
point onto the same figure-building code, not a rewrite of it — see its docstring.
|
|
48
54
|
|
|
49
55
|
**The headless path is not pixel-identical to the GL path.** ``render_preview`` is a
|
|
50
56
|
matplotlib re-drawing of the scene, not the GPU renderer; it approximates. An animation
|
|
@@ -59,13 +65,17 @@ Limitations, stated plainly
|
|
|
59
65
|
changes nothing except that it does not raise. Output is identical either way — blitting
|
|
60
66
|
is an optimisation in matplotlib, not a correctness flag — so ignoring it cannot make a
|
|
61
67
|
saved animation wrong. It is recorded in ``ani._blit`` if you want to assert on it.
|
|
62
|
-
* **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
``
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
* **Live on-screen playback rides the engine's frame callback, not a real timer.**
|
|
69
|
+
``GPULinePlot.run()`` owns its main loop and has no toolkit timer for a
|
|
70
|
+
``TimedAnimation`` to hook the way matplotlib's does — but it does expose
|
|
71
|
+
``add_frame_callback`` (:meth:`glplot.engine.GPULinePlot.add_frame_callback`), a per-frame
|
|
72
|
+
hook driven by wall-clock time, and :class:`TimedAnimation` uses exactly that to step
|
|
73
|
+
itself at ``interval``. A ``FuncAnimation``/``ArtistAnimation`` built against a live
|
|
74
|
+
GLPlot figure therefore *does* play when you call ``plt.show()`` — no ``draw_event``, no
|
|
75
|
+
blitting, just the same full-scene re-render :meth:`Animation.save` uses, paced in real
|
|
76
|
+
time instead of frame-by-frame. :meth:`Animation.pause` / :meth:`Animation.resume` stop and
|
|
77
|
+
restart that stepping. A real matplotlib ``Figure`` is untouched by any of this: it keeps
|
|
78
|
+
using its own canvas and timer, exactly as before.
|
|
69
79
|
* **In-place array mutation is assumed.** A user callback that writes into
|
|
70
80
|
``layer.pts[:, 1]`` leaves no trace numpy can report, so after every callback this module
|
|
71
81
|
marks every layer dirty rather than trying to detect what changed. Cheap, and the only
|
|
@@ -275,46 +285,59 @@ def figure_to_rgb(fig: Any, dpi: Optional[float] = None) -> np.ndarray:
|
|
|
275
285
|
|
|
276
286
|
The single bridge between "a figure" and "a frame", and the only function in this
|
|
277
287
|
module that knows a GLPlot engine from a matplotlib one. See the module docstring for
|
|
278
|
-
the two GLPlot render modes and
|
|
288
|
+
the two GLPlot render modes, and :func:`_render_glplot_frame` for which of them still
|
|
289
|
+
goes through a temporary PNG and which does not.
|
|
279
290
|
|
|
280
291
|
Raises :class:`RuntimeError` naming the figure type if neither renderer applies —
|
|
281
292
|
passing something that is not a figure at all is otherwise diagnosed several frames
|
|
282
293
|
later as a confusing Pillow error.
|
|
283
294
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if _is_mpl_figure(fig):
|
|
295
|
+
if _is_mpl_figure(fig):
|
|
296
|
+
with tempfile.TemporaryDirectory(prefix="glplot-anim-") as tmpdir:
|
|
297
|
+
target = os.path.join(tmpdir, "frame.png")
|
|
288
298
|
fig.savefig(target, dpi=dpi, format="png")
|
|
289
|
-
|
|
290
|
-
_render_glplot_figure(fig, target, _dpi_to_scale(dpi))
|
|
291
|
-
else:
|
|
292
|
-
raise RuntimeError(
|
|
293
|
-
f"Cannot grab a frame from {type(fig).__name__}: expected a "
|
|
294
|
-
f"glplot.engine.GPULinePlot (what glplot.pyplot.figure() returns) or a "
|
|
295
|
-
f"matplotlib Figure. Pass the figure object itself, not an axes or a "
|
|
296
|
-
f"layer."
|
|
297
|
-
)
|
|
299
|
+
return anim_export.normalize_frame(_read_png(target))
|
|
298
300
|
|
|
299
|
-
|
|
301
|
+
if hasattr(fig, "scene"):
|
|
302
|
+
return anim_export.normalize_frame(_render_glplot_frame(fig, _dpi_to_scale(dpi)))
|
|
300
303
|
|
|
304
|
+
raise RuntimeError(
|
|
305
|
+
f"Cannot grab a frame from {type(fig).__name__}: expected a "
|
|
306
|
+
f"glplot.engine.GPULinePlot (what glplot.pyplot.figure() returns) or a "
|
|
307
|
+
f"matplotlib Figure. Pass the figure object itself, not an axes or a "
|
|
308
|
+
f"layer."
|
|
309
|
+
)
|
|
301
310
|
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
|
|
312
|
+
def _render_glplot_frame(fig: Any, scale: float) -> np.ndarray:
|
|
313
|
+
"""Render one frame of a GLPlot engine, through GL if there is a context and Agg if not.
|
|
314
|
+
|
|
315
|
+
**Live GL** still round-trips through a temporary PNG — see the module docstring's
|
|
316
|
+
"Rejected: reimplementing ExportManager.savefig's glReadPixels here", unchanged reasoning
|
|
317
|
+
so this stays unchanged.
|
|
318
|
+
|
|
319
|
+
**Headless** does not: :func:`glplot.utils.preview.render_preview_array` hands back the
|
|
320
|
+
rendered pixels directly from matplotlib's Agg canvas, skipping the PNG encode and the
|
|
321
|
+
disk write/read a file-based caller needs. Profiled: PIL's PNG encoder alone cost as much
|
|
322
|
+
as matplotlib's entire draw, so for an animation's hundreds of headless frames — the
|
|
323
|
+
common case, since a saved animation is usually built without ever calling ``fig.run()``
|
|
324
|
+
— this removes roughly a third of each frame's cost with no change in what gets drawn.
|
|
304
325
|
|
|
305
326
|
``ExportManager.savefig`` prints a "Exported high-res image to ..." line on every call.
|
|
306
327
|
That is fine for one interactive export and intolerable at 300 frames, so stdout is
|
|
307
|
-
swallowed for the duration — this is the only place that print reaches,
|
|
308
|
-
it here beats every alternative that would require editing the engine.
|
|
328
|
+
swallowed for the duration of the GL branch — this is the only place that print reaches,
|
|
329
|
+
and suppressing it here beats every alternative that would require editing the engine.
|
|
309
330
|
"""
|
|
310
331
|
if getattr(fig, "window", None) is not None and hasattr(fig, "export"):
|
|
311
|
-
with
|
|
312
|
-
|
|
313
|
-
|
|
332
|
+
with tempfile.TemporaryDirectory(prefix="glplot-anim-") as tmpdir:
|
|
333
|
+
target = os.path.join(tmpdir, "frame.png")
|
|
334
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
335
|
+
fig.export.savefig(target, scale=scale)
|
|
336
|
+
return _read_png(target)
|
|
314
337
|
|
|
315
|
-
from .utils.preview import
|
|
338
|
+
from .utils.preview import render_preview_array
|
|
316
339
|
|
|
317
|
-
|
|
340
|
+
return render_preview_array(fig, scale)
|
|
318
341
|
|
|
319
342
|
|
|
320
343
|
def _read_png(path: str) -> np.ndarray:
|
|
@@ -419,9 +442,9 @@ class _NullTimer:
|
|
|
419
442
|
|
|
420
443
|
:class:`TimedAnimation` needs *an* event source; it does not need one that works in
|
|
421
444
|
order for :meth:`Animation.save` to render every frame, because saving drives the frame
|
|
422
|
-
sequence directly and never consults the timer. Live playback
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
sequence directly and never consults the timer. Live playback does not go through this
|
|
446
|
+
timer either -- :meth:`TimedAnimation._start_live_playback` drives it straight off
|
|
447
|
+
``fig.add_frame_callback`` instead, since that is the hook the engine actually has.
|
|
425
448
|
"""
|
|
426
449
|
|
|
427
450
|
def __init__(self, interval: Optional[float] = None, callbacks: Any = None) -> None:
|
|
@@ -1195,9 +1218,9 @@ class Animation:
|
|
|
1195
1218
|
"""Base class for animations, with matplotlib's constructor and public methods.
|
|
1196
1219
|
|
|
1197
1220
|
Subclasses supply the frame data (:meth:`new_frame_seq`) and what to do with one
|
|
1198
|
-
(:meth:`_draw_frame`); this class supplies :meth:`save` and the HTML exports
|
|
1199
|
-
|
|
1200
|
-
|
|
1221
|
+
(:meth:`_draw_frame`); this class supplies :meth:`save` and the HTML exports. Live
|
|
1222
|
+
playback (:class:`TimedAnimation`'s ``fig.add_frame_callback`` hook, module docstring)
|
|
1223
|
+
is the other consumer of that same frame-stepping machinery.
|
|
1201
1224
|
"""
|
|
1202
1225
|
|
|
1203
1226
|
def __init__(self, fig: Any, event_source: Any = None, blit: bool = False) -> None:
|
|
@@ -1557,10 +1580,30 @@ class TimedAnimation(Animation):
|
|
|
1557
1580
|
|
|
1558
1581
|
``interval`` (milliseconds per frame) is what :meth:`Animation.save` turns into ``fps``
|
|
1559
1582
|
when the caller does not pass one, which is why ``FuncAnimation(..., interval=20)``
|
|
1560
|
-
followed by a bare ``save()`` produces a 50 fps file.
|
|
1561
|
-
|
|
1583
|
+
followed by a bare ``save()`` produces a 50 fps file.
|
|
1584
|
+
|
|
1585
|
+
It is also what makes live on-screen playback possible, and *controllable*: when ``fig``
|
|
1586
|
+
is a GLPlot figure (no real matplotlib canvas), :meth:`_start_live_playback` hands this
|
|
1587
|
+
animation's frames to ``fig.active_panel.timeline`` -- the same playhead the GUI's
|
|
1588
|
+
Timeline panel already has Play/Pause/Stop buttons and a scrub bar for -- instead of
|
|
1589
|
+
running its own independent clock. ``plt.show()`` after building a ``FuncAnimation``
|
|
1590
|
+
plays it, and if the Timeline panel is open, its transport controls drive this animation
|
|
1591
|
+
exactly as they drive a hand-keyed one. See :meth:`_start_live_playback`.
|
|
1562
1592
|
"""
|
|
1563
1593
|
|
|
1594
|
+
#: A frame sequence at most this long is materialised into a concrete list so the
|
|
1595
|
+
#: Timeline's scrub bar can jump to any frame directly. Past this (or for an unbounded
|
|
1596
|
+
#: ``frames=None`` generator), only forward playback is exact -- see
|
|
1597
|
+
#: :meth:`_materialize_frames`.
|
|
1598
|
+
_MAX_SEEKABLE_FRAMES = 100_000
|
|
1599
|
+
|
|
1600
|
+
#: Duration assigned to the panel's timeline when the frame count is unknowable
|
|
1601
|
+
#: (``frames=None``) or too large to materialise. Arbitrary but not consequential: it
|
|
1602
|
+
#: only sets how far the scrub bar's grid extends before it loops, not how long the
|
|
1603
|
+
#: animation itself runs, since :meth:`_start_live_playback`'s callback keeps stepping
|
|
1604
|
+
#: forward regardless of where in that grid the playhead sits.
|
|
1605
|
+
_UNKNOWN_DURATION_SECONDS = 60.0
|
|
1606
|
+
|
|
1564
1607
|
def __init__(
|
|
1565
1608
|
self,
|
|
1566
1609
|
fig: Any,
|
|
@@ -1576,16 +1619,137 @@ class TimedAnimation(Animation):
|
|
|
1576
1619
|
self._repeat_delay = repeat_delay if repeat_delay is not None else 0
|
|
1577
1620
|
self._repeat = repeat
|
|
1578
1621
|
self._paused = False
|
|
1622
|
+
self._live_timeline: Any = None
|
|
1579
1623
|
if event_source is None:
|
|
1580
1624
|
canvas = _canvas_for(fig)
|
|
1581
1625
|
event_source = canvas.new_timer(interval=self._interval)
|
|
1582
1626
|
super().__init__(fig, event_source=event_source, *args, **kwargs)
|
|
1627
|
+
self._start_live_playback(fig)
|
|
1583
1628
|
|
|
1584
1629
|
@property
|
|
1585
1630
|
def repeat(self) -> bool:
|
|
1586
1631
|
"""Whether the animation loops. Read-only, as in matplotlib."""
|
|
1587
1632
|
return self._repeat
|
|
1588
1633
|
|
|
1634
|
+
def pause(self) -> None:
|
|
1635
|
+
"""Pause the animation.
|
|
1636
|
+
|
|
1637
|
+
Proxies to ``fig.active_panel.timeline.pause()`` when live playback is bound (see
|
|
1638
|
+
:meth:`_start_live_playback`), so this has the same effect as pressing Pause in the
|
|
1639
|
+
Timeline panel. Otherwise a documented no-op, as it always was; see the module
|
|
1640
|
+
docstring.
|
|
1641
|
+
"""
|
|
1642
|
+
self._paused = True
|
|
1643
|
+
if self._live_timeline is not None:
|
|
1644
|
+
self._live_timeline.pause()
|
|
1645
|
+
if self.event_source is not None:
|
|
1646
|
+
with contextlib.suppress(Exception):
|
|
1647
|
+
self.event_source.stop()
|
|
1648
|
+
|
|
1649
|
+
def resume(self) -> None:
|
|
1650
|
+
"""Resume the animation. See :meth:`pause`; proxies to ``timeline.play()`` likewise."""
|
|
1651
|
+
self._paused = False
|
|
1652
|
+
if self._live_timeline is not None:
|
|
1653
|
+
self._live_timeline.play()
|
|
1654
|
+
if self.event_source is not None:
|
|
1655
|
+
with contextlib.suppress(Exception):
|
|
1656
|
+
self.event_source.start()
|
|
1657
|
+
|
|
1658
|
+
def _materialize_frames(self) -> Optional[List[Any]]:
|
|
1659
|
+
"""This animation's frame data as a concrete, randomly-indexable list, or ``None``.
|
|
1660
|
+
|
|
1661
|
+
``None`` means the Timeline can only step forward through the sequence (an
|
|
1662
|
+
unbounded ``frames=None`` generator, or a declared length past
|
|
1663
|
+
:data:`_MAX_SEEKABLE_FRAMES`) -- scrubbing backward then restarts it from frame 0
|
|
1664
|
+
rather than landing on the exact frame, since an arbitrary generator cannot be
|
|
1665
|
+
rewound. Every other case -- :class:`ArtistAnimation` (already a concrete list),
|
|
1666
|
+
an integer or sized ``frames=``, an explicit ``save_count`` -- seeks exactly,
|
|
1667
|
+
because the whole sequence is known and cheap to hold: it is usually the caller's
|
|
1668
|
+
own list or array already sitting in memory, and ``new_frame_seq()`` just re-reads
|
|
1669
|
+
it rather than duplicating anything expensive.
|
|
1670
|
+
"""
|
|
1671
|
+
existing = getattr(self, "_framedata", None)
|
|
1672
|
+
if isinstance(existing, (list, tuple)):
|
|
1673
|
+
return list(existing)
|
|
1674
|
+
save_count = getattr(self, "_save_count", None)
|
|
1675
|
+
if save_count is not None and 0 < save_count <= self._MAX_SEEKABLE_FRAMES:
|
|
1676
|
+
return list(itertools.islice(self.new_frame_seq(), save_count))
|
|
1677
|
+
return None
|
|
1678
|
+
|
|
1679
|
+
def _start_live_playback(self, fig: Any) -> None:
|
|
1680
|
+
"""Bind this animation's frames to ``fig.active_panel.timeline``'s playhead.
|
|
1681
|
+
|
|
1682
|
+
A real matplotlib ``Figure`` already has a toolkit timer driving it -- ``self._canvas``
|
|
1683
|
+
is the real canvas in that case (:func:`_canvas_for`), so this does nothing and that
|
|
1684
|
+
timer keeps doing its job unmodified. Only a GLPlot figure (``_NullCanvas``) with a
|
|
1685
|
+
panel to bind to gets this treatment.
|
|
1686
|
+
|
|
1687
|
+
The bound timeline is configured to this animation's own cadence (``fps = 1000 /
|
|
1688
|
+
interval``, ``duration`` from the frame count or :data:`_UNKNOWN_DURATION_SECONDS`
|
|
1689
|
+
when that is not knowable, ``loop`` from ``repeat``) and started playing -- an
|
|
1690
|
+
animation you just built plays immediately, same as before. From there the engine's
|
|
1691
|
+
own per-frame hook (``fig.add_frame_callback``, invoked *after*
|
|
1692
|
+
``GPULinePlot._advance_timelines`` already moved the playhead this frame -- see
|
|
1693
|
+
``engine.py``) does one thing every frame: compute which frame the playhead's
|
|
1694
|
+
*current* time corresponds to, and draw it if that differs from what is already on
|
|
1695
|
+
screen. It does not advance the clock itself. That is deliberate: Play/Pause only
|
|
1696
|
+
has to flip ``timeline.playing`` (which gates whether the playhead moves at all) and
|
|
1697
|
+
Stop/seek/step only have to move ``timeline.time`` directly (which the GUI's
|
|
1698
|
+
transport buttons already do) for this animation to follow suit exactly the way a
|
|
1699
|
+
hand-keyed one does -- there is no separate "is it playing" state here to keep in
|
|
1700
|
+
sync with the Timeline panel's.
|
|
1701
|
+
"""
|
|
1702
|
+
register = getattr(fig, "add_frame_callback", None)
|
|
1703
|
+
panel = getattr(fig, "active_panel", None)
|
|
1704
|
+
timeline = getattr(panel, "timeline", None)
|
|
1705
|
+
if not callable(register) or not isinstance(self._canvas, _NullCanvas) or timeline is None:
|
|
1706
|
+
return
|
|
1707
|
+
|
|
1708
|
+
frames = self._materialize_frames()
|
|
1709
|
+
interval_s = max(float(self._interval), 1.0) / MS_PER_SECOND
|
|
1710
|
+
duration = (
|
|
1711
|
+
max(len(frames) - 1, 0) * interval_s if frames else self._UNKNOWN_DURATION_SECONDS
|
|
1712
|
+
)
|
|
1713
|
+
timeline.set_fps(1.0 / interval_s)
|
|
1714
|
+
if timeline.is_empty():
|
|
1715
|
+
# No hand-authored keyframes here yet -- the overwhelmingly common case for a
|
|
1716
|
+
# freshly-built animation -- so this animation may as well own the grid exactly:
|
|
1717
|
+
# duration matches its real length, and the scrub bar has nothing past the last
|
|
1718
|
+
# frame to run into. A timeline that already has keyframes on it keeps its own
|
|
1719
|
+
# length (never shrinks) rather than have this animation truncate them.
|
|
1720
|
+
timeline.set_duration(duration)
|
|
1721
|
+
else:
|
|
1722
|
+
timeline.fit_duration(minimum=duration)
|
|
1723
|
+
timeline.set_loop("loop" if self._repeat else "once")
|
|
1724
|
+
timeline.play()
|
|
1725
|
+
self._live_timeline = timeline
|
|
1726
|
+
|
|
1727
|
+
live_seq = {"iter": self.frame_seq}
|
|
1728
|
+
state = {"last_index": None}
|
|
1729
|
+
|
|
1730
|
+
def _tick(now: float) -> None:
|
|
1731
|
+
index = timeline.frame_index()
|
|
1732
|
+
if index == state["last_index"]:
|
|
1733
|
+
return
|
|
1734
|
+
if frames is not None:
|
|
1735
|
+
framedata = frames[min(index, len(frames) - 1)]
|
|
1736
|
+
else:
|
|
1737
|
+
if state["last_index"] is not None and index < state["last_index"]:
|
|
1738
|
+
live_seq["iter"] = self.new_frame_seq() # scrubbed back / looped: restart
|
|
1739
|
+
try:
|
|
1740
|
+
framedata = next(live_seq["iter"])
|
|
1741
|
+
except StopIteration:
|
|
1742
|
+
if not self._repeat:
|
|
1743
|
+
fig.remove_frame_callback(_tick)
|
|
1744
|
+
return
|
|
1745
|
+
live_seq["iter"] = self.new_frame_seq()
|
|
1746
|
+
framedata = next(live_seq["iter"])
|
|
1747
|
+
state["last_index"] = index
|
|
1748
|
+
self._draw_was_started = True
|
|
1749
|
+
self._draw_next_frame(framedata, blit=False)
|
|
1750
|
+
|
|
1751
|
+
register(_tick)
|
|
1752
|
+
|
|
1589
1753
|
|
|
1590
1754
|
class FuncAnimation(TimedAnimation):
|
|
1591
1755
|
"""Animation built by repeatedly calling *func*. The class almost everyone uses.
|
|
@@ -18,7 +18,8 @@ origin, which is what ``glViewport`` wants).
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
|
22
23
|
|
|
23
24
|
from ..controllers import CameraController
|
|
24
25
|
from .camera3d import Axes3DOptions, Camera3D
|
|
@@ -29,6 +30,61 @@ if TYPE_CHECKING:
|
|
|
29
30
|
from ..options import EngineOptions
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@dataclass
|
|
34
|
+
class ColorbarSpec:
|
|
35
|
+
"""One colorbar attached to a :class:`Panel`, drawn as a screen-space overlay.
|
|
36
|
+
|
|
37
|
+
Deliberately not a :class:`Panel` itself: any rect registered on ``fig.panels`` is
|
|
38
|
+
click-to-activate and pan/zoom-able (see ``GPULinePlot._panel_index_at``), so a
|
|
39
|
+
colorbar built that way would silently steal "current axes" the moment a script did
|
|
40
|
+
``im = plt.imshow(...); plt.colorbar(); plt.title(...)`` -- the title would land on
|
|
41
|
+
the colorbar. A ``ColorbarSpec`` instead just describes a strip of the figure and is
|
|
42
|
+
drawn by :func:`glplot.renderers.colorbar.draw_colorbars`, in the same whole-window,
|
|
43
|
+
camera-free pixel space :meth:`GPULinePlot._draw_panel_borders` already draws in.
|
|
44
|
+
|
|
45
|
+
``norm`` is always a concrete, bounded ``matplotlib.colors.Normalize`` instance (never
|
|
46
|
+
a bare scale name and never unbounded) -- the single source of truth the gradient fill,
|
|
47
|
+
the tick locator, and the headless ``savefig()`` reconstruction all read alike, so the
|
|
48
|
+
live bar and the exported PNG cannot independently drift from what was actually asked
|
|
49
|
+
for.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
#: (x0, y0, w, h) in figure fractions, bottom-left origin -- same convention as
|
|
53
|
+
#: :attr:`Panel.rect_frac`, and the strip vacated from the host panel's own rect.
|
|
54
|
+
rect_frac: Tuple[float, float, float, float]
|
|
55
|
+
#: "vertical" for a right/left bar, "horizontal" for a top/bottom one.
|
|
56
|
+
orientation: str
|
|
57
|
+
#: Which edge of the host panel this bar sits against: "right", "left", "top" or
|
|
58
|
+
#: "bottom". Distinct from ``orientation`` because a caller can ask for either
|
|
59
|
+
#: independently, as matplotlib's own ``colorbar()`` does.
|
|
60
|
+
location: str
|
|
61
|
+
#: Colormap name, resolved (never ``None``) at ``colorbar()`` call time.
|
|
62
|
+
cmap: str
|
|
63
|
+
#: A concrete ``matplotlib.colors.Normalize`` (or subclass) instance with real
|
|
64
|
+
#: ``vmin``/``vmax`` already resolved.
|
|
65
|
+
norm: Any
|
|
66
|
+
#: Explicit tick positions, or ``None`` to auto-locate from ``norm``'s type.
|
|
67
|
+
ticks: Optional[Any] = None
|
|
68
|
+
#: A tick label format (matplotlib ``Formatter``, format string, or ``None`` to
|
|
69
|
+
#: auto-format from the locator).
|
|
70
|
+
format: Optional[Any] = None
|
|
71
|
+
#: The colorbar's own axis label (``Colorbar.set_label``), or ``None``.
|
|
72
|
+
label: Optional[str] = None
|
|
73
|
+
#: Matplotlib-parity placement knobs, stored for the headless reconstruction (which
|
|
74
|
+
#: rebuilds a real matplotlib colorbar and can honour them precisely); the live
|
|
75
|
+
#: renderer's own bar geometry does not re-derive a precise aspect-ratio width from
|
|
76
|
+
#: these -- see ``colorbar()``'s docstring.
|
|
77
|
+
fraction: float = 0.15
|
|
78
|
+
pad: float = 0.05
|
|
79
|
+
shrink: float = 1.0
|
|
80
|
+
aspect: float = 20.0
|
|
81
|
+
#: When True, the bar sits *inside* the host panel's own rect (over the plotted
|
|
82
|
+
#: content, near ``location``'s edge) instead of shrinking the panel to make room
|
|
83
|
+
#: outside it. Ticks/label flip to point inward, toward the panel's centre, since
|
|
84
|
+
#: there is no room outside the panel for them to point into.
|
|
85
|
+
inset: bool = False
|
|
86
|
+
|
|
87
|
+
|
|
32
88
|
class Panel:
|
|
33
89
|
"""One plotting region: its scene, camera, interaction state, and window rectangle."""
|
|
34
90
|
|
|
@@ -64,6 +120,30 @@ class Panel:
|
|
|
64
120
|
#: panel that is never animated costs one object and no frame time.
|
|
65
121
|
self.timeline = Timeline()
|
|
66
122
|
|
|
123
|
+
#: Colorbars drawn against this panel's edges. Metadata only -- never a
|
|
124
|
+
#: :class:`Panel` of its own, never registered on ``fig.panels`` -- so a
|
|
125
|
+
#: colorbar can never become the active/pannable axes. See
|
|
126
|
+
#: :class:`ColorbarSpec` for why.
|
|
127
|
+
self.colorbars: List["ColorbarSpec"] = []
|
|
128
|
+
|
|
129
|
+
#: This panel's own axis names and axes title.
|
|
130
|
+
#:
|
|
131
|
+
#: Per panel for the same reason the cameras and scenes are: a split figure plots
|
|
132
|
+
#: four different quantities and has to be able to *name* them four different
|
|
133
|
+
#: ways. These were figure-global until they lived here, so a 2x2 grid could only
|
|
134
|
+
#: ever carry one x-name, one y-name and one title between all four panels --
|
|
135
|
+
#: whichever call ran last won, and the other three axes went unlabelled.
|
|
136
|
+
#:
|
|
137
|
+
#: The engine forwards ``xlabel``/``ylabel`` to the active panel's copy (the same
|
|
138
|
+
#: delegation ``scene``/``camera``/``cache`` use), so ``gplt.xlabel(...)`` keeps
|
|
139
|
+
#: writing "the current axes' x-name" and every existing single-panel caller is
|
|
140
|
+
#: unaffected. ``title`` is *not* forwarded that way -- ``engine.title`` is also
|
|
141
|
+
#: the GLFW window caption and is assigned in ``__init__`` before any panel
|
|
142
|
+
#: exists -- so ``set_title`` writes both, and readers prefer this one.
|
|
143
|
+
self.xlabel: str = ""
|
|
144
|
+
self.ylabel: str = ""
|
|
145
|
+
self.title: str = ""
|
|
146
|
+
|
|
67
147
|
#: Explicit dimensionality: 2, 3, or ``None`` for "infer from the layers".
|
|
68
148
|
#:
|
|
69
149
|
#: None is the default and reproduces GLPlot's historical behaviour exactly — a
|