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.
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/CHANGELOG.rst +31 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/PKG-INFO +1 -1
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure_esm.js +46 -11
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/_plot1d.py +6 -1
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_plot2d.py +27 -5
- anyplotlib-0.4.1/anyplotlib/tests/test_interactive/test_widget_max_extent.py +371 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets1d.py +15 -2
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets2d.py +18 -2
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_root/index.html +3 -3
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_root/switcher.json +5 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/conf.py +1 -1
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/pyproject.toml +1 -1
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/ci.yml +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/docs.yml +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/prepare_release.yml +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/release.yml +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.github/workflows/tests.yml +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/.gitignore +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/AGENTS.md +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Benchmarks/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_eels_explorer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_figure_annotations.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_interactive_fft.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_interactive_fitting.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_ipf_density_map.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_ipf_explorer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_key_bindings.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_particle_picker.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_point_widget.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_threshold_explorer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_arrows.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_circles.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_ellipses.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_horizontal_lines.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_line_segments.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_points.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_polygons.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_rectangles.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_squares.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_texts.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_transform_markers.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Markers/plot_vertical_lines.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_3d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_bar.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_gpu_voxels.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_image2d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_inset.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_label_formatting.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_line_styles.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_spectra1d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_step_and_log.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_tile_backend.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/PlotTypes/plot_twinx.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/README.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_hline.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_point.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_range.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget1d_vline.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_annular.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_arrow.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_circle.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_label.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Examples/Widgets/plot_widget3d_plane.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/LICENSE +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/Makefile +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/README.md +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/FIGURE_ESM.md +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_base_plot.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_binary_frame.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_electron.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_repr_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/_axes.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/axes/_inset_axes.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/callbacks.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/conftest.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/embed.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_figure.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_gridspec.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/figure/_subplots.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/markers.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot1d/_plotbar.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_layer.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_plotmesh.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot2d/_tile_backend.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot3d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plot3d/_plot3d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plotxy/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/plotxy/_plotxy.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/_png_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/bar_basic.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_1d.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/conftest.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_electron/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_embed/test_export_png.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_examples/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_edit_chrome.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_title.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_inset_callout.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_markers/test_markers.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_layers.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_layers_playwright.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/__init__.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_base.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/anyplotlib/widgets/_widgets3d.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_sg_html_scraper.py +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/anyplotlib.svg +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/custom.css +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_static/pyodide_bridge.js +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/_templates/autosummary/class.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/callbacks.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/figure.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/figure_plots.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/index.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/markers.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/api/widgets.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/benchmarking.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/dev/index.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/embedding.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/events.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/getting_started.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/docs/index.rst +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/make.bat +0 -0
- {anyplotlib-0.4.0 → anyplotlib-0.4.1}/upcoming_changes/README.rst +0 -0
- {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.
|
|
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
|
-
|
|
7311
|
-
|
|
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
|
-
|
|
7333
|
-
|
|
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
|
|
1614
|
-
|
|
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
|
|
1620
|
-
h=float(h) if h is not None else
|
|
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.
|
|
8
|
-
<link rel="canonical" href="v0.4.
|
|
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.
|
|
12
|
+
Redirecting to <a href="v0.4.1/">v0.4.1 documentation</a>…
|
|
13
13
|
</p>
|
|
14
14
|
</body>
|
|
15
15
|
</html>
|