anyplotlib 0.2.0__tar.gz → 0.3.0b1__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 (247) hide show
  1. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/CHANGELOG.rst +40 -0
  2. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/PKG-INFO +1 -1
  3. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/FIGURE_ESM.md +225 -1
  4. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/_electron.py +15 -2
  5. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/_repr_utils.py +76 -4
  6. anyplotlib-0.3.0b1/anyplotlib/axes/_inset_axes.py +254 -0
  7. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/callbacks.py +4 -0
  8. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/figure/_figure.py +176 -8
  9. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/figure_esm.js +1132 -45
  10. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot2d/__init__.py +3 -2
  11. anyplotlib-0.3.0b1/anyplotlib/plot2d/_layer.py +121 -0
  12. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot2d/_plot2d.py +385 -23
  13. anyplotlib-0.3.0b1/anyplotlib/tests/test_embed/test_export_png.py +777 -0
  14. anyplotlib-0.3.0b1/anyplotlib/tests/test_interactive/test_edit_chrome.py +435 -0
  15. anyplotlib-0.3.0b1/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +842 -0
  16. anyplotlib-0.3.0b1/anyplotlib/tests/test_layouts/test_inset_callout.py +636 -0
  17. anyplotlib-0.3.0b1/anyplotlib/tests/test_plot2d/test_layers.py +427 -0
  18. anyplotlib-0.3.0b1/anyplotlib/tests/test_plot2d/test_layers_playwright.py +313 -0
  19. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/widgets/__init__.py +2 -2
  20. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/widgets/_widgets2d.py +69 -12
  21. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_root/switcher.json +5 -0
  22. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/conf.py +1 -1
  23. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/pyproject.toml +1 -1
  24. anyplotlib-0.2.0/anyplotlib/axes/_inset_axes.py +0 -126
  25. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.github/workflows/ci.yml +0 -0
  26. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.github/workflows/docs.yml +0 -0
  27. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.github/workflows/prepare_release.yml +0 -0
  28. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.github/workflows/release.yml +0 -0
  29. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.github/workflows/tests.yml +0 -0
  30. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/.gitignore +0 -0
  31. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/AGENTS.md +0 -0
  32. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Benchmarks/README.rst +0 -0
  33. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
  34. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/README.rst +0 -0
  35. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
  36. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_eels_explorer.py +0 -0
  37. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_interactive_fft.py +0 -0
  38. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_interactive_fitting.py +0 -0
  39. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_ipf_density_map.py +0 -0
  40. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_ipf_explorer.py +0 -0
  41. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_key_bindings.py +0 -0
  42. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_particle_picker.py +0 -0
  43. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_point_widget.py +0 -0
  44. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
  45. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
  46. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
  47. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_threshold_explorer.py +0 -0
  48. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
  49. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/README.rst +0 -0
  50. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_arrows.py +0 -0
  51. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_circles.py +0 -0
  52. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_ellipses.py +0 -0
  53. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_horizontal_lines.py +0 -0
  54. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_line_segments.py +0 -0
  55. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_points.py +0 -0
  56. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_polygons.py +0 -0
  57. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_rectangles.py +0 -0
  58. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_squares.py +0 -0
  59. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_texts.py +0 -0
  60. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Markers/plot_vertical_lines.py +0 -0
  61. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/README.rst +0 -0
  62. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_3d.py +0 -0
  63. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_bar.py +0 -0
  64. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
  65. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_image2d.py +0 -0
  66. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_inset.py +0 -0
  67. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_label_formatting.py +0 -0
  68. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_line_styles.py +0 -0
  69. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
  70. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/PlotTypes/plot_spectra1d.py +0 -0
  71. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/README.rst +0 -0
  72. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/README.rst +0 -0
  73. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget1d_hline.py +0 -0
  74. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget1d_range.py +0 -0
  75. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget1d_vline.py +0 -0
  76. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_annular.py +0 -0
  77. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_circle.py +0 -0
  78. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
  79. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_label.py +0 -0
  80. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
  81. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
  82. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/LARGE_IMAGE_WEBGPU_PLAN.md +0 -0
  83. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/LICENSE +0 -0
  84. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/Makefile +0 -0
  85. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/ORIX_BACKEND_PLAN.md +0 -0
  86. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/README.md +0 -0
  87. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/RELEASE_NOTES.md +0 -0
  88. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/RELEASE_PLAN.md +0 -0
  89. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/WEBGPU_PLAN.md +0 -0
  90. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/__init__.py +0 -0
  91. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/_base_plot.py +0 -0
  92. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/_binary_frame.py +0 -0
  93. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/_utils.py +0 -0
  94. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/axes/__init__.py +0 -0
  95. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/axes/_axes.py +0 -0
  96. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/conftest.py +0 -0
  97. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/embed.py +0 -0
  98. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/figure/__init__.py +0 -0
  99. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/figure/_gridspec.py +0 -0
  100. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/figure/_subplots.py +0 -0
  101. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/markers.py +0 -0
  102. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot1d/__init__.py +0 -0
  103. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot1d/_plot1d.py +0 -0
  104. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot1d/_plotbar.py +0 -0
  105. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot2d/_plotmesh.py +0 -0
  106. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot2d/_tile_backend.py +0 -0
  107. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot3d/__init__.py +0 -0
  108. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plot3d/_plot3d.py +0 -0
  109. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plotxy/__init__.py +0 -0
  110. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/plotxy/_plotxy.py +0 -0
  111. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
  112. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
  113. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
  114. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
  115. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
  116. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
  117. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
  118. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
  119. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
  120. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
  121. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
  122. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
  123. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
  124. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
  125. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/__init__.py +0 -0
  126. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
  127. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/_png_utils.py +0 -0
  128. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/bar_basic.png +0 -0
  129. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
  130. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
  131. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
  132. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
  133. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
  134. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
  135. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
  136. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
  137. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
  138. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
  139. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
  140. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_1d.png +0 -0
  141. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
  142. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
  143. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
  144. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
  145. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
  146. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
  147. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
  148. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
  149. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
  150. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
  151. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
  152. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
  153. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
  154. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
  155. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
  156. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
  157. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
  158. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/conftest.py +0 -0
  159. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
  160. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
  161. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
  162. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
  163. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
  164. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_documentation/__init__.py +0 -0
  165. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
  166. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
  167. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
  168. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_electron/__init__.py +0 -0
  169. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
  170. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_embed/__init__.py +0 -0
  171. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
  172. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
  173. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_examples/__init__.py +0 -0
  174. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
  175. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/__init__.py +0 -0
  176. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
  177. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
  178. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
  179. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
  180. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
  181. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
  182. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
  183. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
  184. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
  185. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_title.py +0 -0
  186. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
  187. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
  188. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_labels/__init__.py +0 -0
  189. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
  190. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
  191. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
  192. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/__init__.py +0 -0
  193. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
  194. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
  195. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
  196. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
  197. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
  198. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
  199. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_markers/__init__.py +0 -0
  200. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
  201. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_markers/test_markers.py +0 -0
  202. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
  203. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
  204. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
  205. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
  206. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
  207. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
  208. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
  209. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
  210. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
  211. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
  212. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
  213. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
  214. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
  215. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
  216. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
  217. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
  218. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
  219. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
  220. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
  221. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
  222. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
  223. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
  224. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/widgets/_base.py +0 -0
  225. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/widgets/_widgets1d.py +0 -0
  226. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/anyplotlib/widgets/_widgets3d.py +0 -0
  227. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_root/index.html +0 -0
  228. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_sg_html_scraper.py +0 -0
  229. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_static/anyplotlib.svg +0 -0
  230. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_static/custom.css +0 -0
  231. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_static/pyodide_bridge.js +0 -0
  232. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/_templates/autosummary/class.rst +0 -0
  233. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/callbacks.rst +0 -0
  234. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/figure.rst +0 -0
  235. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/figure_plots.rst +0 -0
  236. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/index.rst +0 -0
  237. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/markers.rst +0 -0
  238. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/api/widgets.rst +0 -0
  239. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/benchmarking.rst +0 -0
  240. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/dev/index.rst +0 -0
  241. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/embedding.rst +0 -0
  242. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/events.rst +0 -0
  243. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/getting_started.rst +0 -0
  244. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/docs/index.rst +0 -0
  245. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/make.bat +0 -0
  246. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/upcoming_changes/README.rst +0 -0
  247. {anyplotlib-0.2.0 → anyplotlib-0.3.0b1}/uv.lock +0 -0
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyplotlib
3
- Version: 0.2.0
3
+ Version: 0.3.0b1
4
4
  Summary: A plotting library using python, javascript and anywidget for performant in browser plotting.
