anyplotlib 0.4.0__tar.gz → 0.4.1__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 (253) hide show
  1. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/CHANGELOG.rst +31 -0
  2. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/PKG-INFO +1 -1
  3. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure_esm.js +46 -11
  4. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/_plot1d.py +6 -1
  5. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_plot2d.py +27 -5
  6. anyplotlib-0.4.1/anyplotlib/tests/test_interactive/test_widget_max_extent.py +371 -0
  7. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets1d.py +15 -2
  8. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets2d.py +18 -2
  9. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_root/index.html +3 -3
  10. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_root/switcher.json +5 -0
  11. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/conf.py +1 -1
  12. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/pyproject.toml +1 -1
  13. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/ci.yml +0 -0
  14. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/docs.yml +0 -0
  15. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/prepare_release.yml +0 -0
  16. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/release.yml +0 -0
  17. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/tests.yml +0 -0
  18. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.gitignore +0 -0
  19. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/AGENTS.md +0 -0
  20. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Benchmarks/README.rst +0 -0
  21. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
  22. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/README.rst +0 -0
  23. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
  24. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_eels_explorer.py +0 -0
  25. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_figure_annotations.py +0 -0
  26. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_interactive_fft.py +0 -0
  27. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_interactive_fitting.py +0 -0
  28. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_ipf_density_map.py +0 -0
  29. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_ipf_explorer.py +0 -0
  30. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_key_bindings.py +0 -0
  31. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_particle_picker.py +0 -0
  32. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_point_widget.py +0 -0
  33. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
  34. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
  35. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
  36. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_threshold_explorer.py +0 -0
  37. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
  38. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/README.rst +0 -0
  39. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_arrows.py +0 -0
  40. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_circles.py +0 -0
  41. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_ellipses.py +0 -0
  42. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_horizontal_lines.py +0 -0
  43. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_line_segments.py +0 -0
  44. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_points.py +0 -0
  45. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_polygons.py +0 -0
  46. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_rectangles.py +0 -0
  47. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_squares.py +0 -0
  48. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_texts.py +0 -0
  49. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_transform_markers.py +0 -0
  50. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_vertical_lines.py +0 -0
  51. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/README.rst +0 -0
  52. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_3d.py +0 -0
  53. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_bar.py +0 -0
  54. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_gpu_voxels.py +0 -0
  55. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
  56. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_image2d.py +0 -0
  57. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_inset.py +0 -0
  58. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_label_formatting.py +0 -0
  59. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_line_styles.py +0 -0
  60. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
  61. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_spectra1d.py +0 -0
  62. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_step_and_log.py +0 -0
  63. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_tile_backend.py +0 -0
  64. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_twinx.py +0 -0
  65. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/README.rst +0 -0
  66. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/README.rst +0 -0
  67. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_hline.py +0 -0
  68. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_point.py +0 -0
  69. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_range.py +0 -0
  70. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_vline.py +0 -0
  71. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_annular.py +0 -0
  72. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_arrow.py +0 -0
  73. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_circle.py +0 -0
  74. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
  75. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_label.py +0 -0
  76. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
  77. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
  78. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget3d_plane.py +0 -0
  79. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/LICENSE +0 -0
  80. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Makefile +0 -0
  81. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/README.md +0 -0
  82. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/FIGURE_ESM.md +0 -0
  83. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/__init__.py +0 -0
  84. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_base_plot.py +0 -0
  85. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_binary_frame.py +0 -0
  86. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_electron.py +0 -0
  87. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_repr_utils.py +0 -0
  88. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_utils.py +0 -0
  89. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/__init__.py +0 -0
  90. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/_axes.py +0 -0
  91. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/_inset_axes.py +0 -0
  92. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/callbacks.py +0 -0
  93. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/conftest.py +0 -0
  94. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/embed.py +0 -0
  95. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/__init__.py +0 -0
  96. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_figure.py +0 -0
  97. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_gridspec.py +0 -0
  98. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_subplots.py +0 -0
  99. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/markers.py +0 -0
  100. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/__init__.py +0 -0
  101. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/_plotbar.py +0 -0
  102. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/__init__.py +0 -0
  103. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_layer.py +0 -0
  104. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_plotmesh.py +0 -0
  105. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_tile_backend.py +0 -0
  106. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot3d/__init__.py +0 -0
  107. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot3d/_plot3d.py +0 -0
  108. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plotxy/__init__.py +0 -0
  109. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plotxy/_plotxy.py +0 -0
  110. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
  111. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
  112. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
  113. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
  114. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
  115. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
  116. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
  117. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
  118. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
  119. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
  120. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
  121. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
  122. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
  123. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
  124. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/__init__.py +0 -0
  125. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
  126. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/_png_utils.py +0 -0
  127. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/bar_basic.png +0 -0
  128. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
  129. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
  130. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
  131. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
  132. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
  133. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
  134. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
  135. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
  136. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
  137. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
  138. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
  139. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_1d.png +0 -0
  140. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
  141. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
  142. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
  143. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
  144. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
  145. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
  146. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
  147. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
  148. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
  149. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
  150. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
  151. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
  152. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
  153. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
  154. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
  155. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
  156. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
  157. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
  158. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/conftest.py +0 -0
  159. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
  160. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
  161. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
  162. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
  163. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
  164. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/__init__.py +0 -0
  165. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
  166. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
  167. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
  168. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_electron/__init__.py +0 -0
  169. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
  170. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/__init__.py +0 -0
  171. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
  172. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
  173. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_export_png.py +0 -0
  174. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_examples/__init__.py +0 -0
  175. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
  176. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/__init__.py +0 -0
  177. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
  178. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
  179. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
  180. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
  181. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_edit_chrome.py +0 -0
  182. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +0 -0
  183. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
  184. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
  185. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
  186. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
  187. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
  188. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_title.py +0 -0
  189. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
  190. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
  191. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/__init__.py +0 -0
  192. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
  193. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
  194. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
  195. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/__init__.py +0 -0
  196. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
  197. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
  198. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
  199. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
  200. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_inset_callout.py +0 -0
  201. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
  202. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
  203. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/__init__.py +0 -0
  204. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
  205. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/test_markers.py +0 -0
  206. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
  207. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
  208. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
  209. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
  210. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
  211. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
  212. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
  213. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
  214. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
  215. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
  216. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_layers.py +0 -0
  217. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_layers_playwright.py +0 -0
  218. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
  219. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
  220. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
  221. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
  222. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
  223. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
  224. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
  225. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
  226. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
  227. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +0 -0
  228. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
  229. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
  230. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
  231. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/__init__.py +0 -0
  232. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_base.py +0 -0
  233. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets3d.py +0 -0
  234. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_sg_html_scraper.py +0 -0
  235. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/anyplotlib.svg +0 -0
  236. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/custom.css +0 -0
  237. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/pyodide_bridge.js +0 -0
  238. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_templates/autosummary/class.rst +0 -0
  239. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/callbacks.rst +0 -0
  240. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/figure.rst +0 -0
  241. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/figure_plots.rst +0 -0
  242. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/index.rst +0 -0
  243. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/markers.rst +0 -0
  244. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/widgets.rst +0 -0
  245. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/benchmarking.rst +0 -0
  246. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/dev/index.rst +0 -0
  247. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/embedding.rst +0 -0
  248. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/events.rst +0 -0
  249. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/getting_started.rst +0 -0
  250. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/index.rst +0 -0
  251. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/make.bat +0 -0
  252. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/upcoming_changes/README.rst +0 -0
  253. {anyplotlib-0.4.0 → anyplotlib-0.4.1}/uv.lock +0 -0
