glplot 0.1.8__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. {glplot-0.1.8 → glplot-0.1.9}/CITATION.cff +2 -0
  2. {glplot-0.1.8 → glplot-0.1.9}/PKG-INFO +3 -1
  3. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/panel.py +81 -1
  4. {glplot-0.1.8 → glplot-0.1.9}/glplot/engine.py +81 -24
  5. {glplot-0.1.8 → glplot-0.1.9}/glplot/pyplot.py +435 -50
  6. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/axis.py +24 -10
  7. glplot-0.1.9/glplot/renderers/colorbar.py +415 -0
  8. glplot-0.1.9/glplot/renderers/contour_labels.py +274 -0
  9. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/scatter.py +27 -8
  10. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/preview.py +238 -13
  11. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/shaders.py +66 -1
  12. {glplot-0.1.8 → glplot-0.1.9}/pyproject.toml +18 -3
  13. glplot-0.1.9/tests/test_contour_labels.py +132 -0
  14. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gallery_integration.py +9 -0
  15. {glplot-0.1.8 → glplot-0.1.9}/tests/test_mpl_compat.py +276 -12
  16. {glplot-0.1.8 → glplot-0.1.9}/tests/test_outline_2d.py +1 -0
  17. {glplot-0.1.8 → glplot-0.1.9}/.gitignore +0 -0
  18. {glplot-0.1.8 → glplot-0.1.9}/CHANGELOG.md +0 -0
  19. {glplot-0.1.8 → glplot-0.1.9}/LICENSE +0 -0
  20. {glplot-0.1.8 → glplot-0.1.9}/README.md +0 -0
  21. {glplot-0.1.8 → glplot-0.1.9}/glplot/__init__.py +0 -0
  22. {glplot-0.1.8 → glplot-0.1.9}/glplot/__main__.py +0 -0
  23. {glplot-0.1.8 → glplot-0.1.9}/glplot/anim/__init__.py +0 -0
  24. {glplot-0.1.8 → glplot-0.1.9}/glplot/anim/applier.py +0 -0
  25. {glplot-0.1.8 → glplot-0.1.9}/glplot/anim/primitives.py +0 -0
  26. {glplot-0.1.8 → glplot-0.1.9}/glplot/anim/processes.py +0 -0
  27. {glplot-0.1.8 → glplot-0.1.9}/glplot/animation.py +0 -0
  28. {glplot-0.1.8 → glplot-0.1.9}/glplot/backend.py +0 -0
  29. {glplot-0.1.8 → glplot-0.1.9}/glplot/controllers.py +0 -0
  30. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/__init__.py +0 -0
  31. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/camera3d.py +0 -0
  32. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/context.py +0 -0
  33. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/layers.py +0 -0
  34. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/layout.py +0 -0
  35. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/legacy.py +0 -0
  36. {glplot-0.1.8 → glplot-0.1.9}/glplot/core/timeline.py +0 -0
  37. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/__init__.py +0 -0
  38. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/actions.py +0 -0
  39. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/app.py +0 -0
  40. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/clipboard.py +0 -0
  41. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/commands.py +0 -0
  42. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/dataio.py +0 -0
  43. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/datasets.py +0 -0
  44. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/dynamics.py +0 -0
  45. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/expressions.py +0 -0
  46. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/fuzzy.py +0 -0
  47. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/generators3d.py +0 -0
  48. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/history.py +0 -0
  49. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/icons.py +0 -0
  50. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/keys.py +0 -0
  51. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/layerops.py +0 -0
  52. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/layerops3d.py +0 -0
  53. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/mathops.py +0 -0
  54. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/__init__.py +0 -0
  55. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/annotate.py +0 -0
  56. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/base.py +0 -0
  57. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/data_editor.py +0 -0
  58. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/dynamics.py +0 -0
  59. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/functions.py +0 -0
  60. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/help.py +0 -0
  61. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/mathlab.py +0 -0
  62. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/objects3d.py +0 -0
  63. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/palette.py +0 -0
  64. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/pipeline.py +0 -0
  65. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/presentation.py +0 -0
  66. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/scene.py +0 -0
  67. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/selection.py +0 -0
  68. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/style.py +0 -0
  69. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/timeline.py +0 -0
  70. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/panels/view3d.py +0 -0
  71. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/pipeline.py +0 -0
  72. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/styles.py +0 -0
  73. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/theme.py +0 -0
  74. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/widgets.py +0 -0
  75. {glplot-0.1.8 → glplot-0.1.9}/glplot/gui/workspace.py +0 -0
  76. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/__init__.py +0 -0
  77. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/axis.py +0 -0
  78. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/effects.py +0 -0
  79. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/hud.py +0 -0
  80. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/hud_state.py +0 -0
  81. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/picking.py +0 -0
  82. {glplot-0.1.8 → glplot-0.1.9}/glplot/managers/renderer_manager.py +0 -0
  83. {glplot-0.1.8 → glplot-0.1.9}/glplot/options.py +0 -0
  84. {glplot-0.1.8 → glplot-0.1.9}/glplot/policy.py +0 -0
  85. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/__init__.py +0 -0
  86. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/axes3d.py +0 -0
  87. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/base.py +0 -0
  88. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/density.py +0 -0
  89. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/exact.py +0 -0
  90. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/fractal.py +0 -0
  91. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/geometry3d.py +0 -0
  92. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/interaction.py +0 -0
  93. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/legend.py +0 -0
  94. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/line_family.py +0 -0
  95. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/patch.py +0 -0
  96. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/polyline.py +0 -0
  97. {glplot-0.1.8 → glplot-0.1.9}/glplot/renderers/text.py +0 -0
  98. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/__init__.py +0 -0
  99. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/anim_export.py +0 -0
  100. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/blending.py +0 -0
  101. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/export.py +0 -0
  102. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/gl_utils.py +0 -0
  103. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/mpl_bridge.py +0 -0
  104. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/mpl_process.py +0 -0
  105. {glplot-0.1.8 → glplot-0.1.9}/glplot/utils/scale.py +0 -0
  106. {glplot-0.1.8 → glplot-0.1.9}/tests/conftest.py +0 -0
  107. {glplot-0.1.8 → glplot-0.1.9}/tests/test_anim_applier.py +0 -0
  108. {glplot-0.1.8 → glplot-0.1.9}/tests/test_anim_export.py +0 -0
  109. {glplot-0.1.8 → glplot-0.1.9}/tests/test_anim_primitives.py +0 -0
  110. {glplot-0.1.8 → glplot-0.1.9}/tests/test_anim_processes.py +0 -0
  111. {glplot-0.1.8 → glplot-0.1.9}/tests/test_animation_api.py +0 -0
  112. {glplot-0.1.8 → glplot-0.1.9}/tests/test_animation_integration.py +0 -0
  113. {glplot-0.1.8 → glplot-0.1.9}/tests/test_api_consistency.py +0 -0
  114. {glplot-0.1.8 → glplot-0.1.9}/tests/test_axes.py +0 -0
  115. {glplot-0.1.8 → glplot-0.1.9}/tests/test_axes3d.py +0 -0
  116. {glplot-0.1.8 → glplot-0.1.9}/tests/test_axis_margins.py +0 -0
  117. {glplot-0.1.8 → glplot-0.1.9}/tests/test_blending_3d.py +0 -0
  118. {glplot-0.1.8 → glplot-0.1.9}/tests/test_camera3d.py +0 -0
  119. {glplot-0.1.8 → glplot-0.1.9}/tests/test_camera_anisotropy.py +0 -0
  120. {glplot-0.1.8 → glplot-0.1.9}/tests/test_camera_projections.py +0 -0
  121. {glplot-0.1.8 → glplot-0.1.9}/tests/test_compositing_3d.py +0 -0
  122. {glplot-0.1.8 → glplot-0.1.9}/tests/test_compositing_api.py +0 -0
  123. {glplot-0.1.8 → glplot-0.1.9}/tests/test_dataio.py +0 -0
  124. {glplot-0.1.8 → glplot-0.1.9}/tests/test_density_tint.py +0 -0
  125. {glplot-0.1.8 → glplot-0.1.9}/tests/test_edge_cases.py +0 -0
  126. {glplot-0.1.8 → glplot-0.1.9}/tests/test_effects_post.py +0 -0
  127. {glplot-0.1.8 → glplot-0.1.9}/tests/test_encodings.py +0 -0
  128. {glplot-0.1.8 → glplot-0.1.9}/tests/test_engine.py +0 -0
  129. {glplot-0.1.8 → glplot-0.1.9}/tests/test_export.py +0 -0
  130. {glplot-0.1.8 → glplot-0.1.9}/tests/test_fractal.py +0 -0
  131. {glplot-0.1.8 → glplot-0.1.9}/tests/test_function_layer.py +0 -0
  132. {glplot-0.1.8 → glplot-0.1.9}/tests/test_generators3d.py +0 -0
  133. {glplot-0.1.8 → glplot-0.1.9}/tests/test_generators3d_extra.py +0 -0
  134. {glplot-0.1.8 → glplot-0.1.9}/tests/test_geometry3d.py +0 -0
  135. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_3d_panels.py +0 -0
  136. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_actions.py +0 -0
  137. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_annotate.py +0 -0
  138. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_clipboard.py +0 -0
  139. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_commands.py +0 -0
  140. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_data_editor.py +0 -0
  141. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_datasets.py +0 -0
  142. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_dynamics.py +0 -0
  143. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_engine_integration.py +0 -0
  144. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_expressions.py +0 -0
  145. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_fields.py +0 -0
  146. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_fuzzy.py +0 -0
  147. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_history.py +0 -0
  148. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_import_safety.py +0 -0
  149. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_impostor_alignment.py +0 -0
  150. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_input_regressions.py +0 -0
  151. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_keyboard_capture.py +0 -0
  152. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_keys.py +0 -0
  153. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_layer_colormaps.py +0 -0
  154. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_layer_kinds.py +0 -0
  155. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_layer_params.py +0 -0
  156. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_legend.py +0 -0
  157. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_mathlab.py +0 -0
  158. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_mathops.py +0 -0
  159. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_pipeline.py +0 -0
  160. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_regression_r2.py +0 -0
  161. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_scene_delete.py +0 -0
  162. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_scene_order.py +0 -0
  163. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_style_layer_editor.py +0 -0
  164. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_styles.py +0 -0
  165. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_styles_extra.py +0 -0
  166. {glplot-0.1.8 → glplot-0.1.9}/tests/test_gui_timeline_panel.py +0 -0
  167. {glplot-0.1.8 → glplot-0.1.9}/tests/test_helpers.py +0 -0
  168. {glplot-0.1.8 → glplot-0.1.9}/tests/test_impostor_scatter_fidelity.py +0 -0
  169. {glplot-0.1.8 → glplot-0.1.9}/tests/test_layerops3d.py +0 -0
  170. {glplot-0.1.8 → glplot-0.1.9}/tests/test_layers.py +0 -0
  171. {glplot-0.1.8 → glplot-0.1.9}/tests/test_mpl_bridge.py +0 -0
  172. {glplot-0.1.8 → glplot-0.1.9}/tests/test_mpl_compat_3d.py +0 -0
  173. {glplot-0.1.8 → glplot-0.1.9}/tests/test_mpl_parity_surface.py +0 -0
  174. {glplot-0.1.8 → glplot-0.1.9}/tests/test_mpl_process.py +0 -0
  175. {glplot-0.1.8 → glplot-0.1.9}/tests/test_objects3d_preview.py +0 -0
  176. {glplot-0.1.8 → glplot-0.1.9}/tests/test_options.py +0 -0
  177. {glplot-0.1.8 → glplot-0.1.9}/tests/test_outline.py +0 -0
  178. {glplot-0.1.8 → glplot-0.1.9}/tests/test_panels.py +0 -0
  179. {glplot-0.1.8 → glplot-0.1.9}/tests/test_performance_benchmarks.py +0 -0
  180. {glplot-0.1.8 → glplot-0.1.9}/tests/test_plot_style_api.py +0 -0
  181. {glplot-0.1.8 → glplot-0.1.9}/tests/test_polyline_lod.py +0 -0
  182. {glplot-0.1.8 → glplot-0.1.9}/tests/test_pyplot.py +0 -0
  183. {glplot-0.1.8 → glplot-0.1.9}/tests/test_pyplot_3d.py +0 -0
  184. {glplot-0.1.8 → glplot-0.1.9}/tests/test_pyplot_integration.py +0 -0
  185. {glplot-0.1.8 → glplot-0.1.9}/tests/test_regression.py +0 -0
  186. {glplot-0.1.8 → glplot-0.1.9}/tests/test_robustness.py +0 -0
  187. {glplot-0.1.8 → glplot-0.1.9}/tests/test_scatter_renderer.py +0 -0
  188. {glplot-0.1.8 → glplot-0.1.9}/tests/test_selection_model.py +0 -0
  189. {glplot-0.1.8 → glplot-0.1.9}/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.8