5
5
  Project-URL: Homepage, https://cssfrancis.github.io/anyplotlib/
6
6
  Project-URL: Repository, https://github.com/CSSFrancis/anyplotlib
@@ -63,6 +63,8 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
63
63
  | `drawScaleBar2d` / `drawColorbar2d` | 1360 / 1436 |
64
64
  | `_drawAxes2d` (ticks, labels, title) | 1491 |
65
65
  | `drawOverlay2d` / `drawMarkers2d` | 1629 / 1685 |
66
+ | **Image layers**: `_layerBytes` / `_layerBitmap` / `_drawLayers2d` | 1553 / 1577 / 1629 |
67
+ | Binary-bytes splice: `_spliceBinaryBytes` / `_registerBinaryPixelListeners` | 675 / 706 |
66
68
  | **3D drawing**: `draw3d` | 1833 |
67
69
  | Event emission `_emitEvent` | 2031 |
68
70
  | 3D event handlers `_attachEvents3d` | 2059 |
@@ -134,7 +136,66 @@ geometry changes (visibility, label, sizes) re-layout automatically.
134
136
  #### `applyLayout()` (line 590)
135
137
  Reads `layout_json`. Builds CSS grid tracks from `panel_specs[].panel_width/height`.
136
138
  Creates panels that don't exist yet, resizes existing ones, removes stale ones.
