anyplotlib 0.4.0__tar.gz → 0.4.2__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 (254) hide show
  1. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/CHANGELOG.rst +31 -0
  2. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/PKG-INFO +1 -1
  3. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/figure_esm.js +46 -11
  4. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot1d/_plot1d.py +6 -1
  5. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot2d/_plot2d.py +71 -26
  6. anyplotlib-0.4.2/anyplotlib/tests/test_interactive/test_widget_max_extent.py +371 -0
  7. anyplotlib-0.4.2/anyplotlib/tests/test_plot2d/test_tile_diagnostics.py +88 -0
  8. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/widgets/_widgets1d.py +15 -2
  9. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/widgets/_widgets2d.py +18 -2
  10. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_root/index.html +3 -3
  11. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_root/switcher.json +10 -0
  12. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/conf.py +1 -1
  13. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/pyproject.toml +1 -1
  14. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.github/workflows/ci.yml +0 -0
  15. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.github/workflows/docs.yml +0 -0
  16. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.github/workflows/prepare_release.yml +0 -0
  17. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.github/workflows/release.yml +0 -0
  18. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.github/workflows/tests.yml +0 -0
  19. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/.gitignore +0 -0
  20. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/AGENTS.md +0 -0
  21. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Benchmarks/README.rst +0 -0
  22. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
  23. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/README.rst +0 -0
  24. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
  25. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_eels_explorer.py +0 -0
  26. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_figure_annotations.py +0 -0
  27. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_interactive_fft.py +0 -0
  28. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_interactive_fitting.py +0 -0
  29. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_ipf_density_map.py +0 -0
  30. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_ipf_explorer.py +0 -0
  31. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_key_bindings.py +0 -0
  32. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_particle_picker.py +0 -0
  33. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_point_widget.py +0 -0
  34. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
  35. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
  36. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
  37. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_threshold_explorer.py +0 -0
  38. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
  39. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/README.rst +0 -0
  40. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_arrows.py +0 -0
  41. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_circles.py +0 -0
  42. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_ellipses.py +0 -0
  43. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_horizontal_lines.py +0 -0
  44. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_line_segments.py +0 -0
  45. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_points.py +0 -0
  46. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_polygons.py +0 -0
  47. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_rectangles.py +0 -0
  48. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_squares.py +0 -0
  49. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_texts.py +0 -0
  50. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_transform_markers.py +0 -0
  51. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Markers/plot_vertical_lines.py +0 -0
  52. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/README.rst +0 -0
  53. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_3d.py +0 -0
  54. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_bar.py +0 -0
  55. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_gpu_voxels.py +0 -0
  56. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
  57. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_image2d.py +0 -0
  58. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_inset.py +0 -0
  59. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_label_formatting.py +0 -0
  60. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_line_styles.py +0 -0
  61. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
  62. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_spectra1d.py +0 -0
  63. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_step_and_log.py +0 -0
  64. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_tile_backend.py +0 -0
  65. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/PlotTypes/plot_twinx.py +0 -0
  66. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/README.rst +0 -0
  67. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/README.rst +0 -0
  68. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget1d_hline.py +0 -0
  69. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget1d_point.py +0 -0
  70. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget1d_range.py +0 -0
  71. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget1d_vline.py +0 -0
  72. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_annular.py +0 -0
  73. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_arrow.py +0 -0
  74. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_circle.py +0 -0
  75. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
  76. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_label.py +0 -0
  77. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
  78. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
  79. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Examples/Widgets/plot_widget3d_plane.py +0 -0
  80. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/LICENSE +0 -0
  81. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/Makefile +0 -0
  82. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/README.md +0 -0
  83. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/FIGURE_ESM.md +0 -0
  84. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/__init__.py +0 -0
  85. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/_base_plot.py +0 -0
  86. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/_binary_frame.py +0 -0
  87. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/_electron.py +0 -0
  88. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/_repr_utils.py +0 -0
  89. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/_utils.py +0 -0
  90. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/axes/__init__.py +0 -0
  91. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/axes/_axes.py +0 -0
  92. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/axes/_inset_axes.py +0 -0
  93. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/callbacks.py +0 -0
  94. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/conftest.py +0 -0
  95. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/embed.py +0 -0
  96. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/figure/__init__.py +0 -0
  97. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/figure/_figure.py +0 -0
  98. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/figure/_gridspec.py +0 -0
  99. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/figure/_subplots.py +0 -0
  100. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/markers.py +0 -0
  101. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot1d/__init__.py +0 -0
  102. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot1d/_plotbar.py +0 -0
  103. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot2d/__init__.py +0 -0
  104. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot2d/_layer.py +0 -0
  105. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot2d/_plotmesh.py +0 -0
  106. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot2d/_tile_backend.py +0 -0
  107. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot3d/__init__.py +0 -0
  108. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plot3d/_plot3d.py +0 -0
  109. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plotxy/__init__.py +0 -0
  110. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/plotxy/_plotxy.py +0 -0
  111. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
  112. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
  113. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
  114. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
  115. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
  116. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
  117. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
  118. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
  119. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
  120. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
  121. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
  122. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
  123. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
  124. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
  125. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/__init__.py +0 -0
  126. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
  127. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/_png_utils.py +0 -0
  128. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/bar_basic.png +0 -0
  129. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
  130. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
  131. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
  132. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
  133. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
  134. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
  135. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
  136. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
  137. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
  138. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
  139. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
  140. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_1d.png +0 -0
  141. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
  142. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
  143. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
  144. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
  145. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
  146. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
  147. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
  148. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
  149. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
  150. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
  151. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
  152. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
  153. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
  154. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
  155. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
  156. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
  157. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
  158. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
  159. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/conftest.py +0 -0
  160. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
  161. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
  162. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
  163. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
  164. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
  165. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_documentation/__init__.py +0 -0
  166. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
  167. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
  168. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
  169. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_electron/__init__.py +0 -0
  170. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
  171. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_embed/__init__.py +0 -0
  172. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
  173. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
  174. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_embed/test_export_png.py +0 -0
  175. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_examples/__init__.py +0 -0
  176. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
  177. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/__init__.py +0 -0
  178. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
  179. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
  180. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
  181. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
  182. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_edit_chrome.py +0 -0
  183. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +0 -0
  184. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
  185. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
  186. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
  187. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
  188. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
  189. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_title.py +0 -0
  190. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
  191. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
  192. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_labels/__init__.py +0 -0
  193. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
  194. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
  195. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
  196. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/__init__.py +0 -0
  197. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
  198. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
  199. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
  200. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
  201. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_inset_callout.py +0 -0
  202. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
  203. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
  204. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_markers/__init__.py +0 -0
  205. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
  206. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_markers/test_markers.py +0 -0
  207. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
  208. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
  209. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
  210. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
  211. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
  212. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
  213. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
  214. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
  215. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
  216. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
  217. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_layers.py +0 -0
  218. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_layers_playwright.py +0 -0
  219. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
  220. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
  221. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
  222. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
  223. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
  224. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
  225. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
  226. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
  227. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
  228. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +0 -0
  229. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
  230. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
  231. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
  232. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/widgets/__init__.py +0 -0
  233. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/widgets/_base.py +0 -0
  234. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/anyplotlib/widgets/_widgets3d.py +0 -0
  235. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_sg_html_scraper.py +0 -0
  236. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_static/anyplotlib.svg +0 -0
  237. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_static/custom.css +0 -0
  238. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_static/pyodide_bridge.js +0 -0
  239. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/_templates/autosummary/class.rst +0 -0
  240. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/callbacks.rst +0 -0
  241. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/figure.rst +0 -0
  242. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/figure_plots.rst +0 -0
  243. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/index.rst +0 -0
  244. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/markers.rst +0 -0
  245. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/api/widgets.rst +0 -0
  246. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/benchmarking.rst +0 -0
  247. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/dev/index.rst +0 -0
  248. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/embedding.rst +0 -0
  249. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/events.rst +0 -0
  250. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/getting_started.rst +0 -0
  251. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/docs/index.rst +0 -0
  252. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/make.bat +0 -0
  253. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/upcoming_changes/README.rst +0 -0
  254. {anyplotlib-0.4.0 → anyplotlib-0.4.2}/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.2
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:
@@ -568,13 +568,17 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
568
568
  self._state["base_width"] = int(ow)