@@ -10,6 +10,37 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by
10
10
 
11
11
  .. towncrier release notes start
12
12
 
13
+ 0.4.1 (2026-07-26)
14
+ ==================
15
+
16
+ New Features
17
+ ------------
18
+
19
+ - Added ``max_extent=`` to :class:`~anyplotlib.widgets.RangeWidget` and
20
+ :class:`~anyplotlib.widgets.RectangleWidget` (and the matching
21
+ ``add_range_widget`` / ``add_rectangle_widget`` factories) — a size cap enforced
22
+ *while dragging*, so the widget physically stops growing instead of being
23
+ clamped after the fact. The dragged edge/corner pins and the opposite one stays
24
+ put, so the selection never jumps under the cursor. ``RangeWidget`` takes a span
25
+ width in data units; ``RectangleWidget`` takes a scalar (both axes) or a
26
+ ``(max_w, max_h)`` pair. Default ``None`` leaves widgets unbounded.
27
+
28
+ Use it when a widget's size drives real downstream work — e.g. an integrating
29
+ selector whose span is a number of frames to read.
30
+
31
+
32
+ Bug Fixes
33
+ ---------
34
+
35
+ - Fixed the band-style :class:`~anyplotlib.widgets.RangeWidget` being impossible
36
+ to drag by its body when narrow. Each edge claimed a fixed ±12 px grab zone, so
37
+ a band under ~24 px wide on screen (routine when zoomed out, or when its span is
38
+ capped) had no grabbable middle: aiming at the body to translate the band caught
39
+ an edge and resized it instead. Each edge now takes at most a third of the
40
+ band's width, leaving the middle third for the move handle. Wide bands are
41
+ unaffected.
42
+
43
+
13
44
  0.4.0 (2026-07-18)
14
45
  ==================