137
- Also creates/updates inset panels from `inset_specs`.
139
+ Also creates/updates inset panels from `inset_specs`, then draws region
140
+ indications from `layout.indications` (on the next frame — see below).
141
+
142
+ #### Inset placement (`_applyAllInsetStates`)
143
+ Each `inset_specs[]` entry carries EITHER `corner` (one of the four corners;
144
+ `anchor` is `null`) OR `anchor` (`[x_frac, y_frac]` of the inset's top-left in
145
+ figure fraction; `corner` is `null`). Corner insets stack per-corner with
146
+ `INSET_GAP`; anchored insets are placed directly at their fraction (clamped
147
+ inside the figure). Minimize / maximize / restore work for both — a maximized
148
+ inset floats centred at ~72 % (z 45); a minimized one collapses to its title
149
+ bar in place.
150
+
151
+ #### Region indications (callouts — `_drawCallouts`)
152
+ `layout.indications` is an array of mark_inset-style callouts, each
153
+ `{inset_id, parent_id, region:[x,y,w,h], color, linestyle, linewidth}`.
154
+ `_drawCallouts()` renders them onto a figure-level `calloutCanvas` (z 30, above
155
+ panels + insets, below maximized-inset float and the resize handle,
156
+ `pointer-events:none`):
157
+ - The **dashed source rect** maps `region` (parent DATA coords) through the
158
+ parent's `_imgToCanvas2d` every draw, so it tracks the parent's zoom/pan; it
159
+ is clipped to the parent's image area.
160
+ - Two **leader lines** connect the rect's corners facing the inset to the
161
+ inset's nearest corners (loc1/loc2-auto by comparing centres); they follow
162
+ the inset's live DOM rect and are **hidden while the inset is minimized**.
163
+
164
+ `_drawCallouts()` is called at the end of `_redrawPanel` / `redrawAll` (tracks
165
+ zoom/pan), at the end of `_applyAllInsetStates` (inset moved), on `applyLayout`
166
+ (deferred one rAF so `getBoundingClientRect` is real), and inside `exportPNG`
167
+ (forced fresh draw, then `calloutCanvas` composited last). All coordinates go
168
+ through element bounding rects relative to the callout canvas, so no layout math
169
+ is duplicated. Cheap no-op when `indications` is empty (just clears the canvas).
170
+ The `if (!parent || !parent.state || !inset || !inset.isInset) continue;` guard
171
+ per indication is defensive/permanent — kept even though a foreign-figure
172
+ `parent_plot` can no longer reach this array at all (see validation below); it
173
+ still protects against other edge cases (e.g. a panel mid-teardown).
174
+
175
+ **`InsetAxes.indicate_region(parent_plot, region, …)` validates both arguments**
176
+ before recording an indication: `parent_plot` must be a panel registered on
177
+ THIS inset's own `Figure` (`self._fig._plots_map.get(pid) is parent_plot` —
178
+ not just "has some `_id`", which is the pre-existing check for "never attached
179
+ to any figure") — a plot that belongs to a *different* `Figure` raises
180
+ `ValueError`. `region` must be exactly 4 finite numbers `(x, y, w, h)` with
181
+ `w > 0` and `h > 0` — `NaN`/`inf`, a degenerate/negative size, or the wrong
182
+ number of values raises `ValueError`. A region that extends OUTSIDE the
183
+ parent's data bounds is explicitly **allowed** (clipping is a visual concern
184
+ handled by `_drawCallouts`'s clip-to-image-area, not a validation error). See
185
+ `test_indicate_region_foreign_figure_parent_raises`,
186
+ `test_indicate_region_foreign_inset_parent_raises`,
187
+ `test_indicate_region_degenerate_region_raises`, and
188
+ `test_indicate_region_out_of_bounds_is_allowed` in
189
+ `tests/test_layouts/test_inset_callout.py`.
190
+
191
+ **Inset removal**: as of this writing there is no `remove_inset` (or
192
+ equivalent) API — `Figure._insets_map` / `_plots_map` are only ever appended
193
+ to, never deleted from, so `indications` (rebuilt fresh from `_insets_map` on
194
+ every `_push_layout()` call) cannot go stale from a removed inset today. If a
195
+ removal API is added later, it MUST also delete the inset's entry from both
196
+ maps — otherwise `layout.indications` would keep emitting an entry whose
197
+ `inset_id` no longer resolves to a live panel (caught by the `_drawCallouts`
198
+ guard above, but a dangling entry all the same).
138
199
 