569
569
  self._state["base_height"] = int(oh)
570
570
  self._overview_stale = False
571
- _TLOG.debug(
572
- "[TILEDBG] overview refresh: overview=%s u8[min=%d max=%d] "
573
- "display=(%s,%s) base_wh=(%d,%d) image_wh=(%s,%s)", (oh, ow),
574
- int(img_u8.min()), int(img_u8.max()),
575
- self._state.get("display_min"), self._state.get("display_max"),
576
- ow, oh, self._state.get("image_width"),
577
- self._state.get("image_height"))
571
+ # min/max are two more full passes over the overview — same guard rule
572
+ # as the FETCH diagnostic below (they are .debug() ARGUMENTS, so lazy
573
+ # formatting does not skip them).
574
+ if _TLOG.isEnabledFor(logging.DEBUG):
575
+ _TLOG.debug(
576
+ "[TILEDBG] overview refresh: overview=%s u8[min=%d max=%d] "
577
+ "display=(%s,%s) base_wh=(%d,%d) image_wh=(%s,%s)", (oh, ow),
578
+ int(img_u8.min()), int(img_u8.max()),
579
+ self._state.get("display_min"), self._state.get("display_max"),
580
+ ow, oh, self._state.get("image_width"),
581
+ self._state.get("image_height"))
578
582
  except Exception as e:
579
583
  _TLOG.warning("[TILEDBG] overview refresh FAILED: %s", e)
580
584
 
@@ -648,28 +652,47 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
648
652
  else:
649
653
  out_h, out_w = dh, max(1, int(round(dh * aspect)))
650
654
  b = self._tile_backend
651
- # DIAGNOSTIC: the raw native region straight from the backend, BEFORE
652
- # sampling — its shape + distinct-value count tells us whether the backend
653
- # actually holds native pixels. If region is 82×82 but the raw crop has
654
- # only ~100 distinct values, the backend source is a downsample (bug).
655
- _raw = np.asarray(b._a[y0:y1, x0:x1]) if hasattr(b, "_a") else None
656
655
  tile = self._tile_backend.sample(
657
656
  x0, x1, y0, y1, out_w, out_h, self._integration_method)
658
657
  if b.origin == "lower":
659
658
  tile = np.flipud(tile)
660
659
  self.set_detail(np.ascontiguousarray(tile), x0, x1, y0, y1)
661
- _bshape = b.full_shape if hasattr(b, "full_shape") else "?"
662
- _rawinfo = ("raw_crop=%s distinct=%d" % (
663
- _raw.shape, int(np.unique(_raw).size))) if _raw is not None else "raw=?"
664
- _tileinfo = "tile_distinct=%d" % int(np.unique(np.asarray(tile)).size)
660
+ # The FETCH diagnostic sorts the raw native crop and the tile
661
+ # (np.unique) 259 ms on a 4096² frame at the zoom where VIEW_OVERFETCH
662
+ # makes the crop the whole image. Those were arguments to a .debug()
663
+ # call, so lazy %-formatting did NOT save them: every pan paid for them
664
+ # at any log level. Guard the whole block, never inline it again.
665
+ if _TLOG.isEnabledFor(logging.DEBUG):
666
+ self._log_fetch_diagnostic(
667
+ b, tile, zoom, x0, x1, y0, y1, rw, rh, out_w, out_h)
668
+ except Exception as e:
669
+ _TLOG.warning("[TILEDBG] view_changed tile update FAILED: %s", e)
670
+
671
+ def _log_fetch_diagnostic(self, b, tile, zoom, x0, x1, y0, y1, rw, rh,
672
+ out_w, out_h) -> None:
673
+ """The expensive [TILEDBG] FETCH line — only ever call this behind an
674
+ ``isEnabledFor(DEBUG)`` guard.
675
+
676
+ The distinct-value counts are the point of it: if the requested region is
677
+ 82×82 but the backend's raw crop has only ~100 distinct values, the backend
678
+ source is a downsample rather than native pixels (a real bug this caught).
679
+ Both counts are full sorts of up to 16 M pixels, hence the guard."""
680
+ try:
681
+ raw = np.asarray(b._a[y0:y1, x0:x1]) if hasattr(b, "_a") else None
682
+ rawinfo = ("raw_crop=%s distinct=%d"
683
+ % (raw.shape, int(np.unique(raw).size))
684
+ ) if raw is not None else "raw=?"
685
+ arr = np.asarray(tile)
665
686
  _TLOG.debug(
666
687
  "[TILEDBG] view_changed FETCH zoom=%.2f region=[x %d:%d y %d:%d] "
667
- "(%dx%d logical) BACKEND_shape=%s %s → tile=%dx%d %s "
668
- "u8[min=%d max=%d]",
669
- zoom, x0, x1, y0, y1, rw, rh, _bshape, _rawinfo, out_w, out_h,
670
- _tileinfo, int(np.asarray(tile).min()), int(np.asarray(tile).max()))
688
+ "(%dx%d logical) BACKEND_shape=%s %s → tile=%dx%d "
689
+ "tile_distinct=%d u8[min=%d max=%d]",
690
+ zoom, x0, x1, y0, y1, rw, rh,
691
+ b.full_shape if hasattr(b, "full_shape") else "?", rawinfo,
692
+ out_w, out_h, int(np.unique(arr).size),
693
+ int(arr.min()), int(arr.max()))
671
694
  except Exception as e:
672
- _TLOG.warning("[TILEDBG] view_changed tile update FAILED: %s", e)
695
+ _TLOG.debug("[TILEDBG] FETCH diagnostic failed: %s", e)
673
696
 
674
697
  @staticmethod
675
698
  def _encode_bytes(arr: np.ndarray) -> str:
@@ -1610,16 +1633,38 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
1610
1633
  def add_rectangle_widget(self, x: float | None = None, y: float | None = None,
1611
1634
  w: float | None = None, h: float | None = None,
1612
1635
  color: str = "#00e5ff", linewidth: float = 2,
1613
- show_handles: bool = True) -> RectangleWidget:
1614
- """Add a draggable rectangle overlay."""
1636
+ show_handles: bool = True,
1637
+ max_extent=None) -> RectangleWidget:
1638
+ """Add a draggable rectangle overlay.
1639
+
1640
+ ``max_extent`` caps the rectangle's size in the widget's coordinates —
1641
+ a scalar caps both axes, a ``(max_w, max_h)`` pair caps them separately.
1642
+ The rectangle then physically stops growing at the cap while dragging
1643
+ (the dragged corner pins, the opposite corner stays put).
1644
+ """
1615
1645
  iw, ih = self._state["image_width"], self._state["image_height"]
1646
+ # The DEFAULT size honours max_extent. Without this a capped rectangle is
1647
+ # born at half the image and then snaps down to the cap on first use —
1648
+ # the ROI visibly appears huge and collapses, which looks like a bug.
1649
+ def _cap(axis_default, cap):
1650
+ return axis_default if cap is None else min(axis_default, float(cap))
1651
+
1652
+ if max_extent is None:
1653
+ cap_w = cap_h = None
1654
+ elif isinstance(max_extent, (tuple, list)):
1655
+ cap_w, cap_h = max_extent[0], max_extent[1]
1656
+ else:
1657
+ cap_w = cap_h = max_extent
1658
+ def_w = _cap(iw * 0.5, cap_w)
1659
+ def_h = _cap(ih * 0.5, cap_h)
1616
1660
  widget = RectangleWidget(lambda: None,
1617
1661
  x=float(x) if x is not None else iw * 0.25,
1618
1662
  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,
1663
+ w=float(w) if w is not None else def_w,
1664
+ h=float(h) if h is not None else def_h,
1621
1665
  color=color, linewidth=linewidth,
1622
- show_handles=show_handles)
1666
+ show_handles=show_handles,
1667
+ max_extent=max_extent)
1623
1668
  widget._push_fn = self._make_widget_push_fn(widget)
1624
1669
  self._widgets[widget.id] = widget
1625
1670
  self._push()