15
46
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyplotlib
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -3185,6 +3185,15 @@ function render({ model, el, onResize }) {
3185
3185
  const [rx,ry]=_imgToCanvas2d(w.x,w.y,st,imgW,imgH);
3186
3186
  const rw=w.w*scale, rh=w.h*scale;
3187
3187
  ovCtx.strokeRect(rx,ry,rw,rh);
3188
+ // Canvas-space geometry readback for Playwright (same role as
3189
+ // window._aplTiming). A test cannot derive a handle's page position from
3190
+ // figure padding: the image->canvas mapping depends on the zoom/extent
3191
+ // state, and the grab radius is only HR px — so a hard-coded corner
3192
+ // silently grabs the BODY instead, and a resize test then asserts
3193
+ // nothing. Publishing the drawn corners lets a test aim at a real handle.
3194
+ if(!window._aplWidgetGeom) window._aplWidgetGeom={};
3195
+ if(!window._aplWidgetGeom[p.id]) window._aplWidgetGeom[p.id]={};
3196
+ window._aplWidgetGeom[p.id][w.id]={type:'rectangle',rx,ry,rw,rh};
3188
3197
  if(_handles){
3189
3198
  _drawHandle2d(ovCtx,rx,ry,w.color);_drawHandle2d(ovCtx,rx+rw,ry,w.color);
3190
3199
  _drawHandle2d(ovCtx,rx,ry+rh,w.color);_drawHandle2d(ovCtx,rx+rw,ry+rh,w.color);
@@ -7214,22 +7223,28 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
7214
7223
  w.r_inner = newR;
7215
7224
  }
7216
7225
  } else if (w.type === 'rectangle') {
7226
+ // max_w / max_h cap the rectangle DURING the drag, so it physically stops
7227
+ // growing rather than being silently clamped afterwards. Each branch pins
7228
+ // the size and leaves the OPPOSITE corner where it is, so the anchor never
7229
+ // shifts and the rectangle cannot jump out from under the cursor.
7230
+ const capW = (v) => (w.max_w == null ? v : Math.min(v, w.max_w));
7231
+ const capH = (v) => (w.max_h == null ? v : Math.min(v, w.max_h));
7217
7232
  if (d.mode === 'move') {
7218
7233
  w.x = s.x + dix; w.y = s.y + diy;
7219
7234
  } else if (d.mode === 'resize_br') {
7220
- w.w = Math.max(1, s.w + dix); w.h = Math.max(1, s.h + diy);
7235
+ w.w = capW(Math.max(1, s.w + dix)); w.h = capH(Math.max(1, s.h + diy));
7221
7236
  } else if (d.mode === 'resize_bl') {
7222
- const newW = Math.max(1, s.w - dix);
7237
+ const newW = capW(Math.max(1, s.w - dix));
7223
7238
  w.x = s.x + (s.w - newW); w.w = newW;
7224
- w.h = Math.max(1, s.h + diy);
7239
+ w.h = capH(Math.max(1, s.h + diy));
7225
7240
  } else if (d.mode === 'resize_tr') {
7226
- w.w = Math.max(1, s.w + dix);
7227
- const newH = Math.max(1, s.h - diy);
7241
+ w.w = capW(Math.max(1, s.w + dix));
7242
+ const newH = capH(Math.max(1, s.h - diy));
7228
7243
  w.y = s.y + (s.h - newH); w.h = newH;
7229
7244
  } else if (d.mode === 'resize_tl') {
7230
- const newW = Math.max(1, s.w - dix);
7245
+ const newW = capW(Math.max(1, s.w - dix));
7231
7246
  w.x = s.x + (s.w - newW); w.w = newW;
7232
- const newH = Math.max(1, s.h - diy);
7247
+ const newH = capH(Math.max(1, s.h - diy));
7233
7248
  w.y = s.y + (s.h - newH); w.h = newH;
7234
7249
  }
7235
7250
  } else if (w.type === 'crosshair') {
@@ -7307,9 +7322,16 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
7307
7322
  if(Math.hypot(mx-px1b,my-pyHalf)<=HR+5)
7308
7323
  return{idx:i,mode:'edge1',wtype:'range',startMX:mx,snapW:{...w}};
7309
7324
  } else {
7310
- if(Math.abs(mx-px0)<=HR+5) return{idx:i,mode:'edge0',wtype:'range',startMX:mx,snapW:{...w}};
7311
- if(Math.abs(mx-px1b)<=HR+5) return{idx:i,mode:'edge1',wtype:'range',startMX:mx,snapW:{...w}};
7325
+ // Band style. The edge grab zones are ±(HR+5)=12 px each, so on a
7326
+ // NARROW band (< ~24 px on screen — routine when the span is capped, or
7327
+ // just zoomed out) the two zones meet and swallow the body: the user
7328
+ // aims at the middle to translate the band and gets an edge resize
7329
+ // instead. Give each edge at most a THIRD of the band, so the middle
7330
+ // third always belongs to 'move' no matter how narrow the band gets.
7312
7331
  const left=Math.min(px0,px1b),right=Math.max(px0,px1b);
7332
+ const grab=Math.min(HR+5,(right-left)/3);
7333
+ if(Math.abs(mx-px0)<=grab) return{idx:i,mode:'edge0',wtype:'range',startMX:mx,snapW:{...w}};
7334
+ if(Math.abs(mx-px1b)<=grab) return{idx:i,mode:'edge1',wtype:'range',startMX:mx,snapW:{...w}};
7313
7335
  if(mx>=left&&mx<=right&&my>=r.y&&my<=r.y+r.h) return{idx:i,mode:'move',wtype:'range',startMX:mx,snapW:{...w}};
7314
7336
  }
7315
7337
  }
@@ -7329,8 +7351,21 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
7329
7351
  if(w.type==='vline'){w.x=xUnit;}
7330
7352
  else if(w.type==='hline'){w.y=st.data_max-((py-r.y)/(r.h||1))*(st.data_max-st.data_min);}
