anyplotlib 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.github/workflows/prepare_release.yml +42 -11
  2. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/CHANGELOG.rst +40 -0
  3. anyplotlib-0.3.0/Examples/Interactive/plot_figure_annotations.py +87 -0
  4. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_voxel_grain_explorer.py +5 -2
  5. anyplotlib-0.3.0/Examples/Markers/plot_transform_markers.py +49 -0
  6. anyplotlib-0.3.0/Examples/PlotTypes/plot_gpu_voxels.py +82 -0
  7. anyplotlib-0.3.0/Examples/PlotTypes/plot_step_and_log.py +43 -0
  8. anyplotlib-0.3.0/Examples/PlotTypes/plot_tile_backend.py +184 -0
  9. anyplotlib-0.3.0/Examples/PlotTypes/plot_twinx.py +45 -0
  10. anyplotlib-0.3.0/Examples/Widgets/plot_widget1d_point.py +40 -0
  11. anyplotlib-0.3.0/Examples/Widgets/plot_widget2d_arrow.py +41 -0
  12. anyplotlib-0.3.0/Examples/Widgets/plot_widget3d_plane.py +47 -0
  13. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/PKG-INFO +1 -1
  14. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/FIGURE_ESM.md +225 -1
  15. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/__init__.py +4 -4
  16. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/_base_plot.py +69 -0
  17. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/_electron.py +15 -2
  18. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/_repr_utils.py +76 -4
  19. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/axes/_axes.py +2 -2
  20. anyplotlib-0.3.0/anyplotlib/axes/_inset_axes.py +254 -0
  21. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/callbacks.py +4 -0
  22. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/figure/_figure.py +176 -8
  23. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/figure_esm.js +1251 -52
  24. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/markers.py +3 -0
  25. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot1d/_plot1d.py +170 -12
  26. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot2d/__init__.py +3 -2
  27. anyplotlib-0.3.0/anyplotlib/plot2d/_layer.py +121 -0
  28. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot2d/_plot2d.py +385 -23
  29. anyplotlib-0.3.0/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
  30. anyplotlib-0.3.0/anyplotlib/tests/test_embed/test_export_png.py +777 -0
  31. anyplotlib-0.3.0/anyplotlib/tests/test_interactive/test_edit_chrome.py +435 -0
  32. anyplotlib-0.3.0/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +842 -0
  33. anyplotlib-0.3.0/anyplotlib/tests/test_layouts/test_inset_callout.py +636 -0
  34. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_visual.py +20 -0
  35. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_markers/test_markers.py +77 -0
  36. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot1d/test_plot1d.py +158 -0
  37. anyplotlib-0.3.0/anyplotlib/tests/test_plot2d/test_layers.py +427 -0
  38. anyplotlib-0.3.0/anyplotlib/tests/test_plot2d/test_layers_playwright.py +313 -0
  39. anyplotlib-0.3.0/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +330 -0
  40. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/widgets/__init__.py +2 -2
  41. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/widgets/_widgets2d.py +69 -12
  42. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_root/index.html +3 -3
  43. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_root/switcher.json +10 -0
  44. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/figure_plots.rst +7 -0
  45. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/index.rst +3 -0
  46. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/conf.py +10 -1
  47. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/pyproject.toml +1 -1
  48. anyplotlib-0.2.0/anyplotlib/axes/_inset_axes.py +0 -126
  49. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.github/workflows/ci.yml +0 -0
  50. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.github/workflows/docs.yml +0 -0
  51. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.github/workflows/release.yml +0 -0
  52. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.github/workflows/tests.yml +0 -0
  53. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/.gitignore +0 -0
  54. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/AGENTS.md +0 -0
  55. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Benchmarks/README.rst +0 -0
  56. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
  57. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/README.rst +0 -0
  58. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
  59. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_eels_explorer.py +0 -0
  60. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_interactive_fft.py +0 -0
  61. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_interactive_fitting.py +0 -0
  62. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_ipf_density_map.py +0 -0
  63. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_ipf_explorer.py +0 -0
  64. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_key_bindings.py +0 -0
  65. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_particle_picker.py +0 -0
  66. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_point_widget.py +0 -0
  67. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
  68. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
  69. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
  70. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Interactive/plot_threshold_explorer.py +0 -0
  71. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/README.rst +0 -0
  72. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_arrows.py +0 -0
  73. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_circles.py +0 -0
  74. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_ellipses.py +0 -0
  75. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_horizontal_lines.py +0 -0
  76. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_line_segments.py +0 -0
  77. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_points.py +0 -0
  78. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_polygons.py +0 -0
  79. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_rectangles.py +0 -0
  80. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_squares.py +0 -0
  81. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_texts.py +0 -0
  82. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Markers/plot_vertical_lines.py +0 -0
  83. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/README.rst +0 -0
  84. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_3d.py +0 -0
  85. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_bar.py +0 -0
  86. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
  87. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_image2d.py +0 -0
  88. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_inset.py +0 -0
  89. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_label_formatting.py +0 -0
  90. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_line_styles.py +0 -0
  91. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
  92. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/PlotTypes/plot_spectra1d.py +0 -0
  93. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/README.rst +0 -0
  94. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/README.rst +0 -0
  95. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget1d_hline.py +0 -0
  96. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget1d_range.py +0 -0
  97. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget1d_vline.py +0 -0
  98. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_annular.py +0 -0
  99. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_circle.py +0 -0
  100. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
  101. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_label.py +0 -0
  102. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
  103. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
  104. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/LARGE_IMAGE_WEBGPU_PLAN.md +0 -0
  105. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/LICENSE +0 -0
  106. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/Makefile +0 -0
  107. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/ORIX_BACKEND_PLAN.md +0 -0
  108. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/README.md +0 -0
  109. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/RELEASE_NOTES.md +0 -0
  110. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/RELEASE_PLAN.md +0 -0
  111. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/WEBGPU_PLAN.md +0 -0
  112. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/_binary_frame.py +0 -0
  113. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/_utils.py +0 -0
  114. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/axes/__init__.py +0 -0
  115. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/conftest.py +0 -0
  116. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/embed.py +0 -0
  117. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/figure/__init__.py +0 -0
  118. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/figure/_gridspec.py +0 -0
  119. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/figure/_subplots.py +0 -0
  120. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot1d/__init__.py +0 -0
  121. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot1d/_plotbar.py +0 -0
  122. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot2d/_plotmesh.py +0 -0
  123. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot2d/_tile_backend.py +0 -0
  124. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot3d/__init__.py +0 -0
  125. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plot3d/_plot3d.py +0 -0
  126. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plotxy/__init__.py +0 -0
  127. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/plotxy/_plotxy.py +0 -0
  128. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
  129. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
  130. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
  131. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
  132. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
  133. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
  134. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
  135. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
  136. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
  137. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
  138. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
  139. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
  140. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
  141. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
  142. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/__init__.py +0 -0
  143. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
  144. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/_png_utils.py +0 -0
  145. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/bar_basic.png +0 -0
  146. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
  147. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
  148. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
  149. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
  150. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
  151. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
  152. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
  153. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
  154. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
  155. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
  156. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
  157. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_1d.png +0 -0
  158. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
  159. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
  160. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
  161. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
  162. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
  163. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
  164. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
  165. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
  166. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
  167. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
  168. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
  169. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
  170. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
  171. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
  172. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
  173. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
  174. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
  175. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/conftest.py +0 -0
  176. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
  177. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
  178. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
  179. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
  180. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
  181. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_documentation/__init__.py +0 -0
  182. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
  183. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
  184. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
  185. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_electron/__init__.py +0 -0
  186. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
  187. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_embed/__init__.py +0 -0
  188. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
  189. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
  190. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_examples/__init__.py +0 -0
  191. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
  192. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/__init__.py +0 -0
  193. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
  194. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
  195. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
  196. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
  197. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
  198. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
  199. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
  200. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
  201. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
  202. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_title.py +0 -0
  203. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
  204. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
  205. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_labels/__init__.py +0 -0
  206. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
  207. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
  208. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
  209. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/__init__.py +0 -0
  210. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
  211. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
  212. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
  213. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
  214. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
  215. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_markers/__init__.py +0 -0
  216. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
  217. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
  218. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
  219. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
  220. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
  221. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
  222. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
  223. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
  224. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
  225. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
  226. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
  227. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
  228. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
  229. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
  230. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
  231. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
  232. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
  233. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
  234. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
  235. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
  236. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
  237. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
  238. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/widgets/_base.py +0 -0
  239. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/widgets/_widgets1d.py +0 -0
  240. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/anyplotlib/widgets/_widgets3d.py +0 -0
  241. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_sg_html_scraper.py +0 -0
  242. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_static/anyplotlib.svg +0 -0
  243. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_static/custom.css +0 -0
  244. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_static/pyodide_bridge.js +0 -0
  245. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/_templates/autosummary/class.rst +0 -0
  246. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/callbacks.rst +0 -0
  247. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/figure.rst +0 -0
  248. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/markers.rst +0 -0
  249. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/api/widgets.rst +0 -0
  250. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/benchmarking.rst +0 -0
  251. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/dev/index.rst +0 -0
  252. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/embedding.rst +0 -0
  253. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/events.rst +0 -0
  254. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/getting_started.rst +0 -0
  255. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/docs/index.rst +0 -0
  256. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/make.bat +0 -0
  257. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/upcoming_changes/README.rst +0 -0
  258. {anyplotlib-0.2.0 → anyplotlib-0.3.0}/uv.lock +0 -0