3
+ Version: 0.1.9
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
 
@@ -18,7 +18,8 @@ origin, which is what ``glViewport`` wants).
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- from typing import TYPE_CHECKING, Optional, Tuple
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
@@ -55,6 +55,8 @@ from .renderers import axes3d
55
55
  from .renderers.density import DensityRenderer
56
56
  from .renderers.exact import ExactLineRenderer
57
57
  from .renderers.interaction import InteractionRenderer
58
+ from .renderers.colorbar import draw_colorbars
59
+ from .renderers.contour_labels import draw_contour_labels
58
60
  from .renderers.legend import draw_legend
59
61
  from .utils.export import ExportManager
60
62
  from .utils.scale import forward as _scale_forward
@@ -238,6 +240,34 @@ class GPULinePlot:
238
240
  def cache(self, value: CacheState) -> None:
239
241
  self.active_panel.cache = value
240
242
 
243
+ @property
244
+ def xlabel(self) -> str:
245
+ """The active panel's x-axis name (``gplt.xlabel()``).
246
+
247
+ Forwarded to the panel for the same reason ``scene``/``camera`` are: a split
248
+ figure plots a different quantity per panel and has to be able to name each one.
249
+ Held as a plain attribute on the engine until this existed, which made it
250
+ figure-global -- a 2x2 grid got one x-name between all four panels.
251
+
252
+ Every reader keeps working unchanged: the live axis renderer reads
253
+ ``plot.xlabel`` inside the per-panel overlay pass (``_draw_panels`` swaps
254
+ ``active_panel_index`` around it), so it now resolves per panel for free.
255
+ """
256
+ return self.active_panel.xlabel
257
+
258
+ @xlabel.setter
259
+ def xlabel(self, value: str) -> None:
260
+ self.active_panel.xlabel = "" if value is None else str(value)
261
+
262
+ @property
263
+ def ylabel(self) -> str:
264
+ """The active panel's y-axis name (``gplt.ylabel()``). See :attr:`xlabel`."""
265
+ return self.active_panel.ylabel
266
+
267
+ @ylabel.setter
268
+ def ylabel(self, value: str) -> None:
269
+ self.active_panel.ylabel = "" if value is None else str(value)
270
+
241
271
  @property
