anyplotlib 0.10.0__tar.gz → 0.10.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.10.0 → anyplotlib-0.10.1}/AGENTS.md +1 -1
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/CHANGELOG.rst +15 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/PKG-INFO +1 -1
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/FIGURE_ESM.md +63 -50
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_repr_utils.py +18 -3
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/figure_esm.js +50 -19
- anyplotlib-0.10.1/anyplotlib/tests/test_embed/test_host_saves_png.py +114 -0
- anyplotlib-0.10.1/anyplotlib/tests/test_plotxy/test_raster_zoom.py +109 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_root/index.html +3 -3
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_root/switcher.json +5 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/conf.py +1 -1
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/exporting.rst +20 -2
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/pyproject.toml +1 -1
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.github/workflows/ci.yml +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.github/workflows/docs.yml +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.github/workflows/prepare_release.yml +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.github/workflows/release.yml +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.github/workflows/tests.yml +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/.gitignore +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Benchmarks/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Benchmarks/plot_benchmark_comparison.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_3d_spectral_viewer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_eels_explorer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_figure_annotations.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_interactive_fft.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_interactive_fitting.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_ipf_density_map.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_ipf_explorer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_key_bindings.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_particle_picker.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_point_widget.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_segment_by_contrast.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_segment_by_contrast_advanced.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_spectra_roi_inspector.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_star_globe_explorer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_threshold_explorer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Interactive/plot_voxel_grain_explorer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_arrows.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_circles.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_ellipses.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_horizontal_lines.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_line_segments.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_points.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_polygons.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_rectangles.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_squares.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_texts.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_transform_markers.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Markers/plot_vertical_lines.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_3d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_3d_texture.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_aspect_ratio.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_bar.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_gpu_voxels.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_gridspec_custom.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_image2d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_inset.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_key_overlays.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_label_formatting.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_line_styles.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_pcolormesh.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_spectra1d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_step_and_log.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_subplots_adjust.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_tile_backend.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/PlotTypes/plot_twinx.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget1d_hline.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget1d_point.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget1d_range.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget1d_vline.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_annular.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_arrow.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_circle.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_crosshair.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_label.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_polygon.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget2d_rectangle.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Examples/Widgets/plot_widget3d_plane.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/LICENSE +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/Makefile +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/README.md +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_base_plot.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_binary_frame.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_electron.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_export.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/axes/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/axes/_axes.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/axes/_inset_axes.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/callbacks.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/conftest.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/embed.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/figure/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/figure/_figure.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/figure/_gridspec.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/figure/_subplots.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/keys.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/markers.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot1d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot1d/_plot1d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot1d/_plotbar.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot2d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot2d/_layer.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot2d/_plot2d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot2d/_plotmesh.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot2d/_tile_backend.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot3d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plot3d/_plot3d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plotxy/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/plotxy/_plotxy.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/_directive.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/_repr_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/_scraper.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/_wheel_builder.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/conftest.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/test_directive.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/test_init.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/test_repr_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/test_scraper.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/sphinx_anywidget/tests/test_wheel_builder.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/_gpu_clim_check.cjs +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/_png_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/bar_basic.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_3col_equal_spectra.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_asymmetric_width_ratios.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_height_ratio_image_histogram.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_image_two_spectra.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_side_by_side_1d.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/gridspec_spanning_top_two_bottom.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/imshow_axis_off.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/imshow_checkerboard.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/imshow_gradient.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/imshow_labels.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/imshow_viridis.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_1d.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_maximized.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_minimized.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_normal_2d.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_stacked.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/inset_stacked_one_minimized.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/pcolormesh_uniform.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_all_linestyles.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_alpha.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_axis_off.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_dashed.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_marker_symbols.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_markers.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_multi.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_sine.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_title.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot1d_twinx.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/plot3d_surface.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/baselines/subplots_2x1.png +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/conftest.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_benchmarks/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_benchmarks/baselines.json +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_benchmarks/benchmarks/baselines.json +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_benchmarks/test_benchmarks.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_benchmarks/test_benchmarks_py.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_documentation/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_documentation/test_bridge.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_documentation/test_push_hook.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_documentation/test_scraper.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_electron/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_electron/test_binary_frame.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/_export_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/conftest.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_3d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_api.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_escaping.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_mount.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_navigated.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_embed_set_image.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_export_menu.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_export_pixel_tokens.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_export_png.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_export_sources.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_export_widget_sync.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_embed/test_savefig.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_examples/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_examples/test_interactive_examples.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/_event_test_utils.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_blit_audit.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_callbacks_playwright.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_callbacks_unit.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_circle_lock_center.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_edit_chrome.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_edit_chrome_playwright.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_event_pause_hold.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_event_plots.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_event_settled.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_events_regression.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_orbit_direction.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_pointer_down_1d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_range_orientation_snap.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_star_globe_linking.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_tile_parity_playwright.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_title.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_touch.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_widget_brush.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_widget_max_extent.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_widget_set_notify.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_widgets.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_interactive/test_widgets_line_2d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_keys/test_keys.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_labels/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_labels/test_label_api.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_labels/test_label_rendering.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_labels/test_no_clipping.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_labels/test_ylabel_clearance.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_batch.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_coord_conversion.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_geom_channel.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_gridspec.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_inset.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_inset_callout.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_interaction.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_layouts/test_visual.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/test_marker_transforms.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/test_markers.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/test_per_marker_colors.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/test_size_units.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_markers/test_text_style.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot1d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot1d/test_line_none_rendering.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot1d/test_plot1d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot1d/test_plotbar.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_axis_ticks.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_colorbar_gap_scalebar.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_colorbar_values.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_detail_tile.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_display_window.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_first_paint_race.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_gpu_image.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_gpu_parity_playwright.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_imshow.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_imshow_rgb.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_layers.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_layers_playwright.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_pcolormesh.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_plot2d_api.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_pointer_pixel_centre.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_rapid_frame_update.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_status_pixel_value.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_tile_backend.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_tile_diagnostics.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_tiled_imshow.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot2d/test_zoom_view_write.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_colors_highlight.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_gpu_depth.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_gpu_fallback.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_gpu_hidden_reveal.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_plot3d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_texture.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plot3d/test_voxels_planes.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/tests/test_plotxy/test_plotxy.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/widgets/__init__.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/widgets/_base.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/widgets/_widgets1d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/widgets/_widgets2d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/anyplotlib/widgets/_widgets3d.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_sg_html_scraper.py +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_static/anyplotlib.svg +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_static/custom.css +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_static/pyodide_bridge.js +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/_templates/autosummary/class.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/callbacks.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/figure.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/figure_plots.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/index.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/keys.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/markers.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/api/widgets.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/benchmarking.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/dev/index.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/embedding.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/events.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/getting_started.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/docs/index.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/make.bat +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/upcoming_changes/.gitkeep +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/upcoming_changes/README.rst +0 -0
- {anyplotlib-0.10.0 → anyplotlib-0.10.1}/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 12,
|
|
118
|
+
the 2-D function table). Both were last verified at 12,438 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,21 @@ Fragment files in ``upcoming_changes/`` are assembled into this file by
|
|
|
10
10
|
|
|
11
11
|
.. towncrier release notes start
|
|
12
12
|
|
|
13
|
+
0.10.1 (2026-09-17)
|
|
14
|
+
===================
|
|
15
|
+
|
|
16
|
+
New Features
|
|
17
|
+
------------
|
|
18
|
+
|
|
19
|
+
- A page embedding a figure in an iframe can post ``{type: 'anyplotlib_host', savesPng: true}`` to say it saves exported PNGs itself; the figure's "Save PNG…" then only sends the image to the host, without the in-figure "Save image as…" preview or the "Save as… (choose folder)" entry. (`#78 <https://github.com/CSSFrancis/anyplotlib/pull/78>`_)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Bug Fixes
|
|
23
|
+
---------
|
|
24
|
+
|
|
25
|
+
- Zooming or panning a coordinate axis no longer blanks an :meth:`~anyplotlib.plotxy.PlotXY.add_raster` image, and every marker drawn after it, until the next update from Python. (`#76 <https://github.com/CSSFrancis/anyplotlib/pull/76>`_)
|
|
26
|
+
|
|
27
|
+
|
|
13
28
|
0.10.0 (2026-09-14)
|
|
14
29
|
===================
|
|
15
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: anyplotlib
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -73,32 +73,34 @@ Rule 5 – Text never clips. Optional gutters earn real layout space:
|
|
|
73
73
|
| 3D event handlers `_attachEvents3d` | 6661 |
|
|
74
74
|
| **1D drawing**: `draw1d` | 6885 |
|
|
75
75
|
| `_drawLine` (1D series + markers) | 7038 |
|
|
76
|
-
| `drawOverlay1d` / `drawMarkers1d` | 7331 /
|
|
77
|
-
| Marker hit-test `_markerHitTest2d` |
|
|
76
|
+
| `drawOverlay1d` / `_rasterBitmap` / `drawMarkers1d` | 7331 / 7419 / 7436 |
|
|
77
|
+
| Marker hit-test `_markerHitTest2d` | 7700 |
|
|
78
78
|
|
|
79
79
|
> **`raster` marker (1D/PlotXY)** — `drawMarkers1d` has a `type==='raster'`
|
|
80
80
|
> branch that blits a single RGBA image across data-coord `extent` (the fast
|
|
81
81
|
> path for dense `PlotXY.pcolormesh` heatmaps). The image bytes ride the geom
|
|
82
82
|
> channel as `st.raster_geom[id]` (Python `Plot1D._GEOM_KEYS`), so view-only
|
|
83
|
-
> redraws never re-transmit them
|
|
84
|
-
> the marker set
|
|
85
|
-
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
| 2D
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
| **
|
|
97
|
-
|
|
|
98
|
-
| **
|
|
99
|
-
|
|
|
100
|
-
| **
|
|
101
|
-
|
|
|
83
|
+
> redraws never re-transmit them. The decoded `OffscreenCanvas` is cached per
|
|
84
|
+
> panel by `_rasterBitmap` (keyed by the marker set's id, re-decoded when the
|
|
85
|
+
> bytes or the shape change) — never on the marker set, which is state the
|
|
86
|
+
> wheel and pan handlers serialise back. The shared `clip_path` block clips it
|
|
87
|
+
> to a curved sector.
|
|
88
|
+
| Panel event dispatch `_attachPanelEvents` | 7957 |
|
|
89
|
+
| 2D events `_attachEvents2d` | 8043 |
|
|
90
|
+
| 1D events `_attachEvents1d` | 8429 |
|
|
91
|
+
| 2D widget drag `_ovHitTest2d` / `_doDrag2d` | 8704 / 8983 |
|
|
92
|
+
| **Brush strokes**: `_brushLiveBegin` / `_brushCommit` / `_brushErase` / `_brushPaintAt` | 8896 / 8910 / 8939 / 8974 |
|
|
93
|
+
| 1D widget drag `_canvasXToFrac1d` … / snapping `_snapVal` | 9108 / 9181 |
|
|
94
|
+
| Shared-axis propagation `_getShareGroups` | 9252 |
|
|
95
|
+
| Figure resize `_applyFigResizeDOM` | 9316 |
|
|
96
|
+
| **Bar chart**: `_barGeom` / `drawBar` / `_attachEventsBar` | 9510 / 9573 / 9949 |
|
|
97
|
+
| Generic redraw `_redrawPanel` | 10139 |
|
|
98
|
+
| **PNG export**: `_compositeCanvas` / `exportCanvas` / `exportPNG` | 10302 / 10498 / 10557 |
|
|
99
|
+
| Native-resolution render `_withNativeSize` | 10278 |
|
|
100
|
+
| **Export UI**: `_toast` / `_downloadCanvas` / `_openMenu` | 10591 / 10713 / 10892 |
|
|
101
|
+
| Export registry `registerExportAction` | 10769 |
|
|
102
|
+
| **Embedding API**: `createLocalModel` / `mount` | 11283 / 11339 |
|
|
103
|
+
| **Navigated embed**: `decodeBlocks` / `mountNavigated` | 11594 / 11981 |
|
|
102
104
|
|
|
103
105
|
> **`brush` widget (2-D)** — the one widget whose drag is *modal*, and the one
|
|
104
106
|
> that must NOT write the model per tick. `_ovHitTest2d` takes an extra `mods`
|
|
@@ -660,13 +662,13 @@ exportCanvas(same opts) → {canvas, width, height} // synchronous, throws
|
|
|
660
662
|
|
|
661
663
|
| Function | Line | Purpose |
|
|
662
664
|
|----------|------|---------|
|
|
663
|
-
| `_cssScale` |
|
|
664
|
-
| `_panelBox` |
|
|
665
|
-
| `_neutralizeView` / `_restoreView` |
|
|
666
|
-
| `_nativeGeom` / `_nativeGuard` |
|
|
667
|
-
| `_withNativeSize` |
|
|
668
|
-
| `_compositeCanvas` |
|
|
669
|
-
| `exportCanvas` / `exportPNG` |
|
|
665
|
+
| `_cssScale` | 10174 | inverse of `_applyScale`'s `transform:scale()` |
|
|
666
|
+
| `_panelBox` | 10185 | the element whose rect bounds one panel |
|
|
667
|
+
| `_neutralizeView` / `_restoreView` | 10194 / 10219 | transient whole-extent view |
|
|
668
|
+
| `_nativeGeom` / `_nativeGuard` | 10234 / 10253 | native size + why-not message |
|
|
669
|
+
| `_withNativeSize` | 10278 | resize → redraw → run → restore |
|
|
670
|
+
| `_compositeCanvas` | 10302 | the compositor (`_drawEl` / `_drawPanel` …) |
|
|
671
|
+
| `exportCanvas` / `exportPNG` | 10498 / 10557 | orchestrator / data-URL wrapper |
|
|
670
672
|
|
|
671
673
|
**The whole pipeline is ONE synchronous task** — theme swap, view reset, native
|
|
672
674
|
resize, composite, restore — so the browser never paints an intermediate state
|
|
@@ -762,13 +764,14 @@ leaders that cross into the panel included. Pinned by
|
|
|
762
764
|
|
|
763
765
|
| Function | Line | Purpose |
|
|
764
766
|
|----------|------|---------|
|
|
765
|
-
| `_toast` |
|
|
766
|
-
| `_copyCanvas` |
|
|
767
|
-
| `_showPngPreview` |
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
771
|
-
| `
|
|
767
|
+
| `_toast` | 10591 | transient bottom-centre message |
|
|
768
|
+
| `_copyCanvas` | 10626 | clipboard write + feature detection |
|
|
769
|
+
| `_showPngPreview` | 10650 | framed-document download fallback |
|
|
770
|
+
| `_hostSavesPng` / `_canPickFile` | 10702 / 10708 | the parent saves PNGs itself / a system Save dialog exists |
|
|
771
|
+
| `_downloadCanvas` | 10713 | `<a download>`, the preview, or the host |
|
|
772
|
+
| `registerExportAction` | 10769 | downstream extension point |
|
|
773
|
+
| `_menuRows` / `_openMenu` | 10823 / 10892 | menu model / DOM |
|
|
774
|
+
| `_panelAtPoint` | 11004 | hit test (insets first — they sit on top) |
|
|
772
775
|
|
|
773
776
|
- **An `exportBtn` badge (⤓, beside the help badge) opens the same menu on an
|
|
774
777
|
ordinary left click.** It is a `role="button"` with `tabIndex=0` and
|
|
@@ -810,9 +813,15 @@ leaders that cross into the panel included. Pinned by
|
|
|
810
813
|
`save_html` page opened directly is top-level) from the unreliable one (VS
|
|
811
814
|
Code webviews, `_repr_html_` iframes, nbconvert output). When framed, the
|
|
812
815
|
result is posted to the parent under the existing
|
|
813
|
-
`anyplotlib_export_png_result` message
|
|
814
|
-
caption points at the browser's own
|
|
815
|
-
permission and is never blocked.
|
|
816
|
+
`anyplotlib_export_png_result` message (`requestId: null`, plus `filename`)
|
|
817
|
+
AND shown as an in-figure preview whose caption points at the browser's own
|
|
818
|
+
"Save image as…", which needs no permission and is never blocked.
|
|
819
|
+
- **Host saves** (`_hostSavesPng`): a parent that posts
|
|
820
|
+
`{type:'anyplotlib_host', savesPng:true}` sets `globalThis.__aplHostSavesPng`
|
|
821
|
+
(the page template's listener, parent frame only). Framed saves then post the
|
|
822
|
+
image WITHOUT the preview, and `_canPickFile()` is false, so the *Save as…*
|
|
823
|
+
row disappears — the host's own Save dialog chooses the folder, and an app
|
|
824
|
+
webview may have no "Save image as…" menu for the preview to point at.
|
|
816
825
|
- **Clipboard**: gated on `isSecureContext && navigator.clipboard &&
|
|
817
826
|
ClipboardItem && clipboard.write`. The Blob is built SYNCHRONOUSLY from the
|
|
818
827
|
data URL (not via the async `toBlob` callback) so the write stays inside the
|
|
@@ -829,6 +838,7 @@ Test hooks: `__apl_menuItems`, `__apl_toastText`, `__apl_menuTheme`,
|
|
|
829
838
|
Tests: `tests/test_embed/test_export_png.py` (the pre-existing contract),
|
|
830
839
|
`test_export_sources.py` (panelId / source / theme / CSS scale),
|
|
831
840
|
`test_export_menu.py` (menu, clipboard, download, registry),
|
|
841
|
+
`test_host_saves_png.py` (the host announcement and the framed save),
|
|
832
842
|
`test_savefig.py` (the Python entry point + view reconciliation).
|
|
833
843
|
|
|
834
844
|
The standalone HTML template (`_repr_utils.build_standalone_html`) captures
|
|
@@ -838,11 +848,14 @@ render()'s api into `_aplRenderApi`, **also assigns it to `window._aplRenderApi`
|
|
|
838
848
|
`{type:'anyplotlib_export_png', requestId, opts}` → `exportPNG(opts)` → replies
|
|
839
849
|
`{type:'anyplotlib_export_png_result', requestId, dataUrl, width, height}` (or
|
|
840
850
|
`{…, error}`) to `event.source` (targetOrigin `'*'`). `opts` is forwarded
|
|
841
|
-
verbatim, so the new fields work over that channel too.
|
|
851
|
+
verbatim, so the new fields work over that channel too. A second listener takes
|
|
852
|
+
`{type:'anyplotlib_host', savesPng}` from `window.parent` only and records it
|
|
853
|
+
in `globalThis.__aplHostSavesPng` (read by `_hostSavesPng`). Both live in
|
|
854
|
+
`PNG_HARVEST_LISTENER`, which the navigated embed installs too.
|
|
842
855
|
|
|
843
856
|
---
|
|
844
857
|
|
|
845
|
-
## Navigated-embed runtime (line
|
|
858
|
+
## Navigated-embed runtime (line 11346 to the end of the file)
|
|
846
859
|
|
|
847
860
|
Everything below `mount()` is module scope, outside `render()`'s closure: pure
|
|
848
861
|
functions over decoded data plus one entry point that wires them to a mounted
|
|
@@ -852,16 +865,16 @@ bindings, let it dispatch", rather than a hand-written program per result kind.
|
|
|
852
865
|
|
|
853
866
|
| Function | Line | Purpose |
|
|
854
867
|
|----------|------|---------|
|
|
855
|
-
| `decodeBlocks` |
|
|
856
|
-
| `dense` |
|
|
857
|
-
| `ragged` |
|
|
858
|
-
| `maskFromWidget` |
|
|
859
|
-
| `rasterDisks` |
|
|
860
|
-
| `robustLevels` / `toU8` |
|
|
861
|
-
| `panelAxis` |
|
|
862
|
-
| `installTouchShim` / `reportEmbedHeight` |
|
|
863
|
-
| `encodeBase64` / `typedArrayBytes` |
|
|
864
|
-
| `mountNavigated` |
|
|
868
|
+
| `decodeBlocks` | 11594 | one base64 `fetch` → one ArrayBuffer → a typed-array view per manifest entry |
|
|
869
|
+
| `dense` | 11620 | `at` / `gather` / `reduce` over a block whose leading axes are the nav axes |
|
|
870
|
+
| `ragged` | 11685 | the same three, over a row-pointer block (`offsets` + one array per column) |
|
|
871
|
+
| `maskFromWidget` | 11768 | rectangle / circle / annulus widget dict → `Uint8Array` (carries `width`/`height`) |
|
|
872
|
+
| `rasterDisks` | 11808 | splat `{x, y, intensity}` rows as filled disks — the base image of a vectors panel |
|
|
873
|
+
| `robustLevels` / `toU8` | 11837 / 11878 | the percentile window and the 8-bit code map, one implementation |
|
|
874
|
+
| `panelAxis` | 11956 | a 1-D panel's decoded x axis (`_1dXArr`, else `x_axis_b64`) |
|
|
875
|
+
| `installTouchShim` / `reportEmbedHeight` | 11893 / 11912 | page chrome: touch → mouse, `postMessage({aplEmbedHeight})` |
|
|
876
|
+
| `encodeBase64` / `typedArrayBytes` | 11932 / 11940 | a 3-D cloud's geometry channel is base64, not the binary side table |
|
|
877
|
+
| `mountNavigated` | 11981 | mount + bind + dispatch; resolves to the mount handle plus `dispatch`/`index`/`blocks` |
|
|
865
878
|
|
|
866
879
|
`mountNavigated(el, page, opts)` is **async** — the blob decode is a `fetch` of
|
|
867
880
|
a `data:` URL — so a host `await`s it. `page` is `{state, blocks, bindings,
|
|
@@ -330,9 +330,10 @@ window.addEventListener('message', (e) => {{
|
|
|
330
330
|
|
|
331
331
|
|
|
332
332
|
# A host page (or the SpyDE report harvester) asks an embedded figure for a
|
|
333
|
-
# composite PNG over postMessage
|
|
334
|
-
#
|
|
335
|
-
#
|
|
333
|
+
# composite PNG over postMessage, and can announce that it saves exported PNGs
|
|
334
|
+
# itself. Both the standalone page and the navigated embed install these
|
|
335
|
+
# listeners; each assigns ``globalThis.__aplExportPNG`` once its figure is
|
|
336
|
+
# mounted, which is also what makes "not ready yet" answerable.
|
|
336
337
|
PNG_HARVEST_LISTENER = '''\
|
|
337
338
|
// ── PNG export protocol ──────────────────────────────────────────────────────
|
|
338
339
|
// A parent page (or the SpyDE report harvester) asks this frame over
|
|
@@ -370,6 +371,20 @@ window.addEventListener('message', (e) => {
|
|
|
370
371
|
reply({ error: String(err && err.message || err) });
|
|
371
372
|
}
|
|
372
373
|
});
|
|
374
|
+
|
|
375
|
+
// A host that saves exported PNGs itself (a desktop app with its own Save
|
|
376
|
+
// dialog) announces it once:
|
|
377
|
+
// → { type: 'anyplotlib_host', savesPng: true }
|
|
378
|
+
// after which the export menu's "Save PNG…" only posts the image to it
|
|
379
|
+
// ← { type: 'anyplotlib_export_png_result', requestId: null,
|
|
380
|
+
// dataUrl, width, height, filename }
|
|
381
|
+
// instead of also showing the in-figure "Save image as…" preview. Only the
|
|
382
|
+
// parent frame may say so.
|
|
383
|
+
window.addEventListener('message', (e) => {
|
|
384
|
+
if (!e.data || e.data.type !== 'anyplotlib_host') return;
|
|
385
|
+
if (e.source !== window.parent || window.parent === window) return;
|
|
386
|
+
globalThis.__aplHostSavesPng = e.data.savesPng === true;
|
|
387
|
+
});
|
|
373
388
|
'''
|
|
374
389
|
|
|
375
390
|
|
|
@@ -7412,6 +7412,27 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
7412
7412
|
ctx.beginPath();ctx.arc(x,y,5,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.restore();
|
|
7413
7413
|
}
|
|
7414
7414
|
|
|
7415
|
+
// The decoded image of raster marker set `id`, re-decoded only when its bytes
|
|
7416
|
+
// or its shape change (the same bytes can be 1×4 or 2×2), or null if they
|
|
7417
|
+
// cannot be decoded. Kept in a per-panel map rather than on the marker set
|
|
7418
|
+
// itself, which is serialised state.
|
|
7419
|
+
function _rasterBitmap(p, id, b64, width, height){
|
|
7420
|
+
if(!p._rasterCache) p._rasterCache=new Map();
|
|
7421
|
+
const cached=p._rasterCache.get(id);
|
|
7422
|
+
if(cached && cached.b64===b64 && cached.width===width && cached.height===height)
|
|
7423
|
+
return cached.bitmap;
|
|
7424
|
+
let bitmap=null;
|
|
7425
|
+
try{
|
|
7426
|
+
const bin=atob(b64);
|
|
7427
|
+
const bytes=new Uint8ClampedArray(bin.length);
|
|
7428
|
+
for(let i=0;i<bin.length;i++) bytes[i]=bin.charCodeAt(i);
|
|
7429
|
+
bitmap=new OffscreenCanvas(width,height);
|
|
7430
|
+
bitmap.getContext('2d').putImageData(new ImageData(bytes, width, height),0,0);
|
|
7431
|
+
}catch(_){ bitmap=null; }
|
|
7432
|
+
p._rasterCache.set(id, {b64, width, height, bitmap});
|
|
7433
|
+
return bitmap;
|
|
7434
|
+
}
|
|
7435
|
+
|
|
7415
7436
|
function drawMarkers1d(p, hoverState) {
|
|
7416
7437
|
const st=p.state; if(!st) return;
|
|
7417
7438
|
const {pw,ph,mkCtx} = p;
|
|
@@ -7424,6 +7445,10 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
7424
7445
|
if (st.y_range && st.y_range.length === 2) { dMin = st.y_range[0]; dMax = st.y_range[1]; }
|
|
7425
7446
|
mkCtx.clearRect(0,0,pw,ph);
|
|
7426
7447
|
const sets=st.markers||[];
|
|
7448
|
+
if(p._rasterCache){
|
|
7449
|
+
const live=new Set(sets.map(set=>set.id));
|
|
7450
|
+
for(const id of [...p._rasterCache.keys()]) if(!live.has(id)) p._rasterCache.delete(id);
|
|
7451
|
+
}
|
|
7427
7452
|
if(!sets.length) return;
|
|
7428
7453
|
const hsi = hoverState ? hoverState.si : -1;
|
|
7429
7454
|
|
|
@@ -7567,33 +7592,26 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
7567
7592
|
} else if(type==='raster'){
|
|
7568
7593
|
// A single RGBA image stretched across data-coord `extent`. Heavy bytes
|
|
7569
7594
|
// ride the geom channel (st.raster_geom[id]); fall back to inline. The
|
|
7570
|
-
//
|
|
7571
|
-
// without re-decoding. The clip block above already scoped any sector.
|
|
7595
|
+
// clip block above already scoped any sector.
|
|
7572
7596
|
const rg = (st.raster_geom && st.raster_geom[ms.id]) || ms;
|
|
7573
7597
|
const b64 = rg.image_b64 || '';
|
|
7574
7598
|
const iw = rg.image_width|0, ih = rg.image_height|0;
|
|
7575
7599
|
if(b64 && iw>0 && ih>0){
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
const imgData=new ImageData(bytes, iw, ih);
|
|
7582
|
-
const oc=new OffscreenCanvas(iw,ih);
|
|
7583
|
-
oc.getContext('2d').putImageData(imgData,0,0);
|
|
7584
|
-
ms._rasterBmp=oc; ms._rasterKey=b64;
|
|
7585
|
-
}catch(_){ ms._rasterBmp=null; }
|
|
7586
|
-
}
|
|
7600
|
+
// The decoded image is cached on the PANEL, not on `ms`: `ms` is part
|
|
7601
|
+
// of the state a view write-back serialises, and a decoded canvas
|
|
7602
|
+
// serialises to `{}`, which would then pass for a cached image and
|
|
7603
|
+
// make drawImage throw — blanking the panel on every zoom.
|
|
7604
|
+
const decoded=_rasterBitmap(p, ms.id, b64, iw, ih);
|
|
7587
7605
|
const ext=ms.extent||[0,1,0,1];
|
|
7588
7606
|
const [ax2,ay2]= tfm==='data' ? _offToCanvas([ext[0],ext[2]]) : _tc2d(ext[0],ext[2]);
|
|
7589
7607
|
const [bx2,by2]= tfm==='data' ? _offToCanvas([ext[1],ext[3]]) : _tc2d(ext[1],ext[3]);
|
|
7590
|
-
if(
|
|
7608
|
+
if(decoded){
|
|
7591
7609
|
mkCtx.save();
|
|
7592
7610
|
// Nearest-neighbour by default (crisp cells); smoothing bilinearly
|
|
7593
7611
|
// interpolates for a smooth heat field (ms.smooth === true).
|
|
7594
7612
|
mkCtx.imageSmoothingEnabled = ms.smooth === true;
|
|
7595
7613
|
if(ms.smooth === true) mkCtx.imageSmoothingQuality = 'high';
|
|
7596
|
-
mkCtx.drawImage(
|
|
7614
|
+
mkCtx.drawImage(decoded, 0,0,iw,ih,
|
|
7597
7615
|
Math.min(ax2,bx2), Math.min(ay2,by2),
|
|
7598
7616
|
Math.abs(bx2-ax2), Math.abs(by2-ay2));
|
|
7599
7617
|
mkCtx.restore();
|
|
@@ -10671,11 +10689,24 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
10671
10689
|
// usePicker → showSaveFilePicker, a real system dialog (Chromium, secure
|
|
10672
10690
|
// context, user gesture) — costs a Chrome permission prompt
|
|
10673
10691
|
// otherwise → <a download>, straight to the downloads folder, no prompt
|
|
10674
|
-
// framed → an in-figure preview,
|
|
10675
|
-
// a SILENT no-op with
|
|
10692
|
+
// framed → the image is posted to the parent, plus an in-figure preview,
|
|
10693
|
+
// since a sandboxed frame makes a.click() a SILENT no-op with
|
|
10694
|
+
// nothing to feature detect
|
|
10695
|
+
// framed, host saves → posted to the parent only: the host has its own
|
|
10696
|
+
// Save dialog (a desktop app), so the preview's "right-click →
|
|
10697
|
+
// Save image as…" would be wrong — an app webview may have no
|
|
10698
|
+
// such menu at all
|
|
10699
|
+
|
|
10700
|
+
// Set by the page template when the parent announces
|
|
10701
|
+
// {type:'anyplotlib_host', savesPng:true} (see PNG_HARVEST_LISTENER).
|
|
10702
|
+
function _hostSavesPng() {
|
|
10703
|
+
return window.self !== window.top && globalThis.__aplHostSavesPng === true;
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10676
10706
|
// Is a system Save dialog available at all? Chromium only, secure context.
|
|
10707
|
+
// A host that saves already asks where to put the file.
|
|
10677
10708
|
function _canPickFile() {
|
|
10678
|
-
return !!(window.isSecureContext
|
|
10709
|
+
return !_hostSavesPng() && !!(window.isSecureContext
|
|
10679
10710
|
&& typeof window.showSaveFilePicker === 'function');
|
|
10680
10711
|
}
|
|
10681
10712
|
|
|
@@ -10718,7 +10749,7 @@ fn fs(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
10718
10749
|
type: 'anyplotlib_export_png_result', requestId: null,
|
|
10719
10750
|
dataUrl, width: canvas.width, height: canvas.height, filename }, '*');
|
|
10720
10751
|
} catch (_) {}
|
|
10721
|
-
_showPngPreview(dataUrl, filename);
|
|
10752
|
+
if (!_hostSavesPng()) _showPngPreview(dataUrl, filename);
|
|
10722
10753
|
return;
|
|
10723
10754
|
}
|
|
10724
10755
|
const url = URL.createObjectURL(blob);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A framed figure's "Save PNG…" when the host page saves PNGs itself.
|
|
3
|
+
|
|
4
|
+
In a frame, "Save PNG…" cannot download (a sandboxed frame makes ``a.click()``
|
|
5
|
+
a silent no-op), so it posts the image to the parent and shows an in-figure
|
|
6
|
+
preview whose caption points at the browser's "Save image as…". A host with its
|
|
7
|
+
own Save dialog — a desktop app, whose webview may have no such menu —
|
|
8
|
+
announces ``{type: 'anyplotlib_host', savesPng: true}``; the figure then only
|
|
9
|
+
posts the image, and drops the "Save as… (choose folder)" row, since the
|
|
10
|
+
host's dialog already chooses the folder.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pathlib
|
|
15
|
+
import tempfile
|
|
16
|
+
from html import escape
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
import anyplotlib as apl
|
|
22
|
+
from anyplotlib._repr_utils import build_standalone_html
|
|
23
|
+
|
|
24
|
+
_PARENT_PAGE = """<!DOCTYPE html>
|
|
25
|
+
<html><head><meta charset="utf-8"/><style>html,body{margin:0;padding:0;}</style></head>
|
|
26
|
+
<body>
|
|
27
|
+
<iframe id="fig" srcdoc="__SRCDOC__" width="360" height="280" style="border:none;"></iframe>
|
|
28
|
+
<script>
|
|
29
|
+
window._saved = [];
|
|
30
|
+
window.addEventListener('message', (e) => {
|
|
31
|
+
if (e.data && e.data.type === 'anyplotlib_export_png_result' && e.data.requestId === null) {
|
|
32
|
+
window._saved.push({filename: e.data.filename, dataUrl: e.data.dataUrl});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
window._announceHost = () => document.getElementById('fig').contentWindow.postMessage(
|
|
36
|
+
{type: 'anyplotlib_host', savesPng: true}, '*');
|
|
37
|
+
</script>
|
|
38
|
+
</body></html>
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_MENU_ROWS = """() => Array.from(document.querySelectorAll('[data-apl-menu] *'))
|
|
42
|
+
.filter((element) => element.children.length === 0)
|
|
43
|
+
.map((element) => element.textContent)"""
|
|
44
|
+
|
|
45
|
+
_OPEN_MENU = """() => document.querySelector(
|
|
46
|
+
'[aria-label="Copy or save this figure"]').click()"""
|
|
47
|
+
|
|
48
|
+
_CLICK_SAVE_PNG = """() => Array.from(document.querySelectorAll('[data-apl-menu] *'))
|
|
49
|
+
.find((element) => element.textContent === 'Save PNG…').click()"""
|
|
50
|
+
|
|
51
|
+
_PREVIEW_SHOWN = "() => document.body.innerText.includes('Save image as')"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def framed_figure(_pw_browser):
|
|
56
|
+
"""Open a parent page holding a figure in an iframe; yield ``(page, frame)``."""
|
|
57
|
+
fig, ax = apl.subplots(1, 1, figsize=(320, 240))
|
|
58
|
+
ax.imshow(np.random.default_rng(3).random((16, 16)).astype(np.float32))
|
|
59
|
+
parent = _PARENT_PAGE.replace(
|
|
60
|
+
"__SRCDOC__", escape(build_standalone_html(fig, resizable=False), quote=True))
|
|
61
|
+
with tempfile.NamedTemporaryFile(
|
|
62
|
+
suffix=".html", mode="w", encoding="utf-8", delete=False) as handle:
|
|
63
|
+
handle.write(parent)
|
|
64
|
+
path = pathlib.Path(handle.name)
|
|
65
|
+
page = _pw_browser.new_page()
|
|
66
|
+
try:
|
|
67
|
+
page.goto(path.as_uri())
|
|
68
|
+
frame = next(frame for frame in page.frames if frame.parent_frame is not None)
|
|
69
|
+
frame.wait_for_function(
|
|
70
|
+
"() => typeof globalThis.__aplExportPNG === 'function'", timeout=15_000)
|
|
71
|
+
yield page, frame
|
|
72
|
+
finally:
|
|
73
|
+
page.close()
|
|
74
|
+
path.unlink(missing_ok=True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _save_png(page, frame):
|
|
78
|
+
"""Open the export menu, click "Save PNG…"; return the menu's rows."""
|
|
79
|
+
frame.evaluate(_OPEN_MENU)
|
|
80
|
+
rows = frame.evaluate(_MENU_ROWS)
|
|
81
|
+
frame.evaluate(_CLICK_SAVE_PNG)
|
|
82
|
+
page.wait_for_function("() => window._saved.length > 0", timeout=5_000)
|
|
83
|
+
frame.wait_for_timeout(100)
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestHostSavesPng:
|
|
88
|
+
def test_without_a_host_the_frame_shows_the_preview(self, framed_figure):
|
|
89
|
+
page, frame = framed_figure
|
|
90
|
+
_save_png(page, frame)
|
|
91
|
+
saved = page.evaluate("() => window._saved")
|
|
92
|
+
assert saved[0]["dataUrl"].startswith("data:image/png;base64,")
|
|
93
|
+
assert saved[0]["filename"].endswith(".png")
|
|
94
|
+
assert frame.evaluate(_PREVIEW_SHOWN)
|
|
95
|
+
|
|
96
|
+
def test_a_host_that_saves_gets_the_image_and_no_preview(self, framed_figure):
|
|
97
|
+
page, frame = framed_figure
|
|
98
|
+
page.evaluate("() => window._announceHost()")
|
|
99
|
+
frame.wait_for_function("() => globalThis.__aplHostSavesPng === true")
|
|
100
|
+
rows = _save_png(page, frame)
|
|
101
|
+
saved = page.evaluate("() => window._saved")
|
|
102
|
+
assert len(saved) == 1 and saved[0]["dataUrl"].startswith("data:image/png;base64,")
|
|
103
|
+
assert not frame.evaluate(_PREVIEW_SHOWN)
|
|
104
|
+
# The host's own dialog chooses the folder.
|
|
105
|
+
assert not any("choose folder" in (row or "") for row in rows), rows
|
|
106
|
+
|
|
107
|
+
def test_only_the_parent_can_announce_itself(self, framed_figure):
|
|
108
|
+
page, frame = framed_figure
|
|
109
|
+
frame.evaluate(
|
|
110
|
+
"() => window.postMessage({type: 'anyplotlib_host', savesPng: true}, '*')")
|
|
111
|
+
frame.wait_for_timeout(100)
|
|
112
|
+
assert frame.evaluate("() => globalThis.__aplHostSavesPng") is None
|
|
113
|
+
_save_png(page, frame)
|
|
114
|
+
assert frame.evaluate(_PREVIEW_SHOWN)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A raster on a coordinate axis survives the view write-back a zoom makes, and
|
|
3
|
+
its decoded image is rebuilt whenever its bytes or its shape change.
|
|
4
|
+
|
|
5
|
+
The wheel handler writes the panel's view state back through the model, and
|
|
6
|
+
the model's change listener redraws the panel from that state after a round
|
|
7
|
+
trip through JSON. The decoded raster must not live in that state: a decoded
|
|
8
|
+
canvas serialises to ``{}``, which then passed for a cached image, made
|
|
9
|
+
``drawImage`` throw, and left the raster — and every marker drawn after it —
|
|
10
|
+
blank until the next push from Python.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
import anyplotlib as apl
|
|
17
|
+
|
|
18
|
+
GRID_PAD = 8 # gridDiv padding: the canvas's offset from the page origin
|
|
19
|
+
|
|
20
|
+
RED = (255, 0, 0, 255)
|
|
21
|
+
GREEN = (0, 255, 0, 255)
|
|
22
|
+
|
|
23
|
+
# Where the red and the green pixels are, across every 2-D canvas.
|
|
24
|
+
_COLOUR_EXTENTS = """() => {
|
|
25
|
+
const found = {red: {count: 0, minX: Infinity, maxX: -Infinity},
|
|
26
|
+
green: {count: 0, minX: Infinity, maxX: -Infinity}}
|
|
27
|
+
for (const canvas of document.querySelectorAll('canvas')) {
|
|
28
|
+
const context = canvas.getContext('2d')
|
|
29
|
+
if (!context || !canvas.width || !canvas.height) continue
|
|
30
|
+
const data = context.getImageData(0, 0, canvas.width, canvas.height).data
|
|
31
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
32
|
+
if (data[i + 3] < 128) continue
|
|
33
|
+
const red = data[i], green = data[i + 1], blue = data[i + 2]
|
|
34
|
+
const colour = red > 150 && green < 80 && blue < 80 ? found.red
|
|
35
|
+
: green > 150 && red < 80 && blue < 80 ? found.green : null
|
|
36
|
+
if (!colour) continue
|
|
37
|
+
const x = (i / 4) % canvas.width
|
|
38
|
+
colour.count++
|
|
39
|
+
colour.minX = Math.min(colour.minX, x)
|
|
40
|
+
colour.maxX = Math.max(colour.maxX, x)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return found
|
|
44
|
+
}"""
|
|
45
|
+
|
|
46
|
+
_TWO_FRAMES = "() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _coordinate_axis():
|
|
50
|
+
fig, ax = apl.subplots(1, 1, figsize=(300, 300))
|
|
51
|
+
return fig, ax.axes2d(xlim=(0.0, 1.0), ylim=(0.0, 1.0), aspect="equal")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _wheel_zoom(page):
|
|
55
|
+
page.mouse.move(GRID_PAD + 150, GRID_PAD + 130)
|
|
56
|
+
for _ in range(3):
|
|
57
|
+
page.mouse.wheel(0, -240)
|
|
58
|
+
page.wait_for_timeout(50)
|
|
59
|
+
page.evaluate(_TWO_FRAMES)
|
|
60
|
+
page.wait_for_timeout(100)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestRasterSurvivesZoom:
|
|
64
|
+
def test_a_wheel_zoom_keeps_the_raster_and_the_markers_after_it(self, interact_page):
|
|
65
|
+
fig, coordinates = _coordinate_axis()
|
|
66
|
+
coordinates.add_raster(np.tile(np.array(RED, np.uint8), (8, 8, 1)),
|
|
67
|
+
extent=(0.0, 1.0, 0.0, 1.0))
|
|
68
|
+
# Drawn after the raster, so a raster that throws takes it down too.
|
|
69
|
+
coordinates.scatter([0.5], [0.5], s=10, c="#00ff00", edgecolors="#00ff00")
|
|
70
|
+
page = interact_page(fig)
|
|
71
|
+
before = page.evaluate(_COLOUR_EXTENTS)
|
|
72
|
+
assert before["red"]["count"] > 1000, f"the raster was not drawn to begin with: {before}"
|
|
73
|
+
assert before["green"]["count"] > 20, f"the marker was not drawn to begin with: {before}"
|
|
74
|
+
|
|
75
|
+
_wheel_zoom(page)
|
|
76
|
+
|
|
77
|
+
after = page.evaluate(_COLOUR_EXTENTS)
|
|
78
|
+
assert after["red"]["count"] > 1000, f"zooming blanked the raster: {before} -> {after}"
|
|
79
|
+
assert after["green"]["count"] > 20, f"zooming blanked the marker: {before} -> {after}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestRasterRedecodes:
|
|
83
|
+
def test_the_same_bytes_in_a_new_shape_are_decoded_again(self, interact_page):
|
|
84
|
+
# Two red pixels then two green ones: as 2×2 the red row sits above the
|
|
85
|
+
# green one; as 4×1 the red half sits left of the green half.
|
|
86
|
+
fig, coordinates = _coordinate_axis()
|
|
87
|
+
square = np.array([[RED, RED], [GREEN, GREEN]], dtype=np.uint8)
|
|
88
|
+
coordinates.add_raster(square, extent=(0.0, 1.0, 0.0, 1.0))
|
|
89
|
+
page = interact_page(fig)
|
|
90
|
+
stacked = page.evaluate(_COLOUR_EXTENTS)
|
|
91
|
+
assert stacked["red"]["maxX"] > stacked["green"]["minX"] + 50, \
|
|
92
|
+
f"the 2x2 raster should put red above green: {stacked}"
|
|
93
|
+
|
|
94
|
+
page.evaluate("""(panelId) => {
|
|
95
|
+
const name = 'panel_' + panelId + '_geom'
|
|
96
|
+
const geom = JSON.parse(window._aplModel.get(name))
|
|
97
|
+
for (const id in geom.raster_geom) {
|
|
98
|
+
geom.raster_geom[id].image_width = 4
|
|
99
|
+
geom.raster_geom[id].image_height = 1
|
|
100
|
+
}
|
|
101
|
+
window._aplModel.set(name, JSON.stringify(geom))
|
|
102
|
+
}""", coordinates._id)
|
|
103
|
+
page.evaluate(_TWO_FRAMES)
|
|
104
|
+
|
|
105
|
+
side_by_side = page.evaluate(_COLOUR_EXTENTS)
|
|
106
|
+
assert side_by_side["red"]["count"] > 1000 and side_by_side["green"]["count"] > 1000, \
|
|
107
|
+
f"the 4x1 raster lost a colour: {side_by_side}"
|
|
108
|
+
assert side_by_side["red"]["maxX"] <= side_by_side["green"]["minX"] + 2, \
|
|
109
|
+
f"the 4x1 raster was drawn from the stale 2x2 image: {side_by_side}"
|
|
@@ -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.10.
|
|
8
|
-
<link rel="canonical" href="v0.10.
|
|
7
|
+
<meta http-equiv="refresh" content="0; url=v0.10.1/" />
|
|
8
|
+
<link rel="canonical" href="v0.10.1/" />
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<p>
|
|
12
|
-
Redirecting to <a href="v0.10.
|
|
12
|
+
Redirecting to <a href="v0.10.1/">v0.10.1 documentation</a>…
|
|
13
13
|
</p>
|
|
14
14
|
</body>
|
|
15
15
|
</html>
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
"version": "dev",
|
|
5
5
|
"url": "https://cssfrancis.github.io/anyplotlib/dev/"
|
|
6
6
|
},
|
|
7
|
+
{
|
|
8
|
+
"name": "v0.10.1 (stable)",
|
|
9
|
+
"version": "v0.10.1",
|
|
10
|
+
"url": "https://cssfrancis.github.io/anyplotlib/v0.10.1/"
|
|
11
|
+
},
|
|
7
12
|
{
|
|
8
13
|
"name": "v0.10.0 (stable)",
|
|
9
14
|
"version": "v0.10.0",
|
|
@@ -17,7 +17,7 @@ from _sg_html_scraper import ViewerScraper # noqa: E402
|
|
|
17
17
|
project = "anyplotlib"
|
|
18
18
|
copyright = "2026, anyplotlib contributors"
|
|
19
19
|
author = "anyplotlib contributors"
|
|
20
|
-
release = "0.10.
|
|
20
|
+
release = "0.10.1"
|
|
21
21
|
|
|
22
22
|
# When built in CI the workflow sets DOCS_VERSION to the tag name (e.g.
|
|
23
23
|
# "v0.1.0") or "dev". Fall back to "dev" for local builds.
|