@@ -11,12 +11,13 @@ on:
11
11
  required: true
12
12
  type: choice
13
13
  options:
14
+ - finalize # drop the bN suffix: release the current beta's base as stable (0.3.0b2 -> 0.3.0)
14
15
  - minor
15
16
  - bugfix
16
17
  - major
17
18
  - pre-release # increments the bN counter on the current base version
18
19
  beta:
19
- description: "Mark as beta pre-release (adds bN suffix; always true for pre-release)"
20
+ description: "Mark as beta pre-release (adds bN suffix). Ignored for 'pre-release' (always beta) and 'finalize' (always stable)."
20
21
  required: false
21
22
  type: boolean
22
23
  default: false
@@ -67,17 +68,42 @@ jobs:
67
68
  minor = int(m.group(2))
68
69
  patch = int(m.group(3))
69
70
  beta_n = int(m.group(4)) if m.group(4) else None
70
-
71
- if bump == "major":
71
+ on_beta = beta_n is not None
72
+
73
+ if bump == "finalize":
74
+ # Release the current beta's base as stable: just drop the bN
75
+ # suffix, keep major.minor.patch. e.g. 0.3.0b2 -> 0.3.0.
76
+ if not on_beta:
77
+ raise SystemExit(
78
+ f"'finalize' requires a beta base version, but current "
79
+ f"version {current!r} has no bN suffix. Use minor / bugfix "
80
+ f"/ major to start a new release instead."
81
+ )
82
+ is_beta = False
83
+ elif bump == "pre-release":
84
+ # Keep the same base; just walk the beta counter forward.
85
+ is_beta = True
86
+ beta_n = (beta_n or 0) + 1
87
+ elif on_beta:
88
+ # We are on a beta of the NEXT release (e.g. 0.3.0b2). The base
89
+ # major.minor.patch is that upcoming version, so minor/bugfix/major
90
+ # must bump relative to the LAST STABLE (base - the in-progress
91
+ # component), not skip a whole version. The common intent from a
92
+ # beta is 'finalize', so steer the user there rather than guess.
93
+ raise SystemExit(
94
+ f"Current version {current!r} is a beta of the upcoming "
95
+ f"{major}.{minor}.{patch} release. To ship it, use bump="
96
+ f"'finalize' (-> {major}.{minor}.{patch}). A '{bump}' bump from "
97
+ f"a beta would skip {major}.{minor}.{patch} entirely "
98
+ f"(e.g. -> {'%d.%d.0' % (major, minor + 1) if bump == 'minor' else '...'}); "
99
+ f"that is almost never intended."
100
+ )
101
+ elif bump == "major":
72
102
  major, minor, patch = major + 1, 0, 0