242
272
  def camera3d(self) -> Camera3D:
243
273
  """The active panel's 3D point of view."""
@@ -324,6 +354,13 @@ class GPULinePlot:
324
354
  """
325
355
  self.title = str(title)
326
356
  self.options.title = str(title)
357
+ # Also the *active panel's* own axes title, so a split figure can title each
358
+ # panel separately. `self.title` stays the window caption (and the single-panel
359
+ # axes title) rather than becoming a forwarding property like `xlabel`/`ylabel`:
360
+ # it is assigned in `__init__` before any panel exists, so forwarding it would
361
+ # write to a panel that is not there yet.
362
+ if getattr(self, "panels", None):
363
+ self.active_panel.title = str(title)
327
364
  self.options.axis_title_fontsize = fontsize
328
365
  self.options.axis_title_color = color
329
366
  self.frame.dirty_ui = True
@@ -2044,35 +2081,38 @@ class GPULinePlot:
2044
2081
  # --------------------------------------------------------
2045
2082
 
2046
2083
  def _draw_overlays(self) -> None:
2047
- """Draw the axis scale labels and the legend for the active panel.
2084
+ """Draw the axis scale labels, contour labels and the legend for the active panel.
2048
2085
 
2049
2086
  Screen-aligned overlays that must repaint every frame (so they stay live and survive
2050
2087
  the cached impostor). Reads the active panel's camera and the engine's current pixel
2051
2088
  dimensions, which ``_draw_panels`` sets to the panel's rect when several panels exist.
2052
2089
  """