7331
7353
  else if(w.type==='range'){
7332
- if(d.mode==='edge0') w.x0=xUnit;
7333
- else if(d.mode==='edge1') w.x1=xUnit;
7354
+ // max_extent caps the span DURING the drag. The edge NOT being dragged is
7355
+ // the anchor and never moves, so the span stops growing at the cap without
7356
+ // the range jumping. (Clamping afterwards by rewriting whichever endpoint
7357
+ // happens to be numerically smaller moves the edge the user is not holding
7358
+ // — that reads as phantom movement.) Dragging the whole band is unaffected:
7359
+ // a translation can't change the width.
7360
+ const cap = (w.max_extent == null ? null : Math.abs(w.max_extent));
7361
+ if(d.mode==='edge0'){
7362
+ w.x0 = (cap!=null && Math.abs(w.x1-xUnit) > cap)
7363
+ ? w.x1 + (xUnit < w.x1 ? -cap : cap) : xUnit;
7364
+ }
7365
+ else if(d.mode==='edge1'){
7366
+ w.x1 = (cap!=null && Math.abs(xUnit-w.x0) > cap)
7367
+ ? w.x0 + (xUnit < w.x0 ? -cap : cap) : xUnit;
7368
+ }
7334
7369
  else {
7335
7370
  const snapPx=_fracToPx1d(xArr.length>=2?_axisValToFrac(xArr,s.x0):0,x0,x1,r);
7336
7371
  const dxUnit=xArr.length>=2?_axisFracToVal(xArr,_canvasXToFrac1d(snapPx+(mx-d.startMX),x0,x1,r))-s.x0:(mx-d.startMX)/(r.w||1);
@@ -756,6 +756,7 @@ class Plot1D(_BasePlot, _PanelMixin, _MarkerMixin):
756
756
  style: str = "band",
757
757
  y: float = 0.0,
758
758
  linewidth: float = 2,
759
+ max_extent: float | None = None,
759
760
  _push: bool = True) -> _RangeWidget:
760
761
  """Add a draggable range overlay to this panel.
761
762
 
@@ -775,6 +776,10 @@ class Plot1D(_BasePlot, _PanelMixin, _MarkerMixin):
775
776
  ``style='fwhm'``. Ignored when ``style='band'``. Default 0.
776
777
  linewidth : float, optional
777
778
  Line stroke width in px. Default 2.
779
+ max_extent : float, optional
780
+ Maximum span width in data units. The span physically stops growing
781
+ at this width while dragging (the dragged edge pins, the opposite
782
+ edge stays put). ``None`` (default) leaves it unbounded.
778
783
  _push : bool, optional
779
784
  Push state to JS immediately. Set to ``False`` when adding
780
785
  several widgets at once; call :meth:`_push` manually afterward.
@@ -787,7 +792,7 @@ class Plot1D(_BasePlot, _PanelMixin, _MarkerMixin):
787
792
  """
788
793
  widget = _RangeWidget(lambda: None, x0=float(x0), x1=float(x1),
789
794
  color=color, style=style, y=float(y),
790
- linewidth=linewidth)
795
+ linewidth=linewidth, max_extent=max_extent)
791
796
  widget._push_fn = self._make_widget_push_fn(widget)
792
797
  self._widgets[widget.id] = widget
793
798
  if _push:
@@ -1610,16 +1610,38 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
1610
1610
  def add_rectangle_widget(self, x: float | None = None, y: float | None = None,
1611
1611
  w: float | None = None, h: float | None = None,
1612
1612
  color: str = "#00e5ff", linewidth: float = 2,
1613
- show_handles: bool = True) -> RectangleWidget:
1614
- """Add a draggable rectangle overlay."""
1613
+ show_handles: bool = True,
1614
+ max_extent=None) -> RectangleWidget:
1615
+ """Add a draggable rectangle overlay.
1616
+
1617
+ ``max_extent`` caps the rectangle's size in the widget's coordinates —
1618
+ a scalar caps both axes, a ``(max_w, max_h)`` pair caps them separately.
1619
+ The rectangle then physically stops growing at the cap while dragging
1620
+ (the dragged corner pins, the opposite corner stays put).
1621
+ """
1615
1622
  iw, ih = self._state["image_width"], self._state["image_height"]
1623
+ # The DEFAULT size honours max_extent. Without this a capped rectangle is
1624
+ # born at half the image and then snaps down to the cap on first use —
1625
+ # the ROI visibly appears huge and collapses, which looks like a bug.
1626
+ def _cap(axis_default, cap):
1627
+ return axis_default if cap is None else min(axis_default, float(cap))
1628
+
1629
+ if max_extent is None:
1630
+ cap_w = cap_h = None
1631
+ elif isinstance(max_extent, (tuple, list)):
1632
+ cap_w, cap_h = max_extent[0], max_extent[1]
1633
+ else:
1634
+ cap_w = cap_h = max_extent
1635
+ def_w = _cap(iw * 0.5, cap_w)
1636
+ def_h = _cap(ih * 0.5, cap_h)
1616
1637
  widget = RectangleWidget(lambda: None,
1617
1638
  x=float(x) if x is not None else iw * 0.25,
1618
1639
  y=float(y) if y is not None else ih * 0.25,
1619
- w=float(w) if w is not None else iw * 0.5,
1620
- h=float(h) if h is not None else ih * 0.5,
1640
+ w=float(w) if w is not None else def_w,
1641
+ h=float(h) if h is not None else def_h,
1621
1642
  color=color, linewidth=linewidth,
1622
- show_handles=show_handles)
1643
+ show_handles=show_handles,
1644
+ max_extent=max_extent)
1623
1645
  widget._push_fn = self._make_widget_push_fn(widget)
1624
1646
  self._widgets[widget.id] = widget
1625
1647
  self._push()