139
200
  #### `_createPanelDOM(id, kind, pw, ph, spec)` (line 763)
140
201
  Builds all canvas/DOM elements for one panel (via `_buildCanvasStack`),
@@ -196,6 +257,111 @@ inverses of the blit geometry.
196
257
 
197
258
  ---
198
259
 
260
+ ## Image layers (multi-image overlay)
261
+
262
+ `Plot2D.add_layer(data, cmap=, alpha=, clim=, visible=)` composites a second
263
+ (third, …) scalar image OVER the base image in the same panel, each with its own
264
+ colormap / clim / alpha. Distinct from `set_overlay_mask` (single-colour boolean
265
+ mask). `Layer.set(...)`, `Layer.set_data(frame)`, `Layer.remove()`,
266
+ `Plot2D.layers`, `Plot2D.remove_layer(layer)`. **Layers and tile mode are mutually
267
+ exclusive** (guard raises in both directions: `add_layer` on a tiled plot, and
268
+ `enable_tile` / `set_data(tile=True)` / auto-tile on a layered plot).
269
+
270
+ **A shape-changing `Plot2D.set_data` on a plot with layers raises `ValueError`.**
271
+ Each layer entry keeps the `(width, height)` it had at `add_layer` / its last
272
+ `Layer.set_data` time (`_encode_layer_pixels`), but `_drawLayers2d` always fits
273
+ every layer's bitmap into the BASE image's *current* `_imgFitRect(iw, ih, …)`
274
+ (`iw`/`ih` = the live `image_width`/`image_height`). So a base `set_data` that
275
+ changes shape while a stale-sized layer is still attached would silently stretch
276
+ that layer's old pixels over the new image instead of erroring — `set_data`
277
+ now checks `data.shape[:2]` against the current `image_height`/`image_width`
278
+ whenever `st.layers` is non-empty and raises before touching any state if they
279
+ differ (same-shape updates, the common live-update case, are unaffected). Remove
280
+ all layers first (`remove_layer`), change the base shape, then re-add them at the
281
+ new size. A layer-FREE plot's shape-changing `set_data` is unaffected and always
282
+ refreshes `image_width`/`image_height` (they're set unconditionally in the
283
+ pushed `fields` dict). See `TestTileGuards` / `TestShapeChangeNoLayers` in
284
+ `tests/test_plot2d/test_layers.py`.
285
+
286
+ **`Layer.set(clim=…)` has three distinct meanings** — `None` (default) leaves
287
+ the clim UNCHANGED (a no-op on that field, not "reset to auto"); a `(vmin, vmax)`
288
+ tuple sets an explicit range and re-quantises the cached frame over it;
289
+ `"auto"` is the sentinel to explicitly RESET to auto — recomputes the display
290
+ range from the layer's own current data (`self._layer_raw[layer_id]`) min/max,
291
+ the same auto-ranging `add_layer(..., clim=None)` does at creation time, and
292
+ re-quantises. Before this, `clim=None` was documented as "auto" but actually
293
+ behaved as a no-op, and there was no way to get back to auto range after
294
+ setting an explicit clim short of `remove()` + `add_layer()` again. See
295
+ `TestSet::test_set_clim_auto_resets_to_data_range` /
296
+ `test_set_clim_auto_matches_add_layer_auto` /
297
+ `test_set_clim_none_is_a_noop` in `tests/test_plot2d/test_layers.py`.
298
+
299
+ ### State + transport (dynamic per-layer pixel keys)
300
+
301
+ The layer *metadata* lives in `st.layers` (a list of small dicts on the light view
302
+ trait):
303
+
304
+ ```
305
+ st.layers = [{ id, cmap, clim_min, clim_max, alpha, visible,
306
+ width, height, colormap_data, image_b64 }, …] # z-order
307
+ ```
308
+
309
+ `image_b64` in each entry is the layer's pixels: a base64 string (Jupyter /
310
+ standalone / `save_html`) OR a `"\x00bin:<adler32>"` change-token (Electron binary
311
+ transport). The JS reads pixels from this entry field on the base64 path.
312
+
313
+ The *heavy pixel bytes* additionally ride a **DYNAMIC geometry key**
314
+ `layer_<id>_b64` — one per layer — mirroring how the base image `image_b64` rides
315
+ the geom channel. The dynamic-key mechanism:
316
+
317
+ - **`Plot2D._GEOM_KEYS` is a PROPERTY** (not a plain frozenset): it returns the
318
+ fixed base set (`image_b64`, `colormap_data`, `overlay_mask_b64`, `detail_b64`)
319
+ UNION the current `layer_<id>_b64` keys. So `Figure._push` splits every layer's
320
+ pixels off the light view trait onto `panel_<id>_geom` and dedup-caches them
321
+ exactly like the base image; a removed layer's key drops out automatically.
322
+ - **`_electron._route_change`** ships each layer key as its own PLOTBIN frame:
323
+ `_is_binary_pixel_key(k)` matches `k in _BINARY_KEYS` OR `layer_*_b64`. The
324
+ binary frame header carries `{"geom": "panel_<id>_geom"}` and `key=layer_<id>_b64`,
325
+ so the receiver builds slot `panel_<id>_geom::layer_<id>_b64` (the same
326
+ `awi_state_binary` handler as the base image — already generic on `hdr.geom` +
327
+ `e.data.key`, no change needed there).
328
+ - **`resolve_pixel_tokens`** (cold path: `save_html` / standalone) materialises
329
+ real base64 for every `layer_<id>_b64` key AND the entry `image_b64` mirror, so a
330
+ snapshot is self-contained.
331
+ - **JS `_spliceBinaryBytes`** scans `__apl_pixbytes` by the `panel_<id>_geom::`
332
+ PREFIX (not the old hardcoded 3-key list) so it splices any `layer_<id>_b64_bytes`
333
+ into `p2._geomCache`. The per-slot binary listeners are registered only for the
334
+ fixed keys (`_registerBinaryPixelListeners`); dynamic layer bytes are consumed by
335
+ the **geom-JSON change handler**, which now also calls `_spliceBinaryBytes` — the
336
+ geom trait always re-pushes when layers change, so a layer's bytes converge into
337
+ the cache regardless of trait arrival order.
338
+
339
+ ### JS compositing (`_drawLayers2d`, called from `draw2d`)
340
+
341
+ After the base image (Canvas2D blit OR WebGPU) and the overlay mask, and BEFORE
342
+ `_drawAxes2d` / markers / widgets, `_drawLayers2d(p, st, imgW, imgH, ctx, iw, ih)`
343
+ draws each **visible** layer bottom-up on `plotCanvas`:
344
+
345
+ - `_layerBytes(st, layer)` prefers `layer_<id>_b64_bytes` (binary) over the entry
346
+ `image_b64` base64;
347
+ - `_layerBitmap(p, st, layer)` builds a LUT-colormapped RGBA `OffscreenCanvas`,
348
+ **cached per layer id** by `(pixel key, cmap, clim)` — rebuilt only when the
349
+ layer's data or appearance changes (a live scrub that only swaps one layer's
350
+ data rebuilds just that layer);
351
+ - it blits with the SAME fit-rect + zoom/pan transform as the base blit
352
+ (`_imgFitRect` + the `zoom>=1` window math) at `ctx.globalAlpha = layer.alpha`,
353
+ so zoom/pan track the base exactly.
354
+
355
+ Because layers draw on `plotCanvas`, they sit UNDER `markersCanvas` /
356
+ `overlayCanvas` (z-order) and are captured by `exportPNG` for free (plotCanvas is
357
+ z1 in the composite). Over a WebGPU base the layers still composite in Canvas2D on
358
+ `plotCanvas` (which sits above the transparent `gpuCanvas`) — verified by
359
+ `test_layers_playwright.py::TestGpuBaseWithLayer`. Per-move perf: only the changed
360
+ layer's LUT bitmap is rebuilt (the box-loop is ~one pass over H×W uint8 → uint32,
361
+ comparable to the base image's `_buildLut32` blit).
362
+
363
+ ---
364
+
199
365
  ## 3D drawing (line ~1840)