2090
+ # Built once, outside the `axis_show_labels` gate below: the contour labels and the
2091
+ # legend are not axis annotations and must still draw when the axis numbers are off.
2092
+ window_world = self.camera_controller.world_window(self.width, self.height)
2093
+ mvp = self.camera_controller.mvp(self.width, self.height)
2094
+ ndc_scale, ndc_offset = self._get_ndc_transform(window_world)
2095
+ ctx_labels = RenderContext(
2096
+ mvp=mvp,
2097
+ window_world=window_world,
2098
+ ndc_scale=ndc_scale,
2099
+ ndc_offset=ndc_offset,
2100
+ width_px=self.width,
2101
+ height_px=self.height,
2102
+ fb_width=self.fb_width,
2103
+ fb_height=self.fb_height,
2104
+ dpr=self.fb_width / max(self.width, 1),
2105
+ mode=self.policy.runtime.current_mode,
2106
+ global_alpha=1.0,
2107
+ lod_keep_prob=1.0,
2108
+ is_density=self.display_density,
2109
+ time=time.perf_counter(),
2110
+ px_offset=self._panel_offset_px,
2111
+ )
2112
+
2053
2113
  # Draw Axis Labels (Scale) on every frame so they update dynamically and persist when
2054
2114
  # the scene is cached.