@@ -0,0 +1,371 @@
1
+ """
2
+ tests/test_interactive/test_widget_max_extent.py
3
+ =================================================
4
+
5
+ ``max_extent`` — a size cap enforced while the widget is being dragged.
6
+
7
+ Why it exists: when a widget's size drives real downstream work (an integrating
8
+ selector whose span is a number of frames to read), an unbounded drag is a
9
+ performance cliff. Clamping *after* the fact is worse than useless — it moves an
10
+ edge the user isn't holding, which reads as the selection jumping around under
11
+ the cursor. So the cap belongs in the drag itself: the dragged edge pins, the
12
+ opposite edge stays put, and the widget visibly stops growing.
13
+
14
+ Also covered: the band hit-test. Each edge of a range widget claimed a fixed
15
+ ±12 px grab zone, so a band narrower than ~24 px on screen (routine when its
16
+ span is capped, or simply when zoomed out) had NO grabbable middle — aiming at
17
+ the body to translate it caught an edge and resized instead. Each edge now takes
18
+ at most a third of the band.
19
+
20
+ Coordinate system for the Playwright tests mirrors figure_esm.js:
21
+ PAD_L=58 PAD_R=12 PAD_T=12 PAD_B=42 GRID_PAD=8
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import numpy as np
26
+ import pytest
27
+
28
+ import anyplotlib as apl
29
+ from anyplotlib.widgets import RangeWidget, RectangleWidget
30
+ from anyplotlib.tests.test_interactive._event_test_utils import (
31
+ _collect_events,
32
+ GRID_PAD, PAD_L, PAD_R, PAD_T, PAD_B,
33
+ )
34
+
35
+ FIG_W, FIG_H = 400, 300
36
+
37
+
38
+ # ═══════════════════════════════════════════════════════════════════════════
39
+ # 1. Python API
40
+ # ═══════════════════════════════════════════════════════════════════════════
41
+
42
+ class TestMaxExtentAttributes:
43
+ def test_range_defaults_to_unbounded(self):
44
+ w = RangeWidget(lambda: None, x0=0, x1=10)
45
+ assert w.max_extent is None
46
+
47
+ def test_range_stores_max_extent(self):
48
+ w = RangeWidget(lambda: None, x0=0, x1=10, max_extent=16)
49
+ assert w.max_extent == 16.0
50
+
51
+ def test_range_max_extent_reaches_the_state_dict(self):
52
+ """JS reads the widget dict, so the cap has to survive to_dict()."""
53
+ w = RangeWidget(lambda: None, x0=0, x1=10, max_extent=16)
54
+ assert w.to_dict()["max_extent"] == 16.0
55
+
56
+ def test_rectangle_defaults_to_unbounded(self):
57
+ w = RectangleWidget(lambda: None, x=0, y=0, w=10, h=10)
58
+ assert w.max_w is None and w.max_h is None
59
+
60
+ def test_rectangle_scalar_caps_both_axes(self):
61
+ w = RectangleWidget(lambda: None, x=0, y=0, w=10, h=10, max_extent=16)
62
+ assert w.max_w == 16.0 and w.max_h == 16.0
63
+
64
+ def test_rectangle_pair_caps_axes_separately(self):
65
+ w = RectangleWidget(lambda: None, x=0, y=0, w=10, h=10,
66
+ max_extent=(8, 32))
67
+ assert w.max_w == 8.0 and w.max_h == 32.0
68
+
69
+ def test_rectangle_max_extent_reaches_the_state_dict(self):
70
+ w = RectangleWidget(lambda: None, x=0, y=0, w=10, h=10, max_extent=16)
71
+ d = w.to_dict()
72
+ assert d["max_w"] == 16.0 and d["max_h"] == 16.0
73
+
74
+
75
+ class TestMaxExtentFactories:
76
+ def test_add_range_widget_passes_max_extent(self):
77
+ fig, ax = apl.subplots(1, 1)
78
+ v = ax.plot(np.zeros(64))
79
+ w = v.add_range_widget(x0=10, x1=20, max_extent=16)
80
+ assert isinstance(w, RangeWidget) and w.max_extent == 16.0
81
+
82
+ def test_add_range_widget_without_max_extent(self):
83
+ fig, ax = apl.subplots(1, 1)
84
+ v = ax.plot(np.zeros(64))
85
+ assert v.add_range_widget(x0=10, x1=20).max_extent is None
86
+
87
+ def test_add_rectangle_widget_passes_max_extent(self):
88
+ fig, ax = apl.subplots(1, 1)
89
+ v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
90
+ w = v.add_rectangle_widget(x=0, y=0, w=4, h=4, max_extent=(8, 12))
91
+ assert w.max_w == 8.0 and w.max_h == 12.0
92
+
93
+
94
+ class TestDefaultSizeHonoursTheCap:
95
+ """A capped rectangle must be BORN at the cap.
96
+
97
+ The default size is half the image, so a capped ROI used to appear huge and
98
+ then snap down to the cap on first use — which reads as a bug ("the ROI is
99
+ always huge and then it clamps down")."""
100
+
101
+ def test_default_size_is_capped(self):
102
+ fig, ax = apl.subplots(1, 1)
103
+ v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
104
+ w = v.add_rectangle_widget(max_extent=16.0)
105
+ assert w.w == 16.0 and w.h == 16.0
106
+
107
+ def test_default_size_respects_a_per_axis_cap(self):
108
+ fig, ax = apl.subplots(1, 1)
109
+ v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
110
+ w = v.add_rectangle_widget(max_extent=(8, 32))
111
+ assert w.w == 8.0 and w.h == 32.0
112
+
113
+ def test_uncapped_default_is_unchanged(self):
114
+ fig, ax = apl.subplots(1, 1)
115
+ v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
116
+ w = v.add_rectangle_widget()
117
+ assert w.w == 128.0 and w.h == 128.0 # half the image, as before
118
+
119
+ def test_explicit_size_wins_over_the_cap_default(self):
120
+ """An explicit w/h is the caller's business; the cap only bounds drags."""
121
+ fig, ax = apl.subplots(1, 1)
122
+ v = ax.imshow(np.zeros((256, 256), dtype=np.float32))
123
+ w = v.add_rectangle_widget(w=4, h=4, max_extent=16.0)
124
+ assert w.w == 4.0 and w.h == 4.0
125
+
126
+ def test_cap_larger_than_the_image_does_not_inflate_the_default(self):
127
+ fig, ax = apl.subplots(1, 1)
128
+ v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
129
+ w = v.add_rectangle_widget(max_extent=1000.0)
130
+ assert w.w == 16.0 and w.h == 16.0 # still half the image
131
+
132
+
133
+ # ═══════════════════════════════════════════════════════════════════════════
134
+ # 2. Drag behaviour in the browser — the logic lives in figure_esm.js, so a
135
+ # Python-only test would prove nothing about what the user experiences.
136
+ # ═══════════════════════════════════════════════════════════════════════════
137
+
138
+ def _plot_rect():
139
+ """Page-coordinate plot area (x, y, w, h)."""
140
+ return (GRID_PAD + PAD_L, GRID_PAD + PAD_T,
141
+ FIG_W - PAD_L - PAD_R, FIG_H - PAD_T - PAD_B)
142
+
143
+
144
+ def _collect_panel_state(page) -> None:
145
+ """Record every ``model.set('panel_<id>_json', ...)`` write, seeded with the
146
+ state the page already holds.
147
+
148
+ The drag handlers push updated widget geometry back through the model, so the
149
+ last write for a panel is its current state. Seeding from ``model.get`` first
150
+ matters because a test may read the BEFORE state, and until something is
151
+ dragged there has been no write to intercept. (There is no global panel
152
+ registry to read — the panel object lives inside the JS closure.)"""
153
+ page.evaluate("""() => {
154
+ window._aplPanelState = {};
155
+ const m = window._aplModel;
156
+ const orig = m.set.bind(m);
157
+ m.set = (k, v) => {
158
+ const mm = /^panel_(.+)_json$/.exec(k);
159
+ if (mm) { try { window._aplPanelState[mm[1]] = JSON.parse(v); } catch(_) {} }
160
+ return orig(k, v);
161
+ };
162
+ }""")
163
+
164
+
165
+ def _seed_panel_state(page, plot_id) -> None:
166
+ """Pull one panel's current state out of the model (no drag needed)."""
167
+ page.evaluate(
168
+ """(pid) => {
169
+ try {
170
+ const v = window._aplModel.get('panel_' + pid + '_json');
171
+ if (v) window._aplPanelState[pid] = JSON.parse(v);
172
+ } catch(_) {}
173
+ }""", str(plot_id))
174
+
175
+
176
+ def _widget_state(page, plot_id, idx=0):
177
+ """The widget dict from the most recent panel-state write, or None."""
178
+ return page.evaluate(
179
+ """([pid, i]) => {
180
+ const st = window._aplPanelState && window._aplPanelState[pid];
181
+ const ws = st && st.overlay_widgets;
182
+ return ws && ws[i] ? ws[i] : null;
183
+ }""", [str(plot_id), idx])
184
+
185
+
186
+ def _x_to_page(xdata, n):
187
+ """Data x -> page x for a line plot of n points spanning the full view."""
188
+ px, _py, pw, _ph = _plot_rect()
189
+ return px + (xdata / float(n - 1)) * pw
190
+
191
+
192
+ @pytest.mark.usefixtures("interact_page")
193
+ class TestRangeDragCap:
194
+ def _setup(self, interact_page, max_extent):
195
+ fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
196
+ n = 128
197
+ plot = ax.plot(np.zeros(n))
198
+ w = plot.add_range_widget(x0=40, x1=50, max_extent=max_extent)
199
+ page = interact_page(fig)
200
+ _collect_events(page)
201
+ _collect_panel_state(page)
202
+ return fig, plot, page, w, n
203
+
204
+ def test_dragging_an_edge_stops_at_the_cap(self, interact_page):
205
+ """Drag the right edge far past the cap: the span pins at max_extent and
206
+ the LEFT edge — the one the user is not holding — must not move."""
207
+ fig, plot, page, w, n = self._setup(interact_page, max_extent=16)
208
+ _px, py, _pw, ph = _plot_rect()
209
+ mid_y = py + ph // 2
210
+
211
+ page.mouse.move(_x_to_page(50, n), mid_y)
212
+ page.mouse.down()
213
+ page.mouse.move(_x_to_page(120, n), mid_y, steps=10) # way past the cap
214
+ page.mouse.up()
215
+ page.wait_for_timeout(80)
216
+
217
+ st = _widget_state(page, plot._id)
218
+ assert st is not None, "widget state should be readable"
219
+ span = abs(st["x1"] - st["x0"])
220
+ assert span <= 16.0 + 1e-6, f"span {span} exceeded the cap"
221
+ assert st["x0"] == pytest.approx(40.0, abs=1.0), \
222
+ "the anchor edge moved — that is the phantom-movement bug"
223
+
224
+ def test_uncapped_range_still_grows_freely(self, interact_page):
225
+ fig, plot, page, w, n = self._setup(interact_page, max_extent=None)
226
+ _px, py, _pw, ph = _plot_rect()
227
+ mid_y = py + ph // 2
228
+
229
+ page.mouse.move(_x_to_page(50, n), mid_y)
230
+ page.mouse.down()
231
+ page.mouse.move(_x_to_page(100, n), mid_y, steps=10)
232
+ page.mouse.up()
233
+ page.wait_for_timeout(80)
234
+
235
+ st = _widget_state(page, plot._id)
236
+ assert abs(st["x1"] - st["x0"]) > 16.0, \
237
+ "no cap was set, so the span should have grown past 16"
238
+
239
+ def test_translating_a_capped_band_keeps_its_width(self, interact_page):
240
+ """Dragging the BODY moves both edges together; a translation can't
241
+ change the width, so the cap must not interfere with it."""
242
+ fig, plot, page, w, n = self._setup(interact_page, max_extent=16)
243
+ _px, py, _pw, ph = _plot_rect()
244
+ mid_y = py + ph // 2
245
+
246
+ _seed_panel_state(page, plot._id)
247
+ before = _widget_state(page, plot._id)
248
+ width0 = abs(before["x1"] - before["x0"])
249
+
250
+ page.mouse.move(_x_to_page(45, n), mid_y) # middle of the band
251
+ page.mouse.down()
252
+ page.mouse.move(_x_to_page(65, n), mid_y, steps=8)
253
+ page.mouse.up()
254
+ page.wait_for_timeout(80)
255
+
256
+ after = _widget_state(page, plot._id)
257
+ assert abs(after["x1"] - after["x0"]) == pytest.approx(width0, abs=0.5)
258
+ assert after["x0"] > before["x0"] + 5, "the band should have moved"
259
+
260
+
261
+ @pytest.mark.usefixtures("interact_page")
262
+ class TestNarrowBandIsGrabbable:
263
+ def test_middle_of_a_narrow_band_translates_rather_than_resizes(
264
+ self, interact_page):
265
+ """THE regression this guards: with fixed +-12 px edge zones, a band
266
+ narrower than ~24 px had no grabbable body, so aiming at the middle
267
+ resized an edge instead of moving the band. Each edge now takes at most
268
+ a third of the band, leaving the middle third for 'move'."""
269
+ fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
270
+ n = 128
271
+ plot = ax.plot(np.zeros(n))
272
+ # ~4 data units of 128 across ~330 px => roughly 10 px on screen.
273
+ plot.add_range_widget(x0=60, x1=64)
274
+ page = interact_page(fig)
275
+ _collect_events(page)
276
+ _collect_panel_state(page)
277
+
278
+ _px, py, _pw, ph = _plot_rect()
279
+ mid_y = py + ph // 2
280
+ _seed_panel_state(page, plot._id)
281
+ before = _widget_state(page, plot._id)
282
+ width0 = abs(before["x1"] - before["x0"])
283
+
284
+ page.mouse.move(_x_to_page(62, n), mid_y) # dead centre of the band
285
+ page.mouse.down()
286
+ page.mouse.move(_x_to_page(82, n), mid_y, steps=8)
287
+ page.mouse.up()
288
+ page.wait_for_timeout(80)
289
+
290
+ after = _widget_state(page, plot._id)
291
+ assert abs(after["x1"] - after["x0"]) == pytest.approx(width0, abs=1.0), \
292
+ "grabbing the middle of a narrow band resized it instead of moving it"
293
+ assert after["x0"] > before["x0"] + 5, "the band should have moved"
294
+
295
+ def test_edges_of_a_wide_band_still_resize(self, interact_page):
296
+ """The narrow-band fix must not cost us edge resizing on normal bands."""
297
+ fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
298
+ n = 128
299
+ plot = ax.plot(np.zeros(n))
300
+ plot.add_range_widget(x0=20, x1=80) # wide: ~155 px on screen
301
+ page = interact_page(fig)
302
+ _collect_events(page)
303
+ _collect_panel_state(page)
304
+
305
+ _px, py, _pw, ph = _plot_rect()
306
+ mid_y = py + ph // 2
307
+ _seed_panel_state(page, plot._id)
308
+ before = _widget_state(page, plot._id)
309
+
310
+ page.mouse.move(_x_to_page(80, n), mid_y) # right edge
311
+ page.mouse.down()
312
+ page.mouse.move(_x_to_page(100, n), mid_y, steps=8)
313
+ page.mouse.up()
314
+ page.wait_for_timeout(80)
315
+
316
+ after = _widget_state(page, plot._id)
317
+ assert after["x1"] > before["x1"] + 5, "the right edge should have moved"
318
+ assert after["x0"] == pytest.approx(before["x0"], abs=1.0), \
319
+ "the left edge should have stayed put"
320
+
321
+
322
+ @pytest.mark.usefixtures("interact_page")
323
+ class TestRectangleDragCap:
324
+ def test_resizing_stops_at_the_cap_and_keeps_the_anchor(self, interact_page):
325
+ fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
326
+ plot = ax.imshow(np.zeros((64, 64), dtype=np.float32))
327
+ widget = plot.add_rectangle_widget(x=8, y=8, w=4, h=4, max_extent=16)
328
+ page = interact_page(fig)
329
+ _collect_events(page)
330
+ _collect_panel_state(page)
331
+
332
+ _seed_panel_state(page, plot._id)
333
+ before = _widget_state(page, plot._id)
334
+ assert before["max_w"] == 16.0
335
+
336
+ # Aim at the REAL bottom-right handle using the canvas geometry the draw
337
+ # path publishes (window._aplWidgetGeom). Deriving it from figure padding
338
+ # doesn't work for a 2-D panel — the image->canvas mapping depends on the
339
+ # zoom/extent state, and the grab radius is only HR=9 px, so a hard-coded
340
+ # corner grabs the BODY ('move'), which legitimately ignores the cap and
341
+ # would leave this test asserting nothing.
342
+ geom = page.evaluate(
343
+ """([pid, wid]) => {
344
+ const g = window._aplWidgetGeom && window._aplWidgetGeom[pid];
345
+ return g ? (g[wid] || Object.values(g)[0] || null) : null;
346
+ }""", [str(plot._id), str(widget.id)])
347
+ assert geom is not None, "widget geometry readback missing"
348
+
349
+ canvas = page.evaluate(
350
+ """() => {
351
+ const c = document.querySelector('canvas');
352
+ const r = c.getBoundingClientRect();
353
+ return {left: r.left, top: r.top};
354
+ }""")
355
+ br_x = canvas["left"] + geom["rx"] + geom["rw"]
356
+ br_y = canvas["top"] + geom["ry"] + geom["rh"]
357
+
358
+ page.mouse.move(br_x, br_y)
359
+ page.mouse.down()
360
+ page.mouse.move(br_x + 300, br_y + 300, steps=12) # far past the cap
361
+ page.mouse.up()
362
+ page.wait_for_timeout(80)
363
+
364
+ after = _widget_state(page, plot._id)
365
+ assert after["w"] > before["w"] + 0.5, \
366
+ "no resize happened — the drag missed the handle"
367
+
368
+ assert after["w"] <= 16.0 + 1e-6, f"width {after['w']} exceeded the cap"
369
+ assert after["h"] <= 16.0 + 1e-6, f"height {after['h']} exceeded the cap"
370
+ assert after["x"] == pytest.approx(before["x"], abs=1e-6), "anchor x moved"
371
+ assert after["y"] == pytest.approx(before["y"], abs=1e-6), "anchor y moved"
@@ -83,13 +83,26 @@ class RangeWidget(Widget):
83
83
  ``style='fwhm'``. Ignored for ``style='band'``. Default ``0.0``.
84
84
  linewidth : float, optional
85
85
  Line stroke width in px. Default 2.
86
+ max_extent : float, optional
87
+ Maximum span width in DATA units. When set, the span physically stops
88
+ growing at this width while dragging: the edge under the cursor is
89
+ pinned and the opposite edge stays put, so the range never exceeds the
90
+ cap and never jumps. ``None`` (default) leaves it unbounded.
91
+
92
+ Use this when span width costs real work downstream — e.g. an
93
+ integrating selector where the width is a number of frames to read.
94
+ Enforcing it in the widget makes the limit visible (the edge simply
95
+ stops) instead of applying a silent clamp after the fact.
86
96
  """
87
97
  def __init__(self, push_fn, *, x0, x1, color="#00e5ff",
88
- style: str = "band", y: float = 0.0, linewidth=2):
98
+ style: str = "band", y: float = 0.0, linewidth=2,
99
+ max_extent=None):
89
100
  super().__init__("range", push_fn,
90
101
  x0=float(x0), x1=float(x1), color=color,
91
102
  style=str(style), y=float(y),
92
- linewidth=float(linewidth))
103
+ linewidth=float(linewidth),
104
+ max_extent=(None if max_extent is None
105
+ else float(max_extent)))
93
106
 
94
107
 
95
108
  class PointWidget(Widget):
@@ -31,14 +31,30 @@ class RectangleWidget(Widget):
31
31
  Outline stroke width in px. Default 2.
32
32
  show_handles : bool, optional
33
33
  Draw the corner grab handles. Default ``True``.
34
+ max_extent : float or (float, float), optional
35
+ Maximum width/height in the widget's coordinates. A scalar caps both
36
+ axes; a ``(max_w, max_h)`` pair caps them separately. When set, the
37
+ rectangle physically stops growing at the cap while dragging — the
38
+ dragged corner pins and the opposite corner stays put. ``None``
39
+ (default) leaves it unbounded.
40
+
41
+ Use this when the rectangle's area costs real work downstream — e.g. an
42
+ integrating ROI whose size is a number of frames to read.
34
43
  """
35
44
  def __init__(self, push_fn, *, x, y, w, h, color="#00e5ff",
36
- linewidth=2, show_handles=True):
45
+ linewidth=2, show_handles=True, max_extent=None):
46
+ if max_extent is None:
47
+ max_w = max_h = None
48
+ elif isinstance(max_extent, (tuple, list)):
49
+ max_w, max_h = (float(max_extent[0]), float(max_extent[1]))
50
+ else:
51
+ max_w = max_h = float(max_extent)
37
52
  super().__init__("rectangle", push_fn,
38
53
  x=float(x), y=float(y),
39
54
  w=float(w), h=float(h), color=color,
40
55
  linewidth=float(linewidth),
41
- show_handles=bool(show_handles))
56
+ show_handles=bool(show_handles),
57
+ max_w=max_w, max_h=max_h)
42
58
 
43
59
 
44
60
  class CircleWidget(Widget):
@@ -4,12 +4,12 @@
4
4
  <meta charset="utf-8" />
5
5
  <title>anyplotlib – redirecting…</title>
6
6
  <!-- Redirect to the latest dev docs. -->
7
- <meta http-equiv="refresh" content="0; url=v0.4.0/" />
8
- <link rel="canonical" href="v0.4.0/" />
7
+ <meta http-equiv="refresh" content="0; url=v0.4.1/" />
8
+ <link rel="canonical" href="v0.4.1/" />
9
9
  </head>
10
10
  <body>
11
11
  <p>
12
- Redirecting to <a href="v0.4.0/">v0.4.0 documentation</a>…
12
+ Redirecting to <a href="v0.4.1/">v0.4.1 documentation</a>…
13
13
  </p>
14
14
  </body>
15
15
  </html>