73
103
  elif bump == "minor":
74
104
  minor, patch = minor + 1, 0
75
105
  elif bump == "bugfix":
76
106
  patch += 1
77
- elif bump == "pre-release":
78
- # Keep the same base; just walk the beta counter forward.
79
- is_beta = True
80
- beta_n = (beta_n or 0) + 1
81
107
 
82
108
  if is_beta:
83
109
  if bump != "pre-release":
@@ -88,10 +114,15 @@ jobs:
88
114
  PYEOF
89
115
  )
90
116
 
117
+ # Derive is_beta from the COMPUTED version (ends in bN?), not the raw
118
+ # input — so 'finalize' is always treated as stable and a mismatched
119
+ # beta checkbox can't mislabel the switcher / skip the root redirect.
120
+ if [[ "$NEW_VERSION" =~ b[0-9]+$ ]]; then IS_BETA_OUT=true; else IS_BETA_OUT=false; fi
121
+
91
122
  echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
92
123
  echo "tag=v$NEW_VERSION" >> "$GITHUB_OUTPUT"
93
124
  echo "branch=release/v$NEW_VERSION" >> "$GITHUB_OUTPUT"
94
- echo "is_beta=${{ inputs.beta }}" >> "$GITHUB_OUTPUT"
125
+ echo "is_beta=$IS_BETA_OUT" >> "$GITHUB_OUTPUT"
95
126
  echo "Bumping (${{ inputs.bump }}): $CURRENT → $NEW_VERSION"
96
127
 
97
128
  # ── Bump version strings ─────────────────────────────────────────────
@@ -118,7 +149,7 @@ jobs:
118
149
  - name: Update docs/switcher.json
119
150
  env:
120
151
  VERSION_TAG: ${{ steps.version.outputs.tag }}
121
- IS_BETA: ${{ inputs.beta }}
152
+ IS_BETA: ${{ steps.version.outputs.is_beta }}
122
153
  shell: python
123
154
  run: |
124
155
  import json, re, pathlib, os
@@ -144,7 +175,7 @@ jobs:
144
175
 
145
176
  # ── Update root redirect for stable releases ─────────────────────────
146
177
  - name: Update root redirect (stable releases only)
147
- if: ${{ inputs.beta == false && inputs.bump != 'pre-release' }}
178
+ if: ${{ steps.version.outputs.is_beta == 'false' && inputs.bump != 'pre-release' }}
148
179
  env:
149
180
  VERSION_TAG: ${{ steps.version.outputs.tag }}
150
181
  shell: python
