anyplotlib 0.7.2__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/AGENTS.md +1 -1
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/CHANGELOG.rst +63 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/PKG-INFO +2 -2
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/FIGURE_ESM.md +30 -29
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/figure_esm.js +34 -6
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot2d/_plot2d.py +123 -9
- anyplotlib-0.8.0/anyplotlib/tests/test_interactive/test_circle_lock_center.py +160 -0
- anyplotlib-0.8.0/anyplotlib/tests/test_plot2d/test_display_window.py +135 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +205 -7
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/widgets/_widgets2d.py +18 -2
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_root/index.html +3 -3
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_root/switcher.json +10 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/conf.py +1 -1
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/pyproject.toml +1 -1
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.github/workflows/ci.yml +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.github/workflows/docs.yml +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.github/workflows/prepare_release.yml +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.github/workflows/release.yml +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.github/workflows/tests.yml +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/.gitignore +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Benchmarks/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_eels_explorer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_figure_annotations.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_interactive_fft.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_interactive_fitting.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_ipf_density_map.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_ipf_explorer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_key_bindings.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_particle_picker.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_point_widget.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_star_globe_explorer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_threshold_explorer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_arrows.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_circles.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_ellipses.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_horizontal_lines.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_line_segments.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_points.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_polygons.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_rectangles.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_squares.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_texts.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_transform_markers.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Markers/plot_vertical_lines.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_3d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_3d_texture.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_bar.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_gpu_voxels.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_image2d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_inset.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_key_overlays.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_label_formatting.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_line_styles.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_spectra1d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_step_and_log.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_tile_backend.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/PlotTypes/plot_twinx.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget1d_hline.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget1d_point.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget1d_range.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget1d_vline.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_annular.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_arrow.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_circle.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_label.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Examples/Widgets/plot_widget3d_plane.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/LICENSE +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/Makefile +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/README.md +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/_base_plot.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/_binary_frame.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/_electron.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/_repr_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/axes/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/axes/_axes.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/axes/_inset_axes.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/callbacks.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/conftest.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/embed.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/figure/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/figure/_figure.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/figure/_gridspec.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/figure/_subplots.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/keys.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/markers.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot1d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot1d/_plot1d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot1d/_plotbar.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot2d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot2d/_layer.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot2d/_plotmesh.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot2d/_tile_backend.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot3d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plot3d/_plot3d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plotxy/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/plotxy/_plotxy.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/_png_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/bar_basic.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_1d.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/conftest.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_documentation/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_electron/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/test_export_pixel_tokens.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/test_export_png.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_embed/test_export_widget_sync.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_examples/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_edit_chrome.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_orbit_direction.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_pointer_down_1d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_range_orientation_snap.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_star_globe_linking.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_title.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_widget_brush.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_widget_max_extent.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_widget_set_notify.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_interactive/test_widgets_line_2d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_keys/test_keys.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_labels/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_labels/test_ylabel_clearance.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_coord_conversion.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_inset_callout.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_markers/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_markers/test_markers.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_markers/test_per_marker_colors.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_markers/test_size_units.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot1d/test_line_none_rendering.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_colorbar_gap_scalebar.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_layers.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_layers_playwright.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_tile_diagnostics.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_gpu_depth.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_texture.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/widgets/__init__.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/widgets/_base.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/widgets/_widgets1d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/anyplotlib/widgets/_widgets3d.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_sg_html_scraper.py +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_static/anyplotlib.svg +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_static/custom.css +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_static/pyodide_bridge.js +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/_templates/autosummary/class.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/callbacks.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/figure.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/figure_plots.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/index.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/keys.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/markers.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/api/widgets.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/benchmarking.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/dev/index.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/embedding.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/events.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/getting_started.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/docs/index.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/make.bat +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/upcoming_changes/.gitkeep +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/upcoming_changes/README.rst +0 -0
- {anyplotlib-0.7.2 → anyplotlib-0.8.0}/uv.lock +0 -0
|
@@ -115,7 +115,7 @@ grep -nE '^\s*(function|const|let) [A-Za-z_]' anyplotlib/figure_esm.js
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
and reconcile against the two numbered tables (the section map near the top and
|
|
118
|
-
the 2-D function table). Both were last verified at
|
|
118
|
+
the 2-D function table). Both were last verified at 10,119 lines.
|
|
119
119
|
|
|
120
120
|
Changelog entries: add a fragment file to `upcoming_changes/` (e.g.
|
|
121
121
|
`123.new_feature.rst`) — towncrier assembles `CHANGELOG.rst` at release time.
|
|
@@ -10,6 +10,69 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by
|
|
|
10
10
|
|
|
11
11
|
.. towncrier release notes start
|
|
12
12
|
|
|
13
|
+
0.8.0 (2026-08-25)
|
|
14
|
+
==================
|
|
15
|
+
|
|
16
|
+
API and Behaviour Changes
|
|
17
|
+
-------------------------
|
|
18
|
+
|
|
19
|
+
- In tile mode, ``set_data``/``update_tile_source`` now re-derive the quantisation band
|
|
20
|
+
``raw_min``/``raw_max`` from the incoming frame when the current band is unset or
|
|
21
|
+
degenerate (it previously stayed as first derived, even from a flat placeholder). A
|
|
22
|
+
band that is already valid is still never re-derived, so a contrast change keeps
|
|
23
|
+
re-windowing in the LUT with no pixel re-encode. One visible consequence: a tiled plot
|
|
24
|
+
born on a placeholder now quantises subsequent frames over the frame's own range
|
|
25
|
+
rather than the display window, matching what ``imshow`` of a large frame has always
|
|
26
|
+
done — so its wire bytes can differ by a rounding step from the equivalent untiled
|
|
27
|
+
plot, while the displayed image is unchanged. (`#60 <https://github.com/CSSFrancis/anyplotlib/pull/60>`_)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
New Features
|
|
31
|
+
------------
|
|
32
|
+
|
|
33
|
+
- Added :meth:`~anyplotlib.plot2d.Plot2D.set_tile_band` to pin the fixed quantisation
|
|
34
|
+
band tile bytes are encoded over, for a consumer that already knows the honest range
|
|
35
|
+
(a camera's bit depth, a detector's saturation point) or whose source starts flat and
|
|
36
|
+
so has no range to auto-derive. It re-samples the overview and any active detail tile
|
|
37
|
+
over the new band in a single push, replacing the practice of reaching into
|
|
38
|
+
``plot._state["raw_min"/"raw_max"]`` and calling ``update_tile_source()`` by hand. (`#60 <https://github.com/CSSFrancis/anyplotlib/pull/60>`_)
|
|
39
|
+
- Added :meth:`~anyplotlib.plot2d.Plot2D.set_display_window`, which moves the
|
|
40
|
+
contrast window without re-quantising the pixels — the non-destructive
|
|
41
|
+
counterpart to :meth:`~anyplotlib.plot2d.Plot2D.set_clim`, and what lets a
|
|
42
|
+
saved page be re-windowed with no Python behind it. (`#61 <https://github.com/CSSFrancis/anyplotlib/pull/61>`_)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Bug Fixes
|
|
46
|
+
---------
|
|
47
|
+
|
|
48
|
+
- Fixed a tiled 2-D image rendering solid black when tile mode was entered on a flat
|
|
49
|
+
placeholder frame (e.g. ``imshow`` of zeros before real data exists). The fixed
|
|
50
|
+
quantisation band ``raw_min``/``raw_max`` was derived from that placeholder — a
|
|
51
|
+
degenerate ``(0, 0)`` — and no later ``set_data`` re-derived it, leaving the two ends
|
|
52
|
+
of the protocol disagreeing about what it meant: the Python encoder treats a
|
|
53
|
+
degenerate band as unset and quantises over the display window, while the renderer
|
|
54
|
+
honoured ``(0, 0)`` and mapped every code below the display floor. The panel rendered
|
|
55
|
+
black on the WebGPU and Canvas2D paths alike, beside perfectly healthy stats and
|
|
56
|
+
histograms, with no warning. ``set_data`` and ``update_tile_source`` now re-derive a
|
|
57
|
+
degenerate band from the incoming frame, and the renderer falls back to the display
|
|
58
|
+
window for a degenerate band in tile mode — in the image LUT and in the colorbar
|
|
59
|
+
tick placement, which read the band the same way. (`#60 <https://github.com/CSSFrancis/anyplotlib/pull/60>`_)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
0.7.3 (2026-08-13)
|
|
63
|
+
==================
|
|
64
|
+
|
|
65
|
+
New Features
|
|
66
|
+
------------
|
|
67
|
+
|
|
68
|
+
- ``Plot2D.add_circle_widget`` gains ``lock_center``: the centre is pinned and
|
|
69
|
+
only the radius is draggable. A grab on the ring body is refused at hit-test
|
|
70
|
+
time and falls through to the plot's own pan, so the hover cursor never
|
|
71
|
+
promises a move and the centre cannot drift. Use it when the centre is fixed by
|
|
72
|
+
the data — a ring on a power spectrum is centred on the DC term, and one nudged
|
|
73
|
+
off-centre silently corrupts every radius measured from it.
|
|
74
|
+
|
|
75
|
+
|
|
13
76
|
0.7.2 (2026-08-03)
|
|
14
77
|
==================
|
|
15
78
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: anyplotlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A plotting library using python, javascript and anywidget for performant in browser plotting.
|
|
5
5
|
Project-URL: Homepage, https://cssfrancis.github.io/anyplotlib/
|
|
6
6
|
Project-URL: Repository, https://github.com/CSSFrancis/anyplotlib
|
|
@@ -59,20 +59,20 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
|
|
|
59
59
|
| `_createInsetDOM` / `_applyAllInsetStates` | 1129 / 1512 |
|
|
60
60
|
| `_resizePanelDOM` | 2225 |
|
|
61
61
|
| **2D drawing**: `_imgFitRect` | 2384 |
|
|
62
|
-
| `draw2d` |
|
|
63
|
-
| `drawScaleBar2d` / `drawColorbar2d` |
|
|
64
|
-
| **Floating keys**: `_keyEnsure` / `_keyRect` / `drawKeys` |
|
|
65
|
-
| `_drawAxes2d` (ticks, labels, title) |
|
|
66
|
-
| `drawOverlay2d` / `drawMarkers2d` |
|
|
67
|
-
| **Image layers**: `_layerBytes` / `_layerBitmap` / `_drawLayers2d` |
|
|
62
|
+
| `draw2d` | 2713 |
|
|
63
|
+
| `drawScaleBar2d` / `drawColorbar2d` | 2908 / 3188 |
|
|
64
|
+
| **Floating keys**: `_keyEnsure` / `_keyRect` / `drawKeys` | 3007 / 3030 / 3043 |
|
|
65
|
+
| `_drawAxes2d` (ticks, labels, title) | 3242 |
|
|
66
|
+
| `drawOverlay2d` / `drawMarkers2d` | 3395 / 3559 |
|
|
67
|
+
| **Image layers**: `_layerBytes` / `_layerBitmap` / `_drawLayers2d` | 2533 / 2557 / 2618 |
|
|
68
68
|
| Binary-bytes splice: `_spliceBinaryBytes` / `_registerBinaryPixelListeners` | 730 / 761 |
|
|
69
|
-
| **3D drawing**: `draw3d` |
|
|
70
|
-
| Event emission `_emitEvent` |
|
|
71
|
-
| 3D event handlers `_attachEvents3d` |
|
|
72
|
-
| **1D drawing**: `draw1d` |
|
|
73
|
-
| `_drawLine` (1D series + markers) |
|
|
74
|
-
| `drawOverlay1d` / `drawMarkers1d` |
|
|
75
|
-
| Marker hit-test `_markerHitTest2d` |
|
|
69
|
+
| **3D drawing**: `draw3d` | 5298 |
|
|
70
|
+
| Event emission `_emitEvent` | 6135 |
|
|
71
|
+
| 3D event handlers `_attachEvents3d` | 6187 |
|
|
72
|
+
| **1D drawing**: `draw1d` | 6408 |
|
|
73
|
+
| `_drawLine` (1D series + markers) | 6561 |
|
|
74
|
+
| `drawOverlay1d` / `drawMarkers1d` | 6854 / 6938 |
|
|
75
|
+
| Marker hit-test `_markerHitTest2d` | 7206 |
|
|
76
76
|
|
|
77
77
|
> **`raster` marker (1D/PlotXY)** — `drawMarkers1d` has a `type==='raster'`
|
|
78
78
|
> branch that blits a single RGBA image across data-coord `extent` (the fast
|
|
@@ -81,16 +81,16 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
|
|
|
81
81
|
> redraws never re-transmit them; the decoded `OffscreenCanvas` is cached on
|
|
82
82
|
> the marker set (`ms._rasterBmp`/`_rasterKey`). The shared `clip_path` block
|
|
83
83
|
> clips it to a curved sector.
|
|
84
|
-
| Panel event dispatch `_attachPanelEvents` |
|
|
85
|
-
| 2D events `_attachEvents2d` |
|
|
86
|
-
| 1D events `_attachEvents1d` |
|
|
87
|
-
| 2D widget drag `_ovHitTest2d` / `_doDrag2d` |
|
|
88
|
-
| **Brush strokes**: `_brushLiveBegin` / `_brushCommit` / `_brushErase` / `_brushPaintAt` |
|
|
89
|
-
| 1D widget drag `_canvasXToFrac1d` … / snapping `_snapVal` |
|
|
90
|
-
| Shared-axis propagation `_getShareGroups` |
|
|
91
|
-
| Figure resize `_applyFigResizeDOM` |
|
|
92
|
-
| **Bar chart**: `_barGeom` / `drawBar` / `_attachEventsBar` |
|
|
93
|
-
| Generic redraw `_redrawPanel` |
|
|
84
|
+
| Panel event dispatch `_attachPanelEvents` | 7463 |
|
|
85
|
+
| 2D events `_attachEvents2d` | 7505 |
|
|
86
|
+
| 1D events `_attachEvents1d` | 7889 |
|
|
87
|
+
| 2D widget drag `_ovHitTest2d` / `_doDrag2d` | 8161 / 8440 |
|
|
88
|
+
| **Brush strokes**: `_brushLiveBegin` / `_brushCommit` / `_brushErase` / `_brushPaintAt` | 8353 / 8367 / 8396 / 8431 |
|
|
89
|
+
| 1D widget drag `_canvasXToFrac1d` … / snapping `_snapVal` | 8565 / 8638 |
|
|
90
|
+
| Shared-axis propagation `_getShareGroups` | 8709 |
|
|
91
|
+
| Figure resize `_applyFigResizeDOM` | 8773 |
|
|
92
|
+
| **Bar chart**: `_barGeom` / `drawBar` / `_attachEventsBar` | 8964 / 9027 / 9403 |
|
|
93
|
+
| Generic redraw `_redrawPanel` | 9593 |
|
|
94
94
|
|
|
95
95
|
> **`brush` widget (2-D)** — the one widget whose drag is *modal*, and the one
|
|
96
96
|
> that must NOT write the model per tick. `_ovHitTest2d` takes an extra `mods`
|
|
@@ -269,12 +269,13 @@ st.colorbar_label_size (label font sizes; optional)
|
|
|
269
269
|
|
|
270
270
|
| Function | Line | Purpose |
|
|
271
271
|
|----------|------|---------|
|
|
272
|
-
| **`_imgFitRect(iw,ih,cw,ch)`** | **
|
|
273
|
-
| `draw2d(p)` |
|
|
274
|
-
| `drawScaleBar2d(p)` |
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
272
|
+
| **`_imgFitRect(iw,ih,cw,ch)`** | **2384** | Largest rect of aspect `iw:ih` centred in `cw×ch`; all 2-D coordinate functions derive from this |
|
|
273
|
+
| `draw2d(p)` | 2713 | Main render: `_resizePanelDOM` → decode → LUT → ImageBitmap → blit; then mask, axes, scale bar, colorbar, overlay, markers |
|
|
274
|
+
| `drawScaleBar2d(p)` | 2908 | Physical scale bar |
|
|
275
|
+
| `_rawBand(st)` / `_buildLut32(st)` | 2433 / 2440 | The quantisation band the u8 bytes were encoded over, then the 256-entry LUT built from it. `_rawBand` mirrors Python `_tile_quant_clim`: a DEGENERATE band (`raw_max <= raw_min`) is UNSET and falls back to `display_min/max`. Both render paths and the colorbar go through it — honouring a `(0, 0)` band paints solid black |
|
|
276
|
+
| `drawColorbar2d(p)` | 3188 | Gradient strip + min/max marks (band-relative, via `_rawBand`) + rotated label centred in the `_cbWidth` gutter |
|
|
277
|
+
| `_drawAxes2d(p)` | 3242 | Ticks (edge labels nudged inward both axes), axis labels + title via `_drawTex` |
|
|
278
|
+
| `drawOverlay2d(p)` / `drawMarkers2d(p)` | 3395 / 3559 | Widgets / marker groups |
|
|
278
279
|
|
|
279
280
|
Zoom model: at `zoom=1` the whole image fills the fit-rect; at `zoom=Z>1` a
|
|
280
281
|
`1/Z` region fills it. `_imgToCanvas2d` / `_canvasToImg2d` must stay exact
|
|
@@ -2415,10 +2415,31 @@ function render({ model, el, onResize }) {
|
|
|
2415
2415
|
};
|
|
2416
2416
|
}
|
|
2417
2417
|
|
|
2418
|
+
// The quantisation band the uint8 image bytes were encoded over, as [lo, hi].
|
|
2419
|
+
// A DEGENERATE band (raw_max <= raw_min) means DIFFERENT things on the two
|
|
2420
|
+
// encoder paths, and the LUT has to read it the way the bytes were written:
|
|
2421
|
+
//
|
|
2422
|
+
// plain (_normalize_image): a constant frame quantises to ALL-ZERO bytes and
|
|
2423
|
+
// reports (c, c) — the band NAMES the single value code 0 stands for, and
|
|
2424
|
+
// hMin + raw/255 * (range||1) reconstructs it. Keep honouring it.
|
|
2425
|
+
// tile (_tile_quant_clim): a degenerate band is treated as UNSET and the
|
|
2426
|
+
// bytes are quantised over the DISPLAY window instead — so they are already
|
|
2427
|
+
// display-mapped and the LUT must be the identity window over them.
|
|
2428
|
+
//
|
|
2429
|
+
// Reading a tiled degenerate band the plain way is GH #60: a plot that entered
|
|
2430
|
+
// tile mode on a flat placeholder carries the band (0, 0), every code maps to
|
|
2431
|
+
// hMin + raw/255 * 1 in [0, 1], lands below any sane display floor, and the
|
|
2432
|
+
// panel renders solid black beside perfectly healthy stats.
|
|
2433
|
+
function _rawBand(st) {
|
|
2434
|
+
const lo=st.raw_min, hi=st.raw_max;
|
|
2435
|
+
if(lo==null||hi==null) return [st.display_min, st.display_max];
|
|
2436
|
+
if(hi>lo) return [lo,hi];
|
|
2437
|
+
return st.tile_enabled ? [st.display_min, st.display_max] : [lo,hi];
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2418
2440
|
function _buildLut32(st) {
|
|
2419
2441
|
const dMin=st.display_min, dMax=st.display_max;
|
|
2420
|
-
const hMin=st
|
|
2421
|
-
const hMax=st.raw_max!=null?st.raw_max:dMax;
|
|
2442
|
+
const [hMin,hMax]=_rawBand(st);
|
|
2422
2443
|
const mode=st.scale_mode||'linear';
|
|
2423
2444
|
const range=hMax-hMin||1;
|
|
2424
2445
|
const cmapData=st.colormap_data||[];
|
|
@@ -3198,8 +3219,7 @@ function render({ model, el, onResize }) {
|
|
|
3198
3219
|
|
|
3199
3220
|
// display_min / display_max tick marks
|
|
3200
3221
|
const dMin=st.display_min, dMax=st.display_max;
|
|
3201
|
-
const hMin=st
|
|
3202
|
-
const hMax=st.raw_max!=null?st.raw_max:dMax;
|
|
3222
|
+
const [hMin,hMax]=_rawBand(st);
|
|
3203
3223
|
const vRange=(hMax-hMin)||1;
|
|
3204
3224
|
function _vToY(v){return imgH-1-((v-hMin)/vRange)*(imgH-1);}
|
|
3205
3225
|
ctx.strokeStyle='rgba(255,255,255,0.85)'; ctx.lineWidth=1.5;
|
|
@@ -8182,7 +8202,13 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
8182
8202
|
// outer radius handle
|
|
8183
8203
|
if (Math.hypot(mx - (ccx + cr), my - ccy) <= HR)
|
|
8184
8204
|
return { idx:i, mode:'resize_r', snapW:{...w}, startMX:mx, startMY:my };
|
|
8185
|
-
// body (inside ring ± tolerance)
|
|
8205
|
+
// body (inside ring ± tolerance). `lock_center` refuses the grab
|
|
8206
|
+
// outright rather than letting the drag run and correcting afterwards:
|
|
8207
|
+
// a centre snapped back on release still tracks the cursor for the
|
|
8208
|
+
// whole drag, which reads as a broken lock. Refusing here also leaves
|
|
8209
|
+
// the hover cursor at 'default' and lets the drag fall through to the
|
|
8210
|
+
// plot's own pan, so the ring is simply not a handle any more.
|
|
8211
|
+
if (w.lock_center) continue;
|
|
8186
8212
|
if (Math.abs(Math.hypot(mx-ccx, my-ccy) - cr) <= Math.max(HR, cr*0.18) ||
|
|
8187
8213
|
Math.hypot(mx-ccx, my-ccy) <= HR)
|
|
8188
8214
|
return { idx:i, mode:'move', snapW:{...w}, startMX:mx, startMY:my };
|
|
@@ -8441,7 +8467,9 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
8441
8467
|
}
|
|
8442
8468
|
|
|
8443
8469
|
if (w.type === 'circle') {
|
|
8444
|
-
|
|
8470
|
+
// A drag already in flight when `lock_center` is turned on must not keep
|
|
8471
|
+
// translating the widget: the hit-test gate only runs at grab time.
|
|
8472
|
+
if (d.mode === 'move' && !w.lock_center) {
|
|
8445
8473
|
w.cx = s.cx + dix; w.cy = s.cy + diy;
|
|
8446
8474
|
} else if (d.mode === 'resize_r') {
|
|
8447
8475
|
// distance from centre in image-px
|
|
@@ -507,11 +507,7 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
507
507
|
# (raw_min/raw_max fixed, display_min/display_max move) with NO pixel re-encode
|
|
508
508
|
# or re-transfer — set_clim just moves the display window (see set_clim). Set
|
|
509
509
|
# once from the full-res range; keep any existing band on a re-enable.
|
|
510
|
-
|
|
511
|
-
or not (self._state.get("raw_max", 0) > self._state.get("raw_min", 0))):
|
|
512
|
-
rng = self._backend_display_range(self._tile_backend)
|
|
513
|
-
if rng is not None:
|
|
514
|
-
self._state["raw_min"], self._state["raw_max"] = rng
|
|
510
|
+
self._ensure_tile_band()
|
|
515
511
|
_was_on = self._tile_on
|
|
516
512
|
if not self._tile_on:
|
|
517
513
|
self._tile_on = True
|
|
@@ -561,6 +557,13 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
561
557
|
setter = getattr(self._tile_backend, "set_array", None)
|
|
562
558
|
if setter is not None:
|
|
563
559
|
setter(array)
|
|
560
|
+
# The quantisation band is derived ONCE (enable_tile) and then held fixed, so
|
|
561
|
+
# a contrast change re-windows in the LUT with no pixel re-encode. But if it
|
|
562
|
+
# was never VALIDLY derived — tile mode started on a flat placeholder, so the
|
|
563
|
+
# probe refused and the band stayed degenerate — the new data in the backend
|
|
564
|
+
# is the first chance to get it right. Must run BEFORE the overview/detail are
|
|
565
|
+
# re-sampled below so they quantise over the corrected band in the SAME push.
|
|
566
|
+
self._ensure_tile_band()
|
|
564
567
|
reg = self._state.get("detail_region") or []
|
|
565
568
|
has_detail = len(reg) == 4
|
|
566
569
|
# Refresh the overview base ONLY when no detail tile is shown (zoomed out) —
|
|
@@ -622,6 +625,70 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
622
625
|
except Exception as e:
|
|
623
626
|
_TLOG.warning("[TILEDBG] overview refresh FAILED: %s", e)
|
|
624
627
|
|
|
628
|
+
def _ensure_tile_band(self) -> bool:
|
|
629
|
+
"""Derive the fixed quantisation band (raw_min/raw_max) from the CURRENT tile
|
|
630
|
+
backend when it is unset or DEGENERATE, by the same rule _tile_quant_clim
|
|
631
|
+
uses to read it back. Returns True iff the band was (re-)derived.
|
|
632
|
+
|
|
633
|
+
The degenerate case is the one that bites: a plot that entered tile mode on a
|
|
634
|
+
flat placeholder (e.g. zeros before real data exists) probes a range of
|
|
635
|
+
(0, 0) — _backend_display_range refuses it, so the band keeps the
|
|
636
|
+
constructor's raw min/max, which for zeros is also (0, 0). Every later
|
|
637
|
+
set_data then re-sampled the overview against a band the encoder treated as
|
|
638
|
+
unset (quantising over the display window) while the frontend LUT honoured
|
|
639
|
+
(0, 0) literally and painted the panel solid black. Calling this on each data
|
|
640
|
+
swap re-derives the band as soon as a frame with real range arrives, so the
|
|
641
|
+
two ends of the protocol agree on what the bytes mean.
|
|
642
|
+
"""
|
|
643
|
+
lo, hi = self._state.get("raw_min"), self._state.get("raw_max")
|
|
644
|
+
if lo is not None and hi is not None and hi > lo:
|
|
645
|
+
return False # a valid band is kept, never re-derived
|
|
646
|
+
if self._tile_backend is None:
|
|
647
|
+
return False
|
|
648
|
+
rng = self._backend_display_range(self._tile_backend)
|
|
649
|
+
if rng is None:
|
|
650
|
+
# Still flat (an all-constant frame): leave the band unset/degenerate —
|
|
651
|
+
# _tile_quant_clim and the JS LUT both fall back to the display window.
|
|
652
|
+
return False
|
|
653
|
+
self._state["raw_min"], self._state["raw_max"] = rng
|
|
654
|
+
_TLOG.debug("[TILEDBG] tile band (re-)derived from backend → raw=(%s,%s)",
|
|
655
|
+
*rng)
|
|
656
|
+
return True
|
|
657
|
+
|
|
658
|
+
def set_tile_band(self, vmin: float, vmax: float) -> None:
|
|
659
|
+
"""PIN the fixed quantisation band the tile bytes are encoded over.
|
|
660
|
+
|
|
661
|
+
Tile mode normally derives this band once, from the full-res data (see
|
|
662
|
+
``enable_tile``), and then holds it: a contrast change re-windows in the LUT
|
|
663
|
+
with no pixel re-encode or re-transfer. A live consumer that already KNOWS the
|
|
664
|
+
honest range — a camera's bit depth, a detector's saturation point — can pin
|
|
665
|
+
it here instead of letting the first frame decide, so the contrast doesn't
|
|
666
|
+
shift when a later frame happens to contain a brighter pixel.
|
|
667
|
+
|
|
668
|
+
Pinning also survives what auto-derivation cannot: a source whose first frames
|
|
669
|
+
are flat (a placeholder, a closed shutter) has no range to derive from, and
|
|
670
|
+
the band stays unset until a frame with real content arrives.
|
|
671
|
+
|
|
672
|
+
Re-samples the overview (and any active detail tile) over the new band in a
|
|
673
|
+
single push, so the pixels and the band the renderer reads them with never
|
|
674
|
+
disagree. Raises if the plot is not in tile mode or the range is degenerate."""
|
|
675
|
+
if not self._tile_on:
|
|
676
|
+
raise RuntimeError(
|
|
677
|
+
"set_tile_band requires tile mode — the band is the quantisation "
|
|
678
|
+
"range for the TILE bytes. Call enable_tile() first (or use "
|
|
679
|
+
"set_clim() to change the display window on a plain plot).")
|
|
680
|
+
lo, hi = float(vmin), float(vmax)
|
|
681
|
+
if not (hi > lo):
|
|
682
|
+
raise ValueError(
|
|
683
|
+
f"set_tile_band requires vmax > vmin, got ({lo}, {hi}). A degenerate "
|
|
684
|
+
f"band is what 'unset' means here — both the encoder and the "
|
|
685
|
+
f"renderer then fall back to the display window.")
|
|
686
|
+
self._state["raw_min"], self._state["raw_max"] = lo, hi
|
|
687
|
+
# Re-encode over the new band. update_tile_source() with no array re-samples
|
|
688
|
+
# from the backend in place (keeping zoom/center and the detail region) and
|
|
689
|
+
# pushes — so the band and the bytes it describes ship together.
|
|
690
|
+
self.update_tile_source()
|
|
691
|
+
|
|
625
692
|
def _tile_quant_clim(self):
|
|
626
693
|
"""The FIXED quantisation band (raw_min/raw_max) the tile bytes are encoded
|
|
627
694
|
over, so a contrast change re-windows in the LUT without re-encoding pixels.
|
|
@@ -1470,7 +1537,11 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
1470
1537
|
raw frame, not merely re-windowing the existing codes (which are saturated
|
|
1471
1538
|
outside the previous band and so couldn't widen past it). For an RGB frame
|
|
1472
1539
|
(no scalar quantisation) or when no raw frame is cached, fall back to a pure
|
|
1473
|
-
display-window update.
|
|
1540
|
+
display-window update.
|
|
1541
|
+
|
|
1542
|
+
See :meth:`set_display_window` for the non-destructive counterpart — the
|
|
1543
|
+
one to reach for when the pixels must not move (a tiled plot, or a
|
|
1544
|
+
serialised figure being re-windowed with no Python behind it)."""
|
|
1474
1545
|
new_min = float(vmin) if vmin is not None else self._state.get("display_min")
|
|
1475
1546
|
new_max = float(vmax) if vmax is not None else self._state.get("display_max")
|
|
1476
1547
|
|
|
@@ -1512,6 +1583,40 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
1512
1583
|
self._state["display_max"] = float(vmax)
|
|
1513
1584
|
self._push()
|
|
1514
1585
|
|
|
1586
|
+
def set_display_window(self, vmin=None, vmax=None) -> None:
|
|
1587
|
+
"""Move the display window WITHOUT re-quantising the pixels.
|
|
1588
|
+
|
|
1589
|
+
The non-destructive counterpart to :meth:`set_clim`. Both change the
|
|
1590
|
+
contrast; they differ in what they do to the data behind it:
|
|
1591
|
+
|
|
1592
|
+
``set_clim``
|
|
1593
|
+
re-encodes the cached raw frame over the new range, so the codes
|
|
1594
|
+
always span exactly the visible band — maximum precision for what is
|
|
1595
|
+
on screen, but the pixels are re-encoded and re-sent, and the old
|
|
1596
|
+
band is gone.
|
|
1597
|
+
``set_display_window``
|
|
1598
|
+
leaves the codes and their ``raw_min``/``raw_max`` band alone and
|
|
1599
|
+
moves only the window the LUT maps through it. Nothing is re-encoded
|
|
1600
|
+
and nothing travels but two floats.
|
|
1601
|
+
|
|
1602
|
+
Use it when the pixels must stay put: a tiled plot, where re-quantising
|
|
1603
|
+
would re-encode the full-res frame on every drag tick (``set_clim``
|
|
1604
|
+
already routes there internally), or a figure that has been serialised
|
|
1605
|
+
and is being re-windowed with no Python behind it — which is how a saved
|
|
1606
|
+
page gets a working contrast control at all.
|
|
1607
|
+
|
|
1608
|
+
The trade is precision. Quantisation spans ``[raw_min, raw_max]``, so a
|
|
1609
|
+
window much narrower than that band resolves in coarse steps, and one
|
|
1610
|
+
WIDER than it recovers nothing: values outside the band were saturated
|
|
1611
|
+
to 0/255 when the frame was encoded. Quantise over the range you want to
|
|
1612
|
+
be able to reach.
|
|
1613
|
+
"""
|
|
1614
|
+
if vmin is not None:
|
|
1615
|
+
self._state["display_min"] = float(vmin)
|
|
1616
|
+
if vmax is not None:
|
|
1617
|
+
self._state["display_max"] = float(vmax)
|
|
1618
|
+
self._push()
|
|
1619
|
+
|
|
1515
1620
|
def set_detail(self, tile=None, x0=None, x1=None, y0=None, y1=None) -> None:
|
|
1516
1621
|
"""Upload a HIGH-RES detail tile covering the LOGICAL image-pixel rectangle
|
|
1517
1622
|
``[x0:x1, y0:y1]`` of the base image (in the SAME orientation as the frame
|
|
@@ -1731,15 +1836,24 @@ class Plot2D(_BasePlot, _PanelMixin, _MarkerMixin):
|
|
|
1731
1836
|
def add_circle_widget(self, cx: float | None = None, cy: float | None = None,
|
|
1732
1837
|
r: float | None = None, color: str = "#00e5ff",
|
|
1733
1838
|
linewidth: float = 2,
|
|
1734
|
-
show_handles: bool = True
|
|
1735
|
-
|
|
1839
|
+
show_handles: bool = True,
|
|
1840
|
+
lock_center: bool = False) -> CircleWidget:
|
|
1841
|
+
"""Add a draggable circle overlay.
|
|
1842
|
+
|
|
1843
|
+
``lock_center`` pins the centre and leaves only the radius draggable —
|
|
1844
|
+
a grab on the ring body is refused and pans the plot instead. Use it
|
|
1845
|
+
when the centre is fixed by the data (a ring on a power spectrum is
|
|
1846
|
+
centred on the DC term), so it cannot be nudged off and silently
|
|
1847
|
+
corrupt every radius measured from it.
|
|
1848
|
+
"""
|
|
1736
1849
|
iw, ih = self._state["image_width"], self._state["image_height"]
|
|
1737
1850
|
widget = CircleWidget(lambda: None,
|
|
1738
1851
|
cx=float(cx) if cx is not None else iw / 2,
|
|
1739
1852
|
cy=float(cy) if cy is not None else ih / 2,
|
|
1740
1853
|
r=float(r) if r is not None else iw * 0.1,
|
|
1741
1854
|
color=color, linewidth=linewidth,
|
|
1742
|
-
show_handles=show_handles
|
|
1855
|
+
show_handles=show_handles,
|
|
1856
|
+
lock_center=lock_center)
|
|
1743
1857
|
widget._push_fn = self._make_widget_push_fn(widget)
|
|
1744
1858
|
self._widgets[widget.id] = widget
|
|
1745
1859
|
self._push()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests/test_interactive/test_circle_lock_center.py
|
|
3
|
+
=================================================
|
|
4
|
+
|
|
5
|
+
``lock_center`` — a circle whose centre is fixed by the data.
|
|
6
|
+
|
|
7
|
+
Why it exists: a ring measured on a power spectrum is centred on the DC term.
|
|
8
|
+
Its centre is not a free parameter, so a draggable one is a control that can
|
|
9
|
+
only ever be wrong — and a ring nudged off-centre silently corrupts every
|
|
10
|
+
radius measured from it.
|
|
11
|
+
|
|
12
|
+
Why it is enforced in the HIT-TEST and not afterwards. The obvious
|
|
13
|
+
implementation is a Python handler that watches the widget and snaps the centre
|
|
14
|
+
back when the drag settles. It cannot work: ``_doDrag2d`` recomputes the
|
|
15
|
+
position every frame from its own grab-time snapshot, so a pushed-back centre
|
|
16
|
+
is overwritten on the next mousemove. The ring tracks the cursor for the whole
|
|
17
|
+
drag and jumps back on release, which reads as a broken lock rather than a
|
|
18
|
+
locked ring. Refusing the grab is the only place the constraint holds.
|
|
19
|
+
|
|
20
|
+
Coordinate system mirrors figure_esm.js — see ``_img_to_page``.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
import anyplotlib as apl
|
|
28
|
+
from anyplotlib.widgets import CircleWidget
|
|
29
|
+
from anyplotlib.tests.test_interactive._event_test_utils import (
|
|
30
|
+
_collect_events, _get_events,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
FIG_W, FIG_H = 400, 300
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
# 1. Python API — the flag has to reach the JS renderer to do anything
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
class TestLockCenterAttribute:
|
|
41
|
+
def test_defaults_to_unlocked(self):
|
|
42
|
+
w = CircleWidget(lambda: None, cx=16, cy=16, r=6)
|
|
43
|
+
assert w.lock_center is False
|
|
44
|
+
|
|
45
|
+
def test_stores_the_flag(self):
|
|
46
|
+
w = CircleWidget(lambda: None, cx=16, cy=16, r=6, lock_center=True)
|
|
47
|
+
assert w.lock_center is True
|
|
48
|
+
|
|
49
|
+
def test_reaches_the_state_dict(self):
|
|
50
|
+
"""JS reads the widget dict, so the flag has to survive to_dict()."""
|
|
51
|
+
w = CircleWidget(lambda: None, cx=16, cy=16, r=6, lock_center=True)
|
|
52
|
+
assert w.to_dict()["lock_center"] is True
|
|
53
|
+
|
|
54
|
+
def test_add_circle_widget_passes_it_through(self):
|
|
55
|
+
fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
|
|
56
|
+
v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
|
|
57
|
+
w = v.add_circle_widget(cx=16, cy=16, r=6, lock_center=True)
|
|
58
|
+
assert w.lock_center is True
|
|
59
|
+
|
|
60
|
+
def test_add_circle_widget_defaults_to_unlocked(self):
|
|
61
|
+
fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
|
|
62
|
+
v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
|
|
63
|
+
assert v.add_circle_widget(cx=16, cy=16, r=6).lock_center is False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
# 2. Real drags in a real browser — the only place the constraint is enforced
|
|
68
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
_OVERLAY_RECT_JS = """() => {
|
|
71
|
+
for (const cv of document.querySelectorAll('canvas')) {
|
|
72
|
+
if (getComputedStyle(cv).pointerEvents === 'all') {
|
|
73
|
+
const r = cv.getBoundingClientRect();
|
|
74
|
+
return {left:r.left, top:r.top, w:r.width, h:r.height};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _img_to_page(page, ix, iy, iw=32, ih=32):
|
|
82
|
+
"""Image px → page coords, for an overlay laid out 'contain' with no zoom."""
|
|
83
|
+
r = page.evaluate(_OVERLAY_RECT_JS)
|
|
84
|
+
assert r is not None, "overlay canvas not found"
|
|
85
|
+
s = min(r["w"] / iw, r["h"] / ih)
|
|
86
|
+
ox = (r["w"] - iw * s) / 2.0
|
|
87
|
+
oy = (r["h"] - ih * s) / 2.0
|
|
88
|
+
return r["left"] + ox + ix * s, r["top"] + oy + iy * s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _setup(interact_page, lock_center):
|
|
92
|
+
fig, ax = apl.subplots(1, 1, figsize=(FIG_W, FIG_H))
|
|
93
|
+
v = ax.imshow(np.zeros((32, 32), dtype=np.float32))
|
|
94
|
+
v.add_circle_widget(cx=16, cy=16, r=6, color="#ff0000",
|
|
95
|
+
lock_center=lock_center)
|
|
96
|
+
page = interact_page(fig)
|
|
97
|
+
_collect_events(page)
|
|
98
|
+
return page
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _drag(page, frm, to, steps=8):
|
|
102
|
+
page.mouse.move(*_img_to_page(page, *frm))
|
|
103
|
+
page.mouse.down()
|
|
104
|
+
page.mouse.move(*_img_to_page(page, *to), steps=steps)
|
|
105
|
+
page.mouse.up()
|
|
106
|
+
page.wait_for_timeout(80)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.usefixtures("interact_page")
|
|
110
|
+
class TestLockedCentreDrag:
|
|
111
|
+
def test_body_drag_does_not_move_a_locked_circle(self, interact_page):
|
|
112
|
+
"""THE regression. Grab the centre and pull: nothing about the widget
|
|
113
|
+
may change — not even transiently, which is why the assertion is on
|
|
114
|
+
every event and not only the last one."""
|
|
115
|
+
page = _setup(interact_page, lock_center=True)
|
|
116
|
+
|
|
117
|
+
_drag(page, (16, 16), (24, 24))
|
|
118
|
+
|
|
119
|
+
moved = [e for e in _get_events(page)
|
|
120
|
+
if e.get("cx") is not None
|
|
121
|
+
and (abs(e["cx"] - 16.0) > 0.5 or abs(e["cy"] - 16.0) > 0.5)]
|
|
122
|
+
assert not moved, (
|
|
123
|
+
f"a locked circle's centre moved during the drag: {moved[:3]}")
|
|
124
|
+
|
|
125
|
+
def test_an_unlocked_circle_still_moves(self, interact_page):
|
|
126
|
+
"""The counterpart, so a lock that accidentally applied to every circle
|
|
127
|
+
would fail here rather than passing the whole file."""
|
|
128
|
+
page = _setup(interact_page, lock_center=False)
|
|
129
|
+
|
|
130
|
+
_drag(page, (16, 16), (20, 22))
|
|
131
|
+
|
|
132
|
+
last = _get_events(page, "pointer_up")[-1]
|
|
133
|
+
assert last["cx"] > 16.0 and last["cy"] > 16.0
|
|
134
|
+
assert last["r"] == pytest.approx(6.0, abs=0.5)
|
|
135
|
+
|
|
136
|
+
def test_the_radius_handle_still_drags_when_locked(self, interact_page):
|
|
137
|
+
"""A locked centre must not cost the measurement. The handle sits at
|
|
138
|
+
the east point (cx+r, cy) = (22, 16); drag it out to (26, 16)."""
|
|
139
|
+
page = _setup(interact_page, lock_center=True)
|
|
140
|
+
|
|
141
|
+
_drag(page, (22, 16), (26, 16))
|
|
142
|
+
|
|
143
|
+
ups = _get_events(page, "pointer_up")
|
|
144
|
+
assert ups, "the radius drag should still emit a pointer_up"
|
|
145
|
+
last = ups[-1]
|
|
146
|
+
assert last["r"] > 6.0, "the radius handle stopped working under the lock"
|
|
147
|
+
assert last["cx"] == pytest.approx(16.0, abs=0.5)
|
|
148
|
+
assert last["cy"] == pytest.approx(16.0, abs=0.5)
|
|
149
|
+
|
|
150
|
+
def test_a_locked_ring_band_is_not_grabbable_either(self, interact_page):
|
|
151
|
+
"""Not just the centre hotspot: the ring BAND is the circle's move
|
|
152
|
+
target, and it is the part a user actually grabs. (22, 16) is the
|
|
153
|
+
handle, so aim at the west point (10, 16) — same band, no handle."""
|
|
154
|
+
page = _setup(interact_page, lock_center=True)
|
|
155
|
+
|
|
156
|
+
_drag(page, (10, 16), (4, 16))
|
|
157
|
+
|
|
158
|
+
moved = [e for e in _get_events(page)
|
|
159
|
+
if e.get("cx") is not None and abs(e["cx"] - 16.0) > 0.5]
|
|
160
|
+
assert not moved, f"the ring band dragged the locked circle: {moved[:3]}"
|