2055
2115
  if self.options.axis_show_labels:
2056
- window_world = self.camera_controller.world_window(self.width, self.height)
2057
- mvp = self.camera_controller.mvp(self.width, self.height)
2058
- ndc_scale, ndc_offset = self._get_ndc_transform(window_world)
2059
- ctx_labels = RenderContext(
2060
- mvp=mvp,
2061
- window_world=window_world,
2062
- ndc_scale=ndc_scale,
2063
- ndc_offset=ndc_offset,
2064
- width_px=self.width,
2065
- height_px=self.height,
2066
- fb_width=self.fb_width,
2067
- fb_height=self.fb_height,
2068
- dpr=self.fb_width / max(self.width, 1),
2069
- mode=self.policy.runtime.current_mode,
2070
- global_alpha=1.0,
2071
- lod_keep_prob=1.0,
2072
- is_density=self.display_density,
2073
- time=time.perf_counter(),
2074
- px_offset=self._panel_offset_px,
2075
- )
2076
2116
  if self._is_pure_3d_scene():
2077
2117
  # A 3D scene's numbers live on the box's edges, not on two screen-aligned
2078
2118
  # spines, so they come from the 3D anchors rather than the AxisManager.
@@ -2082,6 +2122,13 @@ class GPULinePlot:
2082
2122
  self.axis_manager.update(ctx_labels)
2083
2123
  self.renderer_manager.renderers["axis"]._draw_labels(self.axis_manager, ctx_labels)
2084
2124
 
2125
+ # Inline contour values, seated on their own level lines. Same draw list and the
2126
+ # same reasoning as the legend below: not an axis annotation, so outside that
2127
+ # gate, and 2D-only (a 3D contour is real `wireframe3d` geometry with no
2128
+ # screen-space line to seat a number on). Draws nothing unless `clabel()` asked.
2129
+ if not self._is_pure_3d_scene():
2130
+ draw_contour_labels(self, ctx_labels)
2131
+
2085
2132
  # The legend, on the same background draw list as the axis labels and for the
2086
2133
  # same reason: it must composite over the cached impostor, which is not
2087
2134
  # redrawn during an interaction. Deliberately outside the `axis_show_labels`
@@ -2481,6 +2528,11 @@ class GPULinePlot:
2481
2528
  # pixel size via _draw_panels, which swaps them for the panel's rect).
2482
2529
  self._draw_panels(self._draw_overlays)
2483
2530
 
2531
+ # Colorbars, once per frame in whole-window pixel space (not per-panel, like
2532
+ # `_draw_panel_borders` one call earlier) -- see renderers/colorbar.py's module
2533
+ # docstring for why a colorbar is screen-space metadata, not a `Panel`.
2534
+ draw_colorbars(self)
2535
+
2484
2536
  # HUD panels are only updated if HUD is enabled, but begin/end must wrap all
2485
2537
  if self.policy.runtime.hud_enabled_this_frame:
2486
2538
  self.hud.update()
@@ -3303,11 +3355,16 @@ class GPULinePlot:
3303
3355
  from .utils.mpl_process import launch_snapshot_viewer
3304
3356
 
3305
3357
  snap = self.capture_snapshot(scale=2.0)
3358
+ # `or None`, not a bare read: `xlabel`/`ylabel` are now always-present properties
3359
+ # forwarding to the active panel (they used to be attributes that simply did not
3360
+ # exist until `gplt.xlabel()` created them, which is what the `getattr` default
3361
+ # was catching). An unset label is the empty string now, and this viewer's
3362
+ # contract is that "no label" arrives as None.
3306
3363
  launch_snapshot_viewer(
3307
3364
  snap,
3308
- xlabel=getattr(self, "xlabel", None),
3309
- ylabel=getattr(self, "ylabel", None),
3310
- title=getattr(self, "title", None),
3365
+ xlabel=getattr(self, "xlabel", None) or None,
3366
+ ylabel=getattr(self, "ylabel", None) or None,
3367
+ title=getattr(self, "title", None) or None,
3311
3368
  )
3312
3369
 
3313
3370
  def toggle_line_colormap(self) -> None: