anyplotlib 0.1.0b1__tar.gz → 0.2.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 (241) hide show
  1. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/CHANGELOG.rst +102 -0
  2. anyplotlib-0.2.0/LARGE_IMAGE_WEBGPU_PLAN.md +223 -0
  3. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/PKG-INFO +1 -1
  4. anyplotlib-0.2.0/anyplotlib/_binary_frame.py +78 -0
  5. anyplotlib-0.2.0/anyplotlib/_electron.py +188 -0
  6. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/_repr_utils.py +43 -5
  7. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/_utils.py +49 -13
  8. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/axes/_axes.py +11 -3
  9. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/callbacks.py +9 -1
  10. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/figure/_figure.py +46 -0
  11. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/figure_esm.js +1173 -73
  12. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/markers.py +7 -0
  13. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot1d/_plot1d.py +48 -24
  14. anyplotlib-0.2.0/anyplotlib/plot2d/_plot2d.py +1549 -0
  15. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot2d/_plotmesh.py +1 -1
  16. anyplotlib-0.2.0/anyplotlib/plot2d/_tile_backend.py +181 -0
  17. anyplotlib-0.2.0/anyplotlib/tests/_gpu_clim_check.cjs +51 -0
  18. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/_png_utils.py +32 -0
  19. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/conftest.py +149 -0
  20. anyplotlib-0.2.0/anyplotlib/tests/test_electron/test_binary_frame.py +245 -0
  21. anyplotlib-0.2.0/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +315 -0
  22. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_title.py +3 -3
  23. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_visual.py +60 -1
  24. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_markers/test_marker_transforms.py +32 -0
  25. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_detail_tile.py +419 -0
  26. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_first_paint_race.py +259 -0
  27. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_gpu_image.py +98 -0
  28. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +425 -0
  29. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot2d/test_imshow.py +65 -3
  30. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +15 -0
  31. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +160 -0
  32. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_tile_backend.py +99 -0
  33. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +386 -0
  34. anyplotlib-0.2.0/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +76 -0
  35. anyplotlib-0.2.0/anyplotlib/tests/test_plot3d/__init__.py +0 -0
  36. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_root/index.html +3 -3
  37. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_root/switcher.json +5 -0
  38. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/conf.py +1 -1
  39. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/pyproject.toml +1 -1
  40. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/upcoming_changes/README.rst +7 -1
  41. anyplotlib-0.1.0b1/anyplotlib/_electron.py +0 -74
  42. anyplotlib-0.1.0b1/anyplotlib/plot2d/_plot2d.py +0 -726
  43. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.github/workflows/ci.yml +0 -0
  44. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.github/workflows/docs.yml +0 -0
  45. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.github/workflows/prepare_release.yml +0 -0
  46. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.github/workflows/release.yml +0 -0
  47. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.github/workflows/tests.yml +0 -0
  48. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/.gitignore +0 -0
  49. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/AGENTS.md +0 -0
  50. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Benchmarks/README.rst +0 -0
  51. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
  52. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/README.rst +0 -0
  53. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
  54. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_eels_explorer.py +0 -0
  55. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_interactive_fft.py +0 -0
  56. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_interactive_fitting.py +0 -0
  57. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_ipf_density_map.py +0 -0
  58. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_ipf_explorer.py +0 -0
  59. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_key_bindings.py +0 -0
  60. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_particle_picker.py +0 -0
  61. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_point_widget.py +0 -0
  62. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
  63. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
  64. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
  65. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_threshold_explorer.py +0 -0
  66. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
  67. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/README.rst +0 -0
  68. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_arrows.py +0 -0
  69. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_circles.py +0 -0
  70. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_ellipses.py +0 -0
  71. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_horizontal_lines.py +0 -0
  72. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_line_segments.py +0 -0
  73. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_points.py +0 -0
  74. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_polygons.py +0 -0
  75. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_rectangles.py +0 -0
  76. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_squares.py +0 -0
  77. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_texts.py +0 -0
  78. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Markers/plot_vertical_lines.py +0 -0
  79. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/README.rst +0 -0
  80. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_3d.py +0 -0
  81. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_bar.py +0 -0
  82. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
  83. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_image2d.py +0 -0
  84. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_inset.py +0 -0
  85. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_label_formatting.py +0 -0
  86. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_line_styles.py +0 -0
  87. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
  88. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/PlotTypes/plot_spectra1d.py +0 -0
  89. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/README.rst +0 -0
  90. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/README.rst +0 -0
  91. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget1d_hline.py +0 -0
  92. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget1d_range.py +0 -0
  93. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget1d_vline.py +0 -0
  94. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_annular.py +0 -0
  95. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_circle.py +0 -0
  96. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
  97. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_label.py +0 -0
  98. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
  99. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
  100. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/LICENSE +0 -0
  101. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/Makefile +0 -0
  102. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/ORIX_BACKEND_PLAN.md +0 -0
  103. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/README.md +0 -0
  104. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/RELEASE_NOTES.md +0 -0
  105. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/RELEASE_PLAN.md +0 -0
  106. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/WEBGPU_PLAN.md +0 -0
  107. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/FIGURE_ESM.md +0 -0
  108. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/__init__.py +0 -0
  109. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/_base_plot.py +0 -0
  110. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/axes/__init__.py +0 -0
  111. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/axes/_inset_axes.py +0 -0
  112. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/conftest.py +0 -0
  113. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/embed.py +0 -0
  114. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/figure/__init__.py +0 -0
  115. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/figure/_gridspec.py +0 -0
  116. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/figure/_subplots.py +0 -0
  117. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot1d/__init__.py +0 -0
  118. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot1d/_plotbar.py +0 -0
  119. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot2d/__init__.py +0 -0
  120. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot3d/__init__.py +0 -0
  121. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plot3d/_plot3d.py +0 -0
  122. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plotxy/__init__.py +0 -0
  123. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/plotxy/_plotxy.py +0 -0
  124. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
  125. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
  126. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
  127. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
  128. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
  129. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
  130. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
  131. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
  132. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
  133. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
  134. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
  135. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
  136. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
  137. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
  138. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/__init__.py +0 -0
  139. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/bar_basic.png +0 -0
  140. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
  141. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
  142. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
  143. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
  144. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
  145. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
  146. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
  147. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
  148. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
  149. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
  150. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
  151. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_1d.png +0 -0
  152. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
  153. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
  154. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
  155. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
  156. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
  157. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
  158. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
  159. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
  160. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
  161. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
  162. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
  163. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
  164. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
  165. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
  166. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
  167. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
  168. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
  169. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
  170. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
  171. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
  172. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
  173. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
  174. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_documentation/__init__.py +0 -0
  175. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
  176. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
  177. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
  178. {anyplotlib-0.1.0b1/anyplotlib/tests/test_embed → anyplotlib-0.2.0/anyplotlib/tests/test_electron}/__init__.py +0 -0
  179. {anyplotlib-0.1.0b1/anyplotlib/tests/test_examples → anyplotlib-0.2.0/anyplotlib/tests/test_embed}/__init__.py +0 -0
  180. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
  181. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
  182. {anyplotlib-0.1.0b1/anyplotlib/tests/test_interactive → anyplotlib-0.2.0/anyplotlib/tests/test_examples}/__init__.py +0 -0
  183. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
  184. {anyplotlib-0.1.0b1/anyplotlib/tests/test_labels → anyplotlib-0.2.0/anyplotlib/tests/test_interactive}/__init__.py +0 -0
  185. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
  186. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
  187. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
  188. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
  189. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
  190. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
  191. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
  192. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
  193. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
  194. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
  195. {anyplotlib-0.1.0b1/anyplotlib/tests/test_layouts → anyplotlib-0.2.0/anyplotlib/tests/test_labels}/__init__.py +0 -0
  196. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
  197. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
  198. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
  199. {anyplotlib-0.1.0b1/anyplotlib/tests/test_markers → anyplotlib-0.2.0/anyplotlib/tests/test_layouts}/__init__.py +0 -0
  200. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
  201. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
  202. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
  203. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
  204. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
  205. {anyplotlib-0.1.0b1/anyplotlib/tests/test_plot1d → anyplotlib-0.2.0/anyplotlib/tests/test_markers}/__init__.py +0 -0
  206. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_markers/test_markers.py +0 -0
  207. {anyplotlib-0.1.0b1/anyplotlib/tests/test_plot2d → anyplotlib-0.2.0/anyplotlib/tests/test_plot1d}/__init__.py +0 -0
  208. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
  209. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
  210. {anyplotlib-0.1.0b1/anyplotlib/tests/test_plot3d → anyplotlib-0.2.0/anyplotlib/tests/test_plot2d}/__init__.py +0 -0
  211. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
  212. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
  213. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
  214. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
  215. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
  216. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
  217. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
  218. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/widgets/__init__.py +0 -0
  219. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/widgets/_base.py +0 -0
  220. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/widgets/_widgets1d.py +0 -0
  221. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/widgets/_widgets2d.py +0 -0
  222. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/anyplotlib/widgets/_widgets3d.py +0 -0
  223. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_sg_html_scraper.py +0 -0
  224. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_static/anyplotlib.svg +0 -0
  225. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_static/custom.css +0 -0
  226. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_static/pyodide_bridge.js +0 -0
  227. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/_templates/autosummary/class.rst +0 -0
  228. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/callbacks.rst +0 -0
  229. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/figure.rst +0 -0
  230. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/figure_plots.rst +0 -0
  231. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/index.rst +0 -0
  232. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/markers.rst +0 -0
  233. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/api/widgets.rst +0 -0
  234. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/benchmarking.rst +0 -0
  235. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/dev/index.rst +0 -0
  236. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/embedding.rst +0 -0
  237. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/events.rst +0 -0
  238. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/getting_started.rst +0 -0
  239. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/docs/index.rst +0 -0
  240. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/make.bat +0 -0
  241. {anyplotlib-0.1.0b1 → anyplotlib-0.2.0}/uv.lock +0 -0
@@ -10,6 +10,108 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by
10
10
 
11
11
  .. towncrier release notes start
12
12
 
13
+ 0.2.0 (2026-07-10)
14
+ ==================
15
+
16
+ New Features
17
+ ------------
18
+
19
+ - 2-D scalar images can now render on the **GPU via WebGPU** (``imshow(...,
20
+ gpu="auto"|True|False)``): the image uploads as an R8 texture and a WGSL fragment
21
+ shader applies the colormap LUT + contrast (clim) in one draw, replacing the
22
+ per-pixel JavaScript colormap loop. Large images (≳1 megapixel) take the GPU path
23
+ automatically; everything below the threshold, RGB images, ``gpu=False``, and any
24
+ device without WebGPU keep the identical Canvas2D path. ``plot.gpu_active`` reports
25
+ which path ran. Verified on an NVIDIA Pascal GPU.
26
+ - Arbitrarily large images can now display through **tile mode**
27
+ (``imshow(..., tile="auto")``): the figure shows a downsampled overview as its
28
+ base and, after each zoom/pan settles, samples a high-resolution detail tile of
29
+ just the visible region at panel resolution — deep zooms stay crisp without
30
+ ever shipping the full-resolution frame. ``Plot2D.enable_tile`` /
31
+ ``update_tile_source`` swap the underlying frame while the zoom and
32
+ subselection persist (live-data contract), and a pluggable ``TileBackend``
33
+ (default: a fast vectorised numpy box-mean) lets out-of-core or GPU sources
34
+ own the sampling.
35
+ - Markers gained a ``clip_display`` option controlling whether they draw
36
+ outside the current axes view or are clipped to it.
37
+ - ``set_extent`` now updates the axes state (calibrated units / scale bar), so
38
+ applying a calibrated extent after figure creation renders labelled axes
39
+ instead of bare pixels.
40
+ - Regular ``pcolormesh`` meshes are detected and rasterized to an image for
41
+ display (fast IPF-style heatmaps) instead of drawing per-quad.
42
+
43
+
44
+ Bug Fixes
45
+ ---------
46
+
47
+ - Fixed a display freeze under the Electron binary pixel transport: the routing
48
+ layer stripped the pixel key out of the slimmed geom JSON, so the renderer's
49
+ "unchanged → skip re-upload" caches (Canvas2D blit cache, WebGPU texture,
50
+ overlay-mask cache) fell back to a 4-sampled-byte fingerprint of the buffer —
51
+ two frames differing anywhere else collided and the display stayed frozen on
52
+ the old frame (seen as a stale overview after a movie scrub). The slimmed geom
53
+ now carries a small ``\x00bin:<checksum>`` content token under the pixel key,
54
+ binary buffers are additionally stamped with an arrival sequence as a fallback
55
+ key, and the overlay-mask draw path now reads the binary byte side-channel
56
+ (it previously only decoded base64, so masks never displayed over the binary
57
+ transport).
58
+ - Fixed the WebGPU 2-D image path sampling a vertically MIRRORED window when the
59
+ view was panned off-centre: the shader applied a global ``1 - v`` flip after
60
+ interpolating the ``[v0, v1]`` uv window, which sampled ``[1-v1, 1-v0]``
61
+ instead — correct only for a full or vertically-centred view. Symptoms: pan-y
62
+ moved the image the wrong way on GPU-rendered panels, and markers/widgets
63
+ (drawn by the shared Canvas2D overlay transform, which was always correct)
64
+ appeared detached from the image features they marked. The base and
65
+ detail-tile passes share the shader, so both are fixed. GPU-vs-CPU screenshot
66
+ parity tests (zoom, pan, markers, widgets, detail tile) now run on real WebGPU
67
+ in headless Chromium (``channel="chromium"`` + ``--enable-unsafe-webgpu``) and
68
+ skip on machines with no adapter.
69
+ - ``Plot2D.set_data`` no longer makes a float64 copy of every incoming frame.
70
+ The float64 cast now happens lazily in the ``.data`` property (the only reader),
71
+ so a frame stream — e.g. scrubbing an in-situ movie — keeps the source dtype and
72
+ skips a ~12 ms float64 copy of a 4k frame per tick. ``.data`` still returns a
73
+ read-only float64 copy, unchanged for callers.
74
+ - The Electron binary pixel transport now ships the RAW uint8 image bytes end to
75
+ end, instead of base64-encoding them in ``set_data`` only to base64-decode them
76
+ straight back in the routing layer. ``Plot2D.set_data`` stashes the raw bytes on
77
+ the Figure's ``_raw_pixels`` side-table and leaves a tiny content-checksum
78
+ change-token in ``image_b64``; ``_electron._route_change`` ships those bytes to a
79
+ PLOTBIN frame directly. This removes the ~20 ms base64 encode, the ~17 ms decode,
80
+ and the megabyte ``json.dumps`` of the pixel string from every scrub frame — a
81
+ 2.2x faster ``set_data`` (≈98 ms → ≈44 ms on a 2048² frame) and ~25% less
82
+ bytes-on-wire. Non-Electron hosts (Jupyter / Pyodide / standalone / ``save_html``)
83
+ are unchanged: they have no PLOTBIN channel, so the token is resolved back to
84
+ inline base64 via ``Plot2D.resolve_pixel_tokens`` when the figure state is
85
+ serialised for them.
86
+ - Tile mode: a data update while zoomed in (``update_tile_source`` with a detail
87
+ tile shown) refreshes only the detail tile, leaving the overview base on the
88
+ old frame — zooming out then flashed the pre-update frame. The skipped
89
+ overview is now marked stale and re-sampled once on the next view settle
90
+ (riding the same push as the detail/clear), preserving the per-frame skip
91
+ optimisation while never exposing stale base pixels.
92
+ - Interactive zoom/pan on a 2-D image no longer re-serialises (and re-transmits)
93
+ the full image on every mouse tick. The wheel/pan/orbit handlers write only the
94
+ light *view* state back to the ``panel_<id>_json`` trait now, excluding the
95
+ cached geometry (pixels, colormap LUT) that ``_applyGeom`` splices into the panel
96
+ state for drawing. Previously the whole frame was ``JSON.stringify``-d per tick —
97
+ catastrophically so on the binary transport, where the pixel buffer is a
98
+ ``Uint8Array`` that stringifies to a ``{"0":..,"1":..}`` object with one key per
99
+ byte — which stalled zoom on large images.
100
+ - Fixed a first-paint race under the Electron binary transport: binary
101
+ side-table bytes that arrived before ``render()`` attached its listeners were
102
+ stranded, leaving the first frame blank until the next update — they are now
103
+ spliced into the initial paint.
104
+
105
+
106
+ Maintenance
107
+ -----------
108
+
109
+ - ``anyplotlib.__version__`` is now exposed from the package metadata.
110
+ - Per-frame hot-path costs trimmed: the colormap LUT is cached instead of being
111
+ rebuilt every frame (~100 ms), and small-range data rescales in float32
112
+ (~60 ms → ~27 ms per 2048² frame).
113
+
114
+
13
115
  0.1.0 (2026-06-24)
14
116
  ==================
15
117
 
@@ -0,0 +1,223 @@
1
+ # Large-image WebGPU 2D rendering + binary transport — scoping document
2
+
3
+ Status: **Phase 0 + core Phase 1/2 DONE, hardware-verified** (2026-07-06). Branch:
4
+ `feat/webgpu-2d-images`. Owner: @CSSFrancis
5
+
6
+ ## DONE (verified on an NVIDIA Pascal GPU via the SpyDE Electron consumer)
7
+ - **WebGPU 2-D image render path**: a `gpuCanvas` below `plotCanvas`; the normalized
8
+ uint8 frame → R8 texture, the 256-entry colormap → a 256×1 RGBA LUT texture, a
9
+ fullscreen-quad WGSL fragment shader re-stretches by clim (dmin/dmax uniform) and
10
+ samples the LUT. Replaces the 64M-iteration Canvas2D atob+LUT loop with one GPU draw.
11
+ - **Reuses the 3D contract**: the `_gpuDevice()` singleton, first-frame-canvas-then-
12
+ async-swap, `device.lost` → permanent Canvas2D fallback (extended to 2-D panels).
13
+ - **API**: `imshow(..., gpu="auto"|True|False)` → `gpu_mode`; `GPU_IMAGE_THRESHOLD`
14
+ (~1 Mpx) auto-gate; `plot.gpu_active` echo via the `gpu_status` event.
15
+ - **Fallback intact**: RGB images, sub-threshold images, `gpu=False`, and no-device
16
+ all render on Canvas2D (anyplotlib suite green; the DOM keeps `plotCanvas` first so
17
+ `querySelector('canvas')` still resolves the image canvas).
18
+ - **Correctness (review-hardened)**: the shader is a plain IDENTITY LUT lookup —
19
+ `_buildLut32` already bakes clim + scale_mode into the LUT, so the shader must NOT
20
+ re-apply the window (an earlier version double-applied clim; correct only at
21
+ full-range, wrong for any narrowed contrast/log/symlog — fixed). A zoomed/panned
22
+ view falls back to Canvas2D so the base image stays registered with the axes/
23
+ overlays (the GPU quad is full-extent). Nearest sampling on both textures matches
24
+ Canvas2D's `imageSmoothingEnabled=false` pixel-for-pixel.
25
+ - **Zoom/pan v-window fix + headless GPU parity tests (2026-07-09)**: the shader
26
+ applied a global `1 - v` flip AFTER interpolating the `[v0,v1]` window, sampling
27
+ the MIRRORED window `[1-v1, 1-v0]` — correct only when v0+v1==1 (rest / centred
28
+ zoom), so it survived the readback tests but inverted pan-y and detached the
29
+ image from markers/overlays on any vertically off-centre view (base AND detail
30
+ passes; fixed by interpolating v from v1 at the screen bottom to v0 at the top).
31
+ Guarded by `tests/test_plot2d/test_gpu_parity_playwright.py`: GPU-vs-Canvas2D
32
+ PNG parity (rest/zoom/pan/markers/widgets/detail-tile) on REAL WebGPU in
33
+ headless Chromium — `channel="chromium"` (the full build; the default headless
34
+ shell has no `navigator.gpu`) + `--enable-unsafe-webgpu`, skipped when no
35
+ adapter. NB `page.screenshot()` DOES capture the WebGPU canvas there; the
36
+ "swapchain reads black" caveat below applies to Electron offscreen capture.
37
+ - **Verification**: `__apl_gpuReadback` renders the active panel to an OFFSCREEN
38
+ texture (the live swapchain reads black under automation) and copies it to CPU.
39
+ On a real 4k movie frame: min 0 / max 255 / 96% non-black / correct gray values.
40
+ A **narrowed clim [0.3,0.7]** matches the numpy windowed-colormap to meanDiff
41
+ 0.65/255 (regression guard for the double-apply bug; `tests/_gpu_clim_check.cjs`).
42
+ Movie scrub + playback re-upload the texture per frame and stay correct (5/5
43
+ scrub, 5 distinct played frames). GPU resources are freed on panel close / figure
44
+ dispose / device loss (no leak on repeated large-image open/close).
45
+
46
+ ## DEFERRED (documented; not blockers now)
47
+ - **Mipmaps** (smooth downscale-on-zoom): the sampler uses `minFilter:'linear'`
48
+ without a mip chain. Marginal here because LOD already caps the uploaded texture
49
+ near display size; matters for deep zoom-out. Needs an R8 render-based mip chain.
50
+ - **Binary pixel transport** (base64-in-JSON → binary buffer): the Phase-0 headline,
51
+ but LOD decimation already cut the shipped payload ~35× (≤1536 px, ~2 MB not
52
+ ~85 MB) and the GPU shader removed the render cost, so this is now an incremental
53
+ transport optimization spanning Jupyter/Pyodide/standalone/Electron — do it with
54
+ the multi-environment verification it needs.
55
+
56
+ Prerequisite reading: `WEBGPU_PLAN.md` (the 3D points/voxels WebGPU path this extends),
57
+ `anyplotlib/FIGURE_ESM.md` (the `figure_esm.js` section map), `AGENTS.md` (repo conventions).
58
+ Prerequisite reading: `WEBGPU_PLAN.md` (the 3D points/voxels WebGPU path this extends),
59
+ `anyplotlib/FIGURE_ESM.md` (the `figure_esm.js` section map), `AGENTS.md` (repo conventions).
60
+
61
+ This extends the repo's existing hardware-verified `WEBGPU_PLAN.md` (3D instanced points +
62
+ voxels) to **2D large images**, deliberately lifting that doc's "No 2D pipeline changes"
63
+ non-goal (§2). Motivating consumer: a SpyDE in-situ movie viewer that must scrub/play through
64
+ 8k×8k image frames smoothly.
65
+
66
+ ## 1. Goal
67
+
68
+ Render **large 2D image frames** (up to 8k×8k) interactively — smooth scrub/playback of a
69
+ frame stream and smooth zoom — by moving the 2D image path from Canvas2D to **WebGPU** (texture
70
+ upload + WGSL colormap LUT + mipmap downscale) and moving the pixel bytes off **base64-in-JSON**
71
+ onto a **binary transport**.
72
+
73
+ | Workload | Today (Canvas2D) | Target (WebGPU) |
74
+ |---|---|---|
75
+ | 8k×8k frame colormap+draw | ~64M-iter JS LUT loop → OffscreenCanvas | shader LUT, **<5 ms** |
76
+ | Scrub (new frame/tick) | ~85 MB base64/frame + full rebuild | binary uint8 + texture upload, **≥15–30 fps** |
77
+ | Zoom-in on a still | re-blit from OffscreenCanvas | GPU **mipmap**, **60 fps** |
78
+
79
+ ## 2. Non-goals
80
+
81
+ - **Not** replacing Canvas2D for images — it stays the universal baseline, the fallback, the
82
+ small-image path, and the fully-CI-tested path. Default behaviour for small images is
83
+ byte-identical to today.
84
+ - No WebGL2, no three.js, no bundler — raw WebGPU + inline WGSL, mirroring `WEBGPU_PLAN.md`.
85
+ - 1D lines / bars stay Canvas2D. Only the 2D **image** path is affected.
86
+
87
+ ## 3. Coverage & the fallback contract
88
+
89
+ Inherited verbatim from `WEBGPU_PLAN.md` §3: WebGPU is a progressive enhancement. `navigator.gpu`
90
+ present → `requestAdapter()` resolves → device created → *then* a panel may switch. Any failure at
91
+ any point (including mid-session device loss) lands on the Canvas2D path silently and permanently
92
+ for that session. **A figure must never render nothing because GPU was attempted.**
93
+
94
+ ## 4. Reuse (do NOT rebuild)
95
+
96
+ - **Device singleton + fallback state machine**: `_gpuDevice()` (`figure_esm.js` ~L1955,
97
+ module-level `_gpuDevicePromise`), the `p._gpu ∈ {pending,active,unavailable}` per-panel state,
98
+ the "first frame always Canvas2D → swap on device resolve" pattern, and `device.lost` → permanent
99
+ per-session Canvas2D. All proven by the 3D path — the 2D image path plugs into the exact same
100
+ machinery.
101
+ - **The `gpuCanvas`-below-`plotCanvas` split**: decorations (axes, ticks, colorbar, scale bar,
102
+ overlay mask, markers, widgets) keep drawing on the 2D `plotCanvas` **verbatim** — only the image
103
+ raster moves to `gpuCanvas`. Mirrors `WEBGPU_PLAN.md` §4.3.
104
+ - **`gpu_mode` / `_gpu_active` plumbing**: the state field + echo already exist (`st.gpu_mode`,
105
+ ~L1998). Add an image-megapixel `GPU_IMAGE_THRESHOLD` alongside the existing point threshold.
106
+ - **The zoom/letterbox model**: `_imgFitRect` (~L1262) — the GPU draw honors the same fit-rect /
107
+ `zoom` semantics; no new zoom math.
108
+ - **The colormap LUT**: `_build_colormap_lut` (`_utils.py:118`) → `st.colormap_data`
109
+ ([[r,g,b]×256]) already ships to JS. Upload it as a 256×1 texture; the shader samples it.
110
+ - **The normalized payload**: `set_data` already produces `img_u8` (single-channel uint8 via
111
+ `_normalize_image`, `_utils.py:82`) + `display_min/max` (clim) + `raw_min/max`. The WebGPU path
112
+ uploads that **same uint8** (8× smaller than raw float64; keeps current visual behaviour) as an
113
+ **R8 texture**; the shader does the clim re-stretch + LUT — exactly what the JS loop at
114
+ `draw2d` L1379–1381 does today, but on the GPU.
115
+
116
+ ## 5. The function being replaced
117
+
118
+ `draw2d` (`figure_esm.js` ~L1344): today it `atob`-decodes `image_b64`, runs a per-pixel LUT loop
119
+ (L1379–1381, 64M iters at 8k) into an `OffscreenCanvas` 2D context, caches the bitmap in
120
+ `blitCache`, and `_blit2d`-down-blits to the panel. The WebGPU path replaces the decode+LUT+blit
121
+ with: upload R8 texture → WGSL fragment shader (clim uniform + LUT texture) → mipmapped textured
122
+ quad over the `_imgFitRect`. `blitCache`'s "bytes unchanged → reuse" logic maps to "texture
123
+ unchanged → skip re-upload". Canvas2D `draw2d` remains as the fallback when `p._gpu !== 'active'`.
124
+
125
+ ## 6. Binary transport (generalize `WEBGPU_PLAN.md` §4.7 to images)
126
+
127
+ Today `set_data` → `_encode_bytes(img_u8)` → `image_b64` string on the `panel_<id>_geom` trait
128
+ (base64-in-JSON). Change: send the raw `img_u8` **bytes** as an anywidget **binary buffer**
129
+ (`_repr_utils._widget_state` already handles `bytes`; the geom-trait split already isolates heavy
130
+ keys — `FIGURE_ESM.md` ~L237), with a small JSON header (`image_width/height`, `display_min/max`,
131
+ `raw_min/max`, dtype, an optional `lod` level). Keep base64 for **small images and the
132
+ standalone/Pyodide/`save_html` paths** (no binary channel there). Keep `FigureBridge` (`embed.py`)
133
+ transport-agnostic so an Electron embed can supply an even faster channel (shared memory / a
134
+ transferable `ArrayBuffer`) without the library caring. JS: pick up the binary buffer, upload to
135
+ the GPU texture; on the Canvas2D fallback, build the `ImageData` from the same bytes (no atob).
136
+
137
+ ## 7. API surface
138
+
139
+ - `Axes.imshow(..., gpu="auto"|True|False)` and `Plot2D.set_data(..., gpu=...)` — mirror
140
+ `scatter3d`/`voxels`. Default `"auto"`: attempt WebGPU only above `GPU_IMAGE_THRESHOLD`
141
+ (initial ~4 megapixels — below it Canvas2D is already instant). `True` forces an attempt (still
142
+ falls back); `False` never attempts.
143
+ - `plot.gpu_active` — bool echo after first render (reuse `_gpu_active`).
144
+ - Optional `set_data(..., lod=k)` affordance so a consumer can mark a frame as decimated (scrub)
145
+ vs full-res (settle); at minimum, an uploaded full-res texture gets **free** GPU downscale-on-zoom
146
+ via mipmaps, so LOD-on-zoom needs no consumer work.
147
+
148
+ ## 8. Phases (with decision gates, mirroring WEBGPU_PLAN.md style)
149
+
150
+ ### Phase 0 — Binary transport + minimal GPU texture-blit (risk-retire; do first)
151
+
152
+ Add the binary image trait + header (keep b64 fallback). Add a minimal WebGPU 2D image pipeline:
153
+ R8 texture + 256×1 LUT texture + clim uniform + a textured quad over `_imgFitRect`, **no mipmaps
154
+ yet**. Verify a single static 8k image renders identically to Canvas2D via **offscreen-texture
155
+ readback** (the 3D path's proven test method — the WebGPU swapchain doesn't snapshot reliably under
156
+ automation, per `FIGURE_ESM.md` ~L256).
157
+
158
+ **Gate A:** GPU image matches Canvas2D within tolerance on a real GPU, and `gpu=False`/adapter-absent
159
+ renders identically via Canvas2D (automated).
160
+
161
+ ### Phase 1 — Mipmaps + zoom + scrub
162
+
163
+ Generate mipmaps on upload; sample with trilinear filtering so zoom-out/downscale is smooth and
164
+ zoom-in honors `_imgFitRect`. Wire the scrub path: new bytes → texture re-upload (reuse
165
+ `blitCache`-style "unchanged → skip").
166
+
167
+ **Acceptance:** 8k still zooms at 60 fps; a frame stream scrubs ≥15–30 fps on a real GPU; benchmark
168
+ `js_gpu_image_8k` recorded.
169
+
170
+ ### Phase 2 — API + fallback hardening + tests
171
+
172
+ `gpu="auto"|True|False` on `imshow`/`set_data`, `GPU_IMAGE_THRESHOLD`, `plot.gpu_active`, optional
173
+ `lod=`. Canvas2D-parity tests in the normal suite (assert `_gpu_active` false + pixel parity when
174
+ GPU absent); flagged headless-GPU smoke job (skip-on-null-adapter); `upcoming_changes/*.rst`
175
+ towncrier fragment.
176
+
177
+ **Gate B:** full fallback matrix green (adapter-absent, mid-session device loss via forced
178
+ `device.destroy()`, `gpu=False`) + real-GPU image benchmark, before this is released.
179
+
180
+ ### Phase 3 (gated) — raw-float precision path
181
+
182
+ If uint8 quantization proves visibly lossy for scientific contrast adjustment, add an optional R32F
183
+ texture upload (raw floats, 4× bytes) with the clim applied in-shader over true data range. Only
184
+ build on a concrete need (Gate C).
185
+
186
+ ## 9. Biggest risks / do-not-break
187
+
188
+ - **The 3D WebGPU path and the Canvas2D 2D path must not regress** — the 2D image GPU path is
189
+ additive and plugs into the shared `_gpuDevice()` singleton; keep the device/fallback code shared,
190
+ not forked.
191
+ - **Canvas2D stays the default + fallback forever** — no figure may render nothing because GPU was
192
+ attempted (the `WEBGPU_PLAN.md` §3 contract).
193
+ - **Automation can't snapshot the WebGPU swapchain** — test via offscreen-texture readback or state
194
+ echo, not screenshot diffing of the live canvas (per `FIGURE_ESM.md`).
195
+ - **Small-image / standalone / Pyodide paths unchanged** — binary transport is large-image-only;
196
+ b64 remains for the rest.
197
+ - Repo norms (`AGENTS.md`): OO API only, state in `_state` dicts (no new traitlets on Plot2D — the
198
+ Figure adds panel traits dynamically), end every `_state` mutation with `_push()`, `uv run
199
+ pytest`, `uv run playwright install chromium` first, add the towncrier fragment.
200
+
201
+ ## 10. Verify
202
+
203
+ `uv run pytest` (Canvas2D parity + fallback in the normal suite); the flagged GPU smoke job;
204
+ offscreen-readback comparison GPU-vs-Canvas2D on a large image; `js_gpu_image_8k` benchmark on a
205
+ real-GPU machine; a manual Electron-embed pass (the real consumer) once SpyDE pins the new version.
206
+
207
+ ## 11. API sketch
208
+
209
+ ```python
210
+ # Python
211
+ plot = ax.imshow(frame, gpu="auto") # auto-attempt WebGPU above GPU_IMAGE_THRESHOLD
212
+ plot.set_data(next_frame, clim=(lo, hi)) # scrub: binary bytes → texture re-upload
213
+ plot.set_data(decimated, lod=2) # scrub-time LOD hint (optional)
214
+ plot.gpu_active # bool, after first render echo
215
+ ```
216
+
217
+ ```js
218
+ // JS internals (figure_esm.js)
219
+ _gpuDevice() // existing module singleton → Promise<GPUDevice|null>
220
+ p._gpu // 'pending' | 'active' | 'unavailable' (existing)
221
+ _buildImagePipeline(device, p) // NEW: R8 sampled texture + LUT texture + clim uniform
222
+ _drawGpu2d(p) // NEW: image raster on gpuCanvas; decorations still 2D via draw2d
223
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyplotlib
3
- Version: 0.1.0b1
3
+ Version: 0.2.0
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
@@ -0,0 +1,78 @@
1
+ """
2
+ _binary_frame.py — a tiny length-prefixed binary wire format for shipping raw
3
+ image pixels to an Electron host without base64-in-JSON.
4
+
5
+ Used ONLY by the Electron transport (``_electron.py``); Jupyter / Pyodide /
6
+ standalone keep the base64-in-JSON path untouched. The format is a single
7
+ self-describing frame written to a byte stream (stdout), interleaved with the
8
+ existing text ``PLOTAPP:{json}\\n`` lines:
9
+
10
+ PLOTBIN:<header_len>:<payload_len>\\n<header_json_bytes><payload_bytes>
11
+
12
+ - ``PLOTBIN:`` is the ASCII marker (distinct from ``PLOTAPP:``) so a host reading
13
+ the raw stream can tell a binary frame from a text line.
14
+ - ``<header_len>`` / ``<payload_len>`` are ASCII decimal byte counts.
15
+ - ``<header_json_bytes>`` is UTF-8 JSON: ``{fig_id, key, ...metadata}`` (dims,
16
+ dtype, clim — everything the renderer needs EXCEPT the pixels).
17
+ - ``<payload_bytes>`` is the raw payload (e.g. the single-channel uint8 image),
18
+ exactly ``payload_len`` bytes, NO base64, NO JSON.
19
+
20
+ Both the Python producer and the JS host use this one definition, so the wire
21
+ format has a single source of truth and can be unit-tested in isolation (the
22
+ producer's ``encode_frame`` round-trips through ``decode_frame`` without any
23
+ Electron / stdout involved).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import json
28
+
29
+ MARKER = b"PLOTBIN:"
30
+
31
+
32
+ def encode_frame(fig_id: str, key: str, header: dict, payload: bytes) -> bytes:
33
+ """Serialise one binary frame to bytes (marker + lengths + header + payload).
34
+
35
+ ``header`` is arbitrary JSON-able metadata; ``fig_id`` and ``key`` are merged
36
+ in (so the decoder always recovers them). ``payload`` is the raw bytes."""
37
+ hdr = dict(header or {})
38
+ hdr["fig_id"] = fig_id
39
+ hdr["key"] = key
40
+ hdr_bytes = json.dumps(hdr, default=str).encode("utf-8")
41
+ prefix = MARKER + f"{len(hdr_bytes)}:{len(payload)}\n".encode("ascii")
42
+ return prefix + hdr_bytes + bytes(payload)
43
+
44
+
45
+ def decode_frame(buf: bytes):
46
+ """Parse ONE complete frame from ``buf`` (as produced by ``encode_frame``).
47
+
48
+ Returns ``(header_dict, payload_bytes, n_consumed)`` where ``n_consumed`` is
49
+ the number of bytes the frame occupied. Raises ``ValueError`` if ``buf`` does
50
+ not begin with a complete frame. (A streaming host uses ``parse_prefix`` +
51
+ incremental reads instead; this is the whole-buffer convenience for tests.)"""
52
+ if not buf.startswith(MARKER):
53
+ raise ValueError("not a PLOTBIN frame")
54
+ nl = buf.find(b"\n")
55
+ if nl < 0:
56
+ raise ValueError("incomplete PLOTBIN prefix (no newline)")
57
+ prefix = buf[len(MARKER):nl].decode("ascii")
58
+ hdr_len_s, pay_len_s = prefix.split(":")
59
+ hdr_len, pay_len = int(hdr_len_s), int(pay_len_s)
60
+ start = nl + 1
61
+ end_hdr = start + hdr_len
62
+ end_pay = end_hdr + pay_len
63
+ if len(buf) < end_pay:
64
+ raise ValueError("incomplete PLOTBIN frame (truncated body)")
65
+ header = json.loads(buf[start:end_hdr].decode("utf-8"))
66
+ payload = buf[end_hdr:end_pay]
67
+ return header, payload, end_pay
68
+
69
+
70
+ def parse_prefix(line: bytes):
71
+ """Parse a ``PLOTBIN:<hlen>:<plen>`` prefix line (the bytes up to, not
72
+ including, the newline). Returns ``(header_len, payload_len)``. For a
73
+ streaming host that reads the prefix line, then exactly ``header_len +
74
+ payload_len`` more bytes."""
75
+ if not line.startswith(MARKER):
76
+ raise ValueError("not a PLOTBIN prefix")
77
+ hdr_len_s, pay_len_s = line[len(MARKER):].decode("ascii").split(":")
78
+ return int(hdr_len_s), int(pay_len_s)
@@ -0,0 +1,188 @@
1
+ """
2
+ _electron.py
3
+ ============
4
+ Electron app bridge for anyplotlib figures.
5
+
6
+ Registers figures so their trait changes are forwarded to the Electron
7
+ renderer via stdout, and provides dispatch_event() so the renderer can
8
+ send interaction events back to Python.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import json
14
+ import os
15
+ import sys
16
+ import uuid
17
+ import zlib
18
+
19
+ from anyplotlib._binary_frame import encode_frame
20
+
21
+ _figures: dict[str, object] = {} # fig_id -> Figure
22
+
23
+ # Opt-in binary transport for large image pixels (Electron host only). When on,
24
+ # the big ``image_b64`` / ``overlay_mask_b64`` pixel traits are shipped as a raw
25
+ # PLOTBIN binary frame (no base64, no JSON for the pixels) instead of a giant
26
+ # base64 string in the state_update JSON — the base64 encode + 5.6MB JSON line +
27
+ # JS atob cost ~200 ms/frame on a 4k movie. Off by default (base64 path) so this
28
+ # is zero-risk until the host opts in; SpyDE sets APL_BINARY_TRANSPORT=1.
29
+ _BINARY_TRANSPORT = os.environ.get("APL_BINARY_TRANSPORT") == "1"
30
+ # State keys whose value is a base64 pixel string worth sending as binary.
31
+ # detail_b64 is the zoom DETAIL TILE — it MUST be here too, else with binary
32
+ # transport on its _encode_pixels "\x00bin:<checksum>" token stays in the geom JSON
33
+ # and the real bytes are never shipped, so the renderer can't decode it and the crisp
34
+ # zoom tile never displays (you only ever see the downsampled overview base).
35
+ _BINARY_KEYS = frozenset({"image_b64", "overlay_mask_b64", "detail_b64"})
36
+
37
+
38
+ def _route_change(fig_id: str, name: str, value) -> None:
39
+ """Forward ONE trait change to the host — as a raw PLOTBIN binary frame for a
40
+ large image pixel trait (when binary transport is enabled), else as a
41
+ base64-in-JSON ``state_update``. Extracted from the observer so it's unit-
42
+ testable without a real Figure/stdout."""
43
+ # Binary fast path: ship large image pixels as a raw PLOTBIN frame (the
44
+ # renderer rebuilds the bytes from the ArrayBuffer — no atob).
45
+ #
46
+ # The pixel keys ride INSIDE a panel geometry trait — ``panel_<pid>_geom`` is
47
+ # a JSON string ``{"image_b64": <token|b64>, "colormap_data": …, …}`` (see
48
+ # Figure._push). Two producer modes:
49
+ # • RAW (Plot2D.set_data with binary transport): the geom carries a tiny
50
+ # ``"\x00bin:<n>"`` change-token, and the real uint8 bytes sit in the
51
+ # Figure's ``_raw_pixels`` side-table. We emit those bytes directly —
52
+ # NO json-parse of a megabyte string, NO base64 decode. This is the hot
53
+ # scrub path (a 4k movie frame every tick).
54
+ # • BASE64 (initial __init__ frame / non-set_data pushes / robustness): the
55
+ # geom carries a real base64 string, which we decode once here.
56
+ # Either way the REST of the geom (LUT + flags, small) goes as a normal JSON
57
+ # state_update with the pixels removed. A bare pixel trait is handled too.
58
+ if _BINARY_TRANSPORT and isinstance(value, str) and value:
59
+ if name.startswith("panel_") and name.endswith("_geom"):
60
+ try:
61
+ geom = json.loads(value)
62
+ except Exception:
63
+ geom = None
64
+ if isinstance(geom, dict) and any(k in geom for k in _BINARY_KEYS):
65
+ panel_id = name[len("panel_"):-len("_geom")]
66
+ fig = _figures.get(fig_id)
67
+ raw_tbl = getattr(fig, "_raw_pixels", None)
68
+ sent_binary = False
69
+ for k in list(geom.keys()):
70
+ if k not in _BINARY_KEYS or not isinstance(geom[k], str) or not geom[k]:
71
+ continue
72
+ raw = None
73
+ if geom[k].startswith("\x00bin:") and raw_tbl is not None:
74
+ # RAW fast path: bytes are in the side-table (no decode).
75
+ raw = raw_tbl.get((panel_id, k))
76
+ if raw is None:
77
+ # BASE64 path (initial frame / fallback): decode once.
78
+ try:
79
+ raw = base64.b64decode(geom[k])
80
+ except Exception:
81
+ continue # leave it in geom → goes via JSON below
82
+ # Replace the pixel value with a small content TOKEN instead of
83
+ # popping the key: the renderer's caches (blitCache, GPU
84
+ # texture, maskCache) key their "unchanged → skip re-upload"
85
+ # checks on this string. Popping it left st.<key> empty in JS,
86
+ # whose fallback fingerprint samples only 4 bytes of the
87
+ # buffer — two frames differing anywhere else COLLIDED and the
88
+ # skip froze the display on the old frame (found: a movie
89
+ # overview refresh that only moved a mid-frame feature).
90
+ tok = geom[k]
91
+ if not tok.startswith("\x00bin:"):
92
+ tok = f"\x00bin:{zlib.adler32(raw) & 0xFFFFFFFF}"
93
+ geom[k] = tok
94
+ # key carries which pixel field; geom= the trait name so the
95
+ # renderer routes it back into the right panel state.
96
+ emit_binary(fig_id, k,
97
+ {"nbytes": len(raw), "geom": name}, raw)
98
+ sent_binary = True
99
+ if sent_binary:
100
+ # Send the slimmed geom (LUT etc., pixels removed) as JSON.
101
+ emit({"type": "state_update", "fig_id": fig_id, "key": name,
102
+ "value": json.dumps(geom)})
103
+ return
104
+ elif name in _BINARY_KEYS:
105
+ try:
106
+ raw = base64.b64decode(value)
107
+ emit_binary(fig_id, name, {"nbytes": len(raw)}, raw)
108
+ return
109
+ except Exception:
110
+ pass # fall back to the base64-in-JSON path below
111
+ if isinstance(value, (bytes, bytearray)):
112
+ value = {"buffer": base64.b64encode(value).decode()}
113
+ emit({"type": "state_update", "fig_id": fig_id, "key": name, "value": value})
114
+
115
+
116
+ def register(fig) -> str:
117
+ """Register *fig* for bidirectional state sync and return its fig_id."""
118
+ fig_id = uuid.uuid4().hex[:8]
119
+ _figures[fig_id] = fig
120
+
121
+ def _on_change(change):
122
+ _route_change(fig_id, change["name"], change["new"])
123
+
124
+ for name in fig.traits(sync=True):
125
+ if not name.startswith("_"):
126
+ try:
127
+ fig.observe(_on_change, names=[name])
128
+ except Exception:
129
+ pass
130
+
131
+ return fig_id
132
+
133
+
134
+ def resize_figure(fig_id: str, width: int, height: int) -> None:
135
+ """Update fig_width / fig_height and push new layout to the iframe."""
136
+ fig = _figures.get(fig_id)
137
+ if fig is None:
138
+ return
139
+ try:
140
+ # Batch both trait changes so _on_resize fires only once each.
141
+ with fig.hold_trait_notifications():
142
+ fig.fig_width = int(width)
143
+ fig.fig_height = int(height)
144
+ except Exception:
145
+ pass
146
+
147
+
148
+ def dispatch_event(fig_id: str, event_json: str) -> None:
149
+ """Apply a frontend interaction event to the registered figure."""
150
+ fig = _figures.get(fig_id)
151
+ if fig is None:
152
+ return
153
+ try:
154
+ # Figure.show() registers Figure objects which use _dispatch_event(raw_json_str).
155
+ # Standalone widgets use _update_from_js(dict, event_type).
156
+ if hasattr(fig, "_dispatch_event"):
157
+ fig._dispatch_event(event_json)
158
+ elif hasattr(fig, "_update_from_js"):
159
+ fig._update_from_js(json.loads(event_json))
160
+ except Exception:
161
+ pass
162
+
163
+
164
+ def emit(obj: dict) -> None:
165
+ sys.stdout.write(f"PLOTAPP:{json.dumps(obj, default=str)}\n")
166
+ sys.stdout.flush()
167
+
168
+
169
+ def emit_binary(fig_id: str, key: str, header: dict, payload: bytes) -> None:
170
+ """Write a raw PLOTBIN binary frame to stdout (pixels, no base64/JSON).
171
+
172
+ Goes to the RAW ``sys.stdout.buffer`` so the payload is bytes on the wire; the
173
+ Electron host demuxes ``PLOTBIN:`` frames from the same stdout stream that
174
+ carries the text ``PLOTAPP:`` lines. Flushed so the host sees it promptly."""
175
+ frame = encode_frame(fig_id, key, header, payload)
176
+ buf = getattr(sys.stdout, "buffer", None)
177
+ if buf is None: # no binary stdout (unusual) → base64 fallback
178
+ emit({"type": "state_update", "fig_id": fig_id, "key": key,
179
+ "value": base64.b64encode(payload).decode()})
180
+ return
181
+ # NOTE: a host that redirects sys.stdout (e.g. SpyDE points sys.stdout at
182
+ # stderr to keep the protocol channel clean) MUST patch emit_binary to write
183
+ # to its real protocol stdout — otherwise these bytes go to the wrong stream.
184
+ # Flush the TEXT stream first so any pending PLOTAPP: line is on the wire
185
+ # before this binary frame — the host demuxes a single ordered byte stream.
186
+ sys.stdout.flush()
187
+ buf.write(frame)
188
+ buf.flush()
@@ -207,11 +207,49 @@ import(blobUrl).then(mod => {{
207
207
 
208
208
  // ── Inbound state updates from parent-page Pyodide ───────────────────────────
209
209
  window.addEventListener('message', (e) => {{
210
- if (!e.data || e.data.type !== 'awi_state') return;
211
- _fromParent = true;
212
- model.set(e.data.key, e.data.value);
213
- model.save_changes();
214
- _fromParent = false;
210
+ if (!e.data) return;
211
+ if (e.data.type === 'awi_state') {{
212
+ _fromParent = true;
213
+ const _t0 = performance.now();
214
+ model.set(e.data.key, e.data.value);
215
+ model.save_changes();
216
+ if (e.data.key === 'image_b64') {{
217
+ try {{
218
+ globalThis.__apl_paint_ms = performance.now() - _t0;
219
+ globalThis.__apl_paint_n = (globalThis.__apl_paint_n || 0) + 1;
220
+ globalThis.__apl_paint_kind = 'base64';
221
+ }} catch (_) {{}}
222
+ }}
223
+ _fromParent = false;
224
+ return;
225
+ }}
226
+ // Binary image frame (raw pixels, no base64). Set the bytes under a parallel
227
+ // `<key>_bytes` trait (a Uint8Array) that the draw path uses directly — no atob.
228
+ // We also bump the base64 key to a short token so listeners keyed on it still
229
+ // fire, without carrying the (large) base64 string.
230
+ if (e.data.type === 'awi_state_binary') {{
231
+ _fromParent = true;
232
+ const _t0 = performance.now();
233
+ const hdr = e.data.header || {{}};
234
+ const bytes = e.data.buffer instanceof Uint8Array
235
+ ? e.data.buffer : new Uint8Array(e.data.buffer);
236
+ // Stash the raw bytes in a global side-table (the anywidget model does NOT
237
+ // round-trip a Uint8Array set on an ad-hoc trait — model.get returns
238
+ // undefined), then bump a small TOKEN trait to fire the ESM's change: event,
239
+ // which reads the bytes from the side-table. Key = "<geom_trait>::<pixelKey>".
240
+ const slot = (hdr.geom ? hdr.geom : 'fig') + '::' + e.data.key;
241
+ (globalThis.__apl_pixbytes || (globalThis.__apl_pixbytes = {{}}))[slot] = bytes;
242
+ model.set(slot, (bytes ? bytes.length : 0) + ':' + (globalThis.__apl_paint_n || 0));
243
+ model.save_changes(); // triggers the paint synchronously
244
+ // Paint-latency telemetry (message receipt → draw done) for the binary path.
245
+ try {{
246
+ globalThis.__apl_paint_ms = performance.now() - _t0;
247
+ globalThis.__apl_paint_n = (globalThis.__apl_paint_n || 0) + 1;
248
+ globalThis.__apl_paint_kind = 'binary';
249
+ }} catch (_) {{}}
250
+ _fromParent = false;
251
+ return;
252
+ }}
215
253
  }});
216
254
  </script>
217
255
  </body>