200
366
  Orthographic projection; geometry b64-decoded and cached. `draw3d` sorts
201
367
  triangles, draws axes with per-axis `_drawTex` labels (`x/y/z_label_size`).
@@ -309,3 +475,61 @@ Python → JS (set widget position from Python):
309
475
  widget.set(…) → Figure._push_widget → event_json with source:'python'
310
476
  → model.on('change:event_json') patches overlay_widgets + redraws
311
477
  ```
478
+
479
+ ---
480
+
481
+ ## PNG export (`exportPNG`)
482
+
483
+ `render()` now RETURNS an internal API object `{ panels, exportPNG,
484
+ _gpuDisposeImagePanel, _gpuDisposePanel }` (anywidget ignores render()'s return;
485
+ `mount()` captures it — this also fixed a latent bug where the old
486
+ mount-handle `dispose()` referenced `panels`/`_gpuDispose*` from module scope,
487
+ which are inside `render`'s closure, and silently threw). The mount handle
488
+ exposes `handle.exportPNG({scale=1, includeWidgets=false}) →
489
+ Promise<{dataUrl,width,height}>`.
490
+
491
+ `exportPNG(opts)` (inside `render`, near `redrawAll`) composites the WHOLE
492
+ figure onto one offscreen canvas at `devicePixelRatio × scale`:
493
+
494
+ - **WebGPU hazard first**: a WebGPU canvas's drawing buffer is only valid right
495
+ after its render pass, so exportPNG force-calls `draw2d(p)` on every
496
+ active-GPU 2-D panel (`p._gpu==='active' && p.gpuCanvas` visible) to re-submit
497
+ its pass, THEN composites in the SAME synchronous task — so
498
+ `drawImage(gpuCanvas,…)` reads live pixels, not a blank buffer.
499
+ - **Extent**: `fig_width/height + 2×8 px` gridDiv padding (NOT the measured
500
+ `gridDiv` width — a bare `mount()` page has no `.apl-outer` inline-block CSS,
501
+ so the grid container can stretch to the viewport). **Origin**: `gridDiv`'s
502
+ top-left (grid tracks are fixed-px + left-anchored, so panels sit correctly).
503
+ - **Per-panel z-order** (`_drawEl` positions each canvas by its
504
+ `getBoundingClientRect()` relative to the root): gpuCanvas (z0) → plotCanvas
505
+ (z1) → x/yAxisCanvas → cbCanvas → [overlayCanvas z5 only if `includeWidgets`]
506
+ → markersCanvas (z6) → scaleBar (z7) → titleCanvas (z8). Grid panels first,
507
+ then insets (`p.isInset`) on top — **each titled inset's title bar text is
508
+ drawn directly onto the output canvas right after its canvas stack**
509
+ (`_drawInsetTitle`; the title bar is plain DOM — a `<div>`/`<span>`, not a
510
+ canvas — so `_drawEl` alone never captures it; approximates the on-screen
511
+ CSS: 11px sans-serif, `theme.tickText` colour, left-padded to the titleBar's
512
+ rect) — then the figure-level `calloutCanvas` (region indications) composited
513
+ LAST. Status bars / stats overlays are excluded.
514
+ - **Coordinate snapping** (`_drawEl`): `dx`/`dy` are `Math.round()`ed from the
515
+ element's `left`/`top`, and `dw`/`dh` are the ROUNDED `right`/`bottom` edge
516
+ minus the rounded `dx`/`dy` — never `Math.round(width)` directly. This makes
517
+ two elements that share a CSS edge (e.g. adjacent grid panels, or a
518
+ panel's axis-gutter canvas against its plotCanvas) round that shared edge to
519
+ the *same* output pixel on both sides. Without it, at a fractional effective
520
+ scale (`devicePixelRatio × opts.scale` — e.g. a real 150% Windows display, or
521
+ `scale: 1.25`), each element's `dx`/`dw` were computed independently as raw
522
+ floats, and adjacent elements could round their common boundary to different
523
+ output pixels — a 1px background-coloured seam (or overlap) exactly at the
524
+ join. See `TestExportMultiPanel::test_fractional_scale_no_seam_between_panels`
525
+ in `tests/test_embed/test_export_png.py` (reproduced with
526
+ `device_scale_factor=1.5`).
527
+ - Ends with `out.toDataURL('image/png')`; rejects the promise with a message on
528
+ failure (no 2-D context, `toDataURL` throw).
529
+
530
+ The standalone HTML template (`_repr_utils.build_standalone_html`) captures
531
+ render()'s api into `_aplRenderApi` and adds a `message` listener:
532
+ `{type:'anyplotlib_export_png', requestId, opts}` → `exportPNG(opts)` → replies
533
+ `{type:'anyplotlib_export_png_result', requestId, dataUrl, width, height}` (or
534
+ `{…, error}`) to `event.source` (targetOrigin `'*'`) — the same channel the
535
+ `awi_state` postMessages ride. Tests: `tests/test_embed/test_export_png.py`.
@@ -32,9 +32,20 @@ _BINARY_TRANSPORT = os.environ.get("APL_BINARY_TRANSPORT") == "1"
32
32
  # transport on its _encode_pixels "\x00bin:<checksum>" token stays in the geom JSON
33
33
  # and the real bytes are never shipped, so the renderer can't decode it and the crisp
34
34
  # zoom tile never displays (you only ever see the downsampled overview base).
35
+ #
36
+ # Image LAYERS add DYNAMIC pixel keys ``layer_<id>_b64`` (one per layer) that also
37
+ # ride the geom channel and must ship as binary — they can't be enumerated in a
38
+ # fixed frozenset, so ``_is_binary_pixel_key`` matches them by name pattern.
35
39
  _BINARY_KEYS = frozenset({"image_b64", "overlay_mask_b64", "detail_b64"})
36
40
 
37
41
 
42
+ def _is_binary_pixel_key(k: str) -> bool:
43
+ """True for a geom key whose value is raw image pixels worth shipping as a
44
+ PLOTBIN binary frame — the fixed base-image/mask/detail keys OR a dynamic
45
+ per-layer key ``layer_<id>_b64``."""
46
+ return k in _BINARY_KEYS or (k.startswith("layer_") and k.endswith("_b64"))
47
+
48
+
38
49
  def _route_change(fig_id: str, name: str, value) -> None:
39
50
  """Forward ONE trait change to the host — as a raw PLOTBIN binary frame for a
40
51
  large image pixel trait (when binary transport is enabled), else as a
@@ -61,13 +72,15 @@ def _route_change(fig_id: str, name: str, value) -> None:
61
72
  geom = json.loads(value)
62
73
  except Exception:
63
74
  geom = None
64
- if isinstance(geom, dict) and any(k in geom for k in _BINARY_KEYS):
75
+ if isinstance(geom, dict) and any(
76
+ _is_binary_pixel_key(k) for k in geom):
65
77
  panel_id = name[len("panel_"):-len("_geom")]
66
78
  fig = _figures.get(fig_id)
67
79
  raw_tbl = getattr(fig, "_raw_pixels", None)
68
80
  sent_binary = False
69
81
  for k in list(geom.keys()):
70
- if k not in _BINARY_KEYS or not isinstance(geom[k], str) or not geom[k]:
82
+ if (not _is_binary_pixel_key(k)
83
+ or not isinstance(geom[k], str) or not geom[k]):
71
84
  continue
72
85
  raw = None
73
86
  if geom[k].startswith("\x00bin:") and raw_tbl is not None:
@@ -144,6 +144,20 @@ function makeModel(state) {{
144
144
  const _data = Object.assign({{}}, state);
145
145
  const _cbs = {{}};
146
146
  const _anyCbs = [];
147
+ // Keys set() has touched while _fromParent (an inbound awi_state push) is
148
+ // in effect, awaiting the matching save_changes() flush. Mirrors the
149
+ // dirty-set pattern figure_esm.js's own createLocalModel() uses (see
150
+ // mount()): the _fromParent branch below defers listener firing from
151
+ // set() to save_changes() (see the comment there), but save_changes() was
152
+ // firing EVERY registered change:* callback instead of only the key(s)
153
+ // that changed. Concretely: a single set('event_json', ...) (the Python
154
+ // targeted-widget-update side channel, _push_widget) also re-fired every
155
+ // panel's change:panel_<id>_json listener, which re-parses that panel's
156
+ // (untouched, stale) trait and overwrites p.state wholesale — silently
157
+ // reverting the very widget edit event_json just applied, in the same
158
+ // synchronous cascade. Only the _fromParent path needs tracking — the
159
+ // local (!_fromParent) path already fires synchronously in set() itself.
160
+ const _parentDirty = new Set();
147
161
  return {{
148
162
  get(key) {{ return _data[key]; }},
149
163
  set(key, val) {{
@@ -155,12 +169,32 @@ function makeModel(state) {{
155
169
  const ev = 'change:' + key;
156
170
  if (_cbs[ev]) for (const cb of [..._cbs[ev]]) try {{ cb({{ new: val }}); }} catch(_) {{}}
157
171
  for (const cb of [..._anyCbs]) try {{ cb(); }} catch(_) {{}}
172
+ }} else {{
173
+ _parentDirty.add(key);
158
174
  }}
159
175
  }},
160
176
  save_changes() {{
161
- for (const [ev, cbs] of Object.entries(_cbs))
162
- for (const cb of cbs) try {{ cb({{ new: _data[ev.slice(7)] }}); }} catch(_) {{}}
163
- for (const cb of _anyCbs) try {{ cb(); }} catch(_) {{}}
177
+ if (_parentDirty.size) {{
178
+ // Fire only listeners for keys an inbound awi_state set() actually
179
+ // touched since the last flush (each at most once) — not the whole
180
+ // _cbs table. Snapshot + clear BEFORE invoking callbacks, since a
181
+ // callback may itself call set()/save_changes() (re-entrant-safe).
182
+ const keys = [..._parentDirty];
183
+ _parentDirty.clear();
184
+ for (const key of keys) {{
185
+ const ev = 'change:' + key;
186
+ if (_cbs[ev]) for (const cb of [..._cbs[ev]]) try {{ cb({{ new: _data[key] }}); }} catch(_) {{}}
187
+ }}
188
+ for (const cb of [..._anyCbs]) try {{ cb(); }} catch(_) {{}}
189
+ }} else {{
190
+ // Local (!_fromParent) path — unchanged: set() already fired
191
+ // synchronously per-key, so this refires the full table (matches the
192
+ // pre-existing local-set behaviour; harmless since _cbs entries are
193
+ // typically no-ops for a key that hasn't just changed).
194
+ for (const [ev, cbs] of Object.entries(_cbs))
195
+ for (const cb of cbs) try {{ cb({{ new: _data[ev.slice(7)] }}); }} catch(_) {{}}
196
+ for (const cb of _anyCbs) try {{ cb(); }} catch(_) {{}}
197
+ }}
164
198
  // Forward interaction events to the parent-page Pyodide instance.
165
199
  if (!_fromParent && FIG_ID && window.parent !== window && _eventJsonDirty) {{
166
200
  _eventJsonDirty = false;
@@ -193,10 +227,14 @@ const blobUrl = URL.createObjectURL(blob);
193
227
  const el = document.getElementById("widget-root");
194
228
  const model = makeModel(STATE);
195
229
 
230
+ // render() returns an internal API ({{ panels, exportPNG, ... }}) that the PNG
231
+ // export protocol below needs. Held here so the message listener can reach it.
232
+ let _aplRenderApi = null;
233
+
196
234
  import(blobUrl).then(mod => {{
197
235
  const renderFn = mod.default?.render ?? mod.render;
198
236
  if (typeof renderFn === "function") {{
199
- renderFn({{ model, el }});
237
+ _aplRenderApi = renderFn({{ model, el }});
200
238
  }} else {{
201
239
  el.textContent = "ESM has no render() export";
202
240
  }}
@@ -251,6 +289,40 @@ window.addEventListener('message', (e) => {{
251
289
  return;
252
290
  }}
253
291
  }});
292
+
293
+ // ── PNG export protocol ──────────────────────────────────────────────────────
294
+ // Rides the same postMessage channel as the state updates above. A parent page
295
+ // (or the SpyDE report harvester) requests a composite PNG of the whole figure:
296
+ // → {{ type: 'anyplotlib_export_png', requestId, opts }}
297
+ // and receives back, on event.source (targetOrigin '*'):
298
+ // ← {{ type: 'anyplotlib_export_png_result', requestId, dataUrl, width, height }}
299
+ // ← {{ type: 'anyplotlib_export_png_result', requestId, error }} (on failure)
300
+ // `opts` is forwarded verbatim to handle.exportPNG ({{ scale?, includeWidgets? }}).
301
+ window.addEventListener('message', (e) => {{
302
+ if (!e.data || e.data.type !== 'anyplotlib_export_png') return;
303
+ const requestId = e.data.requestId;
304
+ const source = e.source;
305
+ const reply = (msg) => {{
306
+ try {{
307
+ if (source && typeof source.postMessage === 'function') {{
308
+ source.postMessage(Object.assign(
309
+ {{ type: 'anyplotlib_export_png_result', requestId }}, msg), '*');
310
+ }}
311
+ }} catch (_) {{}}
312
+ }};
313
+ try {{
314
+ if (!_aplRenderApi || typeof _aplRenderApi.exportPNG !== 'function') {{
315
+ reply({{ error: 'figure not ready (exportPNG unavailable)' }});
316
+ return;
317
+ }}
318
+ Promise.resolve(_aplRenderApi.exportPNG(e.data.opts || {{}}))
319
+ .then((res) => reply({{
320
+ dataUrl: res.dataUrl, width: res.width, height: res.height }}))
321
+ .catch((err) => reply({{ error: String(err && err.message || err) }}));
322
+ }} catch (err) {{
323
+ reply({{ error: String(err && err.message || err) }});
324
+ }}
325
+ }});
254
326
  </script>
255
327
  </body>
256
328
  </html>