@@ -200,7 +231,7 @@ jobs:
200
231
  - Version bumped to \`${TAG}\` in \`pyproject.toml\` and \`docs/conf.py\`
201
232
  - \`CHANGELOG.rst\` updated from towncrier fragments
202
233
  - \`docs/_root/switcher.json\` updated with the new version entry
203
- $([ '${{ inputs.beta }}' = 'false' ] && echo '- Root redirect updated to point to this release' || echo '')
234
+ $([ '${{ steps.version.outputs.is_beta }}' = 'false' ] && echo '- Root redirect updated to point to this release' || echo '')
204
235
 
205
236
  ### Review checklist
206
237
  - [ ] \`CHANGELOG.rst\` reads well — edit the fragment text directly if needed
@@ -10,6 +10,46 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by
10
10
 
11
11
  .. towncrier release notes start
12
12
 
13
+ 0.3.0b1 (2026-07-13)
14
+ ====================
15
+
16
+ New Features
17
+ ------------
18
+
19
+ - Added :class:`~anyplotlib.widgets.ArrowWidget` (draggable arrow overlay, tail
20
+ at ``(x, y)`` and head at ``(x + u, y + v)``) via ``Plot2D.add_arrow_widget`` /
21
+ ``add_widget("arrow")``, and a ``show_handles`` option (default ``True``) on
22
+ every 2-D overlay widget to hide the grab-handle dots without affecting drag.
23
+ - Added figure-level edit-mode chrome to :class:`~anyplotlib.Figure`: the
24
+ ``edit_chrome`` and ``selected_panel`` traits (per-panel hover / selection
25
+ outlines), figure-background click events, and a figure-level annotation layer
26
+ (``set_figure_markers`` / ``figure_markers``, positioned in figure fractions and
27
+ always included in ``exportPNG``) with figure-level callbacks via
28
+ ``add_event_handler``.
29
+ - Extended :class:`~anyplotlib.Figure` edit-mode interaction:
30
+
31
+ * Circle and rectangle overlay widgets are now **resizable via visible nodes** —
32
+ a circle draws a centre (move) node and an east-point radius node; a rectangle
33
+ draws all four corner nodes (opposite corner anchored on drag). Drawn only
34
+ when ``show_handles`` is ``True``, with matching resize cursors.
35
+ * :class:`~anyplotlib.widgets.ArrowWidget` **tail is now a reshape node**:
36
+ dragging the tail moves it while the head stays anchored (dragging the shaft
37
+ still moves the whole arrow; the head node still re-aims it).
38
+ * The selected-panel and hover **outlines are fully inset** (``outline-offset:
39
+ -2px``) so an edge/corner panel's ring is no longer clipped at the figure's
40
+ right/bottom edge.
41
+ * **Panel drag-swap** under ``edit_chrome``: each grid panel shows a move grip
42
+ in its top-left corner; dragging it over a *different* panel emits a
43
+ figure-level ``pointer_up`` event with ``panel_swap: true`` and
44
+ ``source_panel_id`` / ``target_panel_id`` (new :class:`~anyplotlib.Event`
45
+ fields). anyplotlib performs no layout change itself — the host swaps and
46
+ rebuilds. Releasing on the source panel or empty space cancels cleanly; the
47
+ grip is inert when ``edit_chrome`` is off.
48
+ * The JS ``mount()`` embedding entry point accepts an ``onResize({width,
49
+ height})`` callback, fired (debounced) when the **root container resizes**, so
50
+ an embedding host can relayout the figure to its new box.
51
+
52
+
13
53
  0.2.0 (2026-07-10)
14
54
  ==================
15
55
 
@@ -0,0 +1,87 @@
1
+ """
2
+ Interactive figure annotations — double-click to add, drag to place
3
+ ===================================================================
4
+
5
+ anyplotlib has a *figure-level* annotation layer that floats above the panels,
6
+ positioned in **figure fractions** (0…1, origin top-left) rather than any
7
+ panel's data coordinates. Set it with
8
+ :meth:`~anyplotlib.Figure.set_figure_markers`; the supported kinds are
9
+ ``"text"``, ``"circle"``, ``"rect"`` and ``"arrow"``.
10
+
11
+ Turn on ``fig.edit_chrome`` and every annotation becomes draggable in the
12
+ browser. Combine that with a ``double_click`` handler and you get a simple
13
+ annotate-by-clicking workflow: double-click a feature to drop a labelled
14
+ arrow, then drag it to line it up. Positions round-trip back to Python via
15
+ :attr:`~anyplotlib.Figure.figure_markers`.
16
+ """
17
+ import numpy as np
18
+ import anyplotlib as apl
19
+
20
+ rng = np.random.default_rng(7)
21
+ data = rng.standard_normal((160, 160)).cumsum(0).cumsum(1)
22
+ data = (data - data.min()) / (data.max() - data.min())
23
+
24
+ fig, ax = apl.subplots(1, 1, figsize=(520, 520))
25
+ v = ax.imshow(data, cmap="magma", units="px")
26
+
27
+ # Enable the editable-annotation ("report builder") mode so figure markers are
28
+ # hit-testable and draggable, and the figure emits background/marker events.
29
+ fig.edit_chrome = True
30
+
31
+ # %%
32
+ # Seed a couple of annotations
33
+ # ----------------------------
34
+ # Each marker is a dict with a ``kind`` and fraction-space geometry. A text
35
+ # label and an arrow pointing into the image to start with.
36
+
37
+ fig.set_figure_markers([
38
+ {"kind": "text", "x": 0.5, "y": 0.06,
39
+ "text": "Double-click a feature to annotate it",
40
+ "color": "#ffffff", "fontsize": 14},
41
+ {"kind": "arrow", "x": 0.20, "y": 0.30, "u": 0.12, "v": 0.12,
42
+ "color": "#ffd54f", "linewidth": 2},
43
+ ])
44
+
45
+ fig
46
+
47
+ # %%
48
+ # Double-click to drop a new annotation
49
+ # -------------------------------------
50
+ # On a single-panel figure the panel nearly fills the canvas, so we turn the
51
+ # click's device pixels into a figure fraction with the panel's
52
+ # ``display_width`` / ``display_height`` and append an arrow + label there.
53
+ # Because ``edit_chrome`` is on, the new marker is immediately draggable.
54
+
55
+
56
+ def _on_double_click(event):
57
+ if event.x is None or event.display_width is None:
58
+ return
59
+ fx = float(np.clip(event.x / event.display_width, 0.02, 0.98))
60
+ fy = float(np.clip(event.y / event.display_height, 0.02, 0.98))
61
+ markers = fig.figure_markers # current list (a copy)
62
+ n = sum(1 for m in markers if m["kind"] == "text")
63
+ markers.append({"kind": "arrow", "x": fx, "y": fy,
64
+ "u": 0.08, "v": -0.08, "color": "#40c4ff", "linewidth": 2})
65
+ markers.append({"kind": "text", "x": fx + 0.08, "y": fy - 0.10,
66
+ "text": f"mark {n}", "color": "#40c4ff", "fontsize": 13})
67
+ fig.set_figure_markers(markers)
68
+
69
+
70
+ v.add_event_handler(_on_double_click, "double_click")
71
+
72
+ fig.set_help(
73
+ "Double-click on the image to drop a labelled arrow.\n"
74
+ "Drag any annotation to reposition it (edit mode is on)."
75
+ )
76
+
77
+ fig # Interactive
78
+
79
+ # %%
80
+ # Read the placements back
81
+ # ------------------------
82
+ # After the user drags things around, ``fig.figure_markers`` reflects the
83
+ # current fraction positions — persist them, export them, or feed them into a
84
+ # report.
85
+
86
+ for m in fig.figure_markers:
87
+ print(m["kind"], round(m["x"], 3), round(m["y"], 3))
@@ -29,7 +29,7 @@ import anyplotlib as apl
29
29
  rng = np.random.default_rng(11)
30
30
 
31
31
  # ── 1. Synthetic 3-D polycrystal: nearest-seed voxel grain map ──────────────
32
- N = 48 # volume is N³ voxels, indexed V[z, y, x]
32
+ N = 24 # volume is N³ voxels, indexed V[z, y, x]
33
33
  N_GRAINS = 40
34
34
 
35
35
  seeds = rng.uniform(0, N, size=(N_GRAINS, 3)) # (z, y, x)
@@ -76,7 +76,8 @@ grain_rgb_u8 = (rgb * 255).astype(np.uint8) # (N_GRAINS, 3)
76
76
  # planes. This anchors the highlight exactly where the slices intersect,
77
77
  # shows real slice contents in 3-D, and scales: the on-plane count is
78
78
  # ~3·(N/step)² regardless of N, so it stays fast even for a 256³ volume.
79
- VSTEP = max(1, N // 48) # in-plane downsample ~48² cubes per plane
79
+ VSTEP = max(1, N // 48) # in-plane downsample, capping at ~48² cubes/plane
80
+ # for large N (no downsampling at this N=24)
80
81
 
81
82
  # Voxel cube size in data units. A touch larger than VSTEP so the three
82
83
  # slabs read as solid sheets rather than a dotted grid.
@@ -140,6 +141,8 @@ v_vol = ax_vol.voxels(
140
141
  size=VOXSIZE, alpha=0.55,
141
142
  x_label="x", y_label="y", z_label="z",
142
143
  bounds=((0, N - 1),) * 3, zoom=1.1,
144
+ # gpu="auto" (default): ~1.7k cubes is over the ~1k voxel threshold, so the
145
+ # WebGPU instanced path handles each drag re-slice when WebGPU is present.
143
146
  )
144
147
  v_vol.set_title("Grain volume — drag a plane to re-slice")
145
148
 
@@ -0,0 +1,49 @@
1
+ """
2
+ Coordinate transforms — pin markers to the axes or the screen
3
+ =============================================================
4
+
5
+ Every ``add_*`` marker method takes a ``transform`` that decides which
6
+ coordinate system its positions live in:
7
+
8
+ - ``"data"`` (default) — data coordinates; the marker moves and scales with
9
+ zoom / pan, staying glued to the underlying data.
10
+ - ``"axes"`` — axes-normalised ``(0, 0)`` bottom-left … ``(1, 1)`` top-right;
11
+ the marker stays in the same corner of the panel no matter how you zoom.
12
+ - ``"display"`` — raw CSS pixels within the panel; a fixed-size decoration.
13
+
14
+ The ``"axes"`` and ``"display"`` transforms are how you build overlays that
15
+ should *not* track the data — a navigation index in the corner, a scale bar, a
16
+ persistent legend chip.
17
+ """
18
+ import numpy as np
19
+ import anyplotlib as apl
20
+
21
+ rng = np.random.default_rng(1)
22
+ data = rng.standard_normal((128, 128)).cumsum(0).cumsum(1)
23
+ data = (data - data.min()) / (data.max() - data.min())
24
+ xy = np.linspace(0, 10, 128)
25
+
26
+ fig, ax = apl.subplots(1, 1, figsize=(480, 480))
27
+ v = ax.imshow(data, axes=[xy, xy], units="nm")
28
+
29
+ # Data-anchored label — sits at (5, 5) in nm and rides along on zoom/pan.
30
+ v.add_texts(offsets=[(5, 5)], texts=["feature @ (5, 5)"],
31
+ color="#ffffff", fontsize=12, name="data_label")
32
+
33
+ # Axes-anchored index — stays pinned to the top-left corner of the panel
34
+ # regardless of zoom (0, 1 = top-left in axes fractions).
35
+ v.add_texts(offsets=[(0.04, 0.96)], texts=["frame 3 / 20"],
36
+ transform="axes", color="#ffd54f", fontsize=13, name="nav_index")
37
+
38
+ fig
39
+
40
+ # %%
41
+ # Try it
42
+ # ------
43
+ # Zoom into the image: the white ``feature`` label moves with the data, while
44
+ # the yellow ``frame 3 / 20`` index stays locked to the corner — because it is
45
+ # positioned in ``"axes"`` coordinates.
46
+
47
+ fig.set_help("Zoom in: the corner index stays put; the data label moves.")
48
+
49
+ fig # Interactive
@@ -0,0 +1,82 @@
1
+ """
2
+ GPU-accelerated voxels
3
+ ======================
4
+
5
+ :meth:`~anyplotlib.Axes.voxels` renders shaded translucent cubes for a
6
+ volumetric field. With ``gpu="auto"`` (the default) anyplotlib uses WebGPU
7
+ instancing when a GPU is available and the cube count is large, so hundreds of
8
+ thousands of voxels stay interactive; otherwise it falls back to the Canvas2D
9
+ path. Read :attr:`~anyplotlib.Plot3D.gpu_active` after the first frame to see
10
+ which path was chosen.
11
+
12
+ Here we build a dense spherical shell — enough cubes that the WebGPU path
13
+ kicks in — and drop a draggable :class:`~anyplotlib.PlaneWidget` through it as
14
+ a slice selector.
15
+ """
16
+ import numpy as np
17
+ import anyplotlib as apl
18
+
19
+ # %%
20
+ # Build a volumetric field
21
+ # ------------------------
22
+ # Voxel *centres* are passed as three flat coordinate arrays (not a dense 3-D
23
+ # grid), so you only send the cubes you actually want drawn. We keep the
24
+ # voxels inside a spherical shell and colour them by radius.
25
+
26
+ N = 64
27
+ g = np.arange(N)
28
+ Z, Y, X = np.meshgrid(g, g, g, indexing="ij")
29
+ r = np.sqrt((X - N / 2) ** 2 + (Y - N / 2) ** 2 + (Z - N / 2) ** 2)
30
+ shell = (r > N * 0.30) & (r < N * 0.42) # a hollow sphere
31
+
32
+ xs, ys, zs = X[shell], Y[shell], Z[shell]
33
+ print(f"{xs.size:,} voxels") # tens of thousands → GPU path under gpu='auto'
34
+
35
+ # Colour by radius with the viridis-ish default cycle mapped through intensity.
36
+ t = (r[shell] - r[shell].min()) / (np.ptp(r[shell]) + 1e-9)
37
+ colors = np.stack([0.2 + 0.8 * t, 0.4 * np.ones_like(t), 1.0 - 0.8 * t], axis=1)
38
+
39
+ fig, ax = apl.subplots(1, 1, figsize=(560, 520))
40
+ vol = ax.voxels(
41
+ xs, ys, zs, colors=colors,
42
+ size=1.0, alpha=0.35,
43
+ bounds=((0, N - 1),) * 3,
44
+ azimuth=-55, elevation=28, zoom=1.1,
45
+ gpu="auto", # WebGPU when available, Canvas2D otherwise
46
+ )
47
+ vol.set_title("Spherical shell — drag to rotate, scroll to zoom")
48
+
49
+ # %%
50
+ # Add a slice-selector plane
51
+ # --------------------------
52
+ # A :class:`~anyplotlib.PlaneWidget` is a draggable axis-aligned plane. Voxels
53
+ # lying on it render more opaque, so the current slice pops out of the
54
+ # translucent volume. Drag it along z in the browser, or move it from Python.
55
+
56
+ plane = vol.add_widget("plane", axis="z", position=N // 2,
57
+ color="#40c4ff", alpha=0.18)
58
+
59
+
60
+ @plane.add_event_handler("pointer_move")
61
+ def _on_slice(event):
62
+ # Fires while the plane is dragged; pw.position holds the live slice index.
63
+ print("slice at z =", round(plane.position, 1))
64
+
65
+
66
+ fig.set_help(
67
+ "Drag: rotate · Scroll: zoom · R: reset view\n"
68
+ "Drag the blue plane to slide the z-slice through the shell."
69
+ )
70
+
71
+ fig # Interactive
72
+
73
+ # %%
74
+ # Which render path ran?
75
+ # ----------------------
76
+ # ``gpu_active`` is populated once the browser reports back after the first
77
+ # frame. It is ``True`` when the WebGPU instanced path is live, ``False`` on
78
+ # the Canvas2D fallback, and ``None`` before the first frame (as when this
79
+ # gallery page is built headlessly). Force a path with ``gpu=True`` /
80
+ # ``gpu=False`` if you need determinism.
81
+
82
+ print("gpu_active:", vol.gpu_active)
@@ -0,0 +1,43 @@
1
+ """
2
+ Step lines and log-scale spectra
3
+ ================================
4
+
5
+ Two 1-D options that suit spectral data: a **mid-riser step** line (constant
6
+ within each bin, jumping at bin midpoints) via ``linestyle="step-mid"``, and a
7
+ **logarithmic y-axis** via :meth:`~anyplotlib.Axes.semilogy` (or
8
+ ``ax.plot(..., yscale="log")``).
9
+ """
10
+ import numpy as np
11
+ import anyplotlib as apl
12
+
13
+ # A noisy binned spectrum.
14
+ rng = np.random.default_rng(0)
15
+ energy = np.linspace(0, 20, 60)
16
+ counts = (np.exp(-(energy - 6) ** 2 / 4) * 1000
17
+ + np.exp(-(energy - 13) ** 2 / 8) * 400
18
+ + rng.uniform(0, 20, energy.size))
19
+
20
+ # %%
21
+ # Step line
22
+ # ---------
23
+ # ``linestyle="step-mid"`` draws a horizontal segment centred on each x value
24
+ # with vertical risers between them — the standard way to show histogram-like
25
+ # spectra without implying interpolation between channels.
26
+
27
+ fig, ax = apl.subplots(1, 1, figsize=(560, 340))
28
+ ax.plot(counts, axes=[energy], color="#4fc3f7",
29
+ linestyle="step-mid", label="counts")
30
+
31
+ fig
32
+
33
+ # %%
34
+ # Log y-axis
35
+ # ----------
36
+ # ``semilogy`` is shorthand for a log y-scale, which brings out the small
37
+ # secondary peak that the linear plot flattens. Combine it with the step line
38
+ # for a classic spectroscopy view.
39
+
40
+ fig2, ax2 = apl.subplots(1, 1, figsize=(560, 340))
41
+ ax2.semilogy(counts, axes=[energy], color="#ff7043", linestyle="step-mid")
42
+
43
+ fig2
@@ -0,0 +1,184 @@
1
+ """
2
+ Custom Tile Backend — pan & zoom a huge image you never hold in memory
3
+ ======================================================================
4
+
5
+ For a very large image, anyplotlib does not need the whole array. It asks a
6
+ :class:`~anyplotlib.plot2d._tile_backend.TileBackend` for a downsampled
7
+ *overview* to use as the base texture, and then — on every zoom / pan — for a
8
+ crisp *detail tile* of just the visible region at the panel's resolution.
9
+ anyplotlib owns that zoom/pan → re-tile lifecycle; the backend owns the data.
10
+
11
+ A backend is any object implementing the ``TileBackend`` protocol:
12
+
13
+ ``full_shape`` (H, W), ``dtype``, ``origin``, ``extent()``, and
14
+ ``sample(x0, x1, y0, y1, out_w, out_h, method)`` — return the logical region
15
+ ``[y0:y1, x0:x1]`` resampled to ``(out_h, out_w)``.
16
+
17
+ Because ``sample`` is called *on demand*, the source never has to exist in
18
+ full. Here we make a **procedural** backend that computes a Mandelbrot fractal
19
+ for whatever region is requested — a 65 536 × 65 536 "image" (4.3 gigapixels)
20
+ that is generated tile-by-tile as you explore it. The array is never
21
+ materialised; only the small tiles you look at are ever computed.
22
+ """
23
+ import numpy as np
24
+ import anyplotlib as apl
25
+
26
+ # %%
27
+ # A procedural tile backend
28
+ # -------------------------
29
+ # The Mandelbrot escape count over a data-space window. The full logical
30
+ # image is ``SIZE x SIZE`` pixels mapping to the complex plane
31
+ # ``[-2.5, 1.0] x [-1.75, 1.75]``, but no pixel is computed until ``sample``
32
+ # is called for it.
33
+
34
+ SIZE = 65_536 # 65k x 65k logical pixels ≈ 4.3 gigapixels — never allocated
35
+ CX0, CX1 = -2.5, 1.0
36
+ CY0, CY1 = -1.75, 1.75
37
+
38
+
39
+ class MandelbrotTileBackend:
40
+ """A ``TileBackend`` that synthesises fractal tiles on demand."""
41
+
42
+ max_iter = 80
43
+
44
+ @property
45
+ def full_shape(self):
46
+ return (SIZE, SIZE)
47
+
48
+ @property
49
+ def dtype(self):
50
+ return np.dtype("uint16") # escape counts
51
+
52
+ @property
53
+ def origin(self):
54
+ return "lower" # row 0 at the bottom (matches the y data-axis below)
55
+
56
+ def extent(self):
57
+ # Data-space (x0, x1, y0, y1) the image spans, or None for pixel
58
+ # coordinates. Return None here and set the coordinate axes explicitly
59
+ # on imshow (below) so the ticks read as real/imaginary values.
60
+ return None
61
+
62
+ def sample(self, x0, x1, y0, y1, out_w, out_h, method="mean"):
63
+ # Only the requested window is ever evaluated, at exactly the output
64
+ # resolution the panel asked for — so a zoomed-in tile is as sharp as a
65
+ # zoomed-out overview is cheap. The escape loop is written for speed
66
+ # (it runs once per pan/zoom), using three tricks over the textbook
67
+ # version:
68
+ # * real / imag ``float32`` arithmetic instead of complex128 — half
69
+ # the memory traffic and no complex-multiply overhead;
70
+ # * the escape test is ``|z|² > 4`` (``zr² + zi²``), avoiding the
71
+ # ``sqrt`` in ``abs``;
72
+ # * no shrinking boolean mask. Instead we ``counts += inside`` every
73
+ # iteration (a cheap bool→uint add): each pixel's count is simply
74
+ # how many steps it stayed inside. Escaped pixels keep iterating to
75
+ # ``inf``, which is harmless — ``inf > 4`` just keeps them counted
76
+ # out. This is ~3× faster on the overview and ~8× faster on a
77
+ # deep-zoom tile (where nothing escapes early, so a mask never
78
+ # shrinks and the old code crawled).
79
+ cr = (CX0 + (np.linspace(x0, x1, out_w, dtype=np.float32) / SIZE)
80
+ * (CX1 - CX0))[np.newaxis, :]
81
+ ci = (CY0 + (np.linspace(y0, y1, out_h, dtype=np.float32) / SIZE)
82
+ * (CY1 - CY0))[:, np.newaxis]
83
+ cr = np.ascontiguousarray(np.broadcast_to(cr, (out_h, out_w)))
84
+ ci = np.ascontiguousarray(np.broadcast_to(ci, (out_h, out_w)))
85
+
86
+ zr = np.zeros((out_h, out_w), np.float32)
87
+ zi = np.zeros((out_h, out_w), np.float32)
88
+ counts = np.zeros((out_h, out_w), np.uint16)
89
+ zr2 = np.empty_like(zr)
90
+ zi2 = np.empty_like(zr)
91
+ tmp = np.empty_like(zr)
92
+ # Escaped pixels run off to ±inf; the inf−inf / inf·0 they produce is
93
+ # expected and never touches `counts` (an escaped pixel already fails
94
+ # the `<= 4` test), so silence the overflow/invalid warnings.
95
+ with np.errstate(over="ignore", invalid="ignore"):
96
+ for i in range(self.max_iter):
97
+ np.multiply(zr, zr, out=zr2)
98
+ np.multiply(zi, zi, out=zi2)
99
+ inside = (zr2 + zi2) <= 4.0
100
+ counts += inside # still-inside pixels tick up
101
+ # z ← z² + c (in real / imag form; the shared temporaries
102
+ # above keep this allocation-free inside the loop).
103
+ np.multiply(zr, zi, out=tmp)
104
+ tmp *= 2.0
105
+ tmp += ci # zi_next = 2·zr·zi + ci
106
+ np.subtract(zr2, zi2, out=zr)
107
+ zr += cr # zr_next = zr² − zi² + cr
108
+ zi, tmp = tmp, zi # swap in the new zi
109
+ if i % 16 == 15 and not inside.any():
110
+ break # whole tile escaped — done
111
+ return counts
112
+
113
+
114
+ # %%
115
+ # Wire it into a plot
116
+ # -------------------
117
+ # Pass the backend as ``tile_backend=``. The ``data`` argument is just a
118
+ # placeholder — the backend's ``full_shape`` drives the axes, and ``tile=True``
119
+ # forces the tiled path. Zoom and pan to stream sharp detail tiles; anyplotlib
120
+ # computes only what is on screen.
121
+ #
122
+ # **Tiling and the GPU cooperate.** With ``gpu="auto"`` (the default) and a
123
+ # WebGPU-capable browser, the downsampled overview is uploaded once as a GPU
124
+ # texture and drawn by the shader-side colormap, and the crisp detail tile for
125
+ # the visible region is stitched on top of it each frame. The logical image
126
+ # here is 4.3 gigapixels, which is far above the ~1-megapixel threshold that
127
+ # switches the WebGPU image path on under ``"auto"`` — so the base raster is
128
+ # GPU-composited rather than blitted on the CPU. (Headless/off-screen renders,
129
+ # like this gallery's static thumbnail, have no WebGPU and fall back to
130
+ # Canvas2D; the live "Run in browser" version above uses the GPU when your
131
+ # browser supports it.)
132
+
133
+ backend = MandelbrotTileBackend()
134
+
135
+ # Coordinate axes spanning the logical image, mapped to the complex plane so
136
+ # ticks read as real / imaginary values (the tiling still works in pixels).
137
+ x_axis = np.linspace(CX0, CX1, SIZE)
138
+ y_axis = np.linspace(CY0, CY1, SIZE)
139
+
140
+ fig, ax = apl.subplots(1, 1, figsize=(560, 560))
141
+ v = ax.imshow(
142
+ np.zeros((2, 2)), # placeholder; overview comes from backend.sample
143
+ axes=[x_axis, y_axis],
144
+ tile_backend=backend,
145
+ tile=True,
146
+ gpu="auto", # WebGPU texture path when available (see above)
147
+ cmap="inferno",
148
+ units="Re / Im",
149
+ )
150
+ fig.set_help(
151
+ "Scroll to zoom, drag to pan.\n"
152
+ "Each view fetches a fresh detail tile — the 4.3 GP fractal is\n"
153
+ "generated on demand and never held in memory."
154
+ )
155
+
156
+ fig # Interactive
157
+
158
+ # %%
159
+ # Is the GPU actually painting?
160
+ # -----------------------------
161
+ # Read :attr:`~anyplotlib.Plot2D.gpu_active` to see which path ran. anyplotlib
162
+ # updates it automatically once the browser reports back after the first frame:
163
+ # ``True`` when the WebGPU image path is live, ``False`` on the Canvas2D
164
+ # fallback. It is ``False`` here because the gallery build is headless (no
165
+ # WebGPU); open the live "Run in browser" version above and read ``v.gpu_active``
166
+ # to confirm the accelerated path on your own machine.
167
+
168
+ print("gpu_active:", v.gpu_active)
169
+
170
+ # %%
171
+ # Swap in a real source
172
+ # ---------------------
173
+ # The same protocol wraps any large source without changing the plot: back
174
+ # ``sample`` with a memory-mapped file, a dask/zarr chunk store, or a
175
+ # GPU-resident tensor. For an in-memory ndarray you don't need a custom class
176
+ # at all — :func:`~anyplotlib.plot2d._tile_backend.as_tile_backend` (used
177
+ # internally when you pass a plain array with ``tile="auto"``) wraps it in the
178
+ # default :class:`~anyplotlib.plot2d._tile_backend.NumpyTileBackend`.
179
+ #
180
+ # .. note::
181
+ #
182
+ # ``sample`` should be reasonably fast — it runs once per view change. A
183
+ # slow source (network, disk) still works, but pan/zoom will feel as
184
+ # responsive as the slowest ``sample`` call.
@@ -0,0 +1,45 @@
1
+ """
2
+ Twinned (secondary) y-axis
3
+ ==========================
4
+
5
+ Overlay two series that live on very different scales on a single
6
+ :class:`~anyplotlib.Plot1D` panel, each with its own y-axis. Enable the
7
+ right-hand axis with :meth:`~anyplotlib.Plot1D.add_right_axis`, then add curves
8
+ to it with ``add_line(..., axis="right")`` — they are scaled and labelled
9
+ independently of the left axis.
10
+ """
11
+ import numpy as np
12
+ import anyplotlib as apl
13
+
14
+ x = np.linspace(0, 10, 400)
15
+ signal = np.sin(x) # left axis: −1 … 1
16
+ temperature = 300 + 350 * np.cos(x / 2) # right axis: ~ −50 … 650
17
+
18
+ fig, ax = apl.subplots(1, 1, figsize=(560, 360))
19
+ plot = ax.plot(signal, axes=[x], color="#4fc3f7", label="signal")
20
+ plot.set_ylabel("Amplitude")
21
+
22
+ # %%
23
+ # Add the secondary axis
24
+ # ----------------------
25
+ # ``add_right_axis`` turns on the right-hand y-axis; ``axis="right"`` anchors
26
+ # the new curve to it. The left axis stays fixed at −1 … 1 while the right
27
+ # axis auto-scales to the temperature range.
28
+
29
+ plot.add_right_axis(color="#e05a2b")
30
+ plot.add_line(temperature, x_axis=x, color="#e05a2b",
31
+ axis="right", label="temperature")
32
+ plot.set_right_ylabel("Temperature (K)")
33
+
34
+ fig
35
+
36
+ # %%
37
+ # Pin the secondary range
38
+ # -----------------------
39
+ # By default the right axis auto-scales to its lines. Call
40
+ # :meth:`~anyplotlib.Plot1D.set_right_ylim` to fix it explicitly (for example
41
+ # to line two datasets up at a shared reference level).
42
+
43
+ plot.set_right_ylim(0, 700)
44
+
45
+ fig