From 775df92ce5d31ed83b147c8c98cfa8ccb4c34581 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 16 Jul 2025 20:11:42 -0400 Subject: [PATCH 01/52] Update CODE_OF_CONDUCT.md (#879) --- CODE_OF_CONDUCT.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0ae81f6f0..d73264986 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -105,8 +105,11 @@ advice, in confidence. You can report issues to the fastplotlib core team: -[Kushal Kolar](https://github.com/kushalkolar) -[Caitlin Lewis](https://github.com/clewis7) +Kushal Kolar: +- kushal {at} fastplotlib.org + +Caitlin Lewis: +- caitlin {at} fastplotlib.org If your report involves any members of the fastplotlib core team, or if they feel they have a conflict of interest in handling it, then they will recuse themselves from From 11fed41883bc18d6f64d467e799657cba695f29d Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 17 Jul 2025 10:55:23 +0200 Subject: [PATCH 02/52] Tweaks to adjust to new pygfx blending (#838) * Tweaks to adjust to new pygfx blending * disable ppaa for screenshot tests * also clear for overlay * pin imgui-bundle * update ground truth screenshots to not use AA * update nb screenshot to not use AA --------- Co-authored-by: Kushal Kolar --- .../notebooks/screenshots/nb-astronaut.png | 4 ++-- .../screenshots/nb-astronaut_RGB.png | 4 ++-- examples/notebooks/screenshots/nb-camera.png | 4 ++-- .../nb-image-widget-movie-set_data.png | 4 ++-- .../nb-image-widget-movie-single-0-reset.png | 4 ++-- .../nb-image-widget-movie-single-0.png | 4 ++-- .../nb-image-widget-movie-single-279.png | 4 ++-- ...e-widget-movie-single-50-window-max-33.png | 4 ++-- ...-widget-movie-single-50-window-mean-13.png | 4 ++-- ...-widget-movie-single-50-window-mean-33.png | 4 ++-- ...ge-widget-movie-single-50-window-reset.png | 4 ++-- .../nb-image-widget-movie-single-50.png | 4 ++-- .../nb-image-widget-single-gnuplot2.png | 4 ++-- .../screenshots/nb-image-widget-single.png | 4 ++-- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 ++-- ...idget-zfish-frame-50-frame-apply-reset.png | 4 ++-- ...ge-widget-zfish-frame-50-max-window-13.png | 4 ++-- ...e-widget-zfish-frame-50-mean-window-13.png | 4 ++-- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 ++-- .../nb-image-widget-zfish-frame-50.png | 4 ++-- .../nb-image-widget-zfish-frame-99.png | 4 ++-- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 ++-- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 ++-- ...dget-zfish-grid-frame-50-max-window-13.png | 4 ++-- ...get-zfish-grid-frame-50-mean-window-13.png | 4 ++-- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-99.png | 4 ++-- ...e-widget-zfish-grid-init-mean-window-5.png | 4 ++-- ...fish-grid-set_data-reset-indices-false.png | 4 ++-- ...zfish-grid-set_data-reset-indices-true.png | 4 ++-- ...-image-widget-zfish-init-mean-window-5.png | 4 ++-- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 ++-- ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 4 ++-- ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 ++-- .../notebooks/screenshots/nb-lines-3d.png | 4 ++-- .../notebooks/screenshots/nb-lines-colors.png | 4 ++-- .../notebooks/screenshots/nb-lines-data.png | 4 ++-- .../screenshots/nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/nb-lines.png | 4 ++-- .../screenshots/no-imgui-nb-astronaut.png | 4 ++-- .../screenshots/no-imgui-nb-astronaut_RGB.png | 4 ++-- .../screenshots/no-imgui-nb-camera.png | 4 ++-- .../screenshots/no-imgui-nb-lines-3d.png | 4 ++-- .../screenshots/no-imgui-nb-lines-colors.png | 4 ++-- .../screenshots/no-imgui-nb-lines-data.png | 4 ++-- .../no-imgui-nb-lines-underlay.png | 4 ++-- .../screenshots/no-imgui-nb-lines.png | 4 ++-- examples/screenshots/extent_frac_layout.png | 4 ++-- examples/screenshots/extent_layout.png | 4 ++-- examples/screenshots/gridplot.png | 4 ++-- examples/screenshots/gridplot_non_square.png | 4 ++-- .../screenshots/gridplot_viewports_check.png | 4 ++-- examples/screenshots/heatmap.png | 4 ++-- examples/screenshots/image_cmap.png | 4 ++-- examples/screenshots/image_rgb.png | 4 ++-- examples/screenshots/image_rgbvminvmax.png | 4 ++-- examples/screenshots/image_simple.png | 4 ++-- examples/screenshots/image_small.png | 4 ++-- examples/screenshots/image_vminvmax.png | 4 ++-- examples/screenshots/image_widget.png | 4 ++-- examples/screenshots/image_widget_grid.png | 5 ++--- examples/screenshots/image_widget_imgui.png | 4 ++-- .../screenshots/image_widget_single_video.png | 4 ++-- examples/screenshots/image_widget_videos.png | 4 ++-- .../image_widget_viewports_check.png | 4 ++-- examples/screenshots/imgui_basic.png | 4 ++-- examples/screenshots/line.png | 4 ++-- examples/screenshots/line_cmap.png | 4 ++-- examples/screenshots/line_cmap_more.png | 4 ++-- examples/screenshots/line_collection.png | 4 ++-- .../line_collection_cmap_values.png | 4 ++-- ...ine_collection_cmap_values_qualitative.png | 4 ++-- .../screenshots/line_collection_colors.png | 4 ++-- .../screenshots/line_collection_slicing.png | 4 ++-- examples/screenshots/line_colorslice.png | 4 ++-- examples/screenshots/line_dataslice.png | 4 ++-- examples/screenshots/line_stack.png | 4 ++-- .../linear_region_selectors_match_offsets.png | 4 ++-- examples/screenshots/linear_selector.png | 4 ++-- .../no-imgui-extent_frac_layout.png | 4 ++-- .../screenshots/no-imgui-extent_layout.png | 4 ++-- examples/screenshots/no-imgui-gridplot.png | 4 ++-- .../no-imgui-gridplot_non_square.png | 4 ++-- .../no-imgui-gridplot_viewports_check.png | 4 ++-- examples/screenshots/no-imgui-heatmap.png | 4 ++-- examples/screenshots/no-imgui-image_cmap.png | 4 ++-- examples/screenshots/no-imgui-image_rgb.png | 4 ++-- .../no-imgui-image_rgbvminvmax.png | 4 ++-- .../screenshots/no-imgui-image_simple.png | 4 ++-- examples/screenshots/no-imgui-image_small.png | 4 ++-- .../screenshots/no-imgui-image_vminvmax.png | 4 ++-- examples/screenshots/no-imgui-line.png | 4 ++-- examples/screenshots/no-imgui-line_cmap.png | 4 ++-- .../screenshots/no-imgui-line_cmap_more.png | 4 ++-- .../screenshots/no-imgui-line_collection.png | 4 ++-- .../no-imgui-line_collection_cmap_values.png | 4 ++-- ...ine_collection_cmap_values_qualitative.png | 4 ++-- .../no-imgui-line_collection_colors.png | 4 ++-- .../no-imgui-line_collection_slicing.png | 4 ++-- .../screenshots/no-imgui-line_colorslice.png | 4 ++-- .../screenshots/no-imgui-line_dataslice.png | 4 ++-- examples/screenshots/no-imgui-line_stack.png | 4 ++-- ...-linear_region_selectors_match_offsets.png | 4 ++-- .../screenshots/no-imgui-linear_selector.png | 4 ++-- .../screenshots/no-imgui-rect_frac_layout.png | 4 ++-- examples/screenshots/no-imgui-rect_layout.png | 4 ++-- .../no-imgui-scatter_cmap_iris.png | 4 ++-- .../no-imgui-scatter_colorslice_iris.png | 4 ++-- .../no-imgui-scatter_dataslice_iris.png | 4 ++-- .../screenshots/no-imgui-scatter_iris.png | 4 ++-- .../screenshots/no-imgui-scatter_size.png | 4 ++-- examples/screenshots/rect_frac_layout.png | 4 ++-- examples/screenshots/rect_layout.png | 4 ++-- examples/screenshots/scatter_cmap_iris.png | 4 ++-- .../screenshots/scatter_colorslice_iris.png | 4 ++-- .../screenshots/scatter_dataslice_iris.png | 4 ++-- examples/screenshots/scatter_iris.png | 4 ++-- examples/screenshots/scatter_size.png | 4 ++-- examples/tests/test_examples.py | 22 +++++++++++++------ .../graphics/selectors/_base_selector.py | 3 +-- fastplotlib/layouts/_figure.py | 7 ++++++ 122 files changed, 261 insertions(+), 248 deletions(-) diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 2370c5988..4221e7c6a 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a6e8bb3c72f1be6915e8e78c9a4f269419cfb4faded16e39b5cb11d70bec247 -size 64185 +oid sha256:2ce5e7e3fa03f5450948725f0a983547a66601a5b80f0423d0e99c6a8751cf25 +size 65875 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index 2a7eac585..392c40cdc 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f9f32e86018f87057435f7121b02bbe98823444babb330645bab618e1d586b7 -size 63838 +oid sha256:7d2a7c9fdaa8e334bb7fc4633e0a6e7b52dba7b5b492b48cc3e339e83bcac30d +size 63881 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index bfe226ca4..13d53f996 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2964d0150b38f990a7b804e9057f99505e8c99bb04538a13137989d540704593 -size 47456 +oid sha256:3bde9f5050e83ef47dbfa7fae421abd58e5d97eb6ed4db135c296702d8e78ab4 +size 49720 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 2578ad028..f9194e2fd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78e7e99fafc15cc6edf53cfb2e5b679623ad14e0d594e0ad615088e623be22e1 -size 60988 +oid sha256:db3c05f59d37df596a669106ad01f67ce335a25a0e601a22438de2b9bb77b3dc +size 63059 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 0129cb423..3253f0426 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3f5a721456b5a54e819fc987b8fa1f61d638f578339a7332ad46a22e7aa8fc0 -size 112674 +oid sha256:253ff39f89c71b0bb2f0a2b51d9a7f47a35a0e92651881d6180ac541ff26861d +size 113825 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 0129cb423..3253f0426 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3f5a721456b5a54e819fc987b8fa1f61d638f578339a7332ad46a22e7aa8fc0 -size 112674 +oid sha256:253ff39f89c71b0bb2f0a2b51d9a7f47a35a0e92651881d6180ac541ff26861d +size 113825 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 4908c8b59..29ed34a10 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4511a28e728af412f5006bb456f133aea1fdc9c1922c3174f127c79d9878401d -size 133635 +oid sha256:a727bd83bca1b5c5e35607e34062265b1058683dc3bd67e4ab5afba09253734d +size 136027 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index cfdc3c8a9..d209d78bb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6910106cd799a4327a6650edbc956ddb9b6a489760b86b279c593575ae805b8 -size 120114 +oid sha256:d50360d41aa5e9e7aee9745187da1be087dc0fca10bc2ba32bcc862e35f2a2ef +size 122211 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 92513cf5b..97157abca 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8233dfc429a7fefe96f0fdb89eb2c57188b7963c16db5d1d08f7faefb45d8cb7 -size 105755 +oid sha256:d2059f2f5d7765ebf3c318978acb2c19dd6036e760ea3e070fbbfe2bf2d4a27a +size 107169 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 8bce59baf..b7af3202d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4af684cdaec8f98081862eb8a377cd419efec64cdf08b662a456276b78f1fb5 -size 98091 +oid sha256:eb9a22d365dd08a3a3be2c7065adf5d44c1d67320af36629ce2e5a8ebf527f71 +size 99261 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 61c3c4f6c..b1bc3414f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:133dfe6b0028dda6248df1afde1288c57625be99b25c8224673597de4d4f70fc -size 118588 +oid sha256:2b2f75ca1d712ee3bb70f5b6f07c50660464a014490652d86e9a3a328c3cd54b +size 119942 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 61c3c4f6c..b1bc3414f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:133dfe6b0028dda6248df1afde1288c57625be99b25c8224673597de4d4f70fc -size 118588 +oid sha256:2b2f75ca1d712ee3bb70f5b6f07c50660464a014490652d86e9a3a328c3cd54b +size 119942 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index e8c02adfe..c96a6de51 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c8562f8e1178cf21af98af635006c64010f3c5fc615533d1df8c49479232843 -size 217693 +oid sha256:5f1ca8a4c1ca0f02a6d1217c13f740407073cf70730d5455c81344a5753c4dc7 +size 224053 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 8de4099fb..cf60b005f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c9bae3c9c5521a4054288be7ae548204fc7b0eafbc3e99cb6b649e0be797169 -size 207176 +oid sha256:dde00bbf68eec1d531a19649cb6814d360073f0920837ad965cf816b308fdf97 +size 215376 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 29fe20f44..68d204849 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87a3947d6c59c7f67acca25911e0ab93ddc9231a8c3060d2fffe3c53f39055f2 -size 62263 +oid sha256:f90eb33b10caad47dc47425d517297de86754178e3f6764c30c0066227dd0d76 +size 62383 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index c7944f591..d35a258d1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57c65974362d258ec7be8de391c41d7909ed260b92411f4b0ed8ed03b886a29 -size 73040 +oid sha256:7ec728806cdad8aa89e74095c12637d5ef36c95d967b1ead58c6a6b4d750471d +size 67108 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index eb9c9059d..c8559bc33 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:008381b267ae26e8693ae51e7a4fabc464288ec8aa911ff3a1deb37543cc4fbe -size 115543 +oid sha256:131ea555e5616caed42420c3fa8f1443f14725aa37c44ad526fb45f041d79d3f +size 110570 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index 8b887f5fd..e50e5cce9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fedfec781724d4731f8cc34ffc39388d14dc60dad4a9fae9ff56625edf11f87a -size 94178 +oid sha256:9fecadbc230f00207f4b237205762bde7213e47eb9d8dc5edd24ef844a7170f5 +size 94421 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index ef3aa7a92..7fcb45dcc 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08e8379187754fa14f360ed54f2ed8cf61b3df71a8b6f2e95ff1ed27aa435d60 -size 90105 +oid sha256:f8b7efaf332e0344c9e4dd465cffad2e9bcd317c342363ebd56bd40fc7a84d4c +size 87259 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index c7944f591..70710ca02 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57c65974362d258ec7be8de391c41d7909ed260b92411f4b0ed8ed03b886a29 -size 73040 +oid sha256:54d256826fce09ddf226d6d53679a17e5bee75f510603f897628fd842c5893ba +size 67103 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 0d19a35ce..b36332164 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:848e89e38b9b5ef97d6bb4b301c0ae10cc29f438518721663ae52fa42f492408 -size 65267 +oid sha256:353d8f0e370489b1ad51cfc71cfb8a6748d156541f7f70f5d16343209166da31 +size 59202 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 96a3b12c8..a6c3ac9c6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17cd05ae14cacdef6aa1eca3544246b814ef21762a33f6e785f6d621ea30ff96 -size 80570 +oid sha256:9ca29700c76cfd6396a181b43adacefc31c98cc3d4d9d0c92eada990e477452e +size 84321 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 1df19c904..906af2702 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a673fa1ffa6f746ab9f462b4d592492ec02bfdd3fb53bdf1f71fb9427f8d6d23 -size 105798 +oid sha256:b388954425e2cb45e7982eaab92db51889cf86e21989f5f926ace1ca565bdf90 +size 101869 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 43230f8be..0fc2ff6a1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:446d54cea3d54b0fd92b70abcc090cfee30b19454dce118d9875fbeb8b40b4a8 -size 141294 +oid sha256:ae0a4201dc239e6d6d187ab9d281a1e15210c9f0c3278f404da86337ec597a05 +size 140995 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 0841a8e08..948852987 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99d3706d5574a1236264f556eb3ce6d71e81b65bd8dcce1c1415e5f139316c23 -size 107894 +oid sha256:42d9880fd977db69748ee548f058a7d9c7a5fc69cbd38830e3138467070e24c7 +size 113861 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 28bab9f02..e0cc721d3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffa17fc1b71c5146cae88493ed40c606dd0a99f3e10f3827ac349d5a5d6f6108 -size 112702 +oid sha256:b6c378ccbe50cfd547c2edaa034c7064564eb325ce504659f57325c0991a11f6 +size 115883 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 1df19c904..13b30b0c2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a673fa1ffa6f746ab9f462b4d592492ec02bfdd3fb53bdf1f71fb9427f8d6d23 -size 105798 +oid sha256:19aa140345ac26b6b5542d44334b748408db54881bf73a4b88be66498a2d9433 +size 101937 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 06ed02628..f66664d4d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d3e88eee05bc68dd17918197602fb5c0a959ad74a4f592aea4514e570d29232 -size 103431 +oid sha256:e48ab3a730132a773f784876e173b2fe3165d70121489a4c08270b9bfdc8fdbf +size 99569 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 61702a6d9..e27d39419 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:272156c4261bba40eba92f953a0f5078ad8ff2aa80f06a53f73a3572eb537dd5 -size 111155 +oid sha256:df90973bbba10bab86152973359e0ddbb62dc2b1b9af38cb9b557166edd547f8 +size 110530 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 412822a40..40a7a2a96 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8203f859fe54e2b59a143a9a569c2854640b1501b9ab4f8512520bbf73dae3c6 -size 105658 +oid sha256:c59705a9ca68af29502ec96407cf1690d8852ad871a00b2b1f51a9a3edd90757 +size 101753 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 234924487..8e3be816c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca187ba67e7928c8f96b1f9a0a18bec65f81352701e60c33d47aaadb2756d5c -size 106446 +oid sha256:6c91eca03cd88468aa24f3abca2dbf4a5b336a522749af782df2e4afe9e0d2c6 +size 102718 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 870945ce7..b49eb6f20 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f42367c833a23d3fe10c6fb0d754338c12a30288d9769ad3f8b1159505abf8ff -size 78796 +oid sha256:66158475df991caa5fab6821e0eec5a29ccc41f989f6d691d1c315a4892132fe +size 74392 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 7880fc1d8..9c4e54556 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb99cd81a18fa2f8986c5f00071c45dc778c8aa177f4b02dca6bc5fab122b054 -size 114825 +oid sha256:e6cf738aff9bf8254f4141fc7d8fee4e74e2b65dab7f7799a3fb0c4698d1213b +size 112509 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 82f3d0a9b..117451b7e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31b2b92b9d983950b58b90a09f16199740e35a0737fc1b18904f507ea322d8f2 -size 111118 +oid sha256:af5897e58e484a5b2f3713c663019eda7e215e1edd6e9867413611bd9ed9df0e +size 107951 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 1446c8941..8c60e9557 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fb724e005c6e081ae3bf235e155f3f526c3480facac7479d9b9452aae81baf0 -size 111437 +oid sha256:e4eab140f2b539d0916d24d8d6704e6836cb0e3697985580e5b6fa44473781a3 +size 109170 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index fb84ef21a..b24a62173 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c70c01b3ade199864df227a44fb28a53626df3beecee722a7b782c9a9f4658d8 -size 19907 +oid sha256:49a929160a3b1765e74a9972a4ddbc869d9a1af1599f10baf6e491b2d0680639 +size 18827 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index ab221d83f..1c645ee4e 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b238b085eddb664ff56bd265423d85b35fc70769ebec050b27fefa8fe6380de -size 35055 +oid sha256:8920126b05d3359c7d8b308b0c42553e42d9791381e6d60b5c2c02eadccefd7d +size 32679 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 44b142f55..c05f09a43 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4df736ec3ea90478930a77437949977f8e30f7d9272f65ef9f4908f2103dd11e -size 40679 +oid sha256:3302edd24977282850fae6b73f2e96a296f2b7163acc9de6f8df7b9e6ba48bd6 +size 39894 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index f4a5b4e76..dfed9dbe3 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a8b59386015b4c1eaa85c33c7b041d566ac1ac76fbba829075e9a3af021bedf -size 46228 +oid sha256:94865d4a62545e53fd0d2967177b24c0896a64be28c9d6dcb9395cea5c09b7aa +size 52034 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 8c86b48d0..8e7507793 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:823558e877830b816cc87df0776a92d5316d98a4f40e475cbf997b597c5eb8de -size 30338 +oid sha256:85329a9e4a60d4a2202b61c33d02ff7d6fb7d9edb1593d8e8e6fef6c6c58ca46 +size 27303 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png index 9f9e2013a..efc7baa98 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4758a94e6c066d95569515c0bff8e4c9ec383c65c5928a827550c142214df085 -size 72372 +oid sha256:c80e71dde6812321add9ae9f84c86f9f7d28a65f420da5bd61c4abe537ac7d1c +size 74000 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png index 23d1bd906..4d95ad3c9 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb3c72edc6f41d6f77e44bc68e7f5277525d2548d369925827c14d855dc33bbd -size 71588 +oid sha256:995908845e4e5a21c2ed45de2cdd251386ebfb3f6d2e8f1a420b39944d79347d +size 71874 diff --git a/examples/notebooks/screenshots/no-imgui-nb-camera.png b/examples/notebooks/screenshots/no-imgui-nb-camera.png index 22c70a760..cc8bc83cb 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-camera.png +++ b/examples/notebooks/screenshots/no-imgui-nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6de3880cc22a8f6cdb77305e4d5be520fe92fd54a9a107bdbddf1e6f72c19262 -size 52157 +oid sha256:969a733add867c12f05439b8280bd594e97061a06b56676e700441f8d16c7697 +size 55147 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 1a5a7b548..35be289f6 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0e63c918aac713af2015cb85289c9451be181400834b0f60bcbb50564551f08 -size 20546 +oid sha256:2a796f29923d0a051e0dde891e6fbdb3a796871ec028581c03fb9e648b156968 +size 19246 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index cdce4bf46..aa36be9f3 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bd481f558907ac1af97bd7ee08d58951bada758cc32467c73483fa66e4602f8 -size 36206 +oid sha256:2ae2ce951dba034032df0d708344a84c8421e93f5943738132e924ad5a990772 +size 33612 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 8923be766..9245cf06d 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea39e2651408431ad5e49af378828a41b7b377f7f0098adc8ce2c7b5e10d0234 -size 43681 +oid sha256:95580aef77da982bc42e4dd27383f5b407dadfbb9dd06fc6ed7aabb09a020d51 +size 42595 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index b6b4cf340..4e44f5e72 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a8d4aba2411598ecae1b7f202fbb1a1fa7416a814b7b4c5fdd1e0e584cdb06a -size 49343 +oid sha256:0775cdf2f528cd987622c4431ddfcbc10a1f57766dadaac7bae35f8c8e9c1ed3 +size 55353 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 5d03421a4..9e0273dd9 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2fdaf79703c475521184ab9dc948d3e817160b0162e9d88fcb20207225d0233 -size 31153 +oid sha256:05f9619a5acf17978e3597fb2ef40c47ae63b9c6f8e39b9f5ff8e936b5e8e00c +size 27681 diff --git a/examples/screenshots/extent_frac_layout.png b/examples/screenshots/extent_frac_layout.png index 7fe6d3d37..1df76dc61 100644 --- a/examples/screenshots/extent_frac_layout.png +++ b/examples/screenshots/extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f -size 156297 +oid sha256:fc8ec12042a36992dbda00d04f251b47d58cd6d75eca5c4240cd207878f9282f +size 157607 diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png index dec391ac2..fa89b9afc 100644 --- a/examples/screenshots/extent_layout.png +++ b/examples/screenshots/extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 -size 123409 +oid sha256:823785f993237cb0f2773aea62c3dbbe16fe63f86f83867f1e27b82c8793da9f +size 126051 diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png index 08e6d6b78..27eb82675 100644 --- a/examples/screenshots/gridplot.png +++ b/examples/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f424ec68dbc0761566cd147f3bf5b8f15e4126c3b30b2ff47b6fb48f04d512a -size 252269 +oid sha256:07a1e902cb6ed8c95df8adb4091b2a825ffecbf0e26a9dcffc0d1272a886d7d9 +size 261210 diff --git a/examples/screenshots/gridplot_non_square.png b/examples/screenshots/gridplot_non_square.png index 781de8749..d11a9f104 100644 --- a/examples/screenshots/gridplot_non_square.png +++ b/examples/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ac9ee6fd1118a06a1f0de4eee73e7b6bee188c533da872c5cbaf7119114414f -size 194385 +oid sha256:e40be809e47d7a19fbf1a292a575344414acf5a23609f1796041dd8b91362c97 +size 200969 diff --git a/examples/screenshots/gridplot_viewports_check.png b/examples/screenshots/gridplot_viewports_check.png index b1faf9b69..dba29a356 100644 --- a/examples/screenshots/gridplot_viewports_check.png +++ b/examples/screenshots/gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67dd50d61a0caaf563d95110f99fa24c567ddd778a697715247d697a1b5bb1ac -size 46667 +oid sha256:51080904e1156d0427834195141f9c08270f98dbef55a3c3f2fa2e35037814c6 +size 77856 diff --git a/examples/screenshots/heatmap.png b/examples/screenshots/heatmap.png index defcca301..34f139e19 100644 --- a/examples/screenshots/heatmap.png +++ b/examples/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0789d249cb4cfad21c9f1629721ade26ed734e05b1b13c3a5871793f6271362b -size 91831 +oid sha256:2235079838197971d340f07bd12998e371b4db54e7136e60dbcb8b43f44561a4 +size 92210 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png index 0301d2ed4..2621e7581 100644 --- a/examples/screenshots/image_cmap.png +++ b/examples/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2bbb79716fecce08479fbe7977565daccadf4688c8a99e155db297ecce4c484 -size 199979 +oid sha256:cc9dae363b1490506dda0c7d1aa1fd24232e3daa484178d8a9e88f49a1aa2058 +size 209014 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png index 11129ceaa..e20121cde 100644 --- a/examples/screenshots/image_rgb.png +++ b/examples/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23024936931651cdf4761f2cafcd8002bb12ab86e9efb13ddc99a9bf659c3935 -size 226879 +oid sha256:5500a673085eb0d7d4293bba7f4963482c234cd80622179f2e476e58712cbfd4 +size 233875 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png index afe4de6f7..39a0707d5 100644 --- a/examples/screenshots/image_rgbvminvmax.png +++ b/examples/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 -size 50231 +oid sha256:758869786f243a1c1a2e2214f70f788e0decc5854f2825bd4133008aa481690e +size 46215 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png index 702a1ac5c..44e6fc62f 100644 --- a/examples/screenshots/image_simple.png +++ b/examples/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3eb6f03364226e9f1aae72f6414ad05b0239a15c2a0fbcd71d3718fee477e2c -size 199468 +oid sha256:b726be63c21f174d269441978adda9163759453fd210e60f9027ec45ebe8fbb7 +size 208692 diff --git a/examples/screenshots/image_small.png b/examples/screenshots/image_small.png index d17cb7ab2..c52a160ae 100644 --- a/examples/screenshots/image_small.png +++ b/examples/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dcfc7b8a964db9a950bf4d3217fb171d081251b107977f9acd612fcd5fb0be1 -size 14453 +oid sha256:61375cfac467c64ec7a49b830edf602943613605a82619ee006e8cb94be47f1e +size 14950 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png index afe4de6f7..39a0707d5 100644 --- a/examples/screenshots/image_vminvmax.png +++ b/examples/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fb9cd6d32813df6a9e3bf183f73cb69fdb61d290d7f2a4cc223ab34301351a1 -size 50231 +oid sha256:758869786f243a1c1a2e2214f70f788e0decc5854f2825bd4133008aa481690e +size 46215 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index 23d34ae50..b088518a8 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:220ebb5286b48426f9457b62d6e7f9fe61b5a62b8874c7e010e07e146ae205a5 -size 184633 +oid sha256:4d88b1f2d47b7e3a9b7eb01cf4fce3a061e17e260d04c52c8ac2c7bb11cc27e5 +size 189495 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index a6ccd144a..84ac3f59f 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,4 +1,3 @@ version https://git-lfs.github.com/spec/v1 - -oid sha256:430cd0ee5c05221c42073345480acbeee672c299311f239dc0790a9495d0d758 -size 248046 +oid sha256:ed702632b7e58b2dfaa19d15ad4d234ef08d72fc222d21e43ff934d664ed0b18 +size 254523 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index cb165cc86..934db35ab 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7522a35768d013a257e3cf3b00cce626b023b169484e035f46c635efc553b0bf -size 165747 +oid sha256:0a23cbd21c96fe161de7621f7a733d2e6a67c697efae46d38258443fbd3160b7 +size 173705 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index aa757a950..a3d2c5686 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f0843f4693460ae985c1f33d84936fbcc943d0405e0893186cbee7a5765dbc0 -size 90283 +oid sha256:dae1a4f7614222b9163256257df09c1e01abf6f066ebf59faf82c3134fe0c420 +size 99410 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index 2e289ae3c..dd8dfb8c1 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eec22392f85db1fd375d7ffa995a2719cf86821fe3fe85913f4ab66084eccbf9 -size 290587 +oid sha256:9e19ea5b2e209461926fe343ccf83422d180506da899aa621519b4921dda0d24 +size 335631 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png index 662432e59..d5b4010c7 100644 --- a/examples/screenshots/image_widget_viewports_check.png +++ b/examples/screenshots/image_widget_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c4449f7e97375aa9d7fe1d00364945fc86b568303022157621de21a20d1d13e -size 93914 +oid sha256:91a2c5bae1796ed9605f4de65dba14151ad8deaf253e3fb0aa043a4579da708d +size 117505 diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index 1ff9952a9..39817dd97 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09cc7b0680e53ae1a2689b63f9b0ed641535fcffc99443cd455cc8d9b6923229 -size 36218 +oid sha256:c1bc5f46e04c2a1098b2a7d7f8bf4c4915cd74af40bb55b4fb1d111d82638843 +size 34503 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png index 02603b692..99f87d243 100644 --- a/examples/screenshots/line.png +++ b/examples/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bfaa54bde0967463413ecd2defa8ca18169d534163cc8b297879900e812fee8 -size 167012 +oid sha256:9985e9fc1c58305bca43e00f35ea6017f916652100d2e463e8ea01a0df3b55d6 +size 232883 diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png index 1ecc930e4..1c6352ba0 100644 --- a/examples/screenshots/line_cmap.png +++ b/examples/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0503c008f8869dcf83793c21b15169a93558988c1a5c4edfd2aa93c549d25e1 -size 49343 +oid sha256:3e893699989c9cf04d7b0d90070b85df5fa6135a363f6e447032aff6020bd476 +size 46137 diff --git a/examples/screenshots/line_cmap_more.png b/examples/screenshots/line_cmap_more.png index 4bf597e8b..39d0fcaa3 100644 --- a/examples/screenshots/line_cmap_more.png +++ b/examples/screenshots/line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab4d759dd679a2959c0fda724e7b7a1b7593d6f67ce797f08a5292dd0eb74fb1 -size 125023 +oid sha256:3606f8c1bf83d537275a6b31ae9b9a392c73cb082e82215e348f6deec796aa8e +size 113003 diff --git a/examples/screenshots/line_collection.png b/examples/screenshots/line_collection.png index 382132770..0397d2dde 100644 --- a/examples/screenshots/line_collection.png +++ b/examples/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3b6b973a52f7088536a4f437be2a7f6ebb2787756f9170145a945c53e90093c -size 98950 +oid sha256:5d76339dbfdd246442312d9853b8d57de773f2a6b2337e75d85a2e3e7bfbee0e +size 88722 diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/screenshots/line_collection_cmap_values.png index c00bffdb6..19ce6ab73 100644 --- a/examples/screenshots/line_collection_cmap_values.png +++ b/examples/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45bb6652f477ab0165bf59e504c1935e5781bceea9a891fcfa9975dec92eef4b -size 64720 +oid sha256:7d961d5a91b71d0042c652cc2196ec99586f41cd01a232d3940c41f9948cf4f6 +size 58191 diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/screenshots/line_collection_cmap_values_qualitative.png index 662d3254d..264531b15 100644 --- a/examples/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e5b5cb45e78ae24d72f3cb84e482fac7bf0a98cd9b9b934444d2e67c9910d57 -size 66565 +oid sha256:2fb808787bcd5abd39c9861ff38a4cc8740ca7bc7d0cb35249e7150c80633bb0 +size 61936 diff --git a/examples/screenshots/line_collection_colors.png b/examples/screenshots/line_collection_colors.png index 3b90e5b4c..fbb10cf96 100644 --- a/examples/screenshots/line_collection_colors.png +++ b/examples/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4edf84af27535e4a30b48906ab3cacaeb38d073290828df3c5707620e222b4d3 -size 58635 +oid sha256:8396aa29f5246cb1f7edce74e6dd41313826f53b810e80b0e69b1ea6bf550cc4 +size 49546 diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index e0537a261..6fdb82fb4 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66933c1fa349ebb4dd69b9bf396acb8f0aeeabbf17a3b7054d1f1e038a6e04be -size 129484 +oid sha256:9235513b219ce076171cc9aa7e2c24c87cdad298a7004f52c5245dbd34b0505f +size 117970 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png index f3374e221..e55f4181f 100644 --- a/examples/screenshots/line_colorslice.png +++ b/examples/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d654aa666ac1f4cfbf228fc4c5fbd2f68eed841c7cc6265637d5b836b918314c -size 57989 +oid sha256:0cb016d754d3ea461fb80ae9c545955b4c6ced182b141c125d70c11665ee0c04 +size 53057 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png index 6ecf63b26..c551e15cf 100644 --- a/examples/screenshots/line_dataslice.png +++ b/examples/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9b93af2028eb0186dd75d74c079d5effdb284a8677e6eec1a7fd2c8de4c8498 -size 70489 +oid sha256:d89f1f3dfc01a522c3fc2528219442ee947db4f22ad7f9afc9a0df014eb9addf +size 69625 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png index 9a9ad4fd6..cefe33da1 100644 --- a/examples/screenshots/line_stack.png +++ b/examples/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b6c2d1ee4c49ff5b193b5105b2794c6b5bd7a089a8a2c6fa03e09e02352aa65 -size 121462 +oid sha256:a24c9a72ef5255c1615f478f01ea5f3957e8ab250702386f43a324b178e7458f +size 106509 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index e6fab4c4d..804efdff8 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2eac8ffeb8cd35a0c65d51b0952defea61928abb53c865e681fa72af4ac4347 -size 95750 +oid sha256:f3b2ca05654ad45e6447ee1b028967808553c63cb1a52fa01a99052d121865b3 +size 94695 diff --git a/examples/screenshots/linear_selector.png b/examples/screenshots/linear_selector.png index 8571d664b..1b25a535e 100644 --- a/examples/screenshots/linear_selector.png +++ b/examples/screenshots/linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62ded18658bc5cb41129d27eb21f47f029cf7c75bb6388b5d72af6fe9c5cada9 -size 130919 +oid sha256:fd7dccdb26a32dec9a4c82028610d8da4058e9f590ae9dbfef558a3a976d64b4 +size 141373 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png index 4dc3b2aa6..cc0a4e6ba 100644 --- a/examples/screenshots/no-imgui-extent_frac_layout.png +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 -size 157321 +oid sha256:6567058d5aef3ca5c7b77b1a6b01743235a305d793b6955714c20d681f994f8f +size 158808 diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png index 16d1ff446..68342d1c7 100644 --- a/examples/screenshots/no-imgui-extent_layout.png +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa -size 138936 +oid sha256:b3a82adc73994400122f8f8b25a9f241352cdb46ecfdbd7f2ee65209d4659bf1 +size 140550 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index 7f870cf76..f2f33bec4 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b31f2002053b5934ae78393214e67717d10bd567e590212eaff4062440657acd -size 292558 +oid sha256:0194c56635cba18a34667e087409f01d6c43f73ac1f52a91d31bb466fa4613c1 +size 303365 diff --git a/examples/screenshots/no-imgui-gridplot_non_square.png b/examples/screenshots/no-imgui-gridplot_non_square.png index e08d64805..03d63f7ad 100644 --- a/examples/screenshots/no-imgui-gridplot_non_square.png +++ b/examples/screenshots/no-imgui-gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ef00db82a3559b4d7c77b68838f5876f98a2b9e80ef9ecb257f32c62161b5e -size 216512 +oid sha256:3d4b08a651868fd27329443c6e385fbd2ff1e354ffa5b69e15cb80b724c8dae8 +size 224336 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png index 2a8c0dc6f..5d94d3ec0 100644 --- a/examples/screenshots/no-imgui-gridplot_viewports_check.png +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6818a7c8bdb29567bb09cfe00acaa6872a046d4d35a87ef2be7afa06c2a8a089 -size 44869 +oid sha256:8feae6c310a5f3faa4fed64804287399c6da6eaa96c237cbd239fd8bb1bf1d4d +size 74948 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index e91d06c4f..ea433a5e7 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875c15e74e7ea2eaa6b00ddbdd80b4775ecb1fe0002a5122371d49f975369cce -size 95553 +oid sha256:8621da2931209b9d9a9908f6d9b40e5dbae0e18397011c17c7ad5d4d056ad56b +size 96368 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 2d42899fc..7e0a9394d 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b43bd64ceec8c5c1287a2df57abf7bd148955d6ba97a425b32ae53bad03a051 -size 216050 +oid sha256:4437a07e42167ccfd5262648e7f41e7bf647c3493cce4d3a9ce564e91554a904 +size 226825 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 6be5205ac..1fe8b992f 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42516cd0719d5b33ec32523dd2efe7874398bac6d0aecb5163ff1cb5c105135f -size 244717 +oid sha256:fe254d9c917404d0beb2c7a77252b19cdc687ddd2f4480f78f381c96657a9fbe +size 251941 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 48d8fff95..9ce63bac6 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 -size 50751 +oid sha256:cc1f5e72146dafb696a31f180983dc2a06beb874337968217bf8c22d122a0b37 +size 46434 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index 1e4487757..89da89f25 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cfa6469803f44a682c9ce7337ae265a8d60749070991e6f3a723eb37c5a9a23 -size 215410 +oid sha256:4727d9ffbe0b54d3e562ec3c4071a2641f94e4e13d3714b8317166e0c58a7316 +size 225876 diff --git a/examples/screenshots/no-imgui-image_small.png b/examples/screenshots/no-imgui-image_small.png index 3613a8139..2753047dd 100644 --- a/examples/screenshots/no-imgui-image_small.png +++ b/examples/screenshots/no-imgui-image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ccf0014c7ba7054440e3daf8d4e2a397e9013d1aea804c40dc7302dad4171e -size 13327 +oid sha256:edcd88ffc32388c3ebde4ce42c85a01e0aae6815720cb42c36e025c74dc5c9cb +size 13841 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 48d8fff95..9ce63bac6 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8a99a9172ae5edf98f0d189455fad2074a99f2280c9352675bab8d4c0e3491 -size 50751 +oid sha256:cc1f5e72146dafb696a31f180983dc2a06beb874337968217bf8c22d122a0b37 +size 46434 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index cdc24e382..bfaa7c60a 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3952cf9b0c9d008a885dc4abb3aeaaed6fd94a5db05ba83c6f4c4c76fe6e925 -size 171519 +oid sha256:1d5d2769550a9a6d0dd17b6d464f4da0ebcba4087108666ddd39c427ad304997 +size 240743 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 4f2bbba43..5d14d73d3 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3c9ac8d2b8157ffd575e5ad2b2bb23b684b52403c2f4f021c52d100cfb28a83 -size 49048 +oid sha256:37b8fcb208e36f79b490edd8693238c25cbf0f020a819843bb6fc53f7e282afe +size 45898 diff --git a/examples/screenshots/no-imgui-line_cmap_more.png b/examples/screenshots/no-imgui-line_cmap_more.png index 8125be49f..0ac37b6d1 100644 --- a/examples/screenshots/no-imgui-line_cmap_more.png +++ b/examples/screenshots/no-imgui-line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ddd88200aa824d4e05ba3f94fdb4216a1e7c7137b202cd8fb47997453dfd5a6 -size 126830 +oid sha256:a638d611d94fceceb8627139f19b43902e88cf7b96938f5450812cb57d97ba70 +size 114707 diff --git a/examples/screenshots/no-imgui-line_collection.png b/examples/screenshots/no-imgui-line_collection.png index a31cf55fe..d551a73a1 100644 --- a/examples/screenshots/no-imgui-line_collection.png +++ b/examples/screenshots/no-imgui-line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d807f770c118e668c6bda1919856d7804f716a2bf95a5ae060345df1cd2b3c7 -size 102703 +oid sha256:8df0832df5660c8e130d4e2a7832348c99dd89a79ff2b5d19cfaec47b1b649d9 +size 91884 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values.png b/examples/screenshots/no-imgui-line_collection_cmap_values.png index c909c766f..ef1343f66 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e8612de5c3ee252ce9c8cc8afd5bd6075d5e242e8a93cd025e28ec82526120f -size 64698 +oid sha256:edaa2958fb04d780c2aad7db30c34a15bffc2865d2a695b9db1d562cf313be16 +size 58711 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png index 61d5a21d0..a999af92e 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7847cd4399ce5b43bda9985eb72467ad292744aaeb9e8d210dd6c86c4eb1a090 -size 67959 +oid sha256:a16b359a174c53977d3b06f61dda3a1059418b3fd5bcded088718e332b92e9f6 +size 63313 diff --git a/examples/screenshots/no-imgui-line_collection_colors.png b/examples/screenshots/no-imgui-line_collection_colors.png index 567bb4d06..13677914b 100644 --- a/examples/screenshots/no-imgui-line_collection_colors.png +++ b/examples/screenshots/no-imgui-line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15216a0900bcaef492e5d9e3380db9f28d7b7e4bd11b26eb87ce956666dcd2b1 -size 58414 +oid sha256:d9148b69df1d1e603bc9d179035cdc486952fd1f2788e11cf093a13629fe47e5 +size 49671 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index c9bc6d931..f23d0e4ad 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8d3d7813580be188766c2d0200bcbff28122758d36d0faa846b0bb4dceac654 -size 130453 +oid sha256:8d3b59576ff740247c4cdac40d0b597dbbef24677d07d8895bd6bdff128ec509 +size 117961 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index fe54de5d6..fe4ecbd90 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be429bf910979cf4c9483b8ae1f7aa877fde64fb6ec8a4cf32be143f282c9103 -size 57353 +oid sha256:c89e2f45407beaf0b6b90ec61b11288b2fe1fb5d05c650e6e752c116dbea69b9 +size 52481 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index 649a9df59..b7b39c801 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf873f1479cec065f0062ce58ce78ddfbd5673654aacf0ecdbd559747ae741cb -size 69381 +oid sha256:0b7bb624ee72480a5bb0d1d196d1ccebd8d0b71fb252ddaa1e185bbf9718431a +size 68628 diff --git a/examples/screenshots/no-imgui-line_stack.png b/examples/screenshots/no-imgui-line_stack.png index 3ef24e73a..4c6419068 100644 --- a/examples/screenshots/no-imgui-line_stack.png +++ b/examples/screenshots/no-imgui-line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b9d02719e7051c2a0e848cc828f21be52ac108c6f9be16795d1150a1e215371 -size 123674 +oid sha256:a52cfae62bd5f77799faf6316a2d7de848890b9ac46ced9e1e0fc24c6112bbbc +size 106908 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index d82efa849..82aca044b 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b22ee4506bc532344cfcbd5daa0c4e90d9a831d59f1d916bd28534786947771 -size 97036 +oid sha256:b556ae7316ca24188cdadae410f501f3f2a86d00b004ed3ef26167ea5ba4f990 +size 96191 diff --git a/examples/screenshots/no-imgui-linear_selector.png b/examples/screenshots/no-imgui-linear_selector.png index 4416cb4d5..13125d504 100644 --- a/examples/screenshots/no-imgui-linear_selector.png +++ b/examples/screenshots/no-imgui-linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f1a323dec6d50d1c701632aadbd17d87ee3b3b42171046ca9b1284f93576a3b -size 131922 +oid sha256:f49a58a0e259f45e2e2fa9e311f9e05baafbd62a6d089a3c83167f8779e17cd5 +size 142585 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png index 4dc3b2aa6..cc0a4e6ba 100644 --- a/examples/screenshots/no-imgui-rect_frac_layout.png +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5923e8b9f687f97d488b282b35f16234898ed1038b0737b7b57fb9cbd72ebf34 -size 157321 +oid sha256:6567058d5aef3ca5c7b77b1a6b01743235a305d793b6955714c20d681f994f8f +size 158808 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png index 16d1ff446..68342d1c7 100644 --- a/examples/screenshots/no-imgui-rect_layout.png +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2ffe0a8d625322cc22d2abdde80a3f179f01552dde974bbbd49f9e371ab39aa -size 138936 +oid sha256:b3a82adc73994400122f8f8b25a9f241352cdb46ecfdbd7f2ee65209d4659bf1 +size 140550 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index 0d1f8dbb0..41393517c 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e197c84911cf7711d09653d6c54d7a756fbe4fe80daa84f0cf1a1d516217423 -size 60341 +oid sha256:f0d4cbc55085805b98e8ace0248ccc7afc6a9496f79f3f3c69ffe97d4962e6f3 +size 55639 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 84447c70f..e148c70df 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:780b680de7d3a22d2cb73a6829cad1e1066163e084b8daa9e8362f2543ba62eb -size 36881 +oid sha256:52ba9a0cb355ca13549e0e4d80f0ea544192efe6783cc4615c74be01c9930abb +size 30285 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index a19d66270..dbf6088e6 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b4f6635f48e047944c923ac46a9bd5b77e736f26421978ff74cd37a9677c622 -size 39457 +oid sha256:d2d3c95520c0025c1bdc408ed307d33793c409168a6cc80971bbc00210eda4d4 +size 33735 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index 631672504..e63267a49 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80cc8c1ed5276b0b8cbd5aeb3151182a73984829f889195b57442a58c3124a43 -size 38488 +oid sha256:c5826717eed9a8c44361a760ee31273a510ca0dcdc07cd98957aa1b8445be62a +size 33043 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 241e38ad5..47b8e4ad8 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71f3db93ea28e773c708093319985fb0fe04fae9a8a78d4f4f764f0417979b72 -size 68596 +oid sha256:bcef646cf7587c886a3765df2525d662caf6ad6db08e21c310bd2109b80e1673 +size 65092 diff --git a/examples/screenshots/rect_frac_layout.png b/examples/screenshots/rect_frac_layout.png index 7fe6d3d37..1df76dc61 100644 --- a/examples/screenshots/rect_frac_layout.png +++ b/examples/screenshots/rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5991b755432318310cfc2b4826bd9639cc234883aa06f1895817f710714cb58f -size 156297 +oid sha256:fc8ec12042a36992dbda00d04f251b47d58cd6d75eca5c4240cd207878f9282f +size 157607 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png index dec391ac2..fa89b9afc 100644 --- a/examples/screenshots/rect_layout.png +++ b/examples/screenshots/rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cf23f845932023789e0823a105910e9f701d0f03c04e3c18488f0da62420921 -size 123409 +oid sha256:823785f993237cb0f2773aea62c3dbbe16fe63f86f83867f1e27b82c8793da9f +size 126051 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index c069d6b11..3e0ccbe98 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fad40cf8004e31f7d30f4bb552ee1c7f79a499d3bad310c0eac83396f0aabd62 -size 61193 +oid sha256:5a58f072bb440b9f7e36ceb33405caa30bae419089781cc5c5c67f1381811165 +size 56500 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index 58c2b61fe..f6c5cbff7 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:427587ef9a73bf9c3ea6e739b61d5af7380a5488c454a9d3653019b40d569292 -size 37589 +oid sha256:2b4516dd73a4a2dd916fa4e89d4ddfaf6206178a0842710b43cc0349fa2334ab +size 31079 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index ab61f0405..cd61ff899 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3dd9ad854f41386d353ca0dae689a263eff942817727e328690427e2e62e2f3 -size 40112 +oid sha256:a97f3553a4ed9b99ae88db79233974dbb397449b63cd6b70b6a6d75a2e5e16d1 +size 34551 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index 01bd5cacd..71d440b2a 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7978b93f7eac8176c54ed0e39178424d9cb6474c73e9013d5164d3e88d54c95 -size 39147 +oid sha256:5081e95875350a2bda9db3f8c0cc2ec89abf4aca3e9a19b9c3959dd98206a77c +size 33853 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index 2f6c045f3..0bbe9650f 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb05b8378d94e16094738850dca6328caf7477c641bf474b9deae426344bc7a4 -size 70898 +oid sha256:ec78314216b2add21820209e5d2bfabf9f652b3b393d80f7e5be8341e42d19e6 +size 67328 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 7fbd32e2f..c0cdffbd7 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -47,7 +47,7 @@ def check_skip_imgui(module): @pytest.mark.parametrize("module", examples_to_run, ids=lambda x: x.stem) -def test_examples_run(module, force_offscreen): +def test_examples_run(module, prep_environment): """Run every example marked to see if they run without error.""" if not fpl.IMGUI: check_skip_imgui(module) @@ -56,13 +56,17 @@ def test_examples_run(module, force_offscreen): @pytest.fixture -def force_offscreen(): +def prep_environment(): """Force the offscreen canvas to be selected by the auto gui module.""" + # Make that examples using rendercanvas.auto, will use the offscreen backend os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" + # Disable ppaa on the renderer by default. Otherwise all screenshots change when the ppaa shaders are updated. + os.environ["PYGFX_PPAA"] = "none" try: yield finally: del os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] + del os.environ["PYGFX_PPAA"] def test_that_we_are_on_lavapipe(): @@ -86,7 +90,7 @@ def import_from_path(module_name, filename): @pytest.mark.parametrize("module", examples_to_test, ids=lambda x: x.stem) -def test_example_screenshots(module, force_offscreen): +def test_example_screenshots(module, prep_environment): """Make sure that every example marked outputs the expected.""" if not fpl.IMGUI: @@ -98,7 +102,9 @@ def test_example_screenshots(module, force_offscreen): if fpl.IMGUI: # there doesn't seem to be a resize event for the manual offscreen canvas - example.figure.imgui_renderer._backend.io.display_size = example.figure.canvas.get_logical_size() + example.figure.imgui_renderer._backend.io.display_size = ( + example.figure.canvas.get_logical_size() + ) # run this once so any edge widgets set their sizes and therefore the subplots get the correct rect # hacky but it works for now example.figure.imgui_renderer.render() @@ -148,9 +154,9 @@ def test_example_screenshots(module, force_offscreen): if os.environ["REGENERATE_SCREENSHOTS"] == "1": iio.imwrite(screenshot_path, rgb) - assert ( - screenshot_path.exists() - ), "found # test_example = true but no reference screenshot available" + assert screenshot_path.exists(), ( + "found # test_example = true but no reference screenshot available" + ) ref_img = iio.imread(screenshot_path) @@ -200,5 +206,7 @@ def get_diffs_rgba(slicer): if __name__ == "__main__": + os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" + os.environ["PYGFX_PPAA"] = "none" test_examples_run("simple") test_example_screenshots("simple") diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 5cef0c6b0..2d2787ac8 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -215,7 +215,7 @@ def _fpl_add_plot_area_hook(self, plot_area): wo.add_event_handler(self._toggle_arrow_key_moveable, "double_click") for fill in self._fill: - if fill.material.color_is_transparent: + if fill.material.color.a < 1 or fill.material.opacity < 1: self._pfunc_fill = partial(self._check_fill_pointer_event, fill) self._plot_area.renderer.add_event_handler( self._pfunc_fill, "pointer_down" @@ -393,7 +393,6 @@ def _move_to_pointer(self, ev): self._move_graphic(move_info) def _pointer_enter(self, ev): - if self._hover_responsive is None: return diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index bfd97000b..af1a6a610 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -560,12 +560,19 @@ def _render(self, draw=True): # draw the underlay planes self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) + # With new pygfx' blending, the depth buffer is only cleared after each flush, we need a manual depth + # clear to erase the depth values set by the underlay. + if hasattr(self.renderer, "clear"): + self.renderer.clear(depth=True) + # call the animation functions before render self._call_animate_functions(self._animate_funcs_pre) for subplot in self: subplot._render() # overlay render pass + if hasattr(self.renderer, "clear"): + self.renderer.clear(depth=True) self.renderer.render(self._overlay_scene, self._overlay_camera, flush=False) self.renderer.flush() From dac31cff31fdc7a934f4f8efdbbf2353173ed52d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 17 Jul 2025 12:27:47 -0400 Subject: [PATCH 03/52] add emails to gov doc (#880) * add emails to gov doc * Update CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 3 ++- GOVERNANCE.md | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d73264986..e1b7919f2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -113,7 +113,8 @@ Caitlin Lewis: If your report involves any members of the fastplotlib core team, or if they feel they have a conflict of interest in handling it, then they will recuse themselves from -considering your report. +considering your report. You may also contact the neutral moderator as stated in our +[Governance document](https://github.com/fastplotlib/fastplotlib/blob/main/GOVERNANCE.md#neutral-moderator). # Incident reporting resolution & Code of Conduct enforcement diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 876757d40..9baaaa321 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -25,8 +25,8 @@ The maintainers are the core developers of fastplotlib and together have a compl The current maintainers are: -1. [Kushal Kolar](https://github.com/kushalkolar) -1. [Caitlin Lewis](https://github.com/clewis7) +1. [Kushal Kolar](https://github.com/kushalkolar) - kushal {at} fastplotlib.org +1. [Caitlin Lewis](https://github.com/clewis7) - caitlin {at} fastplotlib.org Responsibilities: @@ -58,7 +58,7 @@ Responsibilities: No voting power, has no stake in the fastplotlib project. -* Reagan Bullins +* Reagan Bullins - reagan {at} fastplotlib.org Responsibilities: From 32ea8262d2d98c5d354bf772efd327948f8b10ff Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 21 Jul 2025 19:17:35 -0400 Subject: [PATCH 04/52] remove graphic suffix (#882) * remove graphic suffix * update quickstart * replace dstack w column stack --- examples/events/drag_points.py | 14 +-- examples/events/image_click.py | 4 +- examples/events/line_data_thickness_event.py | 14 +-- examples/gridplot/multigraphic_gridplot.py | 2 +- examples/guis/sine_cosine_funcs.py | 46 ++++---- examples/heatmap/heatmap.py | 2 +- examples/image/image_cmap.py | 7 +- examples/image/image_rgb.py | 2 +- examples/image/image_rgbvminvmax.py | 6 +- examples/image/image_simple.py | 2 +- examples/image/image_small.py | 2 +- examples/image/image_vminvmax.py | 6 +- examples/line/line.py | 12 +- examples/line/line_cmap.py | 14 +-- examples/line/line_colorslice.py | 38 +++--- examples/line/line_dataslice.py | 20 ++-- examples/misc/cycle_animation.py | 8 +- examples/misc/image_animation.py | 4 +- examples/misc/line3d_animation.py | 4 +- examples/misc/scatter_animation.py | 6 +- examples/misc/tooltips_custom.py | 4 +- examples/notebooks/quickstart.ipynb | 108 +++++++++--------- examples/scatter/scatter_cmap_iris.py | 4 +- examples/scatter/scatter_colorslice.py | 8 +- examples/scatter/scatter_colorslice_iris.py | 8 +- examples/scatter/scatter_dataslice_iris.py | 12 +- examples/scatter/scatter_iris.py | 2 +- .../selection_tools/linear_region_selector.py | 16 +-- .../linear_region_selectors_match_offsets.py | 16 +-- .../rectangle_selector_zoom.py | 4 +- examples/selection_tools/unit_circle.py | 28 ++--- 31 files changed, 211 insertions(+), 212 deletions(-) diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py index 752430c7c..5a679a996 100644 --- a/examples/events/drag_points.py +++ b/examples/events/drag_points.py @@ -23,16 +23,16 @@ figure = fpl.Figure(size=(700, 560)) # add a line -line_graphic = figure[0, 0].add_line(data) +line = figure[0, 0].add_line(data) # add a scatter, share the line graphic buffer! -scatter_graphic = figure[0, 0].add_scatter(data=line_graphic.data, sizes=25, colors="r") +scatter = figure[0, 0].add_scatter(data=line.data, sizes=25, colors="r") is_moving = False vertex_index = None -@scatter_graphic.add_event_handler("pointer_down") +@scatter.add_event_handler("pointer_down") def start_drag(ev: pygfx.PointerEvent): global is_moving global vertex_index @@ -42,7 +42,7 @@ def start_drag(ev: pygfx.PointerEvent): is_moving = True vertex_index = ev.pick_info["vertex_index"] - scatter_graphic.colors[vertex_index] = "cyan" + scatter.colors[vertex_index] = "cyan" @figure.renderer.add_event_handler("pointer_move") @@ -63,13 +63,13 @@ def move_point(ev): if pos is None: # end movement is_moving = False - scatter_graphic.colors[vertex_index] = "r" # reset color + scatter.colors[vertex_index] = "r" # reset color vertex_index = None return # change scatter data # since we are sharing the buffer, the line data will also change - scatter_graphic.data[vertex_index, :-1] = pos[:-1] + scatter.data[vertex_index, :-1] = pos[:-1] # re-enable controller figure[0, 0].controller.enabled = True @@ -83,7 +83,7 @@ def end_drag(ev: pygfx.PointerEvent): # end movement if is_moving: # reset color - scatter_graphic.colors[vertex_index] = "r" + scatter.colors[vertex_index] = "r" is_moving = False vertex_index = None diff --git a/examples/events/image_click.py b/examples/events/image_click.py index 729a67586..b783e1ee0 100644 --- a/examples/events/image_click.py +++ b/examples/events/image_click.py @@ -18,14 +18,14 @@ figure = fpl.Figure(size=(700, 560)) # create image graphic -image_graphic = figure[0, 0].add_image(data=data) +image = figure[0, 0].add_image(data=data) # show the plot figure.show() # adding a click event, we can also use decorators to add event handlers -@image_graphic.add_event_handler("click") +@image.add_event_handler("click") def click_event(ev: pygfx.PointerEvent): # get the click location in screen coordinates xy = (ev.x, ev.y) diff --git a/examples/events/line_data_thickness_event.py b/examples/events/line_data_thickness_event.py index 83f9322cb..d28471644 100644 --- a/examples/events/line_data_thickness_event.py +++ b/examples/events/line_data_thickness_event.py @@ -16,18 +16,18 @@ xs = np.linspace(0, 4 * np.pi, 100) # sine wave ys = np.sin(xs) -sine = np.column_stack([xs, ys]) +sine_data = np.column_stack([xs, ys]) # cosine wave ys = np.cos(xs) -cosine = np.column_stack([xs, ys]) +cosine_data = np.column_stack([xs, ys]) # create line graphics -sine_graphic = figure[0, 0].add_line(data=sine) -cosine_graphic = figure[0, 0].add_line(data=cosine, offset=(0, 4, 0)) +sine = figure[0, 0].add_line(data=sine_data) +cosine = figure[0, 0].add_line(data=cosine_data, offset=(0, 4, 0)) # make a list of the line graphics for convenience -lines = [sine_graphic, cosine_graphic] +lines = [sine, cosine] def change_thickness(ev: fpl.GraphicFeatureEvent): @@ -66,11 +66,11 @@ def change_data(ev: fpl.GraphicFeatureEvent): # set the y-value of the middle 40 points of the sine graphic to 1 # after the sine_graphic sets its data, the event handlers will be called # and therefore the cosine graphic will also set its data using the event data -sine_graphic.data[30:70, 1] = np.ones(40) +sine.data[30:70, 1] = np.ones(40) # set the thickness of the cosine graphic, this will trigger an event # that causes the sine graphic's thickness to also be set from this value -cosine_graphic.thickness = 10 +cosine.thickness = 10 # NOTE: fpl.loop.run() should not be used for interactive sessions # See the "JupyterLab and IPython" section in the user guide diff --git a/examples/gridplot/multigraphic_gridplot.py b/examples/gridplot/multigraphic_gridplot.py index d89168ec9..dad7ee192 100644 --- a/examples/gridplot/multigraphic_gridplot.py +++ b/examples/gridplot/multigraphic_gridplot.py @@ -84,7 +84,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine = np.column_stack([xs, ys]) # make 10 identical waves sine_waves = 10 * [sine] diff --git a/examples/guis/sine_cosine_funcs.py b/examples/guis/sine_cosine_funcs.py index 09a5ec990..f7dd064cf 100644 --- a/examples/guis/sine_cosine_funcs.py +++ b/examples/guis/sine_cosine_funcs.py @@ -61,55 +61,55 @@ def make_circle(center, radius: float, p, q, n_points: int) -> np.ndarray: # create sine and cosine data xs = np.linspace(0, 2 * np.pi, 360) -sine = np.sin(xs * P) -cosine = np.cos(xs * Q) +sine_data = np.sin(xs * P) +cosine_data = np.cos(xs * Q) # circle data circle_data = make_circle(center=(0, 0), p=P, q=Q, radius=1, n_points=360) # make the circle line graphic, set the cmap transform using the sine function -circle_graphic = figure["circle"].add_line( - circle_data, thickness=4, cmap="bwr", cmap_transform=sine +circle = figure["circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine_data ) # line to show the circle radius # use it to indicate the current position of the sine and cosine selctors (below) radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]]) -circle_radius_graphic = figure["circle"].add_line( +circle_radius = figure["circle"].add_line( radius_data, thickness=6, colors="magenta" ) # sine line graphic, cmap transform set from the sine function -sine_graphic = figure["sin"].add_line( - sine, thickness=10, cmap="bwr", cmap_transform=sine +sine = figure["sin"].add_line( + sine_data, thickness=10, cmap="bwr", cmap_transform=sine_data ) # cosine line graphic, cmap transform set from the sine function # illustrates the sine function values on the cosine graphic -cosine_graphic = figure["cos"].add_line( - cosine, thickness=10, cmap="bwr", cmap_transform=sine +cosine = figure["cos"].add_line( + cosine_data, thickness=10, cmap="bwr", cmap_transform=sine_data ) # add linear selectors to the sine and cosine line graphics -sine_selector = sine_graphic.add_linear_selector() -cosine_selector = cosine_graphic.add_linear_selector() +sine_selector = sine.add_linear_selector() +cosine_selector = cosine.add_linear_selector() def set_circle_cmap(ev): # sets the cmap transforms cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic - for g in [sine_graphic, cosine_graphic]: + for g in [sine, cosine]: g.cmap.transform = cmap_transform # set circle cmap transform - circle_graphic.cmap.transform = cmap_transform + circle.cmap.transform = cmap_transform # when the sine or cosine graphic is clicked, the cmap_transform # of the sine, cosine and circle line graphics are all set from # the y-values of the clicked line -sine_graphic.add_event_handler(set_circle_cmap, "click") -cosine_graphic.add_event_handler(set_circle_cmap, "click") +sine.add_event_handler(set_circle_cmap, "click") +cosine.add_event_handler(set_circle_cmap, "click") def set_x_val(ev): @@ -120,7 +120,7 @@ def set_x_val(ev): sine_selector.selection = value cosine_selector.selection = value - circle_radius_graphic.data[1, :-1] = circle_data[index] + circle_radius.data[1, :-1] = circle_data[index] # add same event handler to both graphics sine_selector.add_event_handler(set_x_val, "selection") @@ -138,19 +138,19 @@ def __init__(self, figure, size, location, title): self._q = 1 def _set_data(self): - global sine_graphic, cosine_graphic, circle_graphic, circle_radius_graphic, circle_data + global sine, cosine, circle, circle_radius, circle_data # make new data - sine = np.sin(xs * self._p) - cosine = np.cos(xs * self._q) + sine_data = np.sin(xs * self._p) + cosine_data = np.cos(xs * self._q) circle_data = make_circle(center=(0, 0), p=self._p, q=self._q, radius=1, n_points=360) # set the graphics - sine_graphic.data[:, 1] = sine - cosine_graphic.data[:, 1] = cosine - circle_graphic.data[:, :2] = circle_data - circle_radius_graphic.data[1, :-1] = circle_data[sine_selector.get_selected_index()] + sine.data[:, 1] = sine_data + cosine.data[:, 1] = cosine_data + circle.data[:, :2] = circle_data + circle_radius.data[1, :-1] = circle_data[sine_selector.get_selected_index()] def update(self): flag_set_data = False diff --git a/examples/heatmap/heatmap.py b/examples/heatmap/heatmap.py index 38c9b51a7..3f02206f6 100644 --- a/examples/heatmap/heatmap.py +++ b/examples/heatmap/heatmap.py @@ -21,7 +21,7 @@ data = np.vstack([sine * i for i in range(2_300)]) # plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") +image = figure[0, 0].add_image(data=data, name="heatmap") del data figure.show() diff --git a/examples/image/image_cmap.py b/examples/image/image_cmap.py index f651f438c..8c94c6f17 100644 --- a/examples/image/image_cmap.py +++ b/examples/image/image_cmap.py @@ -8,19 +8,20 @@ # test_example = true # sphinx_gallery_pygfx_docs = 'screenshot' -import fastplotlib as fpl import imageio.v3 as iio +import fastplotlib as fpl + im = iio.imread("imageio:camera.png") figure = fpl.Figure(size=(700, 560)) # plot the image data -image_graphic = figure[0, 0].add_image(data=im, name="random-image") +image = figure[0, 0].add_image(data=im, name="random-image") figure.show() -image_graphic.cmap = "viridis" +image.cmap = "viridis" # NOTE: fpl.loop.run() should not be used for interactive sessions # See the "JupyterLab and IPython" section in the user guide diff --git a/examples/image/image_rgb.py b/examples/image/image_rgb.py index 187dac553..569c09f0b 100644 --- a/examples/image/image_rgb.py +++ b/examples/image/image_rgb.py @@ -16,7 +16,7 @@ figure = fpl.Figure(size=(700, 560)) # plot the image data -image_graphic = figure[0, 0].add_image(data=im, name="iio astronaut") +image = figure[0, 0].add_image(data=im, name="iio astronaut") figure.show() diff --git a/examples/image/image_rgbvminvmax.py b/examples/image/image_rgbvminvmax.py index 02635f134..bf2963daf 100644 --- a/examples/image/image_rgbvminvmax.py +++ b/examples/image/image_rgbvminvmax.py @@ -16,12 +16,12 @@ figure = fpl.Figure(size=(700, 560)) # plot the image data -image_graphic = figure[0, 0].add_image(data=im, name="iio astronaut") +image = figure[0, 0].add_image(data=im, name="iio astronaut") figure.show() -image_graphic.vmin = 0.5 -image_graphic.vmax = 0.75 +image.vmin = 0.5 +image.vmax = 0.75 # NOTE: fpl.loop.run() should not be used for interactive sessions # See the "JupyterLab and IPython" section in the user guide diff --git a/examples/image/image_simple.py b/examples/image/image_simple.py index d0910fb82..2fd7f694c 100644 --- a/examples/image/image_simple.py +++ b/examples/image/image_simple.py @@ -16,7 +16,7 @@ data = iio.imread("imageio:camera.png") # plot the image data -image_graphic = figure[0, 0].add_image(data=data, name="iio camera") +image = figure[0, 0].add_image(data=data, name="iio camera") figure.show() diff --git a/examples/image/image_small.py b/examples/image/image_small.py index 732d61d74..6acfd7250 100644 --- a/examples/image/image_small.py +++ b/examples/image/image_small.py @@ -18,7 +18,7 @@ [[0, 1, 2], [3, 4, 5]] ) -image_graphic = figure[0, 0].add_image(data) +image = figure[0, 0].add_image(data) figure.show() diff --git a/examples/image/image_vminvmax.py b/examples/image/image_vminvmax.py index e2d1c7743..1c290c587 100644 --- a/examples/image/image_vminvmax.py +++ b/examples/image/image_vminvmax.py @@ -16,12 +16,12 @@ data = iio.imread("imageio:astronaut.png") # plot the image data -image_graphic = figure[0, 0].add_image(data=data, name="iio astronaut") +image = figure[0, 0].add_image(data=data, name="iio astronaut") figure.show() -image_graphic.vmin = 0.5 -image_graphic.vmax = 0.75 +image.vmin = 0.5 +image.vmax = 0.75 # NOTE: fpl.loop.run() should not be used for interactive sessions # See the "JupyterLab and IPython" section in the user guide diff --git a/examples/line/line.py b/examples/line/line.py index fb8834759..f7839a1c4 100644 --- a/examples/line/line.py +++ b/examples/line/line.py @@ -16,25 +16,25 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine_data = np.column_stack([xs, ys]) # cosine wave ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] +cosine_data = np.column_stack([xs, ys]) # sinc function a = 0.5 ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] +sinc_data = np.column_stack([xs, ys]) -sine_graphic = figure[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = figure[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = figure[0, 0].add_line(data=sinc, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) figure[0, 0].axes.grids.xy.visible = True figure.show() diff --git a/examples/line/line_cmap.py b/examples/line/line_cmap.py index af24f1c63..3d2b5e8c9 100644 --- a/examples/line/line_cmap.py +++ b/examples/line/line_cmap.py @@ -16,24 +16,24 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine_data = np.column_stack([xs, ys]) # cosine wave ys = np.cos(xs) - 5 -cosine = np.dstack([xs, ys])[0] +cosine_data = np.column_stack([xs, ys]) # cmap_transform from an array, so the colors on the sine line will be based on the sine y-values -sine_graphic = figure[0, 0].add_line( - data=sine, +sine = figure[0, 0].add_line( + data=sine_data, thickness=10, cmap="plasma", - cmap_transform=sine[:, 1] + cmap_transform=sine_data[:, 1] ) # qualitative colormaps, useful for cluster labels or other types of categorical labels labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -cosine_graphic = figure[0, 0].add_line( - data=cosine, +cosine = figure[0, 0].add_line( + data=cosine_data, thickness=10, cmap="tab10", cmap_transform=labels diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index 2d4c0dcaa..b6865eadb 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -16,26 +16,26 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.column_stack([xs, ys]) +sine_data = np.column_stack([xs, ys]) # cosine wave ys = np.cos(xs) -cosine = np.column_stack([xs, ys]) +cosine_data = np.column_stack([xs, ys]) # sinc function a = 0.5 ys = np.sinc(xs) * 3 -sinc = np.column_stack([xs, ys]) +sinc_data = np.column_stack([xs, ys]) -sine_graphic = figure[0, 0].add_line( - data=sine, +sine = figure[0, 0].add_line( + data=sine_data, thickness=5, colors="magenta" ) # you can also use colormaps for lines! -cosine_graphic = figure[0, 0].add_line( - data=cosine, +cosine = figure[0, 0].add_line( + data=cosine_data, thickness=12, cmap="autumn", offset=(0, 3, 0) # places the graphic at a y-axis offset of 3, offsets don't affect data @@ -43,8 +43,8 @@ # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = figure[0, 0].add_line( - data=sinc, +sinc = figure[0, 0].add_line( + data=sinc_data, thickness=5, colors=colors, offset=(0, 6, 0) @@ -52,7 +52,7 @@ zeros = np.zeros(xs.size) zeros_data = np.column_stack([xs, zeros]) -zeros_graphic = figure[0, 0].add_line( +zeros = figure[0, 0].add_line( data=zeros_data, thickness=8, colors="w", @@ -62,25 +62,25 @@ figure.show() # indexing of colors -cosine_graphic.colors[:15] = "magenta" -cosine_graphic.colors[90:] = "red" -cosine_graphic.colors[60] = "w" +cosine.colors[:15] = "magenta" +cosine.colors[90:] = "red" +cosine.colors[60] = "w" # more complex indexing, set the blue value directly from an array -cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65) +cosine.colors[65:90, 0] = np.linspace(0, 1, 90 - 65) # additional fancy indexing using numpy key = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 67, 19]) -sinc_graphic.colors[key] = "Red" +sinc.colors[key] = "Red" # boolean fancy indexing -zeros_graphic.colors[xs < -5] = "green" +zeros.colors[xs < -5] = "green" # assign colormap to an entire line -sine_graphic.cmap = "seismic" +sine.cmap = "seismic" # or to segments of a line -zeros_graphic.cmap[50:75] = "jet" -zeros_graphic.cmap[75:] = "viridis" +zeros.cmap[50:75] = "jet" +zeros.cmap[75:] = "viridis" # NOTE: fpl.loop.run() should not be used for interactive sessions diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index ca0f48518..6ef9d0d90 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -16,35 +16,35 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine_data = np.column_stack([xs, ys]) # cosine wave ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] +cosine_data = np.column_stack([xs, ys]) # sinc function a = 0.5 ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] +sinc_data = np.column_stack([xs, ys]) -sine_graphic = figure[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine = figure[0, 0].add_line(data=sine_data, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = figure[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine = figure[0, 0].add_line(data=cosine_data, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = figure[0, 0].add_line(data=sinc, thickness=5, colors=colors) +sinc = figure[0, 0].add_line(data=sinc_data, thickness=5, colors=colors) figure.show() -cosine_graphic.data[10:50:5, :2] = sine[10:50:5] -cosine_graphic.data[90:, 1] = 7 -cosine_graphic.data[0] = np.array([[-10, 0, 0]]) +cosine.data[10:50:5, :2] = sine_data[10:50:5] +cosine.data[90:, 1] = 7 +cosine.data[0] = np.array([[-10, 0, 0]]) # additional fancy indexing with boolean array bool_key = [True, True, True, False, False] * 20 -sinc_graphic.data[bool_key, 1] = 7 # y vals to 1 +sinc.data[bool_key, 1] = 7 # y vals to 1 # NOTE: fpl.loop.run() should not be used for interactive sessions diff --git a/examples/misc/cycle_animation.py b/examples/misc/cycle_animation.py index 833321453..d1a369c79 100644 --- a/examples/misc/cycle_animation.py +++ b/examples/misc/cycle_animation.py @@ -37,16 +37,16 @@ figure = fpl.Figure(size=(700, 560)) subplot_scatter = figure[0, 0] # use an alpha value since this will be a lot of points -scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +scatter = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) i = 0.05 def cycle_colors(subplot): global i # cycle the red values - scatter_graphic.colors[n_points * 2:, 0] = np.abs(np.sin(i)) - scatter_graphic.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4))) - scatter_graphic.colors[n_points * 2:, 2] = np.abs(np.cos(i)) + scatter.colors[n_points * 2:, 0] = np.abs(np.sin(i)) + scatter.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4))) + scatter.colors[n_points * 2:, 2] = np.abs(np.cos(i)) i += 0.05 subplot_scatter.add_animations(cycle_colors) diff --git a/examples/misc/image_animation.py b/examples/misc/image_animation.py index 1f7ff6109..324a8e727 100644 --- a/examples/misc/image_animation.py +++ b/examples/misc/image_animation.py @@ -16,10 +16,10 @@ figure = fpl.Figure(size=(700, 560)) # plot the image data -image_graphic = figure[0, 0].add_image(data=data, name="random-image") +image = figure[0, 0].add_image(data=data, name="random-image") -# a function to update the image_graphic +# a function to update the image # a figure-level animation function will optionally take the figure as an argument def update_data(figure_instance): new_data = np.random.rand(512, 512) diff --git a/examples/misc/line3d_animation.py b/examples/misc/line3d_animation.py index c1d903e02..f718fff0a 100644 --- a/examples/misc/line3d_animation.py +++ b/examples/misc/line3d_animation.py @@ -19,11 +19,11 @@ zs = phi # make data 3d, with shape [, 3] -spiral = np.dstack([xs, ys, zs])[0] +spiral = np.column_stack([xs, ys, zs]) figure = fpl.Figure(cameras="3d", size=(700, 560)) -line_graphic = figure[0,0].add_line(data=spiral, thickness=3, cmap='jet') +line = figure[0,0].add_line(data=spiral, thickness=3, cmap='jet') marker = figure[0,0].add_scatter(data=spiral[0], sizes=10, name="marker") diff --git a/examples/misc/scatter_animation.py b/examples/misc/scatter_animation.py index ee8d2a10a..d37aea976 100644 --- a/examples/misc/scatter_animation.py +++ b/examples/misc/scatter_animation.py @@ -37,13 +37,13 @@ figure = fpl.Figure(size=(700, 560)) subplot_scatter = figure[0, 0] # use an alpha value since this will be a lot of points -scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +scatter = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) def update_points(subplot): # move every point by a small amount - deltas = np.random.normal(size=scatter_graphic.data.value.shape, loc=0, scale=0.15) - scatter_graphic.data = scatter_graphic.data.value + deltas + deltas = np.random.normal(size=scatter.data.value.shape, loc=0, scale=0.15) + scatter.data = scatter.data.value + deltas subplot_scatter.add_animations(update_points) diff --git a/examples/misc/tooltips_custom.py b/examples/misc/tooltips_custom.py index a62190906..d1cc1e297 100644 --- a/examples/misc/tooltips_custom.py +++ b/examples/misc/tooltips_custom.py @@ -23,7 +23,7 @@ agg = AgglomerativeClustering(n_clusters=3) agg.fit_predict(data) -scatter_graphic = figure[0, 0].add_scatter( +scatter = figure[0, 0].add_scatter( data=data[:, :-1], # use only xy data sizes=15, cmap="Set1", @@ -44,7 +44,7 @@ def tooltip_info(ev) -> str: return info -figure.tooltip_manager.register(scatter_graphic, custom_info=tooltip_info) +figure.tooltip_manager.register(scatter, custom_info=tooltip_info) figure.show() diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 0d8fc3c31..7b7551588 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -99,7 +99,7 @@ "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", + "image = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", "fig.show(sidecar=True)" @@ -132,7 +132,7 @@ }, "outputs": [], "source": [ - "image_graphic.cmap = \"viridis\"" + "image.cmap = \"viridis\"" ] }, { @@ -158,7 +158,7 @@ "source": [ "# some graphic properties behave like arrays\n", "# access the underlying array using .values\n", - "image_graphic.data.value.shape" + "image.data.value.shape" ] }, { @@ -170,8 +170,8 @@ }, "outputs": [], "source": [ - "image_graphic.data[::15, :] = 1\n", - "image_graphic.data[:, ::15] = 1" + "image.data[::15, :] = 1\n", + "image.data[:, ::15] = 1" ] }, { @@ -191,7 +191,7 @@ }, "outputs": [], "source": [ - "image_graphic.data[data > 175] = 255" + "image.data[data > 175] = 255" ] }, { @@ -211,8 +211,8 @@ }, "outputs": [], "source": [ - "image_graphic.vmin = 50\n", - "image_graphic.vmax = 150" + "image.vmin = 50\n", + "image.vmax = 150" ] }, { @@ -281,7 +281,7 @@ }, "outputs": [], "source": [ - "image_graphic.data = gray" + "image.data = gray" ] }, { @@ -303,7 +303,7 @@ }, "outputs": [], "source": [ - "image_graphic.reset_vmin_vmax()" + "image.reset_vmin_vmax()" ] }, { @@ -432,7 +432,7 @@ }, "outputs": [], "source": [ - "image_graphic" + "image" ] }, { @@ -444,7 +444,7 @@ }, "outputs": [], "source": [ - "image_graphic == fig[0, 0][\"sample-image\"]" + "image == fig[0, 0][\"sample-image\"]" ] }, { @@ -561,7 +561,7 @@ "# plot the data\n", "fig_v[0, 0].add_image(data=data, name=\"random-image\")\n", "\n", - "# a function to update the image_graphic\n", + "# a function to update the image\n", "# a figure-level animation function will optionally take the figure as an argument\n", "def update_data(figure_instance):\n", " new_data = np.random.rand(512, 512)\n", @@ -602,13 +602,13 @@ "\n", "data = np.random.rand(512, 512)\n", "\n", - "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", + "image = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", "\n", "# you will need to define a new animation function for this graphic\n", "def update_data_2():\n", " new_data = np.random.rand(512, 512)\n", " # alternatively, you can use the stored reference to the graphic as well instead of indexing the subplot\n", - " image_graphic_instance.data = new_data\n", + " image.data = new_data\n", "\n", "fig_sync.add_animations(update_data_2)\n", "\n", @@ -620,7 +620,7 @@ "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", "metadata": {}, "source": [ - "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where it is convenient to keep your own reference to a `Graphic`" + "#### Keeping a reference to the Graphic instance, as shown above `image`, is useful if you're creating something where it is convenient to keep your own reference to a `Graphic`" ] }, { @@ -678,7 +678,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8e8280da-b421-43a5-a1a6-2a196a408e9a", + "id": "72a665d195e61427", "metadata": {}, "outputs": [], "source": [ @@ -686,16 +686,16 @@ "xs = np.linspace(-10, 10, 100)\n", "# sine wave\n", "ys = np.sin(xs)\n", - "sine = np.column_stack([xs, ys])\n", + "sine_data = np.column_stack([xs, ys])\n", "\n", "# cosine wave\n", "ys = np.cos(xs) + 5\n", - "cosine = np.column_stack([xs, ys])\n", + "cosine_data = np.column_stack([xs, ys])\n", "\n", "# sinc function\n", "a = 0.5\n", "ys = np.sinc(xs) * 3 + 8\n", - "sinc = np.column_stack([xs, ys])" + "sinc_data = np.column_stack([xs, ys])" ] }, { @@ -709,7 +709,7 @@ { "cell_type": "code", "execution_count": null, - "id": "93a5d1e6-d019-4dd0-a0d1-25d1704ab7a7", + "id": "647c268622cc813", "metadata": {}, "outputs": [], "source": [ @@ -720,14 +720,14 @@ "subplot = fig_lines[0, 0]\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = subplot.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "sine = subplot.add_line(data=sine_data, thickness=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", - "cosine_graphic = subplot.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "cosine = subplot.add_line(data=cosine_data, thickness=12, cmap=\"autumn\")\n", "\n", "# or a list of colors for each datapoint\n", "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = subplot.add_line(data=sinc, thickness=5, colors = colors)\n", + "sinc = subplot.add_line(data=sinc_data, thickness=5, colors = colors)\n", "\n", "# show the plot\n", "fig_lines.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" @@ -736,10 +736,8 @@ { "cell_type": "code", "execution_count": null, - "id": "a4060576-2f29-4e4b-a86a-0410c766bd98", - "metadata": { - "tags": [] - }, + "id": "80e1f1bd7ee957e9", + "metadata": {}, "outputs": [], "source": [ "# testing cell, ignore\n", @@ -791,21 +789,21 @@ { "cell_type": "code", "execution_count": null, - "id": "cb0d13ed-ef07-46ff-b19e-eeca4c831037", + "id": "6b7b377fcf487815", "metadata": {}, "outputs": [], "source": [ "# indexing of colors\n", - "cosine_graphic.colors[:15] = \"magenta\"\n", - "cosine_graphic.colors[90:] = \"red\"\n", - "cosine_graphic.colors[60] = \"w\"\n", + "cosine.colors[:15] = \"magenta\"\n", + "cosine.colors[90:] = \"red\"\n", + "cosine.colors[60] = \"w\"\n", "\n", "# indexing to assign colormaps to entire lines or segments\n", - "sinc_graphic.cmap[10:50] = \"gray\"\n", - "sine_graphic.cmap = \"seismic\"\n", + "sinc.cmap[10:50] = \"gray\"\n", + "sine.cmap = \"seismic\"\n", "\n", "# more complex indexing, set the blue value directly from an array\n", - "cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65)" + "cosine.colors[65:90, 0] = np.linspace(0, 1, 90 - 65)" ] }, { @@ -827,7 +825,7 @@ " print(event_data)\n", "\n", "# Will print event data when the color changes\n", - "cosine_graphic.add_event_handler(callback_func, \"colors\")" + "cosine.add_event_handler(callback_func, \"colors\")" ] }, { @@ -839,7 +837,7 @@ "source": [ "# more complex indexing of colors\n", "# from point 15 - 30, set every 3rd point as \"cyan\"\n", - "cosine_graphic.colors[15:50:3] = \"cyan\"" + "cosine.colors[15:50:3] = \"cyan\"" ] }, { @@ -866,22 +864,22 @@ { "cell_type": "code", "execution_count": null, - "id": "d1a4314b-5723-43c7-94a0-b4cbb0e44d60", + "id": "2b2d68d0d8cc321a", "metadata": {}, "outputs": [], "source": [ - "cosine_graphic.data[10:50:5, :2] = sine[10:50:5]\n", - "cosine_graphic.data[90:, 1] = 7" + "cosine.data[10:50:5, :2] = sine_data[10:50:5]\n", + "cosine.data[90:, 1] = 7" ] }, { "cell_type": "code", "execution_count": null, - "id": "682db47b-8c7a-4934-9be4-2067e9fb12d5", + "id": "d43b9aeca4bb8ddd", "metadata": {}, "outputs": [], "source": [ - "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" + "cosine.data[0] = np.array([[-10, 0, 0]])" ] }, { @@ -1356,7 +1354,7 @@ "fig_scatter = fpl.Figure()\n", "subplot_scatter = fig_scatter[0, 0]\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6)\n", + "scatter = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6)\n", "\n", "fig_scatter.show(sidecar=True)" ] @@ -1377,7 +1375,7 @@ "outputs": [], "source": [ "# half of the first cloud's points to red\n", - "scatter_graphic.colors[:n_points:2] = \"r\"" + "scatter.colors[:n_points:2] = \"r\"" ] }, { @@ -1388,7 +1386,7 @@ "outputs": [], "source": [ "# other half of the first cloud's points to purple\n", - "scatter_graphic.colors[1:n_points:2] = \"purple\"" + "scatter.colors[1:n_points:2] = \"purple\"" ] }, { @@ -1399,7 +1397,7 @@ "outputs": [], "source": [ "# set the green value directly\n", - "scatter_graphic.colors[n_points:n_points * 2, 1] = 0.3" + "scatter.colors[n_points:n_points * 2, 1] = 0.3" ] }, { @@ -1410,7 +1408,7 @@ "outputs": [], "source": [ "# set color values directly using an array\n", - "scatter_graphic.colors[n_points * 2:] = np.repeat([[1, 1, 0, 0.5]], n_points, axis=0)" + "scatter.colors[n_points * 2:] = np.repeat([[1, 1, 0, 0.5]], n_points, axis=0)" ] }, { @@ -1421,7 +1419,7 @@ "outputs": [], "source": [ "# change the data, change y-values\n", - "scatter_graphic.data[n_points:n_points * 2, 1] += 15" + "scatter.data[n_points:n_points * 2, 1] += 15" ] }, { @@ -1432,7 +1430,7 @@ "outputs": [], "source": [ "# set x values directly but using an array\n", - "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" + "scatter.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" ] }, { @@ -1473,8 +1471,8 @@ "source": [ "def update_points(subplot):\n", " # move every point by a small amount\n", - " deltas = np.random.normal(size=scatter_graphic.data.value.shape, loc=0, scale=0.15)\n", - " scatter_graphic.data = scatter_graphic.data[:] + deltas\n", + " deltas = np.random.normal(size=scatter.data.value.shape, loc=0, scale=0.15)\n", + " scatter.data = scatter.data[:] + deltas\n", "\n", "subplot_scatter.add_animations(update_points)" ] @@ -1498,9 +1496,9 @@ "def cycle_colors(subplot):\n", " global i\n", " # cycle the red values\n", - " scatter_graphic.colors[n_points * 2:, 0] = np.abs(np.sin(i))\n", - " scatter_graphic.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4)))\n", - " scatter_graphic.colors[n_points * 2:, 2] = np.abs(np.cos(i))\n", + " scatter.colors[n_points * 2:, 0] = np.abs(np.sin(i))\n", + " scatter.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4)))\n", + " scatter.colors[n_points * 2:, 2] = np.abs(np.cos(i))\n", " i += 0.05\n", "\n", "subplot_scatter.add_animations(cycle_colors)" @@ -1979,7 +1977,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/examples/scatter/scatter_cmap_iris.py b/examples/scatter/scatter_cmap_iris.py index 139554dae..24be0c13c 100644 --- a/examples/scatter/scatter_cmap_iris.py +++ b/examples/scatter/scatter_cmap_iris.py @@ -20,7 +20,7 @@ agg = AgglomerativeClustering(n_clusters=3) agg.fit_predict(data) -scatter_graphic = figure[0, 0].add_scatter( +scatter = figure[0, 0].add_scatter( data=data[:, :-1], # use only xy data sizes=15, alpha=0.7, @@ -30,7 +30,7 @@ figure.show() -scatter_graphic.cmap = "tab10" +scatter.cmap = "tab10" if __name__ == "__main__": diff --git a/examples/scatter/scatter_colorslice.py b/examples/scatter/scatter_colorslice.py index a3cacee55..d8bbc620b 100644 --- a/examples/scatter/scatter_colorslice.py +++ b/examples/scatter/scatter_colorslice.py @@ -40,11 +40,11 @@ figure.show() -scatter_graphic = figure[0, 0].graphics[0] +scatter = figure[0, 0].graphics[0] -scatter_graphic.colors[0:75] = "red" -scatter_graphic.colors[75:150] = "white" -scatter_graphic.colors[::2] = "blue" +scatter.colors[0:75] = "red" +scatter.colors[75:150] = "white" +scatter.colors[::2] = "blue" # NOTE: fpl.loop.run() should not be used for interactive sessions # See the "JupyterLab and IPython" section in the user guide diff --git a/examples/scatter/scatter_colorslice_iris.py b/examples/scatter/scatter_colorslice_iris.py index 725374ef7..425a2f1ff 100644 --- a/examples/scatter/scatter_colorslice_iris.py +++ b/examples/scatter/scatter_colorslice_iris.py @@ -19,7 +19,7 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = figure[0, 0].add_scatter( +scatter = figure[0, 0].add_scatter( data=data[:, :-1], sizes=6, alpha=0.7, @@ -28,9 +28,9 @@ figure.show() -scatter_graphic.colors[0:75] = "red" -scatter_graphic.colors[75:150] = "white" -scatter_graphic.colors[::2] = "blue" +scatter.colors[0:75] = "red" +scatter.colors[75:150] = "white" +scatter.colors[::2] = "blue" if __name__ == "__main__": diff --git a/examples/scatter/scatter_dataslice_iris.py b/examples/scatter/scatter_dataslice_iris.py index cc688eeb4..81df632ae 100644 --- a/examples/scatter/scatter_dataslice_iris.py +++ b/examples/scatter/scatter_dataslice_iris.py @@ -20,16 +20,16 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) figure.show() -scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) -scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) -scatter_graphic.data[2] = np.array([[5.2, 2.7, 1.7]]) +scatter.data[0] = np.array([[5, 3, 1.5]]) +scatter.data[1] = np.array([[4.3, 3.2, 1.3]]) +scatter.data[2] = np.array([[5.2, 2.7, 1.7]]) -scatter_graphic.data[10:15] = scatter_graphic.data[0:5] + np.array([1, 1, 1]) -scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1, 1, 0]) +scatter.data[10:15] = scatter.data[0:5] + np.array([1, 1, 1]) +scatter.data[50:100:2] = scatter.data[100:150:2] + np.array([1, 1, 0]) if __name__ == "__main__": diff --git a/examples/scatter/scatter_iris.py b/examples/scatter/scatter_iris.py index 94c8acca1..03b0ee67e 100644 --- a/examples/scatter/scatter_iris.py +++ b/examples/scatter/scatter_iris.py @@ -23,7 +23,7 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) figure.show() diff --git a/examples/selection_tools/linear_region_selector.py b/examples/selection_tools/linear_region_selector.py index 5c6d6e01b..7a9114beb 100644 --- a/examples/selection_tools/linear_region_selector.py +++ b/examples/selection_tools/linear_region_selector.py @@ -37,22 +37,22 @@ ys = np.sin(xs) + np.random.normal(scale=0.2, size=10000) # make sine along x axis -sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys]), thickness=1) +sine_x = figure[0, 0].add_line(np.column_stack([xs, ys]), thickness=1) # x = sine(y), sine(y) > 0 = 0 -sine_y = ys -sine_y[sine_y > 0] = 0 +sine_y_data = ys +sine_y_data[sine_y_data > 0] = 0 # sine along y axis -sine_graphic_y = figure[0, 1].add_line(np.column_stack([ys, xs])) +sine_y = figure[0, 1].add_line(np.column_stack([ys, xs])) # offset the position of the graphic to demonstrate `get_selected_data()` later -sine_graphic_y.position_x = 50 -sine_graphic_y.position_y = 50 +sine_y.position_x = 50 +sine_y.position_y = 50 # add linear selectors -selector_x = sine_graphic_x.add_linear_region_selector((0, 100)) # default axis is "x" -selector_y = sine_graphic_y.add_linear_region_selector(axis="y") +selector_x = sine_x.add_linear_region_selector((0, 100)) # default axis is "x" +selector_y = sine_y.add_linear_region_selector(axis="y") # preallocate array for storing zoomed in data zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.zeros(zoomed_prealloc)]) diff --git a/examples/selection_tools/linear_region_selectors_match_offsets.py b/examples/selection_tools/linear_region_selectors_match_offsets.py index 7ac9cc486..042207152 100644 --- a/examples/selection_tools/linear_region_selectors_match_offsets.py +++ b/examples/selection_tools/linear_region_selectors_match_offsets.py @@ -32,22 +32,22 @@ ys = np.sin(xs) # y = sine(x) # make sine along x axis -sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys]), offset=(10, 10, 0)) +sine_x = figure[0, 0].add_line(np.column_stack([xs, ys]), offset=(10, 10, 0)) # x = sine(y), sine(y) > 0 = 0 -sine_y = ys -sine_y[sine_y > 0] = 0 +sine_y_data = ys +sine_y_data[sine_y_data > 0] = 0 # sine along y axis -sine_graphic_y = figure[0, 1].add_line(np.column_stack([ys, xs]), offset=(10, 10, 0)) +sine_y = figure[0, 1].add_line(np.column_stack([ys, xs]), offset=(10, 10, 0)) # offset the position of the graphic to demonstrate `get_selected_data()` later -sine_graphic_y.position_x = 50 -sine_graphic_y.position_y = 50 +sine_y.position_x = 50 +sine_y.position_y = 50 # add linear selectors -selector_x = sine_graphic_x.add_linear_region_selector() # default axis is "x" -selector_y = sine_graphic_y.add_linear_region_selector(axis="y") +selector_x = sine_x.add_linear_region_selector() # default axis is "x" +selector_y = sine_y.add_linear_region_selector(axis="y") # preallocate array for storing zoomed in data zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.zeros(zoomed_prealloc)]) diff --git a/examples/selection_tools/rectangle_selector_zoom.py b/examples/selection_tools/rectangle_selector_zoom.py index 61e38ffc9..f8ebd975e 100644 --- a/examples/selection_tools/rectangle_selector_zoom.py +++ b/examples/selection_tools/rectangle_selector_zoom.py @@ -18,10 +18,10 @@ ) # add image -image_graphic = figure[0, 0].add_image(data=iio.imread("imageio:camera.png")) +image = figure[0, 0].add_image(data=iio.imread("imageio:camera.png")) # add rectangle selector to image graphic -rectangle_selector = image_graphic.add_rectangle_selector() +rectangle_selector = image.add_rectangle_selector() # add a zoomed plot of the selected data zoom_ig = figure[1, 0].add_image(rectangle_selector.get_selected_data()) diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py index b068d1bc7..143992c62 100644 --- a/examples/selection_tools/unit_circle.py +++ b/examples/selection_tools/unit_circle.py @@ -67,15 +67,15 @@ def make_circle(center, radius: float, n_points: int) -> np.ndarray: # create sine and cosine data xs = np.linspace(0, 2 * np.pi, 360) -sine = np.sin(xs) -cosine = np.cos(xs) +sine_data = np.sin(xs) +cosine_data = np.cos(xs) # circle data circle_data = make_circle(center=(0, 0), radius=1, n_points=360) # make the circle line graphic, set the cmap transform using the sine function -circle_graphic = figure["unit circle"].add_line( - circle_data, thickness=4, cmap="bwr", cmap_transform=sine +circle = figure["unit circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine_data ) # line to show the circle radius @@ -86,36 +86,36 @@ def make_circle(center, radius: float, n_points: int) -> np.ndarray: ) # sine line graphic, cmap transform set from the sine function -sine_graphic = figure["sin(x)"].add_line( - sine, thickness=10, cmap="bwr", cmap_transform=sine +sine = figure["sin(x)"].add_line( + sine_data, thickness=10, cmap="bwr", cmap_transform=sine_data ) # cosine line graphic, cmap transform set from the sine function # illustrates the sine function values on the cosine graphic -cosine_graphic = figure["cos(x)"].add_line( - cosine, thickness=10, cmap="bwr", cmap_transform=sine +cosine = figure["cos(x)"].add_line( + cosine_data, thickness=10, cmap="bwr", cmap_transform=sine_data ) # add linear selectors to the sine and cosine line graphics -sine_selector = sine_graphic.add_linear_selector() -cosine_selector = cosine_graphic.add_linear_selector() +sine_selector = sine.add_linear_selector() +cosine_selector = cosine.add_linear_selector() def set_circle_cmap(ev): # sets the cmap transforms cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic - for g in [sine_graphic, cosine_graphic]: + for g in [sine, cosine]: g.cmap.transform = cmap_transform # set circle cmap transform - circle_graphic.cmap.transform = cmap_transform + circle.cmap.transform = cmap_transform # when the sine or cosine graphic is clicked, the cmap_transform # of the sine, cosine and circle line graphics are all set from # the y-values of the clicked line -sine_graphic.add_event_handler(set_circle_cmap, "click") -cosine_graphic.add_event_handler(set_circle_cmap, "click") +sine.add_event_handler(set_circle_cmap, "click") +cosine.add_event_handler(set_circle_cmap, "click") def set_x_val(ev): From bd48766fc876f781f662b12c66eae4346d13a17f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 23 Jul 2025 08:49:23 -0400 Subject: [PATCH 05/52] add link to quickstart nb at top of guide (#864) * Update guide.rst * Update guide.rst --- docs/source/user_guide/guide.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index 4f3dc64cb..6bbb3c8c8 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -71,7 +71,8 @@ This is just a simple example of how the ``fastplotlib`` API works to create a p However, we are just scratching the surface of what is possible with ``fastplotlib``. Next, let's take a look at the building blocks of ``fastplotlib`` and how they can be used to create more complex visualizations. -Aside from this user guide, the Examples Gallery is the best place to learn specific things in fastplotlib. +In addition to this user guide, the Examples Gallery is the best place to learn how to do specific things in fastplotlib. The `quickstart notebook `_ is also an excellent introduction to the API, even if you do not plan to use ``fastplotlib`` in notebooks. Remember, ``fastplotlib`` code is pretty much identical whether it's used in jupyterlab, Qt, or glfw! + If you still need help don't hesitate to post an issue or discussion post! Figure From 6cfc994f5f2ecd46d78cb194482e1cf14bda1599 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 8 Aug 2025 10:12:04 -0400 Subject: [PATCH 06/52] Update guide.rst (#887) --- docs/source/user_guide/guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index 6bbb3c8c8..8bf255507 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -62,7 +62,7 @@ Before giving a detailed overview of the library, here is a minimal example:: fig.show() if __name__ == "__main__": - fpl.run() + fpl.loop.run() .. image:: ../_static/guide_hello_world.png From 491bccd40fd89a9f07ad614ea13747c22ef9c7b6 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sat, 30 Aug 2025 16:43:27 -0400 Subject: [PATCH 07/52] add kwarg for axes visibility (#891) --- fastplotlib/layouts/_figure.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index af1a6a610..a4e219757 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -592,6 +592,7 @@ def show( self, autoscale: bool = True, maintain_aspect: bool = None, + axes_visible: bool = True, sidecar: bool = False, sidecar_kwargs: dict = None, ): @@ -606,6 +607,9 @@ def show( maintain_aspect: bool, default ``True`` maintain aspect ratio + axes_visible: bool, default ``True`` + show axes + sidecar: bool, default ``True`` display plot in a ``jupyterlab-sidecar``, only in jupyter @@ -644,6 +648,11 @@ def show( _maintain_aspect = maintain_aspect subplot.auto_scale(maintain_aspect=maintain_aspect) + # set axes visibility if False + if not axes_visible: + for subplot in self: + subplot.axes.visible = False + # parse based on canvas type if self.canvas.__class__.__name__ == "JupyterRenderCanvas": if sidecar: From d7d162d96bd99e017e70c778ea674ecd7dd0254a Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 1 Sep 2025 08:37:30 -0400 Subject: [PATCH 08/52] Update README.md (#889) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 5109d26aa..227d5bfb8 100644 --- a/README.md +++ b/README.md @@ -142,3 +142,13 @@ You can also take a look at our [**Roadmap for 2025**](https://github.com/fastpl - [**Amol Pasarkar**](https://github.com/apasarkar) A special thanks to all of the `pygfx` developers and the amazing work they have done. + +# Sponsors + +Fastplotlib is free and open source. We would like to thank the following institutions for helping to support fastplotlib over the past few years. + +- UNC Chapel Hill, Giovannucci Lab & Hantman Lab +- Flatiron Institute CCN, Chklovskii Lab +- Duke University, Pearson Lab + +We are always open to new sponsors that can help further develop and improve the library. From c2469ee813ffe830ad883a45253a045e71105462 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 2 Sep 2025 15:09:57 +0200 Subject: [PATCH 09/52] Fix typo in Visible graphics feature (#892) --- fastplotlib/graphics/features/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index 71e979f77..884cd0109 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -130,7 +130,7 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): class Visible(GraphicFeature): """Access or change the visibility.""" - property_name = "offset" + property_name = "visible" event_info_spec = [ {"dict key": "value", "type": "bool", "description": "new visibility bool"}, ] From 0293fa3fd974ecd7c8f0527abeb96071704a73d2 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 3 Sep 2025 09:01:29 +0200 Subject: [PATCH 10/52] Implement PolygonSelector (#837) * Implement PolygonSelector * Add triangulation * Improve interaction * Add example * More robust triangularion * tweaks * apply limiy * avoid artifact * add get_selected_data * wip interaction * Progress on interaction * Allow restarting and setting the polygon selection * cleanuo * cleanup * Update fastplotlib/graphics/selectors/_polygon.py Co-authored-by: Kushal Kolar * address reviewer comments * allow dragging the whole polygon * fix snapping * Add note to Bermuda lib --------- Co-authored-by: Kushal Kolar --- docs/source/api/graphics/ImageGraphic.rst | 1 + docs/source/api/graphics/LineCollection.rst | 1 + docs/source/api/graphics/LineGraphic.rst | 1 + docs/source/api/graphics/LineStack.rst | 1 + examples/selection_tools/polygon_selector.py | 65 ++ .../graphics/features/_selection_features.py | 105 +++ fastplotlib/graphics/image.py | 46 +- fastplotlib/graphics/line.py | 47 +- fastplotlib/graphics/line_collection.py | 52 +- fastplotlib/graphics/selectors/_polygon.py | 604 ++++++++++--- fastplotlib/graphics/selectors/_rectangle.py | 4 +- fastplotlib/utils/mapbox_earcut.py | 835 ++++++++++++++++++ fastplotlib/utils/triangulation.py | 70 ++ 13 files changed, 1709 insertions(+), 123 deletions(-) create mode 100644 examples/selection_tools/polygon_selector.py create mode 100644 fastplotlib/utils/mapbox_earcut.py create mode 100644 fastplotlib/utils/triangulation.py diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 27bda3d32..c7572aed5 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -48,6 +48,7 @@ Methods ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector ImageGraphic.add_rectangle_selector + ImageGraphic.add_polygon_selector ImageGraphic.clear_event_handlers ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 12c7b5c95..15403927e 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -53,6 +53,7 @@ Methods LineCollection.add_linear_region_selector LineCollection.add_linear_selector LineCollection.add_rectangle_selector + LineCollection.add_polygon_selector LineCollection.clear_event_handlers LineCollection.remove_event_handler LineCollection.remove_graphic diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index c6e18b41b..6ea799594 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -47,6 +47,7 @@ Methods LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector LineGraphic.add_rectangle_selector + LineGraphic.add_polygon_selector LineGraphic.clear_event_handlers LineGraphic.remove_event_handler LineGraphic.rotate diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index e1deb75ae..dbb8fc454 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -53,6 +53,7 @@ Methods LineStack.add_linear_region_selector LineStack.add_linear_selector LineStack.add_rectangle_selector + LineStack.add_polygon_selector LineStack.clear_event_handlers LineStack.remove_event_handler LineStack.remove_graphic diff --git a/examples/selection_tools/polygon_selector.py b/examples/selection_tools/polygon_selector.py new file mode 100644 index 000000000..9b1d0fbac --- /dev/null +++ b/examples/selection_tools/polygon_selector.py @@ -0,0 +1,65 @@ +""" +Polygon Selectors +================= + +Example showing how to use a `PolygonSelector` (a.k.a. lasso selector) with line collections +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +from itertools import product + +# create a figure +figure = fpl.Figure( + size=(700, 560) +) + + +# generate some data +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (50, 50) + +circles = list() +for center in product(range(0, spatial_dims[0], 9), range(0, spatial_dims[1], 9)): + circles.append(make_circle(center, 3, n_points=75)) + +pos_xy = np.vstack(circles) + +# add image +line_collection = figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5) + +# add polygon selector to image graphic +polygon_selector = line_collection.add_polygon_selector(fill_color="#ff00ff22", edge_color="#FFF", vertex_color="#FFF") + + +# add event handler to highlight selected indices +@polygon_selector.add_event_handler("selection") +def color_indices(ev): + line_collection.cmap = "jet" + ixs = ev.get_selected_indices() + # iterate through each of the selected indices, if the array size > 0 that mean it's under the selection + selected_line_ixs = [i for i in range(len(ixs)) if ixs[i].size > 0] + line_collection[selected_line_ixs].colors = "w" + + +# # manually move selector to make a nice gallery image :D +# polygon_selector.selection = (15, 30, 15, 30) + + +figure.show() + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py index 233353401..3052ae3d0 100644 --- a/fastplotlib/graphics/features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -1,9 +1,11 @@ from typing import Sequence import numpy as np +import pygfx as gfx from ...utils import mesh_masks from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance +from ...utils.triangulation import triangulate class LinearSelectionFeature(GraphicFeature): @@ -340,3 +342,106 @@ def set_value(self, selector, value: Sequence[float]): # calls any events self._call_event_handlers(event) + + +class PolygonSelectionFeature(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new array of points that represents the polygon selection", + }, + ] + + event_extra_attrs = [ + { + "attribute": "get_selected_indices", + "type": "callable", + "description": "returns indices under the selector", + }, + { + "attribute": "get_selected_data", + "type": "callable", + "description": "returns data under the selector", + }, + ] + + def __init__( + self, + value: Sequence[tuple[float]], + limits: tuple[float, float, float, float], + ): + super().__init__() + + self._limits = limits + self._value = np.asarray(value).reshape(-1, 3).astype(float) + + @property + def value(self) -> np.ndarray[float]: + """ + The array of the polygon, in data space + """ + return self._value + + @block_reentrance + def set_value(self, selector, value: Sequence[tuple[float]]): + """ + Set the selection of the rectangle selector. + + Parameters + ---------- + selector: PolygonSelector + + value: array + new values (3D points) of the selection + """ + + value = np.asarray(value, dtype=np.float32) + + if not value.shape[1] == 3: + raise TypeError( + "Selection must be an array, tuple, list, or sequence of the shape Nx3." + ) + + # clip values if they are beyond the limits + value[:, 0] = value[:, 0].clip(self._limits[0], self._limits[1]) + value[:, 1] = value[:, 1].clip(self._limits[2], self._limits[3]) + + self._value = value + + if len(value) >= 3: + indices = triangulate(value) + else: + indices = np.zeros((0, 3), np.int32) + + geometry = selector.geometry + + # Need larger buffer? + if len(value) > geometry.positions.nitems: + arr = np.zeros((geometry.positions.nitems * 2, 3), np.float32) + geometry.positions = gfx.Buffer(arr) + if len(indices) > geometry.indices.nitems: + arr = np.zeros((geometry.indices.nitems * 2, 3), np.int32) + geometry.indices = gfx.Buffer(arr) + + geometry.positions.data[: len(value)] = value + geometry.positions.data[len(value) :] = value[-1] if len(value) else (0, 0, 0) + geometry.positions.draw_range = 0, len(value) + geometry.positions.update_full() + + geometry.indices.data[: len(indices)] = indices + geometry.indices.data[len(indices) :] = 0 + geometry.indices.draw_range = 0, len(indices) + geometry.indices.update_full() + + # send event + if len(self._event_handlers) < 1: + return + + event = GraphicFeatureEvent("selection", {"value": self.value}) + + event.get_selected_indices = selector.get_selected_indices + event.get_selected_data = selector.get_selected_data + + # calls any events + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 957607fe1..85e34a413 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -5,7 +5,12 @@ from ..utils import quick_min_max from ._base import Graphic -from .selectors import LinearSelector, LinearRegionSelector, RectangleSelector +from .selectors import ( + LinearSelector, + LinearRegionSelector, + RectangleSelector, + PolygonSelector, +) from .features import ( TextureArray, ImageCmap, @@ -169,7 +174,6 @@ def __init__( # iterate through each texture chunk and create # an _ImageTIle, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: - # create an ImageTile using the texture for this chunk img = _ImageTile( geometry=pygfx.Geometry(grid=texture), @@ -437,3 +441,41 @@ def add_rectangle_selector( selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return selector + + def add_polygon_selector( + self, + selection: List[tuple[float, float]] = None, + fill_color=(0, 0, 0.35, 0.2), + **kwargs, + ) -> PolygonSelector: + """ + Add a :class:`.PolygonSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them + from a plot area just like any other ``Graphic``. + + Parameters + ---------- + selection: List of positions, optional + Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). + + """ + + # min/max limits are image shape + # rows are ys, columns are xs + limits = (0, self._data.value.shape[1], 0, self._data.value.shape[0]) + + selector = PolygonSelector( + selection, + limits, + fill_color=fill_color, + parent=self, + **kwargs, + ) + + self._plot_area.add_graphic(selector, center=False) + + # place above this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) + + return selector diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 4cdc7f413..7e6ecee93 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -5,7 +5,12 @@ import pygfx from ._positions_base import PositionsGraphic -from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector +from .selectors import ( + LinearRegionSelector, + LinearSelector, + RectangleSelector, + PolygonSelector, +) from .features import ( Thickness, VertexPositions, @@ -288,6 +293,46 @@ def add_rectangle_selector( return selector + def add_polygon_selector( + self, + selection: List[tuple[float, float]] = None, + **kwargs, + ) -> PolygonSelector: + """ + Add a :class:`.PolygonSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a + plot area just like any other ``Graphic``. + + Parameters + ---------- + selection: List of positions, optional + Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). + """ + + # remove any nans + data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] + + x_axis_vals = data[:, 0] + y_axis_vals = data[:, 1] + + ymin = np.floor(y_axis_vals.min()).astype(int) + ymax = np.ceil(y_axis_vals.max()).astype(int) + + # min/max limits + limits = (x_axis_vals[0], x_axis_vals[-1], ymin * 1.5, ymax * 1.5) + + selector = PolygonSelector( + selection, + limits, + parent=self, + **kwargs, + ) + + self._plot_area.add_graphic(selector, center=False) + + return selector + # TODO: this method is a bit of a mess, can refactor later def _get_linear_selector_init_args( self, axis: str, padding diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index de4139679..ad4bf74d5 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -7,7 +7,12 @@ from ..utils import parse_cmap_values from ._collection_base import CollectionIndexer, GraphicCollection, CollectionFeature from .line import LineGraphic -from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector +from .selectors import ( + LinearRegionSelector, + LinearSelector, + RectangleSelector, + PolygonSelector, +) class _LineCollectionProperties: @@ -198,19 +203,19 @@ def __init__( if not isinstance(thickness, (float, int)): if len(thickness) != len(data): raise ValueError( - f"len(thickness) != len(data)\n" f"{len(thickness)} != {len(data)}" + f"len(thickness) != len(data)\n{len(thickness)} != {len(data)}" ) if names is not None: if len(names) != len(data): raise ValueError( - f"len(names) != len(data)\n" f"{len(names)} != {len(data)}" + f"len(names) != len(data)\n{len(names)} != {len(data)}" ) if metadatas is not None: if len(metadatas) != len(data): raise ValueError( - f"len(metadata) != len(data)\n" f"{len(metadatas)} != {len(data)}" + f"len(metadata) != len(data)\n{len(metadatas)} != {len(data)}" ) if kwargs_lines is not None: @@ -447,7 +452,7 @@ def add_linear_region_selector( def add_rectangle_selector( self, - selection: tuple[float, float, float, float] = None, + selection: tuple[float, float, float] = None, **kwargs, ) -> RectangleSelector: """ @@ -486,6 +491,43 @@ def add_rectangle_selector( return selector + def add_polygon_selector( + self, + selection: List[tuple[float, float]] = None, + **kwargs, + ) -> PolygonSelector: + """ + Add a :class:`.PolygonSelector`. Selectors are just ``Graphic`` objects, so you can manage, + remove, or delete them from a plot area just like any other ``Graphic``. + + Parameters + ---------- + selection: List of positions, optional + Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). + """ + bbox = self.world_object.get_world_bounding_box() + + xdata = np.array(self.data[:, 0]) + xmin, xmax = (np.nanmin(xdata), np.nanmax(xdata)) + + ydata = np.array(self.data[:, 1]) + ymin = np.floor(ydata.min()).astype(int) + + ymax = np.ptp(bbox[:, 1]) + + limits = (xmin, xmax, ymin - (ymax * 1.5 - ymax), ymax * 1.5) + + selector = PolygonSelector( + selection, + limits, + parent=self, + **kwargs, + ) + + self._plot_area.add_graphic(selector, center=False) + + return selector + def _get_linear_selector_init_args(self, axis, padding): # use bbox to get size and center bbox = self.world_object.get_world_bounding_box() diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 22e42e63e..1bca2aca9 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -1,152 +1,532 @@ +import warnings from typing import * -import numpy as np +from dataclasses import dataclass +from numbers import Real +import numpy as np import pygfx -from ._base_selector import BaseSelector, MoveInfo from .._base import Graphic +from .._collection_base import GraphicCollection +from ..features._selection_features import PolygonSelectionFeature +from ._base_selector import BaseSelector + + +@dataclass +class MoveInfo: + """Movement info specific to the polygon selector.""" + + # The interaction mode: None, 'create', or 'drag' + mode: str + + # The index of the point in the polygon that is currently being manipulated + index: int + + # The index of the point in the polygon to snap to. This is used to merge (i.e. delete) points, and to finish se the polygon. + snap_index: int + + # The position of the cursor at the start of a drag + start_pos: np.ndarray | None + + # The position of the vertices at the start of a drag + start_positions: np.ndarray | None class PolygonSelector(BaseSelector): + _features = {"selection": PolygonSelectionFeature} + + @property + def parent(self) -> Graphic | None: + """Graphic that selector is associated with.""" + return self._parent + + @property + def selection(self) -> np.ndarray[float]: + """ + The polygon as an array of 3D points. The shape is [n_points, 3]. + """ + return self._selection.value.copy() + + @selection.setter + def selection(self, selection: np.ndarray[float]): + graphic = self._parent + + if isinstance(graphic, GraphicCollection): + pass + + self._selection.set_value(self, selection) + + @property + def limits(self) -> Tuple[float, float, float, float]: + """Return the limits of the selector.""" + return self._limits + + @limits.setter + def limits(self, values: Tuple[float, float, float, float]): + if len(values) != 4 or not all(map(lambda v: isinstance(v, Real), values)): + raise TypeError("limits must be an iterable of two numeric values") + self._limits = tuple( + map(round, values) + ) # if values are close to zero things get weird so round them + self._selection._limits = self._limits + def __init__( self, - edge_color="magenta", - edge_width: float = 3, + selection: Optional[Sequence[Tuple[float]]], + limits: Sequence[float], parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35, 0.2), + edge_color=(0.8, 0.6, 0), + edge_thickness: float = 4, + vertex_color=(0.7, 0.4, 0), + vertex_size: float = 12, name: str = None, ): - self.parent = parent + self._parent = parent + self._resizable = bool(resizable) - group = pygfx.Group() + BaseSelector.__init__(self, name=name, parent=parent) + self._move_info = MoveInfo("none", -1, -1, None, None) - self._set_world_object(group) - - self.edge_color = edge_color - self.edge_width = edge_width + # Initialize geometry with space for 8 points. The buffers are oversized, so we only need to create new buffers when the allocated space is full. + # The points are 3D, even though the z-component is always 0. Indices represent the faces (i.e. the triangles). + self.geometry = pygfx.Geometry( + positions=np.zeros((8, 3), np.float32), + indices=np.zeros((8, 3), np.int32), + ) - self._move_info: MoveInfo = None + # The draw range allows us to draw only part of the buffer, i.e. it allows us to oversize our buffers to avoid creating a new one for every added point. + self.geometry.positions.draw_range = 0, 0 + self.geometry.indices.draw_range = 0, 0 - self._current_mode = None + self._line = pygfx.Line( + self.geometry, + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), + ) + self._points = pygfx.Points( + self.geometry, + pygfx.PointsMaterial(size=vertex_size, color=vertex_color, pick_write=True), + ) + self._indicator = pygfx.Points( + pygfx.Geometry(positions=[[0, 0, 0]]), + pygfx.PointsMaterial(size=15, color=vertex_color, opacity=0.3), + ) + self._indicator.visible = False + self._mesh = pygfx.Mesh( + self.geometry, pygfx.MeshBasicMaterial(color=fill_color, pick_write=True) + ) + group = pygfx.Group().add(self._line, self._points, self._mesh, self._indicator) + self._set_world_object(group) - BaseSelector.__init__(self, name=name) + # Order in z, so stuff stays pickable + self._line.local.z = 0.1 + self._points.local.z = 0.2 - def get_vertices(self) -> np.ndarray: - """Get the vertices for the polygon""" - vertices = list() - for child in self.world_object.children: - vertices.append(child.geometry.positions.data[:, :2]) + if selection is None: + selection = [] + self._selection = PolygonSelectionFeature(selection, (0, 0, 0, 0)) - return np.vstack(vertices) + self.edge_color = edge_color + self.edge_width = edge_thickness + self.limits = limits + self.selection = self.selection # trigger positions to be created + + def get_selected_data( + self, graphic: Graphic = None, mode: str = "full" + ) -> Union[np.ndarray, List[np.ndarray]]: + """ + Get the ``Graphic`` data bounded by the current selection. + Returns a view of the data array. + + If the ``Graphic`` is a collection, such as a ``LineStack``, it returns a list of views of the full array. + Can be performed on the ``parent`` Graphic or on another graphic by passing to the ``graphic`` arg. + + Parameters + ---------- + graphic: Graphic, optional, default ``None`` + if provided, returns the data selection from this graphic instead of the graphic set as ``parent`` + mode: str, default 'full' + One of 'full', 'partial', or 'ignore'. Indicates how selected data should be returned based on the + selectors position over the graphic. Only used for ``LineGraphic``, ``LineCollection``, and ``LineStack`` + | If 'full', will return all data bounded by the x and y limits of the selector even if partial indices + along one axis are not fully covered by the selector. + | If 'partial' will return only the data that is bounded by the selector, missing indices not bounded by the + selector will be set to NaNs + | If 'ignore', will only return data for graphics that have indices completely bounded by the selector + + Returns + ------- + np.ndarray or List[np.ndarray] + view or list of views of the full array, returns empty array if selection is empty + """ + source = self._get_source(graphic) + ixs = self.get_selected_indices(source) + + # do not need to check for mode for images, because the selector is bounded by the image shape + # will always be `full` + if "Image" in source.__class__.__name__: + return source.data[ixs[:, 1], ixs[:, 0]] + + if mode not in ["full", "partial", "ignore"]: + raise ValueError( + f"`mode` must be one of 'full', 'partial', or 'ignore', you have passed {mode}" + ) + if "Line" in source.__class__.__name__: + if isinstance(source, GraphicCollection): + data_selections: List[np.ndarray] = list() + + for i, g in enumerate(source.graphics): + # want to keep same length as the original line collection + if ixs[i].size == 0: + data_selections.append( + np.array([], dtype=np.float32).reshape(0, 3) + ) + else: + # s gives entire slice of data along the x + s = slice( + ixs[i][0], ixs[i][-1] + 1 + ) # add 1 because these are direct indices + # slices n_datapoints dim + + # calculate missing ixs using set difference + # then calculate shift + missing_ixs = ( + np.setdiff1d(np.arange(ixs[i][0], ixs[i][-1] + 1), ixs[i]) + - ixs[i][0] + ) + + match mode: + # take all ixs, ignore missing + case "full": + data_selections.append(g.data[s]) + # set missing ixs data to NaNs + case "partial": + if len(missing_ixs) > 0: + data = g.data[s].copy() + data[missing_ixs] = np.nan + data_selections.append(data) + else: + data_selections.append(g.data[s]) + # ignore lines that do not have full ixs to start + case "ignore": + if len(missing_ixs) > 0: + data_selections.append( + np.array([], dtype=np.float32).reshape(0, 3) + ) + else: + data_selections.append(g.data[s]) + return data_selections + else: # for lines + if ixs.size == 0: + # empty selection + return np.array([], dtype=np.float32).reshape(0, 3) + + s = slice( + ixs[0], ixs[-1] + 1 + ) # add 1 to end because these are direct indices + # slices n_datapoints dim + # slice with min, max is faster than using all the indices + + # get missing ixs + missing_ixs = np.setdiff1d(np.arange(ixs[0], ixs[-1] + 1), ixs) - ixs[0] + + match mode: + # return all, do not care about missing + case "full": + return source.data[s] + # set missing to NaNs + case "partial": + if len(missing_ixs) > 0: + data = source.data[s].copy() + data[missing_ixs] = np.nan + return data + else: + return source.data[s] + # missing means nothing will be returned even if selector is partially over data + # warn the user and return empty + case "ignore": + if len(missing_ixs) > 0: + warnings.warn( + "You have selected 'ignore' mode. Selected graphic has incomplete indices. " + "Move the selector or change the mode to one of `partial` or `full`." + ) + return np.array([], dtype=np.float32) + else: + return source.data[s] + + def get_selected_indices( + self, graphic: Graphic = None + ) -> np.ndarray | tuple[np.ndarray]: + """ + Returns the indices of the ``Graphic`` data bounded by the current selection. + + These are the data indices which correspond to the data under the selector. + + Parameters + ---------- + graphic: Graphic, default ``None`` + If provided, returns the selection indices from this graphic instead of the graphic set as ``parent`` + + Returns + ------- + Union[np.ndarray, List[np.ndarray]] + data indicies of the selection + | array of (x, y) indices if the graphic is an image + | list of indices along the x-dimension for each line if graphic is a line collection + | array of indices along the x-dimension if graphic is a line + """ + # get indices from source + source = self._get_source(graphic) + + # selector (xmin, xmax, ymin, ymax) values + polygon = self.selection[:, :2] + + # Empty ... + if len(polygon) == 0: + if "Image" in source.__class__.__name__: + return np.zeros((0, 2), np.int32) + if "Line" in source.__class__.__name__: + if isinstance(source, GraphicCollection): + return [np.zeros((0, 1), np.int32) for _ in source.graphics] + else: + return np.zeros((0, 1), np.int32) + + # Get bounding box to be able to do first selection + xmin, xmax = polygon[:, 0].min(), polygon[:, 0].max() + ymin, ymax = polygon[:, 1].min(), polygon[:, 1].max() + + # image data does not need to check for mode because the selector is always bounded + # to the image + if "Image" in source.__class__.__name__: + shape = source.data.value.shape + col_ixs = np.arange(max(0, xmin), min(xmax, shape[1] - 1), dtype=int) + row_ixs = np.arange(max(0, ymin), min(ymax, shape[0] - 1), dtype=int) + indices = [] + for y in row_ixs: + for x in col_ixs: + p = np.array([x, y], np.float32) + if point_in_polygon((x, y), polygon): + indices.append(p) + return np.array(indices, np.int32).reshape(-1, 2) + + if "Line" in source.__class__.__name__: + if isinstance(source, GraphicCollection): + ixs = list() + for g in source.graphics: + points = g.data.value[:, :2] + g.offset[:2] + g_ixs = np.where( + (points[:, 0] >= xmin) + & (points[:, 0] <= xmax) + & (points[:, 1] >= ymin) + & (points[:, 1] <= ymax) + )[0] + g_ixs = np.array( + [i for i in g_ixs if point_in_polygon(points[i], polygon)], + g_ixs.dtype, + ) + ixs.append(g_ixs) + else: + # map only this graphic + points = source.data.value[:2] + ixs = np.where( + (points[:, 0] >= xmin) + & (points[:, 0] <= xmax) + & (points[:, 1] >= ymin) + & (points[:, 1] <= ymax) + )[0] + ixs = np.array( + [i for i in ixs if point_in_polygon(points[i], polygon)], + ixs.dtype, + ) + + return ixs def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area - # click to add new segment - self._plot_area.renderer.add_event_handler(self._add_segment, "click") - # pointer move to change endpoint of segment self._plot_area.renderer.add_event_handler( - self._move_segment_endpoint, "pointer_move" + self._on_pointer_down, "pointer_down" ) - - # click to finish existing segment - self._plot_area.renderer.add_event_handler(self._finish_segment, "click") - - # double click to finish polygon - self._plot_area.renderer.add_event_handler(self._finish_polygon, "double_click") - - self.position_z = len(self._plot_area) + 10 - - def _add_segment(self, ev): - """After click event, adds a new line segment""" - self._current_mode = "add" - - position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo( - start_selection=None, - start_position=position, - delta=np.zeros_like(position), - source=None, - ) - - # line with same position for start and end until mouse moves - data = np.array([position, position]) - - new_line = pygfx.Line( - geometry=pygfx.Geometry(positions=data.astype(np.float32)), - material=pygfx.LineMaterial( - thickness=self.edge_width, - color=pygfx.Color(self.edge_color), - pick_write=True, - ), + self._plot_area.renderer.add_event_handler( + self._on_pointer_move, "pointer_move" ) + self._plot_area.renderer.add_event_handler(self._on_pointer_up, "pointer_up") - self.world_object.add(new_line) - - def _move_segment_endpoint(self, ev): - """After mouse pointer move event, moves endpoint of current line segment""" - if self._move_info is None: - return - self._current_mode = "move" + self.position_z = len(self._plot_area) + 10 + if len(self.selection) == 0: + self._start_move_mode("create", -1) + + def start_new_polygon(self): + """Remove the current polygon and start drawing a new one.""" + self.selection = np.zeros((0, 3), np.float32) + self._start_move_mode("create", -1) + + def _start_move_mode(self, what, index, start_pos=None): + self._plot_area.controller.enabled = False + self._move_info.mode = what + self._move_info.index = index + self._move_info.snap_index = None + self._indicator.material.size = 15 + self._indicator.visible = True + if start_pos is not None: + self._move_info.start_pos = start_pos + self._move_info.start_positions = self.selection.copy() + self._indicator.visible = False + + def _end_move_mode(self): + if self._move_info.mode == "create": + self.world_object.children[0].material.loop = True + self._plot_area.controller.enabled = True + self._move_info.mode = None + self._move_info.start_pos = None + self._move_info.start_positions = None + self._indicator.visible = False + + def _on_pointer_down(self, ev): world_pos = self._plot_area.map_screen_to_world(ev) - if world_pos is None: return - # change endpoint - self.world_object.children[-1].geometry.positions.data[1] = np.array( - [world_pos] - ).astype(np.float32) - self.world_object.children[-1].geometry.positions.update_range() - - def _finish_segment(self, ev): - """After click event, ends a line segment""" - # should start a new segment - if self._move_info is None: - return - - # since both _add_segment and _finish_segment use the "click" callback - # this is to block _finish_segment right after a _add_segment call - if self._current_mode == "add": + if self._move_info.mode == "create": + # Add a polygon or finish it + if self._move_info.snap_index is not None: + pass # on release we finish the polygon + else: + self._insert_polygon_vertex(999999, world_pos) + + elif self._move_info.mode is None: + # Maybe initiate a drag + if ev.target is self._points: + index = ev.pick_info["vertex_index"] + self._start_move_mode("drag", index) + elif ev.target is self._line: + index = ev.pick_info["vertex_index"] + if ev.pick_info["segment_coord"] > 0: + index += 1 + self._insert_polygon_vertex(index, world_pos) + self._start_move_mode("drag", index) + elif ev.target is self._mesh: + index = None # move whole polygon + self._start_move_mode("drag", index, world_pos) + + def _on_pointer_move(self, ev): + """After mouse pointer move event, moves endpoint of current line segment""" + if self._move_info.mode is None: return - - # just make move info None so that _move_segment_endpoint is not called - # and _add_segment gets triggered for "click" - self._move_info = None - - self._current_mode = "finish-segment" - - def _finish_polygon(self, ev): - """finishes the polygon, disconnects events""" world_pos = self._plot_area.map_screen_to_world(ev) - if world_pos is None: return - # make new line to connect first and last vertices - data = np.vstack( - [world_pos, self.world_object.children[0].geometry.positions.data[0]] - ) - - new_line = pygfx.Line( - geometry=pygfx.Geometry(positions=data.astype(np.float32)), - material=pygfx.LineMaterial( - thickness=self.edge_width, - color=pygfx.Color(self.edge_color), - pick_write=True, - ), - ) - - self.world_object.add(new_line) - - handlers = { - self._add_segment: "click", - self._move_segment_endpoint: "pointer_move", - self._finish_segment: "click", - self._finish_polygon: "double_click", - } - - for handler, event in handlers.items(): - self._plot_area.renderer.remove_event_handler(handler, event) + # Are we close to a point that we can snap to? + # The concept of snapping does multiple things: + # - preventing the user from creating points that are very close to each-other, + # - allowing the user to finish the polygon by connecting to the start-point when in 'create' mode. + # - allowing the user to merge points by dragging one onto its neighbour. + index = self._move_info.index + snap_index = None + if ev.target is self._points: + snap_index = ev.pick_info["vertex_index"] + if snap_index == index: # dont snap to moving point + snap_index = None + if len(self.selection) < 4: + snap_index = None + if self._move_info.mode == "create" and snap_index != 0: + snap_index = None + if self._move_info.mode == "drag" and index is not None: + last_index = len(self.selection) - 1 + if not ( + (index == 0 and snap_index == last_index) + or (index == last_index and snap_index == 0) + or (snap_index in (index - 1, index + 1)) + ): + snap_index = None + self._move_info.snap_index = snap_index + + # Show state of snap index to user + if snap_index is not None: + world_pos = self.geometry.positions.data[snap_index] + self._indicator.material.size = 30 + else: + self._indicator.material.size = 15 + + # Move the positions being moved a bit down in depth, so its de-preferred in picking + world_pos = (world_pos[0], world_pos[1], -0.05) + + self._indicator.local.position = world_pos + + # Update data + if self._move_info.mode in ("create", "drag"): + data = self.selection + if len(data) > 0: + if self._move_info.index is None: + delta = world_pos - self._move_info.start_pos + data[:] = self._move_info.start_positions + delta + else: + data[self._move_info.index] = world_pos + self._selection.set_value(self, data) + + def _on_pointer_up(self, ev): + if self._move_info.mode in ("create", "drag"): + # Update data to set depth (z) to zero again + data = self.selection + data[:, 2] = 0 + self._selection.set_value(self, data) + # If we snapped, we dissolve (i.e. delete the vertex being moved) + if self._move_info.snap_index is not None: + assert self._move_info.index is not None + self._delete_polygon_vertex(self._move_info.index) + + # Moving the mouse up may end the move action + if self._move_info.mode == "create": + if self._move_info.snap_index is not None: + self._end_move_mode() + elif self._move_info.mode == "drag": + self._end_move_mode() + + def _insert_polygon_vertex(self, i, world_pos): + selection = self.selection + if len(selection) == 0: + data = np.vstack([selection, world_pos, world_pos]) + else: + data = np.vstack([selection[:i], world_pos, selection[i:]]) + self._selection.set_value(self, data) + + def _delete_polygon_vertex(self, i): + selection = self.selection + if i < 0: + data = selection[:i] + else: + data = np.vstack([selection[:i], selection[i + 1 :]]) + self._selection.set_value(self, data) + + +def is_left(p0, p1, p2): + """Test if point p2 is left of the line formed by p0 → p1""" + return (p1[0] - p0[0]) * (p2[1] - p0[1]) - (p2[0] - p0[0]) * (p1[1] - p0[1]) + + +def point_in_polygon(point, polygon): + """Determines if the point is inside the polygon using the winding number algorithm.""" + wn = 0 # winding number counter + n = len(polygon) + + for i in range(n): + p0 = polygon[i] + p1 = polygon[(i + 1) % n] + + if p0[1] <= point[1]: # start y <= point.y + if p1[1] > point[1]: # upward crossing + if is_left(p0, p1, point) > 0: + wn += 1 # point is left of edge + else: # start y > point.y + if p1[1] <= point[1]: # downward crossing + if is_left(p0, p1, point) < 0: + wn -= 1 # point is right of edge + + return wn != 0 diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py index db7691e07..fc62faf5c 100644 --- a/fastplotlib/graphics/selectors/_rectangle.py +++ b/fastplotlib/graphics/selectors/_rectangle.py @@ -337,7 +337,6 @@ def get_selected_data( f"`mode` must be one of 'full', 'partial', or 'ignore', you have passed {mode}" ) if "Line" in source.__class__.__name__: - if isinstance(source, GraphicCollection): data_selections: List[np.ndarray] = list() @@ -431,7 +430,7 @@ def get_selected_indices( Parameters ---------- graphic: Graphic, default ``None`` - If provided, returns the selection indices from this graphic instrad of the graphic set as ``parent`` + If provided, returns the selection indices from this graphic instead of the graphic set as ``parent`` Returns ------- @@ -479,7 +478,6 @@ def get_selected_indices( return ixs def _move_graphic(self, move_info: MoveInfo): - # If this the first move in this drag, store initial selection if move_info.start_selection is None: move_info.start_selection = self.selection diff --git a/fastplotlib/utils/mapbox_earcut.py b/fastplotlib/utils/mapbox_earcut.py new file mode 100644 index 000000000..ecb129593 --- /dev/null +++ b/fastplotlib/utils/mapbox_earcut.py @@ -0,0 +1,835 @@ +# The code below is copied from https://github.com/MIERUNE/earcut-py/blob/cb30bff5458fca224c573187f36d889068ebd4e0/src/earcut/__init__.py +# which is a port of Mapbox' JS earcut (https://github.com/mapbox/earcut) version 2.2.4 +# The code is not modified, except maybe formatting to keep the linter happy. +# +# ISC License +# +# Copyright (c) 2016, Mapbox +# Copyright (c) 2023, MIERUNE Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any purpose +# with or without fee is hereby granted, provided that the above copyright notice +# and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. + +import math +from typing import Optional + + +def earcut(data, hole_indices=None, dim=2): + has_holes = bool(hole_indices) + outer_len = hole_indices[0] * dim if has_holes else len(data) + outer_node = _linked_list(data, 0, outer_len, dim, True) + triangles = [] + + if (not outer_node) or outer_node.next == outer_node.prev: + return triangles + + min_x = min_y = inv_size = None + + if has_holes: + outer_node = _eliminate_holes(data, hole_indices, outer_node, dim) + + # if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox + if len(data) > 80 * dim: + min_x = max_x = data[0] + min_y = max_y = data[1] + + for i in range(dim, outer_len, dim): + x = data[i] + y = data[i + 1] + if x < min_x: + min_x = x + if y < min_y: + min_y = y + if x > max_x: + max_x = x + if y > max_y: + max_y = y + + # minX, minY and invSize are later used to transform coords into integers for z-order calculation + inv_size = max(max_x - min_x, max_y - min_y) + inv_size = 32767 / inv_size if inv_size != 0 else 0 + + _earcut_linked(outer_node, triangles, dim, min_x, min_y, inv_size) + + return triangles + + +# create a circular doubly linked list from polygon points in the specified winding order +def _linked_list(data, start, end, dim, clockwise): + last = None + + if clockwise == (_signed_area(data, start, end, dim) > 0): + for i in range(start, end, dim): + last = _insert_node(i, data[i], data[i + 1], last) + else: + for i in reversed(range(start, end, dim)): + last = _insert_node(i, data[i], data[i + 1], last) + + if last and _equals(last, last.next): + _remove_node(last) + last = last.next + + return last + + +# eliminate colinear or duplicate points +def _filter_points(start, end=None): + if not start: + return start + + if not end: + end = start + + p = start + while True: + again = False + + if not p.steiner and (_equals(p, p.next) or _area(p.prev, p, p.next) == 0): + _remove_node(p) + p = end = p.prev + if p == p.next: + break + again = True + + else: + p = p.next + + if (not again) and p == end: + break + + return end + + +# main ear slicing loop which triangulates a polygon (given as a linked list) +def _earcut_linked(ear, triangles, dim, min_x, min_y, inv_size, _pass=0): + if not ear: + return + + # interlink polygon nodes in z-order + if not _pass and inv_size: + _index_curve(ear, min_x, min_y, inv_size) + + stop = ear + + # iterate through ears, slicing them one by one + while ear.prev != ear.next: + prev = ear.prev + next = ear.next + is_ear = ( + _is_ear_hashed(ear, min_x, min_y, inv_size) if inv_size else _is_ear(ear) + ) + + if is_ear: + # cut off the triangle + triangles.append(prev.i // dim) + triangles.append(ear.i // dim) + triangles.append(next.i // dim) + + _remove_node(ear) + + # skipping the next vertex leads to less sliver triangles + ear = next.next + stop = next.next + + continue + + ear = next + + # if we looped through the whole remaining polygon and can't find any more ears + if ear == stop: + # try filtering points and slicing again + if not _pass: + _earcut_linked( + _filter_points(ear), triangles, dim, min_x, min_y, inv_size, 1 + ) + + # if this didn't work, try curing all small self-intersections locally + elif _pass == 1: + ear = _cure_local_intersections(_filter_points(ear), triangles, dim) + _earcut_linked(ear, triangles, dim, min_x, min_y, inv_size, 2) + + # as a last resort, try splitting the remaining polygon into two + elif _pass == 2: + _split_earcut(ear, triangles, dim, min_x, min_y, inv_size) + + break + + +# check whether a polygon node forms a valid ear with adjacent nodes +def _is_ear(ear): + a = ear.prev + b = ear + c = ear.next + + if _area(a, b, c) >= 0: + return False # reflex, can't be an ear + + # now make sure we don't have other points inside the potential ear + ax = a.x + ay = a.y + bx = b.x + by = b.y + cx = c.x + cy = c.y + + # triangle bbox; min & max are calculated like this for speed + x0 = (ax if ax < cx else cx) if ax < bx else (bx if bx < cx else cx) + y0 = (ay if ay < cy else cy) if ay < by else (by if by < cy else cy) + x1 = (ax if ax > cx else cx) if ax > bx else (bx if bx > cx else cx) + y1 = (ay if ay > cy else cy) if ay > by else (by if by > cy else cy) + + p = c.next + while p != a: + if ( + (p.x >= x0 and p.x <= x1 and p.y >= y0 and p.y <= y1) + and _point_in_triangle(ax, ay, bx, by, cx, cy, p.x, p.y) + and _area(p.prev, p, p.next) >= 0 + ): + return False + p = p.next + + return True + + +def _is_ear_hashed(ear, min_x, min_y, inv_size): + a = ear.prev + b = ear + c = ear.next + + if _area(a, b, c) >= 0: + return False # reflex, can't be an ear + + ax = a.x + ay = a.y + bx = b.x + by = b.y + cx = c.x + cy = c.y + + # triangle bbox; min & max are calculated like this for speed + x0 = (ax if ax < cx else cx) if ax < bx else (bx if bx < cx else cx) + y0 = (ay if ay < cy else cy) if ay < by else (by if by < cy else cy) + x1 = (ax if ax > cx else cx) if ax > bx else (bx if bx > cx else cx) + y1 = (ay if ay > cy else cy) if ay > by else (by if by > cy else cy) + + # z-order range for the current triangle bbox + min_z = _z_order(x0, y0, min_x, min_y, inv_size) + max_z = _z_order(x1, y1, min_x, min_y, inv_size) + + p = ear.prev_z + n = ear.next_z + + # look for points inside the triangle in both directions + while p and p.z >= min_z and n and n.z <= max_z: + if ( + (p.x >= x0 and p.x <= x1 and p.y >= y0 and p.y <= y1) + and (p != a and p != c) + and _point_in_triangle(ax, ay, bx, by, cx, cy, p.x, p.y) + and _area(p.prev, p, p.next) >= 0 + ): + return False + p = p.prev_z + + if ( + (n.x >= x0 and n.x <= x1 and n.y >= y0 and n.y <= y1) + and (n != a and n != c) + and _point_in_triangle(ax, ay, bx, by, cx, cy, n.x, n.y) + and _area(n.prev, n, n.next) >= 0 + ): + return False + n = n.next_z + + # look for remaining points in decreasing z-order + while p and p.z >= min_z: + if ( + (p != ear.prev and p != ear.next) + and _point_in_triangle(ax, ay, bx, by, cx, cy, p.x, p.y) + and _area(p.prev, p, p.next) >= 0 + ): + return False + p = p.prev_z + + # look for remaining points in increasing z-order + while n and n.z <= max_z: + if ( + (n != ear.prev and n != ear.next) + and _point_in_triangle(ax, ay, bx, by, cx, cy, n.x, n.y) + and _area(n.prev, n, n.next) >= 0 + ): + return False + n = n.next_z + + return True + + +# go through all polygon nodes and cure small local self-intersections +def _cure_local_intersections(start, triangles, dim): + p = start + while True: + a = p.prev + b = p.next.next + + if ( + not _equals(a, b) + and _intersects(a, p, p.next, b) + and _locally_inside(a, b) + and _locally_inside(b, a) + ): + triangles.append(a.i // dim) + triangles.append(p.i // dim) + triangles.append(b.i // dim) + + # remove two nodes involved + _remove_node(p) + _remove_node(p.next) + + p = start = b + + p = p.next + if p == start: + break + + return _filter_points(p) + + +# try splitting polygon into two and triangulate them independently +def _split_earcut(start, triangles, dim, min_x, min_y, inv_size): + # look for a valid diagonal that divides the polygon into two + a = start + while True: + b = a.next.next + while b != a.prev: + if a.i != b.i and _is_valid_diagonal(a, b): + # split the polygon in two by the diagonal + c = _split_polygon(a, b) + + # filter colinear points around the cuts + a = _filter_points(a, a.next) + c = _filter_points(c, c.next) + + # run earcut on each half + _earcut_linked(a, triangles, dim, min_x, min_y, inv_size) + _earcut_linked(c, triangles, dim, min_x, min_y, inv_size) + return + b = b.next + a = a.next + if a == start: + break + + +# link every hole into the outer loop, producing a single-ring polygon without holes +def _eliminate_holes(data, hole_indices, outer_node, dim): + queue = [] + _len = len(hole_indices) + + for i in range(_len): + start = hole_indices[i] * dim + end = hole_indices[i + 1] * dim if i < _len - 1 else len(data) + lst = _linked_list(data, start, end, dim, False) + if lst: + if lst == lst.next: + lst.steiner = True + queue.append(_get_leftmost(lst)) + + queue.sort(key=lambda i: i.x) + + # process holes from left to right + for q_i in queue: + outer_node = _eliminate_hole(q_i, outer_node) + + return outer_node + + +# find a bridge between vertices that connects hole with an outer ring and and link it +def _eliminate_hole(hole, outer_node): + bridge = _find_hole_bridge(hole, outer_node) + if not bridge: + return outer_node + + bridge_reverse = _split_polygon(bridge, hole) + + _filter_points(bridge_reverse, bridge_reverse.next) + return _filter_points(bridge, bridge.next) + + +# David Eberly's algorithm for finding a bridge between hole and outer polygon +def _find_hole_bridge(hole, outer_node): + p = outer_node + hx = hole.x + hy = hole.y + qx = -math.inf + m = None + + # find a segment intersected by a ray from the hole's leftmost point to the left + # segment's endpoint with lesser x will be potential connection point + while True: + px = p.x + py = p.y + if hy <= py and hy >= p.next.y and p.next.y != py: + x = px + (hy - py) * (p.next.x - px) / (p.next.y - py) + if x <= hx and x > qx: + qx = x + m = p if px < p.next.x else p.next + if x == hx: + # hole touches outer segment; pick leftmost endpoint + return m + p = p.next + if p == outer_node: + break + + if not m: + return None + + # look for points inside the triangle of hole point, segment intersection and endpoint + # if there are no points found, we have a valid connection + # otherwise choose the point of the minimum angle with the ray as connection point + + stop = m + mx = m.x + my = m.y + tan_min = math.inf + + p = m + + while True: + px = p.x + py = p.y + if (hx >= px and px >= mx and hx != px) and _point_in_triangle( + hx if hy < my else qx, + hy, + mx, + my, + qx if hy < my else hx, + hy, + px, + py, + ): + tan = abs(hy - py) / (hx - px) # tangential + + if _locally_inside(p, hole) and ( + tan < tan_min + or ( + tan == tan_min + and (px > m.x or (px == m.x and _sector_contains_sector(m, p))) + ) + ): + m = p + tan_min = tan + + p = p.next + if p == stop: + break + + return m + + +# whether sector in vertex m contains sector in vertex p in the same coordinates +def _sector_contains_sector(m, p): + return _area(m.prev, m, p.prev) < 0 and _area(p.next, m, m.next) < 0 + + +# interlink polygon nodes in z-order +def _index_curve(start, min_x, min_y, inv_size): + p = start + while True: + if p.z is None: + p.z = _z_order(p.x, p.y, min_x, min_y, inv_size) + p.prev_z = p.prev + p.next_z = p.next + p = p.next + if p == start: + break + + p.prev_z.next_z = None + p.prev_z = None + + _sort_linked(p) + + +# Simon Tatham's linked list merge sort algorithm +# http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html +def _sort_linked(_list): + in_size = 1 + + while True: + p = _list + _list = None + tail = None + num_merges = 0 + + while p: + num_merges += 1 + q = p + p_size = 0 + for i in range(in_size): + p_size += 1 + q = q.next_z + if not q: + break + q_size = in_size + + while p_size > 0 or (q_size > 0 and q): + if p_size != 0 and (q_size == 0 or not q or p.z <= q.z): + e = p + p = p.next_z + p_size -= 1 + else: + e = q + q = q.next_z + q_size -= 1 + + if tail: + tail.next_z = e + else: + _list = e + + e.prev_z = tail + tail = e + + p = q + + tail.next_z = None + in_size *= 2 + + if num_merges <= 1: + break + + return _list + + +# z-order of a point given coords and inverse of the longer side of data bbox +def _z_order(x, y, min_x, min_y, inv_size): + # coords are transformed into non-negative 15-bit integer range + x = int((x - min_x) * inv_size) + y = int((y - min_y) * inv_size) + + x = (x | (x << 8)) & 0x00FF00FF + x = (x | (x << 4)) & 0x0F0F0F0F + x = (x | (x << 2)) & 0x33333333 + x = (x | (x << 1)) & 0x55555555 + + y = (y | (y << 8)) & 0x00FF00FF + y = (y | (y << 4)) & 0x0F0F0F0F + y = (y | (y << 2)) & 0x33333333 + y = (y | (y << 1)) & 0x55555555 + + return x | (y << 1) + + +# find the leftmost node of a polygon ring +def _get_leftmost(start): + p = start + leftmost = start + + while True: + if p.x < leftmost.x or (p.x == leftmost.x and p.y < leftmost.y): + leftmost = p + + p = p.next + if p == start: + break + + return leftmost + + +# check if a point lies within a convex triangle +def _point_in_triangle(ax, ay, bx, by, cx, cy, px, py): + pax = ax - px + pay = ay - py + pbx = bx - px + pby = by - py + pcx = cx - px + pcy = cy - py + return ( + pcx * pay - pax * pcy >= 0 + and pax * pby - pbx * pay >= 0 + and pbx * pcy - pcx * pby >= 0 + ) + + +# check if a diagonal between two polygon nodes is valid (lies in polygon interior) +def _is_valid_diagonal(a, b): + return ( + # dones't intersect other edges + (a.next.i != b.i and a.prev.i != b.i and not _intersects_polygon(a, b)) + and ( + # locally visible + (_locally_inside(a, b) and _locally_inside(b, a) and _middle_inside(a, b)) + # does not create opposite-facing sectors + and (_area(a.prev, a, b.prev) or _area(a, b.prev, b)) + # special zero-length case + or ( + _equals(a, b) + and _area(a.prev, a, a.next) > 0 + and _area(b.prev, b, b.next) > 0 + ) + ) + ) + + +# signed area of a triangle +def _area(p, q, r): + px = p.x + py = p.y + qx = q.x + qy = q.y + rx = r.x + ry = r.y + return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) + + +# check if two points are equal +def _equals(p1, p2): + return p1.x == p2.x and p1.y == p2.y + + +# check if two segments intersect +def _intersects(p1, q1, p2, q2): + o1 = _sign(_area(p1, q1, p2)) + o2 = _sign(_area(p1, q1, q2)) + o3 = _sign(_area(p2, q2, p1)) + o4 = _sign(_area(p2, q2, q1)) + + if ( + (o1 != o2 and o3 != o4) # general case + or ( + o1 == 0 and _on_segment(p1, p2, q1) + ) # p1, q1 and p2 are collinear and p2 lies on p1q1 + or ( + o2 == 0 and _on_segment(p1, q2, q1) + ) # p1, q1 and q2 are collinear and q2 lies on p1q1 + or ( + o3 == 0 and _on_segment(p2, p1, q2) + ) # p2, q2 and p1 are collinear and p1 lies on p2q2 + or ( + o4 == 0 and _on_segment(p2, q1, q2) + ) # p2, q2 and q1 are collinear and q1 lies on p2q2 + ): + return True + + return False + + +# for collinear points p, q, r, check if point q lies on segment pr +def _on_segment(p, q, r): + return ( + q.x <= max(p.x, r.x) + and q.x >= min(p.x, r.x) + and q.y <= max(p.y, r.y) + and q.y >= min(p.y, r.y) + ) + + +def _sign(num): + if num > 0: + return 1 + elif num < 0: + return -1 + else: + return 0 + + +# check if a polygon diagonal intersects any polygon segments +def _intersects_polygon(a, b): + p = a + while True: + pi = p.i + ai = a.i + bi = b.i + pnext = p.next + pnexti = pnext.i + if (pi != ai and pnexti != ai and pi != bi and pnexti != bi) and _intersects( + p, pnext, a, b + ): + return True + + p = pnext + if p == a: + break + + return False + + +# check if a polygon diagonal is locally inside the polygon +def _locally_inside(a, b): + aprev = a.prev + anext = a.next + if _area(aprev, a, anext) < 0: + return _area(a, b, anext) >= 0 and _area(a, aprev, b) >= 0 + else: + return _area(a, b, aprev) < 0 or _area(a, anext, b) < 0 + + +# check if the middle point of a polygon diagonal is inside the polygon +def _middle_inside(a, b): + p = a + inside = False + px = (a.x + b.x) / 2 + py = (a.y + b.y) / 2 + while True: + p_x = p.x + p_y = p.y + p_next = p.next + p_next_y = p_next.y + if ( + (p_y > py) != (p_next_y > py) + and p_next.y != p_y + and (px < (p_next.x - p_x) * (py - p_y) / (p_next_y - p_y) + p_x) + ): + inside = not inside + p = p_next + if p == a: + break + + return inside + + +# link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two +# if one belongs to the outer ring and another to a hole, it merges it into a single ring +def _split_polygon(a, b): + a2 = _Node(a.i, a.x, a.y) + b2 = _Node(b.i, b.x, b.y) + an = a.next + bp = b.prev + + a.next = b + b.prev = a + + a2.next = an + an.prev = a2 + b2.next = a2 + a2.prev = b2 + bp.next = b2 + b2.prev = bp + + return b2 + + +# create a node and optionally link it with previous one (in a circular doubly linked list) +def _insert_node(i, x, y, last): + p = _Node(i, x, y) + + if not last: + p.prev = p + p.next = p + + else: + p.next = last.next + p.prev = last + last.next.prev = p + last.next = p + + return p + + +def _remove_node(p): + p.next.prev = p.prev + p.prev.next = p.next + + if p.prev_z: + p.prev_z.next_z = p.next_z + + if p.next_z: + p.next_z.prev_z = p.prev_z + + +class _Node: + __slots__ = ["i", "x", "y", "prev", "next", "z", "prev_z", "next_z", "steiner"] + i: int + x: float + y: float + prev: Optional["_Node"] + next: Optional["_Node"] + z: Optional[int] + prev_z: Optional["_Node"] + next_z: Optional["_Node"] + steiner: bool + + def __init__(self, i, x, y): + # vertex index in coordinates array + self.i = i + + # vertex coordinates + self.x = x + self.y = y + + # previous and next vertex nodes in a polygon ring + self.prev = None + self.next = None + + # z-order curve value + self.z = None + + # previous and next nodes in z-order + self.prev_z = None + self.next_z = None + + # indicates whether this is a steiner point + self.steiner = False + + +def _signed_area(data, start, end, dim): + sum = 0 + j = end - dim + for i in range(start, end, dim): + sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]) + j = i + + return sum + + +# return a percentage difference between the polygon area and its triangulation area +# used to verify correctness of triangulation +def deviation(data, hole_indices, dim, triangles): + has_holes = hole_indices and len(hole_indices) + outer_len = hole_indices[0] * dim if has_holes else len(data) + + polygon_area = abs(_signed_area(data, 0, outer_len, dim)) + if has_holes: + _len = len(hole_indices) + for i in range(_len): + start = hole_indices[i] * dim + end = hole_indices[i + 1] * dim if i < _len - 1 else len(data) + polygon_area -= abs(_signed_area(data, start, end, dim)) + + triangles_area = 0 + for i in range(0, len(triangles), 3): + a = triangles[i] * dim + b = triangles[i + 1] * dim + c = triangles[i + 2] * dim + triangles_area += abs( + (data[a] - data[c]) * (data[b + 1] - data[a + 1]) + - (data[a] - data[b]) * (data[c + 1] - data[a + 1]) + ) + + if polygon_area == 0 and triangles_area == 0: + return 0 + return abs((triangles_area - polygon_area) / polygon_area) + + +# turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts +def flatten(data): + dim = len(data[0][0]) + vertices = [] + holes = [] + hole_index = 0 + + for i in range(len(data)): + for j in range(len(data[i])): + for d in range(dim): + vertices.append(data[i][j][d]) + + if i > 0: + hole_index += len(data[i - 1]) + holes.append(hole_index) + + return (vertices, holes, dim) diff --git a/fastplotlib/utils/triangulation.py b/fastplotlib/utils/triangulation.py new file mode 100644 index 000000000..7abe089de --- /dev/null +++ b/fastplotlib/utils/triangulation.py @@ -0,0 +1,70 @@ +import logging + +import numpy as np +from .mapbox_earcut import earcut as mapbox_earcut + + +logger = logging.getLogger("fastplotlib") + + +# Note: the current triangulation is in pure Python. If the results or performance of the current implementation +# proves inadequate, we can have a look at Bermuda: https://github.com/napari/bermuda + + +def triangulate(positions, method="earcut"): + """Triangulate the given vertex positions. + + Returns an Nx3 integer array of faces that form a surface-mesh over the + given positions, where N is the length of the positions minus 2, + expressed in (local) vertex indices. The faces won't contain any + forbidden_edges. + """ + if len(positions) < 3: + return np.zeros((0,), np.int32) + if len(positions) == 3: + return np.array([0, 1, 2], np.int32) + + # Anticipating more variations ... + if method == "earcut": + method = "mapbox_earcut" + + if method == "naive": + faces = _triangulate_naive(positions) + elif method == "mapbox_earcut": + positions2d = positions[:, :2].flatten() + faces = mapbox_earcut(positions2d) + faces = np.array(faces, np.int32).reshape(-1, 3) + else: + raise ValueError(f"Invalid triangulation method: {method}") + + return faces + + +def _triangulate_naive(positions, forbidden_edges=None): + """This tesselation algorithm simply creates edges from one vertex to all the others.""" + + nverts = len(positions) + nfaces = nverts - 2 + forbidden_edges = forbidden_edges or [] + + # Determine a good point to be a reference + forbidden_start_points = set() + for i1, i2 in forbidden_edges: + forbidden_start_points.add(i1) + forbidden_start_points.add(i2) + for i in range(len(positions)): + if i not in forbidden_start_points: + start_point = i + break + else: + # In real meshes this cannot happen, but it can from the POV of this function's API + raise RuntimeError("Cannot tesselate.") + + # Collect the faces + faces = [] + i0 = start_point + for i in range(start_point, start_point + nfaces): + i1 = (i + 1) % nverts + i2 = (i + 2) % nverts + faces.append([i0, i1, i2]) + return np.array(faces, np.int32) From 0e75f6c79eb872641b40ab0821df850ae9eeb352 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sat, 27 Sep 2025 08:36:12 +0200 Subject: [PATCH 11/52] Refactor handling of alpha (#873) * Refactor handling of alpha * x * preserve formatting of transform * Tweaks * Set aa on for text * add alpha_mode arg * Use alpha_mode everywhere * fix axes * alpha_mode for ruler * Update tests for color * Update env var * Set alpha mode for selectors * Assign alpha_mode and render_queue * Use weighted_blend in one example * Selectors dont write depth * Nor do legends and tooltios * use more the same render_queue and a constant in some places * Use ob.render_order instead of z * Update examples/gridplot/multigraphic_gridplot.py Co-authored-by: Kushal Kolar * Use an enum for render_queue values * use GraphicsFeature for alpha and alpha_mode * bump pygfx * fix syntax * fix init for alpha and alpha_mode * allow material to be None * update examples * new notebook screenshots * Remove last refs to wgpu.gui * imgui optional dependency just points to wgpu[imgui] * update imgui things to v1.92 * update Frame, resize handle not visible if subpot isnt resizeable, update IMGUI_TOOLBAR_HEIGHT * black * update screenshots * update more screenshots * more retries and longer delay for grabbing sklearn dataset * update api docs * use diabetes dataset because of download issues with cali housing dataset * EdgeWindow window flags was causing problems with autodoc --------- Co-authored-by: Kushal Kolar --- docs/source/api/graphic_features/Alpha.rst | 35 ++++ .../source/api/graphic_features/AlphaMode.rst | 35 ++++ .../api/graphic_features/VertexCmap.rst | 1 - docs/source/api/graphic_features/index.rst | 2 + docs/source/api/graphics/Graphic.rst | 2 + docs/source/api/graphics/ImageGraphic.rst | 4 +- docs/source/api/graphics/LineCollection.rst | 4 +- docs/source/api/graphics/LineGraphic.rst | 4 +- docs/source/api/graphics/LineStack.rst | 4 +- docs/source/api/graphics/ScatterGraphic.rst | 2 + docs/source/api/graphics/TextGraphic.rst | 2 + docs/source/api/layouts/imgui_figure.rst | 1 + .../api/selectors/LinearRegionSelector.rst | 2 + docs/source/api/selectors/LinearSelector.rst | 2 + .../api/selectors/RectangleSelector.rst | 2 + docs/source/api/tools/HistogramLUTTool.rst | 2 + docs/source/conf.py | 1 + docs/source/user_guide/event_tables.rst | 198 ++++++++++++++++++ examples/events/scatter_hover_transforms.py | 30 +-- examples/gridplot/multigraphic_gridplot.py | 16 +- examples/misc-dev/garbage_collection.py | 12 +- examples/misc-dev/selector_performance.ipynb | 26 +-- .../notebooks/screenshots/nb-astronaut.png | 4 +- .../screenshots/nb-astronaut_RGB.png | 4 +- examples/notebooks/screenshots/nb-camera.png | 4 +- .../nb-image-widget-movie-set_data.png | 4 +- .../nb-image-widget-movie-single-0-reset.png | 4 +- .../nb-image-widget-movie-single-0.png | 4 +- .../nb-image-widget-movie-single-279.png | 4 +- ...e-widget-movie-single-50-window-max-33.png | 4 +- ...-widget-movie-single-50-window-mean-13.png | 4 +- ...-widget-movie-single-50-window-mean-33.png | 4 +- ...ge-widget-movie-single-50-window-reset.png | 4 +- .../nb-image-widget-movie-single-50.png | 4 +- .../nb-image-widget-single-gnuplot2.png | 4 +- .../screenshots/nb-image-widget-single.png | 4 +- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 +- ...idget-zfish-frame-50-frame-apply-reset.png | 4 +- ...ge-widget-zfish-frame-50-max-window-13.png | 4 +- ...e-widget-zfish-frame-50-mean-window-13.png | 4 +- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-frame-50.png | 4 +- .../nb-image-widget-zfish-frame-99.png | 4 +- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 +- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 +- ...dget-zfish-grid-frame-50-max-window-13.png | 4 +- ...get-zfish-grid-frame-50-mean-window-13.png | 4 +- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-grid-frame-50.png | 4 +- .../nb-image-widget-zfish-grid-frame-99.png | 4 +- ...e-widget-zfish-grid-init-mean-window-5.png | 4 +- ...fish-grid-set_data-reset-indices-false.png | 4 +- ...zfish-grid-set_data-reset-indices-true.png | 4 +- ...-image-widget-zfish-init-mean-window-5.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 4 +- ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 +- .../notebooks/screenshots/nb-lines-3d.png | 4 +- .../notebooks/screenshots/nb-lines-colors.png | 4 +- .../notebooks/screenshots/nb-lines-data.png | 4 +- .../screenshots/nb-lines-underlay.png | 4 +- examples/notebooks/screenshots/nb-lines.png | 4 +- .../screenshots/no-imgui-nb-astronaut.png | 4 +- .../screenshots/no-imgui-nb-astronaut_RGB.png | 4 +- .../screenshots/no-imgui-nb-camera.png | 4 +- .../screenshots/no-imgui-nb-lines-3d.png | 4 +- .../screenshots/no-imgui-nb-lines-colors.png | 4 +- .../screenshots/no-imgui-nb-lines-data.png | 4 +- .../no-imgui-nb-lines-underlay.png | 4 +- .../screenshots/no-imgui-nb-lines.png | 4 +- examples/scatter/scatter_colorslice.py | 4 +- examples/screenshots/extent_frac_layout.png | 4 +- examples/screenshots/extent_layout.png | 4 +- examples/screenshots/gridplot.png | 4 +- examples/screenshots/gridplot_non_square.png | 4 +- .../screenshots/gridplot_viewports_check.png | 4 +- examples/screenshots/heatmap.png | 4 +- examples/screenshots/image_cmap.png | 4 +- examples/screenshots/image_rgb.png | 4 +- examples/screenshots/image_rgbvminvmax.png | 4 +- examples/screenshots/image_simple.png | 4 +- examples/screenshots/image_small.png | 4 +- examples/screenshots/image_vminvmax.png | 4 +- examples/screenshots/image_widget.png | 4 +- examples/screenshots/image_widget_grid.png | 4 +- examples/screenshots/image_widget_imgui.png | 4 +- .../screenshots/image_widget_single_video.png | 4 +- examples/screenshots/image_widget_videos.png | 4 +- .../image_widget_viewports_check.png | 4 +- examples/screenshots/imgui_basic.png | 4 +- examples/screenshots/line.png | 4 +- examples/screenshots/line_cmap.png | 4 +- examples/screenshots/line_cmap_more.png | 4 +- examples/screenshots/line_collection.png | 4 +- .../line_collection_cmap_values.png | 4 +- ...ine_collection_cmap_values_qualitative.png | 4 +- .../screenshots/line_collection_colors.png | 4 +- .../screenshots/line_collection_slicing.png | 4 +- examples/screenshots/line_colorslice.png | 4 +- examples/screenshots/line_dataslice.png | 4 +- examples/screenshots/line_stack.png | 4 +- .../linear_region_selectors_match_offsets.png | 4 +- examples/screenshots/linear_selector.png | 4 +- .../no-imgui-extent_frac_layout.png | 4 +- .../screenshots/no-imgui-extent_layout.png | 4 +- examples/screenshots/no-imgui-gridplot.png | 4 +- .../no-imgui-gridplot_non_square.png | 4 +- .../no-imgui-gridplot_viewports_check.png | 4 +- examples/screenshots/no-imgui-heatmap.png | 4 +- examples/screenshots/no-imgui-image_cmap.png | 4 +- examples/screenshots/no-imgui-image_rgb.png | 4 +- .../no-imgui-image_rgbvminvmax.png | 4 +- .../screenshots/no-imgui-image_simple.png | 4 +- examples/screenshots/no-imgui-image_small.png | 4 +- .../screenshots/no-imgui-image_vminvmax.png | 4 +- examples/screenshots/no-imgui-line.png | 4 +- examples/screenshots/no-imgui-line_cmap.png | 4 +- .../screenshots/no-imgui-line_cmap_more.png | 4 +- .../screenshots/no-imgui-line_collection.png | 4 +- .../no-imgui-line_collection_cmap_values.png | 4 +- ...ine_collection_cmap_values_qualitative.png | 4 +- .../no-imgui-line_collection_colors.png | 4 +- .../no-imgui-line_collection_slicing.png | 4 +- .../screenshots/no-imgui-line_colorslice.png | 4 +- .../screenshots/no-imgui-line_dataslice.png | 4 +- examples/screenshots/no-imgui-line_stack.png | 4 +- ...-linear_region_selectors_match_offsets.png | 4 +- .../screenshots/no-imgui-linear_selector.png | 4 +- .../screenshots/no-imgui-rect_frac_layout.png | 4 +- examples/screenshots/no-imgui-rect_layout.png | 4 +- .../no-imgui-scatter_cmap_iris.png | 4 +- .../no-imgui-scatter_colorslice_iris.png | 4 +- .../no-imgui-scatter_dataslice_iris.png | 4 +- .../screenshots/no-imgui-scatter_iris.png | 4 +- .../screenshots/no-imgui-scatter_size.png | 4 +- examples/screenshots/rect_frac_layout.png | 4 +- examples/screenshots/rect_layout.png | 4 +- examples/screenshots/scatter_cmap_iris.png | 4 +- .../screenshots/scatter_colorslice_iris.png | 4 +- .../screenshots/scatter_dataslice_iris.png | 4 +- examples/screenshots/scatter_iris.png | 4 +- examples/screenshots/scatter_size.png | 4 +- examples/selection_tools/polygon_selector.py | 8 +- examples/tests/test_examples.py | 6 +- fastplotlib/graphics/_axes.py | 38 +++- fastplotlib/graphics/_base.py | 63 +++++- fastplotlib/graphics/_positions_base.py | 16 +- fastplotlib/graphics/features/__init__.py | 4 +- fastplotlib/graphics/features/_base.py | 4 +- fastplotlib/graphics/features/_common.py | 54 +++++ .../graphics/features/_positions_graphics.py | 31 +-- fastplotlib/graphics/features/utils.py | 31 +-- fastplotlib/graphics/line.py | 9 +- fastplotlib/graphics/line_collection.py | 33 +-- fastplotlib/graphics/scatter.py | 12 +- fastplotlib/graphics/selectors/_linear.py | 24 ++- .../graphics/selectors/_linear_region.py | 47 ++++- fastplotlib/graphics/selectors/_polygon.py | 87 ++++++-- fastplotlib/graphics/selectors/_rectangle.py | 76 ++++++- fastplotlib/graphics/text.py | 10 + fastplotlib/layouts/_frame.py | 31 ++- fastplotlib/layouts/_graphic_methods_mixin.py | 20 -- fastplotlib/layouts/_imgui_figure.py | 44 ++-- fastplotlib/layouts/_plot_area.py | 1 + fastplotlib/layouts/_utils.py | 2 +- fastplotlib/legends/legend.py | 35 +++- fastplotlib/tools/_tooltip.py | 27 ++- fastplotlib/ui/_base.py | 9 +- fastplotlib/ui/_subplot_toolbar.py | 15 +- .../ui/right_click_menus/_colormap_picker.py | 34 +-- .../ui/right_click_menus/_standard_menu.py | 19 +- fastplotlib/utils/__init__.py | 1 + fastplotlib/utils/enums.py | 14 ++ fastplotlib/widgets/image_widget/_sliders.py | 6 - pyproject.toml | 6 +- tests/test_positions_graphics.py | 39 ++-- 176 files changed, 1129 insertions(+), 594 deletions(-) create mode 100644 docs/source/api/graphic_features/Alpha.rst create mode 100644 docs/source/api/graphic_features/AlphaMode.rst create mode 100644 fastplotlib/utils/enums.py diff --git a/docs/source/api/graphic_features/Alpha.rst b/docs/source/api/graphic_features/Alpha.rst new file mode 100644 index 000000000..1ee1f66ac --- /dev/null +++ b/docs/source/api/graphic_features/Alpha.rst @@ -0,0 +1,35 @@ +.. _api.Alpha: + +Alpha +***** + +===== +Alpha +===== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Alpha_api + + Alpha + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Alpha_api + + Alpha.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Alpha_api + + Alpha.add_event_handler + Alpha.block_events + Alpha.clear_event_handlers + Alpha.remove_event_handler + Alpha.set_value + diff --git a/docs/source/api/graphic_features/AlphaMode.rst b/docs/source/api/graphic_features/AlphaMode.rst new file mode 100644 index 000000000..40e58195c --- /dev/null +++ b/docs/source/api/graphic_features/AlphaMode.rst @@ -0,0 +1,35 @@ +.. _api.AlphaMode: + +AlphaMode +********* + +========= +AlphaMode +========= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: AlphaMode_api + + AlphaMode + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: AlphaMode_api + + AlphaMode.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: AlphaMode_api + + AlphaMode.add_event_handler + AlphaMode.block_events + AlphaMode.clear_event_handlers + AlphaMode.remove_event_handler + AlphaMode.set_value + diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst index 77d96aaf6..fd1013620 100644 --- a/docs/source/api/graphic_features/VertexCmap.rst +++ b/docs/source/api/graphic_features/VertexCmap.rst @@ -20,7 +20,6 @@ Properties .. autosummary:: :toctree: VertexCmap_api - VertexCmap.alpha VertexCmap.buffer VertexCmap.name VertexCmap.shared diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 90a58fe8e..578d62b45 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -29,6 +29,8 @@ Graphic Features Name Offset Rotation + Alpha + AlphaMode Visible Deleted GraphicFeatureEvent diff --git a/docs/source/api/graphics/Graphic.rst b/docs/source/api/graphics/Graphic.rst index cf68888f5..da6424e3e 100644 --- a/docs/source/api/graphics/Graphic.rst +++ b/docs/source/api/graphics/Graphic.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: Graphic_api + Graphic.alpha + Graphic.alpha_mode Graphic.axes Graphic.block_events Graphic.deleted diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index c7572aed5..457ba27ee 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: ImageGraphic_api + ImageGraphic.alpha + ImageGraphic.alpha_mode ImageGraphic.axes ImageGraphic.block_events ImageGraphic.cmap @@ -47,8 +49,8 @@ Methods ImageGraphic.add_event_handler ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector - ImageGraphic.add_rectangle_selector ImageGraphic.add_polygon_selector + ImageGraphic.add_rectangle_selector ImageGraphic.clear_event_handlers ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 15403927e..ffbb52f2b 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: LineCollection_api + LineCollection.alpha + LineCollection.alpha_mode LineCollection.axes LineCollection.block_events LineCollection.cmap @@ -52,8 +54,8 @@ Methods LineCollection.add_graphic LineCollection.add_linear_region_selector LineCollection.add_linear_selector - LineCollection.add_rectangle_selector LineCollection.add_polygon_selector + LineCollection.add_rectangle_selector LineCollection.clear_event_handlers LineCollection.remove_event_handler LineCollection.remove_graphic diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 6ea799594..ddcb00c41 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: LineGraphic_api + LineGraphic.alpha + LineGraphic.alpha_mode LineGraphic.axes LineGraphic.block_events LineGraphic.cmap @@ -46,8 +48,8 @@ Methods LineGraphic.add_event_handler LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector - LineGraphic.add_rectangle_selector LineGraphic.add_polygon_selector + LineGraphic.add_rectangle_selector LineGraphic.clear_event_handlers LineGraphic.remove_event_handler LineGraphic.rotate diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index dbb8fc454..4373454be 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: LineStack_api + LineStack.alpha + LineStack.alpha_mode LineStack.axes LineStack.block_events LineStack.cmap @@ -52,8 +54,8 @@ Methods LineStack.add_graphic LineStack.add_linear_region_selector LineStack.add_linear_selector - LineStack.add_rectangle_selector LineStack.add_polygon_selector + LineStack.add_rectangle_selector LineStack.clear_event_handlers LineStack.remove_event_handler LineStack.remove_graphic diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 968f0e091..48d30d01f 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: ScatterGraphic_api + ScatterGraphic.alpha + ScatterGraphic.alpha_mode ScatterGraphic.axes ScatterGraphic.block_events ScatterGraphic.cmap diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 60cd97f40..0de52942b 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: TextGraphic_api + TextGraphic.alpha + TextGraphic.alpha_mode TextGraphic.axes TextGraphic.block_events TextGraphic.deleted diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index 0abfcc067..e1922a9f4 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -23,6 +23,7 @@ Properties ImguiFigure.cameras ImguiFigure.canvas ImguiFigure.controllers + ImguiFigure.default_imgui_font ImguiFigure.guis ImguiFigure.imgui_renderer ImguiFigure.layout diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 2c23bc82a..35b5ae1f4 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: LinearRegionSelector_api + LinearRegionSelector.alpha + LinearRegionSelector.alpha_mode LinearRegionSelector.axes LinearRegionSelector.axis LinearRegionSelector.block_events diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index c7a8e978a..9cbe6fb26 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: LinearSelector_api + LinearSelector.alpha + LinearSelector.alpha_mode LinearSelector.axes LinearSelector.axis LinearSelector.block_events diff --git a/docs/source/api/selectors/RectangleSelector.rst b/docs/source/api/selectors/RectangleSelector.rst index 24928c817..dc9727069 100644 --- a/docs/source/api/selectors/RectangleSelector.rst +++ b/docs/source/api/selectors/RectangleSelector.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: RectangleSelector_api + RectangleSelector.alpha + RectangleSelector.alpha_mode RectangleSelector.axes RectangleSelector.axis RectangleSelector.block_events diff --git a/docs/source/api/tools/HistogramLUTTool.rst b/docs/source/api/tools/HistogramLUTTool.rst index 7c3237490..128dbb889 100644 --- a/docs/source/api/tools/HistogramLUTTool.rst +++ b/docs/source/api/tools/HistogramLUTTool.rst @@ -20,6 +20,8 @@ Properties .. autosummary:: :toctree: HistogramLUTTool_api + HistogramLUTTool.alpha + HistogramLUTTool.alpha_mode HistogramLUTTool.axes HistogramLUTTool.block_events HistogramLUTTool.cmap diff --git a/docs/source/conf.py b/docs/source/conf.py index e4ff72237..59d0a2885 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -126,6 +126,7 @@ autodoc_typehints = "description" autodoc_typehints_description_target = "documented_params" +autodoc_preserve_defaults = True intersphinx_mapping = { "python": ("https://docs.python.org/3", None), diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index 1b9b2f7ec..0ae9e974d 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -113,6 +113,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -258,6 +280,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -384,6 +428,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -497,6 +563,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -629,6 +717,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -761,6 +871,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -838,6 +970,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -917,6 +1071,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ @@ -996,6 +1172,28 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + visible ^^^^^^^ diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py index 18e6f3de5..f7b733109 100644 --- a/examples/events/scatter_hover_transforms.py +++ b/examples/events/scatter_hover_transforms.py @@ -13,9 +13,9 @@ # test_example = false # sphinx_gallery_pygfx_docs = 'screenshot' -from sklearn.datasets import fetch_california_housing +from sklearn.datasets import load_diabetes from sklearn.preprocessing import ( - Normalizer, + StandardScaler, QuantileTransformer, PowerTransformer, ) @@ -24,30 +24,16 @@ import pygfx # get the dataset -dataset = fetch_california_housing(n_retries=5, delay=20) -X_full, y = dataset.data, dataset.target -feature_names = dataset.feature_names - -feature_mapping = { - "MedInc": "Median income in block", - "HouseAge": "Median house age in block", - "AveRooms": "Average number of rooms", - "AveBedrms": "Average number of bedrooms", - "Population": "Block population", - "AveOccup": "Average house occupancy", - "Latitude": "House block latitude", - "Longitude": "House block longitude", -} +dataset = load_diabetes(scaled=False) + # Take only 2 features to make visualization easier -# Feature MedInc has a long tail distribution. -# Feature AveOccup has a few but very large outliers. -features = ["MedInc", "AveOccup"] -features_idx = [feature_names.index(feature) for feature in features] -X = X_full[:, features_idx] +X = dataset["data"][:, (2, 6)] +# target +y = dataset["target"] # list of our scalers and their names as strings -scalers = [PowerTransformer, QuantileTransformer, Normalizer] +scalers = [PowerTransformer, QuantileTransformer, StandardScaler] names = ["Original Data", *[s.__name__ for s in scalers]] # fastplotlib code starts here, make a figure diff --git a/examples/gridplot/multigraphic_gridplot.py b/examples/gridplot/multigraphic_gridplot.py index dad7ee192..cbf546e2a 100644 --- a/examples/gridplot/multigraphic_gridplot.py +++ b/examples/gridplot/multigraphic_gridplot.py @@ -17,7 +17,7 @@ figure = fpl.Figure( shape=(2, 2), names=[["image-overlay", "circles"], ["line-stack", "scatter"]], - size=(700, 560) + size=(700, 560), ) img = iio.imread("imageio:coffee.png") @@ -36,6 +36,7 @@ # add overlay to image figure["image-overlay"].add_image(data=overlay) + # generate some circles def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: theta = np.linspace(0, 2 * np.pi, n_points) @@ -55,10 +56,10 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # things like class labels, cluster labels, etc. cmap_transform = [ - 0, 1, 1, 2, - 0, 0, 1, 1, - 2, 2, 8, 3, - 1, 9, 1, 5 + [0, 1, 1, 2], + [0, 0, 1, 1], + [2, 2, 8, 3], + [1, 9, 1, 5], ] # add an image to overlay the circles on @@ -70,10 +71,10 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: figure["circles"].add_line_collection( circles, cmap="tab10", - cmap_transform=cmap_transform, + cmap_transform=np.asarray(cmap_transform).ravel(), thickness=3, alpha=0.5, - name="circles-graphic" + name="circles-graphic", ) # move the circles graphic so that it is centered over the image @@ -115,4 +116,3 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: if __name__ == "__main__": print(__doc__) fpl.loop.run() - diff --git a/examples/misc-dev/garbage_collection.py b/examples/misc-dev/garbage_collection.py index baa92e848..56ef0792b 100644 --- a/examples/misc-dev/garbage_collection.py +++ b/examples/misc-dev/garbage_collection.py @@ -1,9 +1,9 @@ import numpy as np -from wgpu.gui.auto import WgpuCanvas, run +from rendercanvas.auto import RenderCanvas, loop import pygfx as gfx import subprocess -canvas = WgpuCanvas() +canvas = RenderCanvas() renderer = gfx.WgpuRenderer(canvas) scene = gfx.Scene() camera = gfx.OrthographicCamera(5000, 5000) @@ -28,7 +28,11 @@ def draw(): def print_nvidia(msg=""): print(msg) print( - subprocess.check_output(["nvidia-smi", "--format=csv", "--query-gpu=memory.used"]).decode().split("\n")[1] + subprocess.check_output( + ["nvidia-smi", "--format=csv", "--query-gpu=memory.used"] + ) + .decode() + .split("\n")[1] ) print() @@ -57,4 +61,4 @@ def remove_img(*args): renderer.add_event_handler(add_img, "double_click") draw() -run() +loop.run() diff --git a/examples/misc-dev/selector_performance.ipynb b/examples/misc-dev/selector_performance.ipynb index 39dbba6b8..bbbe8c40b 100644 --- a/examples/misc-dev/selector_performance.ipynb +++ b/examples/misc-dev/selector_performance.ipynb @@ -32,10 +32,10 @@ " xs = radius * np.sin(theta)\n", " ys = radius * np.cos(theta)\n", " zs = np.zeros(xs.size)\n", - " \n", + "\n", " xs += center[0]\n", " ys += center[1]\n", - " \n", + "\n", " return np.ascontiguousarray(np.column_stack([xs, ys, zs]).astype(np.float32))" ] }, @@ -76,7 +76,7 @@ }, "outputs": [], "source": [ - "from wgpu.gui.auto import WgpuCanvas, run\n", + "from rendercanvas.auto import RenderCanvas\n", "import pygfx as gfx" ] }, @@ -89,7 +89,7 @@ }, "outputs": [], "source": [ - "canvas = WgpuCanvas()\n", + "canvas = RenderCanvas()\n", "renderer = gfx.WgpuRenderer(canvas)" ] }, @@ -163,7 +163,7 @@ "\n", "for l in lines[100:1000]:\n", " l.visible = False\n", - " \n", + "\n", "# canvas.request_draw()\n", "\n", "time() - t1" @@ -186,7 +186,7 @@ " ys = np.sin(xs) * 10\n", " else:\n", " ys = np.cos(xs) * 10\n", - " \n", + "\n", " temporal.append(ys)" ] }, @@ -233,7 +233,7 @@ "def update_visible(ev):\n", " ixs_visible = ev.pick_info[\"selected_indices\"]\n", " ixs_hide = np.setdiff1d(np.arange(len(circles)), ixs_visible)\n", - " \n", + "\n", " # very fast, 20 ms to change 1,000\n", " for i, g in enumerate(contours.graphics):\n", " if not g.visible and i in ixs_visible:\n", @@ -320,10 +320,10 @@ "\n", "for c in contours.graphics[100:1000]:\n", " c.visible = True\n", - " \n", + "\n", "# for i in range(100, 1000):\n", "# contours.graphics[i].world_object.visible = True\n", - " \n", + "\n", "time() - t1" ] }, @@ -342,7 +342,7 @@ "for c in circles:\n", " start_offset += c.shape[0]\n", " zero_alpha_ixs += [start_offset - 1, start_offset]\n", - " \n", + "\n", "zero_alpha_ixs = zero_alpha_ixs[:-1]" ] }, @@ -370,13 +370,13 @@ "def set_visible_alpha(ev):\n", " ixs_visible = ev.pick_info[\"selected_indices\"]\n", " ixs_hide = np.setdiff1d(np.arange(len(circles)), ixs_visible)\n", - " \n", + "\n", " for i in ixs_visible:\n", " contours.world_object.geometry.colors.data[(i * 75) + 1:(i * 75) + 74, -1] = 1\n", - " \n", + "\n", " for i in ixs_hide:\n", " contours.world_object.geometry.colors.data[(i * 75) + 1:(i * 75) + 74, -1] = 0\n", - " \n", + "\n", " contours.world_object.geometry.colors.update_range()" ] }, diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 4221e7c6a..cebcc4f74 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ce5e7e3fa03f5450948725f0a983547a66601a5b80f0423d0e99c6a8751cf25 -size 65875 +oid sha256:c0f32394238fe9b317c11eea52ded0c663a9a37a4bb9ac3aaf7ec05ba7c39e55 +size 66265 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index 392c40cdc..6a9c3b26f 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d2a7c9fdaa8e334bb7fc4633e0a6e7b52dba7b5b492b48cc3e339e83bcac30d -size 63881 +oid sha256:96e2e88b1eed0844bd9f5066d876ec00123cccabbe0f03ceb4dad8c4feb80005 +size 64221 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 13d53f996..f89a7da0f 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bde9f5050e83ef47dbfa7fae421abd58e5d97eb6ed4db135c296702d8e78ab4 -size 49720 +oid sha256:0a2280fb566459dd868b930bfe8ddd22ee15998d50f23ef44ec31d065f81353b +size 49947 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index f9194e2fd..17eac72c3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db3c05f59d37df596a669106ad01f67ce335a25a0e601a22438de2b9bb77b3dc -size 63059 +oid sha256:2c5d1bee0215d1b5fa2fd5f07c372337a2c0f8d7532092faf189b41b5ae90796 +size 64032 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 3253f0426..888490a86 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:253ff39f89c71b0bb2f0a2b51d9a7f47a35a0e92651881d6180ac541ff26861d -size 113825 +oid sha256:c5dc016ac3b2cb6553ca67fcf2450eba3334b6280c66b01583adac672100f3f6 +size 115971 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 3253f0426..888490a86 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:253ff39f89c71b0bb2f0a2b51d9a7f47a35a0e92651881d6180ac541ff26861d -size 113825 +oid sha256:c5dc016ac3b2cb6553ca67fcf2450eba3334b6280c66b01583adac672100f3f6 +size 115971 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 29ed34a10..1b000ac9d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a727bd83bca1b5c5e35607e34062265b1058683dc3bd67e4ab5afba09253734d -size 136027 +oid sha256:ca0b4a4a366c08cbe2ce612023ed0d46fc4121bfdb397b97c01f0b4fcc846ba9 +size 137759 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index d209d78bb..8e3b3f443 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d50360d41aa5e9e7aee9745187da1be087dc0fca10bc2ba32bcc862e35f2a2ef -size 122211 +oid sha256:deb5ef697a6240d66d1fc32364cdb9c30876a809706b4ecc7182c02fdad893c3 +size 124444 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 97157abca..24f0af167 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2059f2f5d7765ebf3c318978acb2c19dd6036e760ea3e070fbbfe2bf2d4a27a -size 107169 +oid sha256:0d656095fa6f30f00f4beeefdbbc57403d51a442908cd9c69c4d9366023c51e5 +size 108924 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index b7af3202d..14446219c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb9a22d365dd08a3a3be2c7065adf5d44c1d67320af36629ce2e5a8ebf527f71 -size 99261 +oid sha256:97b2cd703d8a465137834e264ba723c58e861c7e82d2675b5bb00b0ba0546c30 +size 101247 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index b1bc3414f..45bf24ea5 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b2f75ca1d712ee3bb70f5b6f07c50660464a014490652d86e9a3a328c3cd54b -size 119942 +oid sha256:b85764ef017fd475f49c6d1b57d4a347ab63aaf4c3e6a36bc1de6d61a678c6df +size 122086 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index b1bc3414f..45bf24ea5 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b2f75ca1d712ee3bb70f5b6f07c50660464a014490652d86e9a3a328c3cd54b -size 119942 +oid sha256:b85764ef017fd475f49c6d1b57d4a347ab63aaf4c3e6a36bc1de6d61a678c6df +size 122086 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index c96a6de51..d42da0d01 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f1ca8a4c1ca0f02a6d1217c13f740407073cf70730d5455c81344a5753c4dc7 -size 224053 +oid sha256:5e52ce739f941d0cb6353322238e5a3da6cb02b9e8d504a1027e381f3ee74474 +size 225937 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index cf60b005f..e07679554 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dde00bbf68eec1d531a19649cb6814d360073f0920837ad965cf816b308fdf97 -size 215376 +oid sha256:b66a299b9b6aae8e2161a1afa789fdf2f4763c681628f9c7bdcf353da2b35656 +size 216922 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 68d204849..a91af9da9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f90eb33b10caad47dc47425d517297de86754178e3f6764c30c0066227dd0d76 -size 62383 +oid sha256:3a5fe6c2b790dc062ac0aeddce24f462da2d6dcf752d86bd400ccf28f1c912a7 +size 64992 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index d35a258d1..7b2239958 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ec728806cdad8aa89e74095c12637d5ef36c95d967b1ead58c6a6b4d750471d -size 67108 +oid sha256:99ea64fe85c0ff218afb91389a4d38c3e6b184859b4635d237635b914b47ee03 +size 69473 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index c8559bc33..6614a8021 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:131ea555e5616caed42420c3fa8f1443f14725aa37c44ad526fb45f041d79d3f -size 110570 +oid sha256:70c5d778b6acd1c97252deccce6ded218380a08fb04f872489838f6da516349b +size 113931 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index e50e5cce9..70acf36ab 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fecadbc230f00207f4b237205762bde7213e47eb9d8dc5edd24ef844a7170f5 -size 94421 +oid sha256:1f2f1291f3a03ce5100598663baa62a2a68d31c548ccfeb901ba63b0f2edfe77 +size 97498 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index 7fcb45dcc..d7d8a6c43 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8b7efaf332e0344c9e4dd465cffad2e9bcd317c342363ebd56bd40fc7a84d4c -size 87259 +oid sha256:c5f6e02933fbf04ac8bb97ee959756a4c2701dc7f62d7922c334d507cdac66e2 +size 89628 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index 70710ca02..7b2239958 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54d256826fce09ddf226d6d53679a17e5bee75f510603f897628fd842c5893ba -size 67103 +oid sha256:99ea64fe85c0ff218afb91389a4d38c3e6b184859b4635d237635b914b47ee03 +size 69473 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index b36332164..6fd1a14ab 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:353d8f0e370489b1ad51cfc71cfb8a6748d156541f7f70f5d16343209166da31 -size 59202 +oid sha256:3792fa22b1bef03871dab9cd0c8a34d58b3fe9ecb565f17e772a54f0459a7f63 +size 60801 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index a6c3ac9c6..5e60750e1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ca29700c76cfd6396a181b43adacefc31c98cc3d4d9d0c92eada990e477452e -size 84321 +oid sha256:5d588c44d786cf667640e1b4664e6c3daaf946fb8373f5afb7ea6dd97ee445a9 +size 86503 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 906af2702..969404d6a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b388954425e2cb45e7982eaab92db51889cf86e21989f5f926ace1ca565bdf90 -size 101869 +oid sha256:87c51655ffee08b1c892b8f7773b7b2137bb52b1457bbfb1c5f4ae342ab508ea +size 104071 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 0fc2ff6a1..4c677b40e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae0a4201dc239e6d6d187ab9d281a1e15210c9f0c3278f404da86337ec597a05 -size 140995 +oid sha256:57f298f02a1df8fa797e3773a3e976dc3ce6ce67d7c781676582119e6452001b +size 144021 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 948852987..05fbce7f7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42d9880fd977db69748ee548f058a7d9c7a5fc69cbd38830e3138467070e24c7 -size 113861 +oid sha256:c3c5adf6bf3f031bd9a9ea6041156905039c9bc56cbffcf0079e08b6cd5ac547 +size 116844 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index e0cc721d3..6247c5905 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6c378ccbe50cfd547c2edaa034c7064564eb325ce504659f57325c0991a11f6 -size 115883 +oid sha256:42a6cfcb2b1fe8919bf15a4e52fccf65d518d34628ad7c5b5ba9bfabc1a18e36 +size 118412 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 13b30b0c2..b35f45233 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19aa140345ac26b6b5542d44334b748408db54881bf73a4b88be66498a2d9433 -size 101937 +oid sha256:59856df53d455010f8d5c582259ef587ba7c7022e4a923b9d11c6944d82d474f +size 103953 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index f66664d4d..128feac14 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e48ab3a730132a773f784876e173b2fe3165d70121489a4c08270b9bfdc8fdbf -size 99569 +oid sha256:ddd3b78f4e2436c688e780b501fec740eb0fe6460deb2122979d11da43ccaf22 +size 101213 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index e27d39419..2b2d1e8d1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df90973bbba10bab86152973359e0ddbb62dc2b1b9af38cb9b557166edd547f8 -size 110530 +oid sha256:24eac0c3c168f4afe74cbb3d9ee95d393664341e35b11eb1b3972446a21616c1 +size 113053 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 40a7a2a96..3eb50089a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c59705a9ca68af29502ec96407cf1690d8852ad871a00b2b1f51a9a3edd90757 -size 101753 +oid sha256:3fc6b1fcd3df0d8dc3446f9570b37016150d353806b2d13f6e777ca0eb4b27f6 +size 104045 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 8e3be816c..260de77d8 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c91eca03cd88468aa24f3abca2dbf4a5b336a522749af782df2e4afe9e0d2c6 -size 102718 +oid sha256:6a02745049f9167c2bbb2d44f0959dd9f251254c3e812457b284c6890bda3bc4 +size 105426 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index b49eb6f20..31219597e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66158475df991caa5fab6821e0eec5a29ccc41f989f6d691d1c315a4892132fe -size 74392 +oid sha256:9dae5806f87b217215098f1b74b910e778c0fb3249411564ce2baf59a002c52b +size 77044 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 9c4e54556..19724d061 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6cf738aff9bf8254f4141fc7d8fee4e74e2b65dab7f7799a3fb0c4698d1213b -size 112509 +oid sha256:3bf8dc7928b28a341d5938b315d143a017de43e907ff91423064d2265f8afd40 +size 112855 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 117451b7e..ff2aa5de7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af5897e58e484a5b2f3713c663019eda7e215e1edd6e9867413611bd9ed9df0e -size 107951 +oid sha256:c67aa2880595c3cf18e69d6a2886606b039d38eebc1219b765294af01f7ac619 +size 108348 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 8c60e9557..c0ec3fa5d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4eab140f2b539d0916d24d8d6704e6836cb0e3697985580e5b6fa44473781a3 -size 109170 +oid sha256:dce2e94163c894b254c5295e54852a988ea063c92ebd38bc3fdf09d78e8d66e9 +size 110236 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index b24a62173..4895996d6 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a929160a3b1765e74a9972a4ddbc869d9a1af1599f10baf6e491b2d0680639 -size 18827 +oid sha256:2924aef7286417e78f023810d1f8cebee85e4ee944537bb28a1d2979cc5eacb5 +size 17957 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 1c645ee4e..d75d84229 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8920126b05d3359c7d8b308b0c42553e42d9791381e6d60b5c2c02eadccefd7d -size 32679 +oid sha256:ab117d2995b8ec4c9656d7b7101ded007db167592d92b1d7ae2d2751011fb505 +size 30939 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index c05f09a43..44f6e486b 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3302edd24977282850fae6b73f2e96a296f2b7163acc9de6f8df7b9e6ba48bd6 -size 39894 +oid sha256:c602844bcbd32d94838d5c9c74c4620162bd65bd87bbabe9c60b417fdefdc5ee +size 37158 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index dfed9dbe3..4ad000048 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94865d4a62545e53fd0d2967177b24c0896a64be28c9d6dcb9395cea5c09b7aa -size 52034 +oid sha256:5895cfb7c24749b54acb26c4903ebd4508b65ef436a6e8de3ac7f40ac03d76f7 +size 50003 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 8e7507793..c422fcb80 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85329a9e4a60d4a2202b61c33d02ff7d6fb7d9edb1593d8e8e6fef6c6c58ca46 -size 27303 +oid sha256:fec20ab8f0cb4948dc05afff33191cdc81cb54c722900c6b8705a4bb7efef8e8 +size 23473 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png index efc7baa98..174b8181d 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c80e71dde6812321add9ae9f84c86f9f7d28a65f420da5bd61c4abe537ac7d1c -size 74000 +oid sha256:a9195062f4f4cc37e3975468e8ae397aa9c8064e7f89f2821f6a9ca2b204f0a3 +size 73410 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png index 4d95ad3c9..75ef377aa 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:995908845e4e5a21c2ed45de2cdd251386ebfb3f6d2e8f1a420b39944d79347d -size 71874 +oid sha256:a5e46f43c0b440811c626853d5995466347c3a51f49cf129133a8e0ad3c8cea7 +size 71277 diff --git a/examples/notebooks/screenshots/no-imgui-nb-camera.png b/examples/notebooks/screenshots/no-imgui-nb-camera.png index cc8bc83cb..aa9ccb37b 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-camera.png +++ b/examples/notebooks/screenshots/no-imgui-nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:969a733add867c12f05439b8280bd594e97061a06b56676e700441f8d16c7697 -size 55147 +oid sha256:07080677bb75b39ee90d86d15172e08423bf764cb48f75100f3d9bc6c1107024 +size 54539 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 35be289f6..0af986c10 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a796f29923d0a051e0dde891e6fbdb3a796871ec028581c03fb9e648b156968 -size 19246 +oid sha256:8f3c230460cd4397c24e07e0b671581bb9234254687a7ed839a5500b44ee1485 +size 17783 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index aa36be9f3..2ca81d14d 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ae2ce951dba034032df0d708344a84c8421e93f5943738132e924ad5a990772 -size 33612 +oid sha256:73872b407bf32309aac7ef7f1169a5cae62e5d6cc5c9ac5df7dcd716fb9da91d +size 31437 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 9245cf06d..6161902ef 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95580aef77da982bc42e4dd27383f5b407dadfbb9dd06fc6ed7aabb09a020d51 -size 42595 +oid sha256:26fe3ec674af9d19e08e8ad3246c1ca69bb5ac5a6e4cb23077b8cebf2860ba85 +size 39168 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index 4e44f5e72..ea3e6170c 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0775cdf2f528cd987622c4431ddfcbc10a1f57766dadaac7bae35f8c8e9c1ed3 -size 55353 +oid sha256:4db9bf43b9be8055a13fec7cdd6d1c11fe14db4078616e4e19690a96890d5224 +size 52777 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 9e0273dd9..79c88a22e 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05f9619a5acf17978e3597fb2ef40c47ae63b9c6f8e39b9f5ff8e936b5e8e00c -size 27681 +oid sha256:a8fae40886e496e5377613ab6a08f20d11d41c037f4ec243d57c33948078674d +size 23462 diff --git a/examples/scatter/scatter_colorslice.py b/examples/scatter/scatter_colorslice.py index d8bbc620b..5b18d2e0e 100644 --- a/examples/scatter/scatter_colorslice.py +++ b/examples/scatter/scatter_colorslice.py @@ -36,7 +36,9 @@ colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points # use an alpha value since this will be a lot of points -figure[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) +figure[0, 0].add_scatter( + data=cloud, sizes=3, colors=colors, alpha_mode="weighted_blend", alpha=0.6 +) figure.show() diff --git a/examples/screenshots/extent_frac_layout.png b/examples/screenshots/extent_frac_layout.png index 1df76dc61..3059e78d0 100644 --- a/examples/screenshots/extent_frac_layout.png +++ b/examples/screenshots/extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc8ec12042a36992dbda00d04f251b47d58cd6d75eca5c4240cd207878f9282f -size 157607 +oid sha256:fa5563fc8bd9f7b24f3f25a98aa1ce6de3afea9b5c1f1cd7cc580fa4866fe796 +size 144210 diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png index fa89b9afc..23303dd28 100644 --- a/examples/screenshots/extent_layout.png +++ b/examples/screenshots/extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:823785f993237cb0f2773aea62c3dbbe16fe63f86f83867f1e27b82c8793da9f -size 126051 +oid sha256:e1716c3f2e1309643a53759e9b5226440c9ac2e6be930f52439305a91c5ca914 +size 117466 diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png index 27eb82675..0f13fd803 100644 --- a/examples/screenshots/gridplot.png +++ b/examples/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07a1e902cb6ed8c95df8adb4091b2a825ffecbf0e26a9dcffc0d1272a886d7d9 -size 261210 +oid sha256:f0cf92add943b5fafe8615200922714302386277e81cb0643d0024b9f3b31f4a +size 260740 diff --git a/examples/screenshots/gridplot_non_square.png b/examples/screenshots/gridplot_non_square.png index d11a9f104..607b7c1d3 100644 --- a/examples/screenshots/gridplot_non_square.png +++ b/examples/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e40be809e47d7a19fbf1a292a575344414acf5a23609f1796041dd8b91362c97 -size 200969 +oid sha256:69bb806090ae96978b51f46f36394384db31add4c2b0290936859a3703f25291 +size 198799 diff --git a/examples/screenshots/gridplot_viewports_check.png b/examples/screenshots/gridplot_viewports_check.png index dba29a356..34fd20c6c 100644 --- a/examples/screenshots/gridplot_viewports_check.png +++ b/examples/screenshots/gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51080904e1156d0427834195141f9c08270f98dbef55a3c3f2fa2e35037814c6 -size 77856 +oid sha256:a5569c549098c0b02d32a76f82897f32f748fc297b20b71b9081da551768d467 +size 45126 diff --git a/examples/screenshots/heatmap.png b/examples/screenshots/heatmap.png index 34f139e19..a6cbbb91d 100644 --- a/examples/screenshots/heatmap.png +++ b/examples/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2235079838197971d340f07bd12998e371b4db54e7136e60dbcb8b43f44561a4 -size 92210 +oid sha256:75cf8ae3241a526044fbcd920f36a77c27752ce06cb920bd2ed6b7b8041f6125 +size 89156 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png index 2621e7581..c2b466eca 100644 --- a/examples/screenshots/image_cmap.png +++ b/examples/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc9dae363b1490506dda0c7d1aa1fd24232e3daa484178d8a9e88f49a1aa2058 -size 209014 +oid sha256:d3cfe74816efe00318c6a73d2411458a84af486f1d3099ca80807c5fd9fc3d73 +size 209290 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png index e20121cde..85905def5 100644 --- a/examples/screenshots/image_rgb.png +++ b/examples/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5500a673085eb0d7d4293bba7f4963482c234cd80622179f2e476e58712cbfd4 -size 233875 +oid sha256:152f02896e634a5424ef3045cdb256c05831d76ea37c7b222f42c8009f60aaae +size 234270 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png index 39a0707d5..c9453b9ff 100644 --- a/examples/screenshots/image_rgbvminvmax.png +++ b/examples/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:758869786f243a1c1a2e2214f70f788e0decc5854f2825bd4133008aa481690e -size 46215 +oid sha256:b6da94023f204e3feb6aaa52505c32d5218b111295691e622a1f04300855fa64 +size 43251 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png index 44e6fc62f..5ef9fb7a6 100644 --- a/examples/screenshots/image_simple.png +++ b/examples/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b726be63c21f174d269441978adda9163759453fd210e60f9027ec45ebe8fbb7 -size 208692 +oid sha256:34e67246f8880636cf31e59c9624d2361880bdc5f539457fa98e484f3605d080 +size 209033 diff --git a/examples/screenshots/image_small.png b/examples/screenshots/image_small.png index c52a160ae..e67c4a22e 100644 --- a/examples/screenshots/image_small.png +++ b/examples/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61375cfac467c64ec7a49b830edf602943613605a82619ee006e8cb94be47f1e -size 14950 +oid sha256:e1a336451b1eea664734b7331e8e87de6af7cd92f7bfae44fbb91aa05d79d092 +size 12876 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png index 39a0707d5..c9453b9ff 100644 --- a/examples/screenshots/image_vminvmax.png +++ b/examples/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:758869786f243a1c1a2e2214f70f788e0decc5854f2825bd4133008aa481690e -size 46215 +oid sha256:b6da94023f204e3feb6aaa52505c32d5218b111295691e622a1f04300855fa64 +size 43251 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index b088518a8..e0eb67609 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d88b1f2d47b7e3a9b7eb01cf4fce3a061e17e260d04c52c8ac2c7bb11cc27e5 -size 189495 +oid sha256:c2cc3c37ee28e16d94b1d437d5ef9ac6c73dfc7f8bf8e39c61d63c8bda5bd26f +size 188762 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index 84ac3f59f..21d0ebacf 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed702632b7e58b2dfaa19d15ad4d234ef08d72fc222d21e43ff934d664ed0b18 -size 254523 +oid sha256:4ff7ba5c7a1ea220fb49936fa71c92f2fd5c50b3aa51837db0a5b3f5f53f2461 +size 245187 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index 934db35ab..51594b863 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a23cbd21c96fe161de7621f7a733d2e6a67c697efae46d38258443fbd3160b7 -size 173705 +oid sha256:851714956ea3727d8b6d66d88fc7d57b08ac39d5af0cb3b3e409e870dfd37e76 +size 174624 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index a3d2c5686..51a7d83fd 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dae1a4f7614222b9163256257df09c1e01abf6f066ebf59faf82c3134fe0c420 -size 99410 +oid sha256:ae426cb3b41874c1f20e3579a539b3229ca164ead978e127366c8d7185544cc4 +size 93730 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index dd8dfb8c1..c7c45c39a 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e19ea5b2e209461926fe343ccf83422d180506da899aa621519b4921dda0d24 -size 335631 +oid sha256:0054d3c7e63d951a892f06f206e2ceb4e96c19d69266f5401cbac7da80c40361 +size 311541 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png index d5b4010c7..a0366440c 100644 --- a/examples/screenshots/image_widget_viewports_check.png +++ b/examples/screenshots/image_widget_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91a2c5bae1796ed9605f4de65dba14151ad8deaf253e3fb0aa043a4579da708d -size 117505 +oid sha256:bce01da442cabd92e0944215ece40a9887ef2eff5d49320a046893c31d640418 +size 85531 diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index 39817dd97..5296a04f1 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1bc5f46e04c2a1098b2a7d7f8bf4c4915cd74af40bb55b4fb1d111d82638843 -size 34503 +oid sha256:f3e3fe17f1e35e6a5abb2ed312b1bbdb6b0ec3c3f8fff48be0d28f9385c76e65 +size 35881 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png index 99f87d243..19ecfcaf3 100644 --- a/examples/screenshots/line.png +++ b/examples/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9985e9fc1c58305bca43e00f35ea6017f916652100d2e463e8ea01a0df3b55d6 -size 232883 +oid sha256:bace8f1e9179eb9418c80e273dfb290be4229334542092cd5f001fc429c430f8 +size 152561 diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png index 1c6352ba0..d998fb0c4 100644 --- a/examples/screenshots/line_cmap.png +++ b/examples/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e893699989c9cf04d7b0d90070b85df5fa6135a363f6e447032aff6020bd476 -size 46137 +oid sha256:4efa1e76e172a57f3a9b90223a8be2ddc48e6c54ea8ec94ce3f83b1fa0b6cc08 +size 37829 diff --git a/examples/screenshots/line_cmap_more.png b/examples/screenshots/line_cmap_more.png index 39d0fcaa3..2b61ed446 100644 --- a/examples/screenshots/line_cmap_more.png +++ b/examples/screenshots/line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3606f8c1bf83d537275a6b31ae9b9a392c73cb082e82215e348f6deec796aa8e -size 113003 +oid sha256:3e74c93cbf4e88d6eb13f599b2b70ca15c739d0ba0b9e1670431f4f5078e3fce +size 89687 diff --git a/examples/screenshots/line_collection.png b/examples/screenshots/line_collection.png index 0397d2dde..908c87729 100644 --- a/examples/screenshots/line_collection.png +++ b/examples/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d76339dbfdd246442312d9853b8d57de773f2a6b2337e75d85a2e3e7bfbee0e -size 88722 +oid sha256:232820431835de3a7795724827e8a179e5e47e32d7fabbef702b57497f16a917 +size 69488 diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/screenshots/line_collection_cmap_values.png index 19ce6ab73..4bbdaa0fe 100644 --- a/examples/screenshots/line_collection_cmap_values.png +++ b/examples/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d961d5a91b71d0042c652cc2196ec99586f41cd01a232d3940c41f9948cf4f6 -size 58191 +oid sha256:a3e27e891b1a3232780f159aa75592886f34492b4df1256b465379c3e4593552 +size 41492 diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/screenshots/line_collection_cmap_values_qualitative.png index 264531b15..e746c2884 100644 --- a/examples/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fb808787bcd5abd39c9861ff38a4cc8740ca7bc7d0cb35249e7150c80633bb0 -size 61936 +oid sha256:964d2bccf0bd64de80c0617ddbf432ae3006ba27823a9b2107d42a2d4d6173b5 +size 44534 diff --git a/examples/screenshots/line_collection_colors.png b/examples/screenshots/line_collection_colors.png index fbb10cf96..39c3ad9e0 100644 --- a/examples/screenshots/line_collection_colors.png +++ b/examples/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8396aa29f5246cb1f7edce74e6dd41313826f53b810e80b0e69b1ea6bf550cc4 -size 49546 +oid sha256:d3475c7485cc00633208be388b148564aea1d1883786e5402ea03bb576a8483f +size 33098 diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index 6fdb82fb4..428e6c33d 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9235513b219ce076171cc9aa7e2c24c87cdad298a7004f52c5245dbd34b0505f -size 117970 +oid sha256:a88282f40fb1c02dfe7921793128e9b7abe1bbb85de0e12abc20cb23d3ecc720 +size 73949 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png index e55f4181f..c34889af7 100644 --- a/examples/screenshots/line_colorslice.png +++ b/examples/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cb016d754d3ea461fb80ae9c545955b4c6ced182b141c125d70c11665ee0c04 -size 53057 +oid sha256:65593126a03d7368c746e5f1669109de167a16d1277f74562ce742095047af5d +size 43146 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png index c551e15cf..10a4fa573 100644 --- a/examples/screenshots/line_dataslice.png +++ b/examples/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d89f1f3dfc01a522c3fc2528219442ee947db4f22ad7f9afc9a0df014eb9addf -size 69625 +oid sha256:bee8eadc3a297dd1d666910d43ed14cc368577b94b5d8ef9f6443819eb2f1134 +size 44689 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png index cefe33da1..fd68e397c 100644 --- a/examples/screenshots/line_stack.png +++ b/examples/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a24c9a72ef5255c1615f478f01ea5f3957e8ab250702386f43a324b178e7458f -size 106509 +oid sha256:32aa5e6f7c47a99e2720267095c09184a3706fcfaa549661137358047ba6eab8 +size 55274 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index 804efdff8..99830c8f4 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3b2ca05654ad45e6447ee1b028967808553c63cb1a52fa01a99052d121865b3 -size 94695 +oid sha256:558a8d46400d51abd319e73e7953bda0ec36c99a18db0bdfcdefdf96dd77d630 +size 62670 diff --git a/examples/screenshots/linear_selector.png b/examples/screenshots/linear_selector.png index 1b25a535e..dfa96362b 100644 --- a/examples/screenshots/linear_selector.png +++ b/examples/screenshots/linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd7dccdb26a32dec9a4c82028610d8da4058e9f590ae9dbfef558a3a976d64b4 -size 141373 +oid sha256:1b7973ea6abdbbeb5feb9fdeb3c94ed7f0e4930c8b98f304f2938329678686a7 +size 105205 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png index cc0a4e6ba..fc66f9c05 100644 --- a/examples/screenshots/no-imgui-extent_frac_layout.png +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6567058d5aef3ca5c7b77b1a6b01743235a305d793b6955714c20d681f994f8f -size 158808 +oid sha256:527415b6e9d9e92170471752ba508feb90b50a82e4925228f795c6ddc92b02ca +size 144308 diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png index 68342d1c7..87fac1088 100644 --- a/examples/screenshots/no-imgui-extent_layout.png +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3a82adc73994400122f8f8b25a9f241352cdb46ecfdbd7f2ee65209d4659bf1 -size 140550 +oid sha256:16b1f946e5678461a64ceaaad967de103e3c6c3706425fc3a4af331fc6874b4d +size 128985 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index f2f33bec4..4f3ec0fd2 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0194c56635cba18a34667e087409f01d6c43f73ac1f52a91d31bb466fa4613c1 -size 303365 +oid sha256:f579888d452a84831dae2551ff26fa3ab56d930b399bd7db5f41c6208f775d22 +size 299163 diff --git a/examples/screenshots/no-imgui-gridplot_non_square.png b/examples/screenshots/no-imgui-gridplot_non_square.png index 03d63f7ad..19b1b2088 100644 --- a/examples/screenshots/no-imgui-gridplot_non_square.png +++ b/examples/screenshots/no-imgui-gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d4b08a651868fd27329443c6e385fbd2ff1e354ffa5b69e15cb80b724c8dae8 -size 224336 +oid sha256:597bcd71e8be59b6c039251cf6414f1600d53296fd07d6abb71dafd9119d1550 +size 219788 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png index 5d94d3ec0..0db1e0959 100644 --- a/examples/screenshots/no-imgui-gridplot_viewports_check.png +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8feae6c310a5f3faa4fed64804287399c6da6eaa96c237cbd239fd8bb1bf1d4d -size 74948 +oid sha256:ed363583146d44a83f55ad79a08e3ffd01fcbec00c6e8d9939595d87bd21dc4a +size 41628 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index ea433a5e7..adc5f67db 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8621da2931209b9d9a9908f6d9b40e5dbae0e18397011c17c7ad5d4d056ad56b -size 96368 +oid sha256:fa9967b6148bc7a0b7d5e8688492f65147bf263bc3762a6b53f2feb249297fec +size 92602 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 7e0a9394d..6a8019802 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4437a07e42167ccfd5262648e7f41e7bf647c3493cce4d3a9ce564e91554a904 -size 226825 +oid sha256:4c128d5e0d522156442b38a407f8622991ff54e7bd668e65b2930febf1c43d2c +size 224933 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 1fe8b992f..5181f0ce5 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe254d9c917404d0beb2c7a77252b19cdc687ddd2f4480f78f381c96657a9fbe -size 251941 +oid sha256:002b3454abe51b48dfcda3bdd4fc64d69ede8f9315656fe1077e75fef91d39e0 +size 250469 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 9ce63bac6..9d7b91f40 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc1f5e72146dafb696a31f180983dc2a06beb874337968217bf8c22d122a0b37 -size 46434 +oid sha256:feda947f567dc4c2d484ca25458520afb8e8eee1ff6e8fb2be32db3b93739d6c +size 43046 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index 89da89f25..fdaf65c31 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4727d9ffbe0b54d3e562ec3c4071a2641f94e4e13d3714b8317166e0c58a7316 -size 225876 +oid sha256:c44bb64828621bf1b2074cb5d2409ea81222060ede9d68d7e5354fc8a5e8badb +size 223784 diff --git a/examples/screenshots/no-imgui-image_small.png b/examples/screenshots/no-imgui-image_small.png index 2753047dd..c346ae19c 100644 --- a/examples/screenshots/no-imgui-image_small.png +++ b/examples/screenshots/no-imgui-image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edcd88ffc32388c3ebde4ce42c85a01e0aae6815720cb42c36e025c74dc5c9cb -size 13841 +oid sha256:2a40a4da4f4f83acc4e17223eeb69f3472b07cb9408b8c01a0d786885469e674 +size 11325 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 9ce63bac6..9d7b91f40 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc1f5e72146dafb696a31f180983dc2a06beb874337968217bf8c22d122a0b37 -size 46434 +oid sha256:feda947f567dc4c2d484ca25458520afb8e8eee1ff6e8fb2be32db3b93739d6c +size 43046 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index bfaa7c60a..061dc4a8d 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d5d2769550a9a6d0dd17b6d464f4da0ebcba4087108666ddd39c427ad304997 -size 240743 +oid sha256:d75db62e1d6e73604741bfd4c3255fcaeb9061256302a867f0d31525e1924fa8 +size 155225 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 5d14d73d3..2b4ac1fc1 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37b8fcb208e36f79b490edd8693238c25cbf0f020a819843bb6fc53f7e282afe -size 45898 +oid sha256:77b4423c05e64a2087729106e00ac46157ec949b5074904be7825c8b14d5bd08 +size 36610 diff --git a/examples/screenshots/no-imgui-line_cmap_more.png b/examples/screenshots/no-imgui-line_cmap_more.png index 0ac37b6d1..0fc69ecaa 100644 --- a/examples/screenshots/no-imgui-line_cmap_more.png +++ b/examples/screenshots/no-imgui-line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a638d611d94fceceb8627139f19b43902e88cf7b96938f5450812cb57d97ba70 -size 114707 +oid sha256:2dbc6f7f47c52c9813562fbe57c5d50635ae447abf3c01fbd4668829bd172305 +size 90316 diff --git a/examples/screenshots/no-imgui-line_collection.png b/examples/screenshots/no-imgui-line_collection.png index d551a73a1..1ce43345f 100644 --- a/examples/screenshots/no-imgui-line_collection.png +++ b/examples/screenshots/no-imgui-line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8df0832df5660c8e130d4e2a7832348c99dd89a79ff2b5d19cfaec47b1b649d9 -size 91884 +oid sha256:4a31775cb50544545d90db48f77d2c3eba761e1dad16aed60ebf7ef2e4064625 +size 73001 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values.png b/examples/screenshots/no-imgui-line_collection_cmap_values.png index ef1343f66..f7a10c187 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edaa2958fb04d780c2aad7db30c34a15bffc2865d2a695b9db1d562cf313be16 -size 58711 +oid sha256:1072328cb684bd785fea7726c68633205e91f5d7b1545acc61e9affe9c318e76 +size 37900 diff --git a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png index a999af92e..6f359a994 100644 --- a/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png +++ b/examples/screenshots/no-imgui-line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a16b359a174c53977d3b06f61dda3a1059418b3fd5bcded088718e332b92e9f6 -size 63313 +oid sha256:f595b2e350cc646629bddd3df257d6fed37c60e588366b1e2c5e0ecfa84015a8 +size 40677 diff --git a/examples/screenshots/no-imgui-line_collection_colors.png b/examples/screenshots/no-imgui-line_collection_colors.png index 13677914b..b150d6225 100644 --- a/examples/screenshots/no-imgui-line_collection_colors.png +++ b/examples/screenshots/no-imgui-line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9148b69df1d1e603bc9d179035cdc486952fd1f2788e11cf093a13629fe47e5 -size 49671 +oid sha256:48351bdfefead7fd809564508bf5196edc8805b750a09b913f1b71651ca11cea +size 28560 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index f23d0e4ad..39b0d9c71 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d3b59576ff740247c4cdac40d0b597dbbef24677d07d8895bd6bdff128ec509 -size 117961 +oid sha256:d0c5dc9e6feabf2d61748009ae02a1c34fa81fb1bfd41232075177f7bf185a44 +size 72750 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index fe4ecbd90..a1ac57f50 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c89e2f45407beaf0b6b90ec61b11288b2fe1fb5d05c650e6e752c116dbea69b9 -size 52481 +oid sha256:203d8f4f6348e7fee0e374f8721c0cd3ca5e796fba9d15fb757c8b3b6b0e81cc +size 41064 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index b7b39c801..795781388 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b7bb624ee72480a5bb0d1d196d1ccebd8d0b71fb252ddaa1e185bbf9718431a -size 68628 +oid sha256:d7857e80f3a9c1a8e100f2e34c4181cf694efd190be87d0ad39b675d1594bf77 +size 43603 diff --git a/examples/screenshots/no-imgui-line_stack.png b/examples/screenshots/no-imgui-line_stack.png index 4c6419068..c8c76c66e 100644 --- a/examples/screenshots/no-imgui-line_stack.png +++ b/examples/screenshots/no-imgui-line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a52cfae62bd5f77799faf6316a2d7de848890b9ac46ced9e1e0fc24c6112bbbc -size 106908 +oid sha256:58b9b4b15262cccaa67ddafdecbed8a38e1a3391a73ec4ab372fba58ac534391 +size 55076 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 82aca044b..74006a70b 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b556ae7316ca24188cdadae410f501f3f2a86d00b004ed3ef26167ea5ba4f990 -size 96191 +oid sha256:e35aafe0f4f2527dfa2ef7ac2a80dd55a6e0418ac0427ac9e4bb1ba86b95de8f +size 60559 diff --git a/examples/screenshots/no-imgui-linear_selector.png b/examples/screenshots/no-imgui-linear_selector.png index 13125d504..4152fb23c 100644 --- a/examples/screenshots/no-imgui-linear_selector.png +++ b/examples/screenshots/no-imgui-linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f49a58a0e259f45e2e2fa9e311f9e05baafbd62a6d089a3c83167f8779e17cd5 -size 142585 +oid sha256:c9fc51ee3954815c291d117f5b8e7e80bd3a312207dba5e51d7fffd6e532287b +size 104130 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png index cc0a4e6ba..fc66f9c05 100644 --- a/examples/screenshots/no-imgui-rect_frac_layout.png +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6567058d5aef3ca5c7b77b1a6b01743235a305d793b6955714c20d681f994f8f -size 158808 +oid sha256:527415b6e9d9e92170471752ba508feb90b50a82e4925228f795c6ddc92b02ca +size 144308 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png index 68342d1c7..87fac1088 100644 --- a/examples/screenshots/no-imgui-rect_layout.png +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3a82adc73994400122f8f8b25a9f241352cdb46ecfdbd7f2ee65209d4659bf1 -size 140550 +oid sha256:16b1f946e5678461a64ceaaad967de103e3c6c3706425fc3a4af331fc6874b4d +size 128985 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index 41393517c..d5280bdb5 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0d4cbc55085805b98e8ace0248ccc7afc6a9496f79f3f3c69ffe97d4962e6f3 -size 55639 +oid sha256:191963308144dd97037e26f9b26ea97e0af1a1645f929777bd82931925e954c3 +size 43850 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index e148c70df..7eab4a984 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52ba9a0cb355ca13549e0e4d80f0ea544192efe6783cc4615c74be01c9930abb -size 30285 +oid sha256:354e8137170a14df5aaa9c1b28ce16e29e48d8e7f103cb2d3dd760cd148e6462 +size 24218 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index dbf6088e6..5a830f181 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2d3c95520c0025c1bdc408ed307d33793c409168a6cc80971bbc00210eda4d4 -size 33735 +oid sha256:1da7bfe315865d3eb481fa8d9b191abf6ab326d73abdd352f750c6c21ff1c99d +size 26311 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index e63267a49..aeb4850eb 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5826717eed9a8c44361a760ee31273a510ca0dcdc07cd98957aa1b8445be62a -size 33043 +oid sha256:dd1cd6c957c83abdd8832fdc47640c5a525af6121c48993c194ecd1780aef770 +size 25669 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 47b8e4ad8..47b4fe61a 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcef646cf7587c886a3765df2525d662caf6ad6db08e21c310bd2109b80e1673 -size 65092 +oid sha256:ed4c1c689eb446bed9f5ce016161f67e5bd3975f66731ec58ee13626ad9dccb9 +size 42266 diff --git a/examples/screenshots/rect_frac_layout.png b/examples/screenshots/rect_frac_layout.png index 1df76dc61..3059e78d0 100644 --- a/examples/screenshots/rect_frac_layout.png +++ b/examples/screenshots/rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc8ec12042a36992dbda00d04f251b47d58cd6d75eca5c4240cd207878f9282f -size 157607 +oid sha256:fa5563fc8bd9f7b24f3f25a98aa1ce6de3afea9b5c1f1cd7cc580fa4866fe796 +size 144210 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png index fa89b9afc..23303dd28 100644 --- a/examples/screenshots/rect_layout.png +++ b/examples/screenshots/rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:823785f993237cb0f2773aea62c3dbbe16fe63f86f83867f1e27b82c8793da9f -size 126051 +oid sha256:e1716c3f2e1309643a53759e9b5226440c9ac2e6be930f52439305a91c5ca914 +size 117466 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index 3e0ccbe98..e8c87cdfc 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a58f072bb440b9f7e36ceb33405caa30bae419089781cc5c5c67f1381811165 -size 56500 +oid sha256:3b71e5397b6eebff7ad540463515e0cba15cf8d4a9c63a2303d737c8f199c7d8 +size 45051 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index f6c5cbff7..d7271ce71 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b4516dd73a4a2dd916fa4e89d4ddfaf6206178a0842710b43cc0349fa2334ab -size 31079 +oid sha256:d4f95d45d58779e7afe2b6898621c3e8d46c2dadc04406ed9ffa09c8c84c19b3 +size 25582 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index cd61ff899..65599a790 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a97f3553a4ed9b99ae88db79233974dbb397449b63cd6b70b6a6d75a2e5e16d1 -size 34551 +oid sha256:a39a4a3f705c3dff50c3dad0a70d3d8412e5a5672169c7a68b90abb6030861bc +size 27533 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index 71d440b2a..b74c04a06 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5081e95875350a2bda9db3f8c0cc2ec89abf4aca3e9a19b9c3959dd98206a77c -size 33853 +oid sha256:862d8d1c5025add59f37206b88184eee8977ef6f9ada4518013c8270459288d7 +size 26977 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index 0bbe9650f..c0867d92c 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec78314216b2add21820209e5d2bfabf9f652b3b393d80f7e5be8341e42d19e6 -size 67328 +oid sha256:f67e1ad8120b2077002459917e99cd3e80a9b1ccc55e76923f2d86c791c8100d +size 44273 diff --git a/examples/selection_tools/polygon_selector.py b/examples/selection_tools/polygon_selector.py index 9b1d0fbac..b43b34811 100644 --- a/examples/selection_tools/polygon_selector.py +++ b/examples/selection_tools/polygon_selector.py @@ -13,9 +13,7 @@ from itertools import product # create a figure -figure = fpl.Figure( - size=(700, 560) -) +figure = fpl.Figure(size=(700, 560)) # generate some data @@ -39,7 +37,9 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: line_collection = figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5) # add polygon selector to image graphic -polygon_selector = line_collection.add_polygon_selector(fill_color="#ff00ff22", edge_color="#FFF", vertex_color="#FFF") +polygon_selector = line_collection.add_polygon_selector( + fill_color="#ff00ff", edge_color="#FFF", vertex_color="#FFF" +) # add event handler to highlight selected indices diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index c0cdffbd7..105325b3d 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -61,12 +61,12 @@ def prep_environment(): # Make that examples using rendercanvas.auto, will use the offscreen backend os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" # Disable ppaa on the renderer by default. Otherwise all screenshots change when the ppaa shaders are updated. - os.environ["PYGFX_PPAA"] = "none" + os.environ["PYGFX_DEFAULT_PPAA"] = "none" try: yield finally: del os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] - del os.environ["PYGFX_PPAA"] + del os.environ["PYGFX_DEFAULT_PPAA"] def test_that_we_are_on_lavapipe(): @@ -207,6 +207,6 @@ def get_diffs_rgba(slicer): if __name__ == "__main__": os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" - os.environ["PYGFX_PPAA"] = "none" + os.environ["PYGFX_DEFAULT_PPAA"] = "none" test_examples_run("simple") test_example_screenshots("simple") diff --git a/fastplotlib/graphics/_axes.py b/fastplotlib/graphics/_axes.py index 10774fc2a..8063e2819 100644 --- a/fastplotlib/graphics/_axes.py +++ b/fastplotlib/graphics/_axes.py @@ -3,6 +3,8 @@ import pygfx from pylinalg import quat_from_vecs, vec_transform_quat +from ..utils.enums import RenderQueue + GRID_PLANES = ["xy", "xz", "yz"] @@ -181,9 +183,15 @@ def __init__( } # create ruler for each dim - self._x = pygfx.Ruler(**x_kwargs) - self._y = pygfx.Ruler(**y_kwargs) - self._z = pygfx.Ruler(**z_kwargs) + self._x = pygfx.Ruler(alpha_mode="solid", **x_kwargs) + self._y = pygfx.Ruler(alpha_mode="solid", **y_kwargs) + self._z = pygfx.Ruler(alpha_mode="solid", **z_kwargs) + + # We render the lines and ticks as solid, but enable aa for text for prettier glyphs + for ruler in self._x, self._y, self._z: + ruler.text.material.alpha_mode = "auto" + ruler.text.material.render_queue = RenderQueue.auto + 50 + ruler.text.material.aa = True self._offset = offset @@ -226,15 +234,23 @@ def __init__( if grid_kwargs is None: grid_kwargs = dict() - grid_kwargs = { - "major_step": 10, - "minor_step": 1, - "thickness_space": "screen", - "major_thickness": 2, - "minor_thickness": 0.5, - "infinite": True, + # The grid is a bit weird, because it makes use of transparency to fade off in the distance. + # But w want it to write depth, so that objects that are drawn behind it are partually hidden. + # So we set alha_mode to 'auto'. We make it draw earlier than other 'auto' objects, under the + # assumption that most interesting stuff is in front of the grid, and artifacts behind the grid are less + # bad than those in front. Note that fully opaque objects blend perfectly fine with the grid. Artifacts + # should only emerge for objects that have semi-transparent fragments. + grid_kwargs = dict( + alpha_mode="auto", + render_queue=RenderQueue.auto + 50, + major_step=10, + minor_step=1, + thickness_space="screen", + major_thickness=2, + minor_thickness=0.5, + infinite=True, **grid_kwargs, - } + ) if grids: _grids = dict() diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index bc3486696..ab58c7a5c 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -5,7 +5,7 @@ import numpy as np import pylinalg as la -from wgpu.gui.base import log_exception +from rendercanvas.base import log_exception try: from imgui_bundle import imgui @@ -22,6 +22,8 @@ Name, Offset, Rotation, + Alpha, + AlphaMode, Visible, ) from ._axes import Axes @@ -67,6 +69,8 @@ def __init_subclass__(cls, **kwargs): "name": Name, "offset": Offset, "rotation": Rotation, + "alpha": Alpha, + "alpha_mode": AlphaMode, "visible": Visible, "deleted": Deleted, } @@ -77,6 +81,8 @@ def __init__( name: str = None, offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), + alpha: float = 1.0, + alpha_mode: str = "auto", visible: bool = True, metadata: Any = None, ): @@ -93,6 +99,29 @@ def __init__( rotation: (float, float, float, float), default (0, 0, 0, 1) rotation quaternion + alpha: (float), default 1.0 + The global alpha value, i.e. opacity, of the graphic. + + The alpha value for the colors. If you make your a graphic transparent, consider setting ``alpha_mode`` + to 'blend' or 'weighted_blend' so it won't write to the depth buffer. + + alpha_mode: (str), default "auto", + The alpha-mode, e.g. 'auto', 'blend', 'weighted_blend', 'solid', or 'dither'. + + * 'solid': the points do not have semi-transparent fragments. Writes to the depth buffer. + * 'auto': like 'solid', but allows semi-transparent fragments. + * 'blend': the points are considered transparent, and don't write to the depth buffer. + The points are blended in the order they are drawn. + * 'weighted_blend': like 'blend', but the result does not depend on the order in which points are rendered, + nor is their distance to the camera. + * 'dither': use stochastic transparency. Although the result is a bit noisy, the points distance to the camera + is properly taken into account, which may be better for 3D point clouds. Writes to the depth buffer. + + For details see https://docs.pygfx.org/stable/transparency.html + + visible: (bool), default True + Whether the graphic is visible. + metadata: Any, optional metadata attached to this Graphic, this is for the user to manage @@ -119,6 +148,8 @@ def __init__( self._deleted = Deleted(False) self._rotation = Rotation(rotation) self._offset = Offset(offset) + self._alpha = Alpha(alpha) + self._alpha_mode = AlphaMode(alpha_mode) self._visible = Visible(visible) self._block_events = False @@ -158,6 +189,24 @@ def rotation(self) -> np.ndarray: def rotation(self, value: np.ndarray | list | tuple): self._rotation.set_value(self, value) + @property + def alpha(self) -> float: + """The opacity of the graphic""" + return self._alpha.value + + @alpha.setter + def alpha(self, value: float): + self._alpha.set_value(self, value) + + @property + def alpha_mode(self) -> str: + """How the alpha is handled by the renderer""" + return self._alpha_mode.value + + @alpha_mode.setter + def alpha_mode(self, value: str): + self._alpha_mode.set_value(self, value) + @property def visible(self) -> bool: """Whether the graphic is visible""" @@ -194,14 +243,17 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo - self.world_object.visible = self.visible + wo.visible = self.visible + if wo.material is not None: + wo.material.opacity = self.alpha + wo.material.alpha_mode = self.alpha_mode # set offset if it's not (0., 0., 0.) - if not all(self.world_object.world.position == self.offset): + if not all(wo.world.position == self.offset): self.offset = self.offset # set rotation if it's not (0., 0., 0., 1.) - if not all(self.world_object.world.rotation == self.rotation): + if not all(wo.world.rotation == self.rotation): self.rotation = self.rotation @property @@ -460,8 +512,7 @@ def right_click_menu(self): def right_click_menu(self, menu): if not IMGUI: raise ImportError( - "imgui is required to set right-click menus:\n" - "pip install imgui_bundle" + "imgui is required to set right-click menus:\npip install imgui_bundle" ) self._right_click_menu = menu diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 4a4f5a797..143c4cc85 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -46,7 +46,7 @@ def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str @property def cmap(self) -> VertexCmap: """ - Control the cmap, cmap transform, or cmap alpha + Control the cmap or cmap transform For supported colormaps see the ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ """ @@ -77,7 +77,6 @@ def __init__( data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", uniform_color: bool = False, - alpha: float = 1.0, cmap: str | VertexCmap = None, cmap_transform: np.ndarray = None, isolated_buffer: bool = True, @@ -112,7 +111,6 @@ def __init__( self._colors, cmap_name=cmap, transform=cmap_transform, - alpha=alpha, ) elif isinstance(cmap, VertexCmap): # use existing cmap instance @@ -129,9 +127,7 @@ def __init__( self._colors = colors self._colors._shared += 1 # blank colormap instance - self._cmap = VertexCmap( - self._colors, cmap_name=None, transform=None, alpha=alpha - ) + self._cmap = VertexCmap(self._colors, cmap_name=None, transform=None) else: if uniform_color: if not isinstance(colors, str): # not a single color @@ -139,16 +135,14 @@ def __init__( raise TypeError( "must pass a single color if using `uniform_colors=True`" ) - self._colors = UniformColor(colors, alpha=alpha) + self._colors = UniformColor(colors) self._cmap = None else: self._colors = VertexColors( - colors, - n_colors=self._data.value.shape[0], - alpha=alpha, + colors, n_colors=self._data.value.shape[0] ) self._cmap = VertexCmap( - self._colors, cmap_name=None, transform=None, alpha=alpha + self._colors, cmap_name=None, transform=None ) self._size_space = SizeSpace(size_space) diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index 18bcf5187..086efd546 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -36,7 +36,7 @@ LinearRegionSelectionFeature, RectangleSelectionFeature, ) -from ._common import Name, Offset, Rotation, Visible, Deleted +from ._common import Name, Offset, Rotation, Alpha, AlphaMode, Visible, Deleted __all__ = [ @@ -65,6 +65,8 @@ "Name", "Offset", "Rotation", + "Alpha", + "AlphaMode", "Visible", "Deleted", "GraphicFeatureEvent", diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index d32904ae5..153f6aad2 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -4,7 +4,7 @@ import numpy as np from numpy.typing import NDArray -from wgpu.gui.base import log_exception +from rendercanvas.base import log_exception import pygfx @@ -318,7 +318,7 @@ def __len__(self): raise NotImplementedError def __repr__(self): - return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" + return f"{self.__class__.__name__} buffer data:\n{self.value.__repr__()}" def block_reentrance(set_value): diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index 884cd0109..646ee6945 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -127,6 +127,60 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): self._call_event_handlers(event) +class Alpha(GraphicFeature): + """The alpha value (i.e. opacity) of a graphic.""" + + property_name = "alpha" + event_info_spec = [ + {"dict key": "value", "type": "float", "description": "new alpha value"}, + ] + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + wo = graphic.world_object + if wo.material is not None: + wo.material.opacity = value + self._value = value + + event = GraphicFeatureEvent(type="alpha", info={"value": value}) + self._call_event_handlers(event) + + +class AlphaMode(GraphicFeature): + """The alpha-mode value of a graphic (i.e. how alpha is handled by the renderer).""" + + property_name = "alpha_mode" + event_info_spec = [ + {"dict key": "value", "type": "str", "description": "new alpha mode"}, + ] + + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + @block_reentrance + def set_value(self, graphic, value: str): + wo = graphic.world_object + if wo.material is not None: + wo.alpha_mode = value + self._value = value + + event = GraphicFeatureEvent(type="alpha_mode", info={"value": value}) + self._call_event_handlers(event) + + class Visible(GraphicFeature): """Access or change the visibility.""" diff --git a/fastplotlib/graphics/features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py index 868701079..21202cdf3 100644 --- a/fastplotlib/graphics/features/_positions_graphics.py +++ b/fastplotlib/graphics/features/_positions_graphics.py @@ -40,7 +40,6 @@ def __init__( self, colors: str | np.ndarray | tuple[float] | list[float] | list[str], n_colors: int, - alpha: float = None, isolated_buffer: bool = True, ): """ @@ -55,11 +54,8 @@ def __init__( n_colors: int number of colors, if passing in a single str or single RGBA array - alpha: float, optional - alpha value for the colors - """ - data = parse_colors(colors, n_colors, alpha) + data = parse_colors(colors, n_colors) super().__init__(data=data, isolated_buffer=isolated_buffer) @@ -158,13 +154,10 @@ class UniformColor(GraphicFeature): }, ] - def __init__( - self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0 - ): + def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): """Manages uniform color for line or scatter material""" - v = (*tuple(pygfx.Color(value))[:-1], alpha) # apply alpha - self._value = pygfx.Color(v) + self._value = pygfx.Color(value) super().__init__() @property @@ -427,7 +420,6 @@ def __init__( vertex_colors: VertexColors, cmap_name: str | None, transform: np.ndarray | None, - alpha: float = 1.0, ): """ Sliceable colormap feature, manages a VertexColors instance and @@ -439,7 +431,6 @@ def __init__( self._vertex_colors = vertex_colors self._cmap_name = cmap_name self._transform = transform - self._alpha = alpha if self._cmap_name is not None: if not isinstance(self._cmap_name, str): @@ -457,7 +448,6 @@ def __init__( cmap_name=self._cmap_name, transform=self._transform, ) - colors[:, -1] = alpha # set vertex colors from cmap self._vertex_colors[:] = colors @@ -481,7 +471,6 @@ def __setitem__(self, key: slice, cmap_name): colors = parse_cmap_values( n_colors=n_elements, cmap_name=cmap_name, transform=self._transform ) - colors[:, -1] = self.alpha self._cmap_name = cmap_name self._vertex_colors[key] = colors @@ -517,8 +506,6 @@ def transform( n_colors=self.value.shape[0], cmap_name=self._cmap_name, transform=values ) - colors[:, -1] = self.alpha - self._transform = values if indices is None: @@ -528,18 +515,6 @@ def transform( self._emit_event("cmap.transform", indices, values) - @property - def alpha(self) -> float: - """Get or set the alpha level""" - return self._alpha - - @alpha.setter - def alpha(self, value: float, indices: slice | list | np.ndarray = None): - self._vertex_colors[indices, -1] = value - self._alpha = value - - self._emit_event("cmap.alpha", indices, value) - def __len__(self): raise NotImplementedError( "len not implemented for `cmap`, use len(colors) instead" diff --git a/fastplotlib/graphics/features/utils.py b/fastplotlib/graphics/features/utils.py index e2f6e3428..408610e1e 100644 --- a/fastplotlib/graphics/features/utils.py +++ b/fastplotlib/graphics/features/utils.py @@ -6,9 +6,7 @@ def parse_colors( - colors: str | np.ndarray | list[str] | tuple[str], - n_colors: int | None, - alpha: float | None = None, + colors: str | np.ndarray | list[str] | tuple[str], n_colors: int | None ): """ @@ -16,8 +14,6 @@ def parse_colors( ---------- colors n_colors - alpha - key Returns ------- @@ -30,20 +26,22 @@ def parse_colors( colors = colors.tolist() # if the color is provided as a numpy array if isinstance(colors, np.ndarray): - if colors.shape == (4,): # single RGBA array + if colors.shape == (3,): # single RGB array + data = np.repeat(np.array([colors]), n_colors, axis=0) + elif colors.shape == (4,): # single RGBA array data = np.repeat(np.array([colors]), n_colors, axis=0) # else assume it's already a stack of RGBA arrays, keep this directly as the data elif colors.ndim == 2: - if colors.shape[1] != 4 and colors.shape[0] != n_colors: + if not (colors.shape[1] in (3, 4) and colors.shape[0] == n_colors): raise ValueError( "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + "RGB or RGBA arrays for each datapoint in the shape [n_datapoints, 3] or [n_datapoints, 4]" ) data = colors else: raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + "Valid array color arguments must be a single RGB(A) array or a stack of " + "RGB(A) arrays for each datapoint in the shape [n_datapoints, 3] or [n_datapoints, 4]" ) # if the color is provided as list or tuple @@ -58,8 +56,8 @@ def parse_colors( data = np.vstack([np.array(pygfx.Color(c)) for c in colors]) - # if it's a single RGBA array as a tuple/list - elif len(colors) == 4: + # if it's a single RGB/RGBA array as a tuple/list + elif len(colors) in (3, 4): c = pygfx.Color(colors) data = np.repeat(np.array([c]), n_colors, axis=0) @@ -70,18 +68,11 @@ def parse_colors( ) elif isinstance(colors, str): if colors == "random": - data = np.random.rand(n_colors, 4) - data[:, -1] = alpha + data = np.random.rand(n_colors, 3) else: data = make_pygfx_colors(colors, n_colors) else: # assume it's a single color, use pygfx.Color to parse it data = make_pygfx_colors(colors, n_colors) - if alpha is not None: - if isinstance(alpha, float): - data[:, -1] = alpha - else: - raise TypeError("if alpha is provided it must be of type `float`") - return to_gpu_supported_dtype(data) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 7e6ecee93..bd3bbc397 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -37,7 +37,6 @@ def __init__( thickness: float = 2.0, colors: str | np.ndarray | Sequence = "w", uniform_color: bool = False, - alpha: float = 1.0, cmap: str = None, cmap_transform: np.ndarray | Sequence = None, isolated_buffer: bool = True, @@ -66,9 +65,6 @@ def __init__( if True, uses a uniform buffer for the line color, basically saves GPU VRAM when the entire line has a single color - alpha: float, optional, default 1.0 - alpha value for the colors - cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors". For supported colormaps see the @@ -89,7 +85,6 @@ def __init__( data=data, colors=colors, uniform_color=uniform_color, - alpha=alpha, cmap=cmap, cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, @@ -104,9 +99,12 @@ def __init__( else: MaterialCls = pygfx.LineMaterial + aa = kwargs.get("alpha_mode", "auto") in ("blend", "weighted_blend") + if uniform_color: geometry = pygfx.Geometry(positions=self._data.buffer) material = MaterialCls( + aa=aa, thickness=self.thickness, color_mode="uniform", color=self.colors, @@ -115,6 +113,7 @@ def __init__( ) else: material = MaterialCls( + aa=aa, thickness=self.thickness, color_mode="vertex", pick_write=True, diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index ad4bf74d5..3d183593a 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -72,7 +72,7 @@ def cmap(self) -> CollectionFeature: """ Get or set a cmap along the line collection. - Optionally set using a tuple ("cmap", , ) to set the transform and/or alpha. + Optionally set using a tuple ("cmap", ) to set the transform.. Example: line_collection.cmap = ("jet", sine_transform_vals, 0.7) @@ -84,23 +84,20 @@ def cmap(self) -> CollectionFeature: def cmap(self, args): if isinstance(args, str): name = args - transform, alpha = None, 1.0 + transform = None elif len(args) == 1: name = args[0] - transform, alpha = None, None - + transform = None elif len(args) == 2: name, transform = args - alpha = None - - elif len(args) == 3: - name, transform, alpha = args + else: + raise ValueError( + "Too many values for cmap (note that alpha is deprecated, set alpha on the graphic instead)" + ) - colors = parse_cmap_values( + self.colors = parse_cmap_values( n_colors=len(self), cmap_name=name, transform=transform ) - colors[:, -1] = alpha - self.colors = colors @property def thickness(self) -> np.ndarray: @@ -132,7 +129,6 @@ def __init__( thickness: float | Sequence[float] = 2.0, colors: str | Sequence[str] | np.ndarray | Sequence[np.ndarray] = "w", uniform_colors: bool = False, - alpha: float = 1.0, cmap: Sequence[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, @@ -164,9 +160,6 @@ def __init__( | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line - alpha: float, optional - alpha value for colors, if colors is a ``str`` - cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines @@ -253,7 +246,7 @@ def __init__( else: if isinstance(colors, np.ndarray): # single color for all lines in the collection as RGBA - if colors.shape == (4,): + if colors.shape in [(3,), (4,)]: single_color = True # colors specified for each line as array of shape [n_lines, RGBA] @@ -268,8 +261,7 @@ def __init__( elif isinstance(colors, str): if colors == "random": - colors = np.random.rand(len(data), 4) - colors[:, -1] = alpha + colors = np.random.rand(len(data), 3) single_color = False else: # parse string color @@ -568,7 +560,6 @@ def __init__( data: List[np.ndarray], thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", - alpha: float = 1.0, cmap: Iterable[str] | str = None, cmap_transform: np.ndarray | List = None, name: str = None, @@ -602,9 +593,6 @@ def __init__( | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line - alpha: float, optional - alpha value for colors, if colors is a ``str`` - cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines @@ -646,7 +634,6 @@ def __init__( data=data, thickness=thickness, colors=colors, - alpha=alpha, cmap=cmap, cmap_transform=cmap_transform, name=name, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index b31022f5b..73095714b 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -29,7 +29,6 @@ def __init__( data: Any, colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", uniform_color: bool = False, - alpha: float = 1.0, cmap: str = None, cmap_transform: np.ndarray = None, isolated_buffer: bool = True, @@ -55,9 +54,6 @@ def __init__( if True, uses a uniform buffer for the scatter point colors. Useful if you need to save GPU VRAM when all points have the same color. - alpha: float, optional, default 1.0 - alpha value for the colors - cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors". For supported colormaps see the @@ -89,7 +85,6 @@ def __init__( data=data, colors=colors, uniform_color=uniform_color, - alpha=alpha, cmap=cmap, cmap_transform=cmap_transform, isolated_buffer=isolated_buffer, @@ -99,8 +94,13 @@ def __init__( n_datapoints = self.data.value.shape[0] + aa = kwargs.get("alpha_mode", "auto") in ("blend", "weighted_blend") + geo_kwargs = {"positions": self._data.buffer} - material_kwargs = {"pick_write": True} + material_kwargs = dict( + pick_write=True, + aa=aa, + ) self._size_space = SizeSpace(size_space) if uniform_color: diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 64a673768..033736a5f 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -5,6 +5,7 @@ import numpy as np import pygfx +from ...utils.enums import RenderQueue from .._base import Graphic from .._collection_base import GraphicCollection from ..features._selection_features import LinearSelectionFeature @@ -145,17 +146,34 @@ def __init__( line_inner = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=line_data), - material=material(thickness=thickness, color=edge_color, pick_write=True), + material=material( + thickness=thickness, + color=edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), ) self.line_outer = pygfx.Line( geometry=pygfx.Geometry(positions=line_data), material=material( - thickness=thickness + 6, color=self.colors_outer, pick_write=True + thickness=thickness + 6, + color=self.colors_outer, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) - line_inner.world.z = self.line_outer.world.z + 1 + # Inner line goes on top of the outer line + line_inner.render_order = 1 world_object = pygfx.Group() diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index e93e2a147..ee6849144 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -8,6 +8,7 @@ from .._collection_base import GraphicCollection from ..features._selection_features import LinearRegionSelectionFeature from ._base_selector import BaseSelector, MoveInfo +from ...utils.enums import RenderQueue class LinearRegionSelector(BaseSelector): @@ -140,7 +141,13 @@ def __init__( mesh = pygfx.Mesh( pygfx.box_geometry(1, size, 1), pygfx.MeshBasicMaterial( - color=pygfx.Color(self.fill_color), pick_write=True + color=pygfx.Color(self.fill_color), + alpha_mode="blend", + opacity=0.4, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) @@ -148,12 +155,21 @@ def __init__( mesh = pygfx.Mesh( pygfx.box_geometry(size, 1, 1), pygfx.MeshBasicMaterial( - color=pygfx.Color(self.fill_color), pick_write=True + color=pygfx.Color(self.fill_color), + alpha_mode="blend", + opacity=0.4, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) else: raise ValueError("`axis` must be one of 'x' or 'y'") + # Render the mesh before the lines + mesh.render_order = -1 + # the fill of the selection self.fill = mesh # no x, y offsets for linear region selector @@ -188,7 +204,15 @@ def __init__( positions=init_line_data.copy() ), # copy so the line buffer is isolated pygfx.LineMaterial( - thickness=edge_thickness, color=self.edge_color, pick_write=True + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + opacity=1, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) line1 = pygfx.Line( @@ -196,16 +220,20 @@ def __init__( positions=init_line_data.copy() ), # copy so the line buffer is isolated pygfx.LineMaterial( - thickness=edge_thickness, color=self.edge_color, pick_write=True + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + opacity=1, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) self.edges: tuple[pygfx.Line, pygfx.Line] = (line0, line1) - - # add the edge lines - for edge in self.edges: - edge.world.z = -0.5 - group.add(edge) + group.add(*self.edges) # TODO: if parent offset changes, we should set the selector offset too, use offset evented property # TODO: add check if parent is `None`, will throw error otherwise @@ -369,7 +397,6 @@ def get_selected_indices( return np.arange(*bounds, dtype=int) def _move_graphic(self, move_info: MoveInfo): - # If this the first move in this drag, store initial selection if move_info.start_selection is None: move_info.start_selection = self.selection diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 1bca2aca9..e02c627ac 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -7,6 +7,7 @@ import numpy as np import pygfx +from ...utils.enums import RenderQueue from .._base import Graphic from .._collection_base import GraphicCollection from ..features._selection_features import PolygonSelectionFeature @@ -77,7 +78,7 @@ def __init__( limits: Sequence[float], parent: Graphic = None, resizable: bool = True, - fill_color=(0, 0, 0.35, 0.2), + fill_color=(0, 0, 0.35), edge_color=(0.8, 0.6, 0), edge_thickness: float = 4, vertex_color=(0.7, 0.4, 0), @@ -104,27 +105,62 @@ def __init__( self._line = pygfx.Line( self.geometry, pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True + thickness=edge_thickness, + color=edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) self._points = pygfx.Points( self.geometry, - pygfx.PointsMaterial(size=vertex_size, color=vertex_color, pick_write=True), + pygfx.PointsMaterial( + size=vertex_size, + color=vertex_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), ) self._indicator = pygfx.Points( pygfx.Geometry(positions=[[0, 0, 0]]), - pygfx.PointsMaterial(size=15, color=vertex_color, opacity=0.3), + pygfx.PointsMaterial( + size=15, + color=vertex_color, + alpha_mode="blend", + opacity=0.3, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + ), ) self._indicator.visible = False self._mesh = pygfx.Mesh( - self.geometry, pygfx.MeshBasicMaterial(color=fill_color, pick_write=True) + self.geometry, + pygfx.MeshBasicMaterial( + color=fill_color, + alpha_mode="blend", + opacity=0.4, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), ) group = pygfx.Group().add(self._line, self._points, self._mesh, self._indicator) self._set_world_object(group) - # Order in z, so stuff stays pickable - self._line.local.z = 0.1 - self._points.local.z = 0.2 + # Points go on top of lines, which go on top of the mesh. And indicator in between. + self._line.render_order = 1 + self._indicator.render_order = 2 + self._points.render_order = 3 if selection is None: selection = [] @@ -225,9 +261,8 @@ def get_selected_data( # empty selection return np.array([], dtype=np.float32).reshape(0, 3) - s = slice( - ixs[0], ixs[-1] + 1 - ) # add 1 to end because these are direct indices + # add 1 to end because these are direct indices + s = slice(ixs[0], ixs[-1] + 1) # slices n_datapoints dim # slice with min, max is faster than using all the indices @@ -419,7 +454,7 @@ def _on_pointer_move(self, ev): """After mouse pointer move event, moves endpoint of current line segment""" if self._move_info.mode is None: return - world_pos = self._plot_area.map_screen_to_world(ev) + world_pos = self._plot_area.map_screen_to_world((ev.x, ev.y)) if world_pos is None: return @@ -430,9 +465,24 @@ def _on_pointer_move(self, ev): # - allowing the user to merge points by dragging one onto its neighbour. index = self._move_info.index snap_index = None - if ev.target is self._points: - snap_index = ev.pick_info["vertex_index"] - if snap_index == index: # dont snap to moving point + + # Use numpy to select the nearest point. + # This is because we cannot use picking on the actual points because + # then we'd always pick the point being moved. We don't use a depth buffer + # so we cannot move the point backwards to avoid it being picked. + # An advantage is that we can make the snap-radius larger than the size of the points. + world_pos2 = self._plot_area.map_screen_to_world((ev.x + 1, ev.y)) + world_pos_scale = float(np.linalg.norm(world_pos - world_pos2)) + snap_radius = 20 # logical screen pixels + if len(self.selection) > 0: + distances = np.linalg.norm(self.selection[:, :2] - world_pos[:2], axis=1) + distances /= world_pos_scale + distances[index] = np.inf + snap_index = int(np.argmin(distances)) + if distances[snap_index] > snap_radius: + snap_index = None + + if snap_index == index: # just in case, dont snap to moving point snap_index = None if len(self.selection) < 4: snap_index = None @@ -455,9 +505,6 @@ def _on_pointer_move(self, ev): else: self._indicator.material.size = 15 - # Move the positions being moved a bit down in depth, so its de-preferred in picking - world_pos = (world_pos[0], world_pos[1], -0.05) - self._indicator.local.position = world_pos # Update data @@ -473,10 +520,6 @@ def _on_pointer_move(self, ev): def _on_pointer_up(self, ev): if self._move_info.mode in ("create", "drag"): - # Update data to set depth (z) to zero again - data = self.selection - data[:, 2] = 0 - self._selection.set_value(self, data) # If we snapped, we dissolve (i.e. delete the vertex being moved) if self._move_info.snap_index is not None: assert self._move_info.index is not None diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py index fc62faf5c..e30165dae 100644 --- a/fastplotlib/graphics/selectors/_rectangle.py +++ b/fastplotlib/graphics/selectors/_rectangle.py @@ -6,6 +6,7 @@ import pygfx from .._collection_base import GraphicCollection +from ...utils.enums import RenderQueue from .._base import Graphic from ..features import RectangleSelectionFeature from ._base_selector import BaseSelector, MoveInfo @@ -135,7 +136,13 @@ def __init__( self.fill = pygfx.Mesh( pygfx.box_geometry(width, height, 1), pygfx.MeshBasicMaterial( - color=pygfx.Color(self.fill_color), pick_write=True + color=pygfx.Color(self.fill_color), + alpha_mode="blend", + opacity=0.4, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, ), ) @@ -153,7 +160,15 @@ def __init__( left_line = pygfx.Line( pygfx.Geometry(positions=left_line_data.copy()), - pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + ), ) # position data for the right edge line @@ -166,7 +181,15 @@ def __init__( right_line = pygfx.Line( pygfx.Geometry(positions=right_line_data.copy()), - pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + ), ) # position data for the left edge line @@ -179,7 +202,15 @@ def __init__( bottom_line = pygfx.Line( pygfx.Geometry(positions=bottom_line_data.copy()), - pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + ), ) # position data for the right edge line @@ -192,7 +223,15 @@ def __init__( top_line = pygfx.Line( pygfx.Geometry(positions=top_line_data.copy()), - pygfx.LineMaterial(thickness=edge_thickness, color=self.edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, + color=self.edge_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + ), ) self.edges: Tuple[pygfx.Line, pygfx.Line, pygfx.Line, pygfx.Line] = ( @@ -203,9 +242,10 @@ def __init__( ) # left line, right line, bottom line, top line # add the edge lines + for edge in self.edges: - edge.world.z = -0.5 - group.add(edge) + edge.render_order = 1 + group.add(*self.edges) # vertices top_left_vertex_data = (xmin, ymax, 1) @@ -221,6 +261,11 @@ def __init__( color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, ), ) @@ -232,6 +277,11 @@ def __init__( color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, ), ) @@ -243,6 +293,11 @@ def __init__( color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, ), ) @@ -254,6 +309,11 @@ def __init__( color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, ), ) @@ -265,7 +325,7 @@ def __init__( ) for vertex in self.vertices: - vertex.world.z = -0.25 + vertex.render_order = 2 group.add(vertex) self._selection = RectangleSelectionFeature(selection, limits=self._limits) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index fd0e9d702..9f1aeb8af 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -1,6 +1,7 @@ import pygfx import numpy as np +from ..utils.enums import RenderQueue from ._base import Graphic from .features import ( TextData, @@ -78,12 +79,21 @@ def __init__( self._outline_color = TextOutlineColor(outline_color) self._outline_thickness = TextOutlineThickness(outline_thickness) + # Text is usually used for annotations and the like. But we still want it to write depth. + # We make it render later than other 'auto' objects, assuming that most of these don't have transparent fragments. + + # The aa is on because it makes the glyphs prettier. It can result in artifacts, but these often express a different color outline + # which is actually not so bad; it would look weird on a line, but for text it helps the contrast of the glyph! + world_object = pygfx.Text( text=self.text, font_size=self.font_size, screen_space=screen_space, anchor=anchor, material=pygfx.TextMaterial( + alpha_mode="auto", + render_queue=RenderQueue.auto + 50, + aa=True, color=self.face_color, outline_color=self.outline_color, outline_thickness=self.outline_thickness, diff --git a/fastplotlib/layouts/_frame.py b/fastplotlib/layouts/_frame.py index cd2a1cbc2..1c308590f 100644 --- a/fastplotlib/layouts/_frame.py +++ b/fastplotlib/layouts/_frame.py @@ -1,6 +1,7 @@ import numpy as np import pygfx +from ..utils.enums import RenderQueue from ._rect import RectManager from ._utils import IMGUI_TOOLBAR_HEIGHT from ..utils.types import SelectorColorStates @@ -37,7 +38,7 @@ # wgsl shader snippet for SDF function that defines the resize handler, a lower right triangle. sdf_wgsl_resize_handle = """ -// hardcode square root of 2 +// hardcode square root of 2 let m_sqrt_2 = 1.4142135; // given a distance from an origin point, this defines the hypotenuse of a lower right triangle @@ -171,16 +172,28 @@ def __init__( else: title_text = title self._title_graphic = TextGraphic(title_text, font_size=16, face_color="white") + m = self._title_graphic.world_object.material + m.alpha_mode = "blend" + m.render_queue = RenderQueue.background + m.depth_write = False + m.depth_test = False wobjects.append(self._title_graphic.world_object) # init mesh of size 1 to graphically represent rect geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(color=self.plane_color.idle, pick_write=True) + material = pygfx.MeshBasicMaterial( + alpha_mode="blend", + render_queue=RenderQueue.background, + color=self.plane_color.idle, + depth_write=False, + depth_test=False, + pick_write=True, + ) self._plane = pygfx.Mesh(geometry, material) wobjects.append(self._plane) - # otherwise text isn't visible - self._plane.world.z = 0.5 + # Plane gets rendered before text and point + self._plane.render_order = -1 # create resize handler at point (x1, y1) x1, y1 = self.extent[[1, 3]] @@ -189,21 +202,21 @@ def __init__( # subtract 7 so that the bottom right corner of the triangle is at the center pygfx.Geometry(positions=[[x1 - 7, -y1 + 7, 0]]), pygfx.PointsMarkerMaterial( + alpha_mode="blend", + render_queue=RenderQueue.background, color=self.resize_handle_color.idle, marker="custom", custom_sdf=sdf_wgsl_resize_handle, size=12, size_space="screen", + depth_write=False, + depth_test=False, pick_write=True, ), ) if not resizeable: - # set all color states to transparent if Frame isn't resizeable - c = (0, 0, 0, 0) - self._resize_handle.material.color = c - self._resize_handle.material.edge_width = 0 - self.resize_handle_color = SelectorColorStates(c, c, c) + self._resize_handle.visible = False wobjects.append(self._resize_handle) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index cb9cd04c0..1a041547b 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -89,7 +89,6 @@ def add_line_collection( thickness: Union[float, Sequence[float]] = 2.0, colors: Union[str, Sequence[str], numpy.ndarray, Sequence[numpy.ndarray]] = "w", uniform_colors: bool = False, - alpha: float = 1.0, cmap: Union[Sequence[str], str] = None, cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, @@ -122,9 +121,6 @@ def add_line_collection( | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line - alpha: float, optional - alpha value for colors, if colors is a ``str`` - cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines @@ -162,7 +158,6 @@ def add_line_collection( thickness, colors, uniform_colors, - alpha, cmap, cmap_transform, name, @@ -180,7 +175,6 @@ def add_line( thickness: float = 2.0, colors: Union[str, numpy.ndarray, Sequence] = "w", uniform_color: bool = False, - alpha: float = 1.0, cmap: str = None, cmap_transform: Union[numpy.ndarray, Sequence] = None, isolated_buffer: bool = True, @@ -210,9 +204,6 @@ def add_line( if True, uses a uniform buffer for the line color, basically saves GPU VRAM when the entire line has a single color - alpha: float, optional, default 1.0 - alpha value for the colors - cmap: str, optional Apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors". For supported colormaps see the @@ -235,7 +226,6 @@ def add_line( thickness, colors, uniform_color, - alpha, cmap, cmap_transform, isolated_buffer, @@ -248,7 +238,6 @@ def add_line_stack( data: List[numpy.ndarray], thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", - alpha: float = 1.0, cmap: Union[Iterable[str], str] = None, cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, @@ -283,9 +272,6 @@ def add_line_stack( | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line - alpha: float, optional - alpha value for colors, if colors is a ``str`` - cmap: Iterable of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines @@ -329,7 +315,6 @@ def add_line_stack( data, thickness, colors, - alpha, cmap, cmap_transform, name, @@ -348,7 +333,6 @@ def add_scatter( data: Any, colors: str | numpy.ndarray | tuple[float] | list[float] | list[str] = "w", uniform_color: bool = False, - alpha: float = 1.0, cmap: str = None, cmap_transform: numpy.ndarray = None, isolated_buffer: bool = True, @@ -375,9 +359,6 @@ def add_scatter( if True, uses a uniform buffer for the scatter point colors. Useful if you need to save GPU VRAM when all points have the same color. - alpha: float, optional, default 1.0 - alpha value for the colors - cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors". For supported colormaps see the @@ -410,7 +391,6 @@ def add_scatter( data, colors, uniform_color, - alpha, cmap, cmap_transform, isolated_buffer, diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index c54890239..046c622ea 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -66,7 +66,15 @@ def __init__( self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) - fronts_path = str( + # This loads both the Roboto Font and FontAwesome 6 icons and creates and merged font + # allowing us to use both without pushing and popping to display icons or regular text + sans_serif_font = str( + Path(imgui_bundle.__file__).parent.joinpath( + "assets", "fonts", "Roboto", "Roboto-Regular.ttf" + ) + ) + + fa_6_fonts_path = str( Path(imgui_bundle.__file__).parent.joinpath( "assets", "fonts", "Font_Awesome_6_Free-Solid-900.otf" ) @@ -74,12 +82,20 @@ def __init__( io = imgui.get_io() - self._fa_icons = io.fonts.add_font_from_file_ttf( - fronts_path, 16, glyph_ranges_as_int_list=[fa.ICON_MIN_FA, fa.ICON_MAX_FA] + self._default_imgui_font = io.fonts.add_font_from_file_ttf( + sans_serif_font, 14, imgui.ImFontConfig() + ) + + font_config = imgui.ImFontConfig() + font_config.merge_mode = True + + self._default_imgui_font = io.fonts.add_font_from_file_ttf( + fa_6_fonts_path, + 14, + font_config, ) - io.fonts.build() - self.imgui_renderer.backend.create_fonts_texture() + imgui.push_font(self._default_imgui_font, self._default_imgui_font.legacy_size) self.imgui_renderer.set_gui(self._draw_imgui) @@ -88,12 +104,10 @@ def __init__( ) for i, subplot in enumerate(self._subplots.ravel()): - toolbar = SubplotToolbar(subplot=subplot, fa_icons=self._fa_icons) + toolbar = SubplotToolbar(subplot=subplot) self._subplot_toolbars[i] = toolbar - self._right_click_menu = StandardRightClickMenu( - figure=self, fa_icons=self._fa_icons - ) + self._right_click_menu = StandardRightClickMenu(figure=self) self._popups: dict[str, Popup] = {} @@ -102,6 +116,10 @@ def __init__( self.register_popup(ColormapPicker) + @property + def default_imgui_font(self) -> imgui.ImFont: + return self._default_imgui_font + @property def guis(self) -> dict[str, EdgeWindow]: """GUI windows added to the Figure""" @@ -125,7 +143,7 @@ def _render(self, draw=False): self.canvas.request_draw() def _draw_imgui(self) -> imgui.ImDrawData: - imgui.new_frame() + # imgui.new_frame() for subplot, toolbar in zip( self._subplots.ravel(), self._subplot_toolbars.ravel() @@ -144,11 +162,11 @@ def _draw_imgui(self) -> imgui.ImDrawData: self._right_click_menu.update() - imgui.end_frame() + # imgui.end_frame() - imgui.render() + # imgui.render() - return imgui.get_draw_data() + # return imgui.get_draw_data() def add_gui(self, gui: EdgeWindow): """ diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 2542fc215..feda52930 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -111,6 +111,7 @@ def __init__( (0.0, 0.0, 0.0, 1.0), (0.0, 0.0, 0.0, 1.0), (0.0, 0.0, 0.0, 1.0), + alpha_mode="blend", ) self._background = pygfx.Background(None, self._background_material) self.scene.add(self._background) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 98a6268f1..49120c71a 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -17,7 +17,7 @@ # number of pixels taken by the imgui toolbar when present -IMGUI_TOOLBAR_HEIGHT = 39 +IMGUI_TOOLBAR_HEIGHT = 36 def make_canvas_and_renderer( diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index 69a556109..b24bcac58 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -5,6 +5,7 @@ import numpy as np import pygfx +from ..utils.enums import RenderQueue from ..graphics import Graphic from ..graphics.features import GraphicFeatureEvent from ..graphics import LineGraphic, ScatterGraphic, ImageGraphic @@ -74,22 +75,32 @@ def __init__( self._line_world_object = pygfx.Line( geometry=pygfx.Geometry(positions=data), - material=material(thickness=8, color=self._color), + material=material( + alpha_mode="blend", + render_queue=RenderQueue.overlay, + thickness=8, + color=self._color, + depth_write=False, + depth_test=False, + ), ) # self._line_world_object.world.x = position[0] self._label_world_object = pygfx.Text( - geometry=pygfx.TextGeometry( - text=str(label), - font_size=6, - screen_space=False, - anchor="middle-left", - ), + text=str(label), + font_size=6, + screen_space=False, + anchor="middle-left", material=pygfx.TextMaterial( + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.overlay, color="w", outline_color="w", outline_thickness=0, + depth_write=False, + depth_test=False, ), ) @@ -175,10 +186,18 @@ def __init__( self._mesh = pygfx.Mesh( pygfx.box_geometry(50, 10, 1), pygfx.MeshBasicMaterial( - color=pygfx.Color([0.1, 0.1, 0.1, 1]), wireframe_thickness=10 + alpha_mode="blend", + render_queue=RenderQueue.overlay, + color=pygfx.Color([0.1, 0.1, 0.1, 1]), + wireframe_thickness=10, + depth_write=False, + depth_test=False, ), ) + # Plane gets rendered before text and line + self._mesh.render_order = -1 + self.world_object.add(self._mesh) self.world_object.add(self._legend_items_group) diff --git a/fastplotlib/tools/_tooltip.py b/fastplotlib/tools/_tooltip.py index 2fbdfcec2..f6c9cf531 100644 --- a/fastplotlib/tools/_tooltip.py +++ b/fastplotlib/tools/_tooltip.py @@ -3,6 +3,7 @@ import numpy as np import pygfx +from ..utils.enums import RenderQueue from ..graphics import LineGraphic, ImageGraphic, ScatterGraphic, Graphic from ..graphics.features import GraphicFeatureEvent @@ -59,19 +60,28 @@ def __init__(self): screen_space=False, anchor="bottom-left", material=pygfx.TextMaterial( + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.overlay, color="w", outline_color="w", outline_thickness=0.0, + depth_write=False, + depth_test=False, pick_write=False, ), ) # plane for the background of the text object geometry = pygfx.plane_geometry(1, 1) - material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.3, 0.95)) + material = pygfx.MeshBasicMaterial( + alpha_mode="blend", + render_queue=RenderQueue.overlay, + color=(0.1, 0.1, 0.3, 0.95), + depth_write=False, + depth_test=False, + ) self._plane = pygfx.Mesh(geometry, material) - # else text not visible - self._plane.world.z = 0.5 # line to outline the plane mesh self._line = pygfx.Line( @@ -87,8 +97,17 @@ def __init__(self): dtype=np.float32, ) ), - material=pygfx.LineThinMaterial(thickness=1.0, color=(0.8, 0.8, 1.0, 1.0)), + material=pygfx.LineThinMaterial( + alpha_mode="blend", + render_queue=RenderQueue.overlay, + thickness=1.0, + color=(0.8, 0.8, 1.0, 1.0), + depth_write=False, + depth_test=False, + ), ) + # Plane gets rendered before text and line + self._plane.render_order = -1 self._world_object = pygfx.Group() self._world_object.add(self._plane, self._text, self._line) diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index e31dd8d4a..3e763e08c 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -1,3 +1,4 @@ +import enum from typing import Literal import numpy as np @@ -42,7 +43,7 @@ def __init__( size: int, location: Literal["bottom", "right"], title: str, - window_flags: int = imgui.WindowFlags_.no_collapse + window_flags: enum.IntFlag = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize, *args, **kwargs, @@ -64,8 +65,8 @@ def __init__( title: str window title - window_flags: int - window flag enum, valid flags are: + window_flags: enum.IntFlag + Window flag enum, can be compared with ``|`` operator. Valid flags are: .. code-block:: py @@ -109,7 +110,6 @@ def __init__( self._location = location self._title = title self._window_flags = window_flags - self._fa_icons = self._figure._fa_icons self._x, self._y, self._width, self._height = self.get_rect() @@ -231,7 +231,6 @@ def __init__(self, figure: Figure, *args, **kwargs): super().__init__() self._figure = figure - self._fa_icons = self._figure._fa_icons self.is_open = False diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py index a06e81b90..435de4206 100644 --- a/fastplotlib/ui/_subplot_toolbar.py +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -6,14 +6,13 @@ class SubplotToolbar(Window): - def __init__(self, subplot: Subplot, fa_icons: imgui.ImFont): + def __init__(self, subplot: Subplot): """ Subplot toolbar shown below all subplots """ super().__init__() self._subplot = subplot - self._fa_icons = fa_icons def update(self): # get subplot rect @@ -32,42 +31,32 @@ def update(self): imgui.begin(f"Toolbar-{hex(id(self._subplot))}", p_open=None, flags=flags) - # icons for buttons - imgui.push_font(self._fa_icons) - # push ID to prevent conflict between multiple figs with same UI imgui.push_id(self._id_counter) with imgui_ctx.begin_horizontal(f"toolbar-{hex(id(self._subplot))}"): # autoscale button if imgui.button(fa.ICON_FA_MAXIMIZE): self._subplot.auto_scale() - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("autoscale scene") # center scene - imgui.push_font(self._fa_icons) if imgui.button(fa.ICON_FA_ALIGN_CENTER): self._subplot.center_scene() - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("center scene") - imgui.push_font(self._fa_icons) # checkbox controller _, self._subplot.controller.enabled = imgui.checkbox( fa.ICON_FA_COMPUTER_MOUSE, self._subplot.controller.enabled ) - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("enable/disable controller") - imgui.push_font(self._fa_icons) - # checkbox maintain_apsect + # checkbox maintain_aspect _, self._subplot.camera.maintain_aspect = imgui.checkbox( fa.ICON_FA_EXPAND, self._subplot.camera.maintain_aspect ) - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("maintain aspect") diff --git a/fastplotlib/ui/right_click_menus/_colormap_picker.py b/fastplotlib/ui/right_click_menus/_colormap_picker.py index 3c48bd4d8..199a8ff6d 100644 --- a/fastplotlib/ui/right_click_menus/_colormap_picker.py +++ b/fastplotlib/ui/right_click_menus/_colormap_picker.py @@ -26,14 +26,13 @@ class ColormapPicker(Popup): name = "colormap-picker" def __init__(self, figure): - super().__init__(figure=figure, fa_icons=None) + super().__init__(figure=figure) self.renderer = self._figure.renderer self.imgui_renderer = self._figure.imgui_renderer # maps str cmap names -> int texture IDs - self._texture_ids: dict[str, int] = {} - self._textures = list() + self._cmap_texture_refs: dict[str, imgui.ImTextureRef] = dict() # make all colormaps and upload representative texture for each cmap to the GPU for name in all_cmaps: @@ -45,8 +44,7 @@ def __init__(self, figure): data = np.vstack([[data]] * 2).astype(np.uint8) # upload the texture to the GPU, get the texture ID and texture - self._texture_ids[name], texture = self._create_texture_and_upload(data) - self._textures.append(texture) + self._cmap_texture_refs[name] = self._create_texture_and_upload(data) # used to set the states of the UI self._lut_tool = None @@ -83,12 +81,8 @@ def _create_texture_and_upload(self, data: np.ndarray) -> tuple[int, GPUTexture] # get a view texture_view = texture.create_view() - # get the id so that imgui can display it - id_texture = ctypes.c_int32(id(texture_view)).value - # add texture view to the backend so that it can be retrieved for rendering - self.imgui_renderer.backend._texture_views[id_texture] = texture_view - - return id_texture, texture + # return texture ref + return self.imgui_renderer.backend.register_texture(texture_view) def open(self, pos: tuple[int, int], lut_tool): """ @@ -121,10 +115,19 @@ def close(self): self.is_open = False def _add_cmap_menu_item(self, cmap_name: str): - texture_id = self._texture_ids[cmap_name] + # white border around cmap image + imgui.push_style_color(imgui.Col_.border, (1.0, 1.0, 1.0, 1.0)) + imgui.push_style_var(imgui.StyleVar_.image_border_size, 1.0) + + # cmap image + texture_ref = self._cmap_texture_refs[cmap_name] imgui.image( - texture_id, image_size=(50, self._texture_height), border_col=(1, 1, 1, 1) + texture_ref, + image_size=(50, self._texture_height), ) + # pop white border + imgui.pop_style_var() + imgui.pop_style_color() imgui.same_line() @@ -148,10 +151,7 @@ def update(self): self.is_open = True # make the cmap image height the same as the text height - self._texture_height = ( - self.imgui_renderer.backend.io.font_global_scale - * imgui.get_font().font_size - ) - 2 + self._texture_height = (imgui.get_font_size()) - 2 if imgui.menu_item("Reset vmin-vmax", "", False)[0]: self._lut_tool.image_graphic.reset_vmin_vmax() diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 4bb59c51d..bb9e5bdef 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -22,8 +22,8 @@ def flip_axis(subplot: PlotArea, axis: str, flip: bool): class StandardRightClickMenu(Popup): """Right click menu that is shown on subplots""" - def __init__(self, figure, fa_icons): - super().__init__(figure=figure, fa_icons=fa_icons) + def __init__(self, figure): + super().__init__(figure=figure) self._last_right_click_pos = None self._mouse_down: bool = False @@ -182,19 +182,4 @@ def update(self): imgui.end_menu() - # renderer blend modes - if imgui.begin_menu("Blend mode"): - for blend_mode in sorted( - self.get_subplot().renderer._blenders_available.keys() - ): - clicked, _ = imgui.menu_item( - label=blend_mode, - shortcut="", - p_selected=self.get_subplot().renderer.blend_mode == blend_mode, - ) - - if clicked: - self.get_subplot().renderer.blend_mode = blend_mode - imgui.end_menu() - imgui.end_popup() diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index dce4d96f9..dd527ca67 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -5,6 +5,7 @@ from .functions import * from .gpu import enumerate_adapters, select_adapter, print_wgpu_report from ._plot_helpers import * +from .enums import * @dataclass diff --git a/fastplotlib/utils/enums.py b/fastplotlib/utils/enums.py new file mode 100644 index 000000000..5de3b2e0a --- /dev/null +++ b/fastplotlib/utils/enums.py @@ -0,0 +1,14 @@ +from enum import IntEnum + + +class RenderQueue(IntEnum): + # Defaults by PyGfx + background = 1000 + opaque = 2000 + opaque_with_discard = 2400 + auto = 2600 + transparent = 3000 + overlay = 4000 + # For selectors we use a render_queue of 3500, which is at the end of what is considered the group of transparent objects. + # So it's rendered later than the normal scene (2000 - 3000), but before overlays like legends and tooltips. + selector = 3500 diff --git a/fastplotlib/widgets/image_widget/_sliders.py b/fastplotlib/widgets/image_widget/_sliders.py index c8ad67f39..393b13273 100644 --- a/fastplotlib/widgets/image_widget/_sliders.py +++ b/fastplotlib/widgets/image_widget/_sliders.py @@ -56,19 +56,15 @@ def update(self): flag_index_changed = False # reset vmin-vmax using full orig data - imgui.push_font(self._fa_icons) if imgui.button(label=fa.ICON_FA_CIRCLE_HALF_STROKE + fa.ICON_FA_FILM): self._image_widget.reset_vmin_vmax() - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("reset contrast limits using full movie/stack") # reset vmin-vmax using currently displayed ImageGraphic data - imgui.push_font(self._fa_icons) imgui.same_line() if imgui.button(label=fa.ICON_FA_CIRCLE_HALF_STROKE): self._image_widget.reset_vmin_vmax_frame() - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("reset contrast limits using current frame") @@ -78,7 +74,6 @@ def update(self): # buttons and slider UI elements for each dim for dim in self._image_widget.slider_dims: imgui.push_id(f"{self._id_counter}_{dim}") - imgui.push_font(self._fa_icons) if self._playing[dim]: # show pause button if playing @@ -119,7 +114,6 @@ def update(self): imgui.same_line() # loop checkbox _, self._loop = imgui.checkbox(label=fa.ICON_FA_ROTATE, v=self._loop) - imgui.pop_font() if imgui.is_item_hovered(0): imgui.set_tooltip("loop playback") diff --git a/pyproject.toml b/pyproject.toml index cda3b65b1..debca6d6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ keywords = [ requires-python = ">= 3.10" dependencies = [ "numpy>=1.23.0", - "pygfx==0.12.0", - "wgpu>=0.20.0", + "pygfx==0.13", + "wgpu", # Let pygfx constrain the wgpu version "cmap>=0.1.3", # (this comment keeps this list multiline in VSCode) ] @@ -55,7 +55,7 @@ tests = [ "scikit-learn", "tqdm", ] -imgui = ["imgui-bundle>=1.6.0,<1.92.0"] +imgui = ["wgpu[imgui]"] dev = ["fastplotlib[docs,notebook,tests,imgui]"] [project.urls] diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index ed791b6fa..40d3561aa 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -73,12 +73,11 @@ def test_sizes_slice(): @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("colors", [None, *generate_color_inputs("b")]) @pytest.mark.parametrize("uniform_color", [True, False]) -@pytest.mark.parametrize("alpha", [1.0, 0.5, 0.0]) -def test_uniform_color(graphic_type, colors, uniform_color, alpha): +def test_uniform_color(graphic_type, colors, uniform_color): fig = fpl.Figure() kwargs = dict() - for kwarg in ["colors", "uniform_color", "alpha"]: + for kwarg in ["colors", "uniform_color"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] @@ -95,10 +94,10 @@ def test_uniform_color(graphic_type, colors, uniform_color, alpha): assert isinstance(graphic.colors, pygfx.Color) if colors is None: # default white - assert graphic.colors == pygfx.Color([1, 1, 1, alpha]) + assert graphic.colors == pygfx.Color([1, 1, 1]) else: # should be blue - assert graphic.colors == pygfx.Color([0, 0, 1, alpha]) + assert graphic.colors == pygfx.Color([0, 0, 1]) # check pygfx material npt.assert_almost_equal( @@ -111,13 +110,13 @@ def test_uniform_color(graphic_type, colors, uniform_color, alpha): # default white npt.assert_almost_equal( graphic.colors.value, - np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + np.repeat([[1, 1, 1, 1.0]], repeats=len(graphic.data), axis=0), ) else: # blue npt.assert_almost_equal( graphic.colors.value, - np.repeat([[0, 0, 1, alpha]], repeats=len(graphic.data), axis=0), + np.repeat([[0, 0, 1, 1.0]], repeats=len(graphic.data), axis=0), ) # check geometry @@ -167,18 +166,16 @@ def test_positions_graphics_data( @pytest.mark.parametrize("graphic_type", ["line", "scatter"]) @pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) @pytest.mark.parametrize("uniform_color", [None, False]) -@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_positions_graphic_vertex_colors( graphic_type, colors, uniform_color, - alpha, ): # test different ways of passing vertex colors fig = fpl.Figure() kwargs = dict() - for kwarg in ["colors", "uniform_color", "alpha"]: + for kwarg in ["colors", "uniform_color"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] @@ -190,9 +187,6 @@ def test_positions_graphic_vertex_colors( elif graphic_type == "scatter": graphic = fig[0, 0].add_scatter(data=data, **kwargs) - if alpha is None: # default arg - alpha = 1 - # color per vertex # uniform colors is default False, or set to False assert isinstance(graphic._colors, VertexColors) @@ -203,14 +197,14 @@ def test_positions_graphic_vertex_colors( # default npt.assert_almost_equal( graphic.colors.value, - np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + np.repeat([[1, 1, 1, 1.0]], repeats=len(graphic.data), axis=0), ) else: if len(colors) != len(graphic.data): # should be single red, regardless of input variant (i.e. str, array, RGBA tuple, etc. npt.assert_almost_equal( graphic.colors.value, - np.repeat([[1, 0, 0, alpha]], repeats=len(graphic.data), axis=0), + np.repeat([[1, 0, 0, 1.0]], repeats=len(graphic.data), axis=0), ) else: # multi colors @@ -225,20 +219,18 @@ def test_positions_graphic_vertex_colors( @pytest.mark.parametrize( "cmap_transform", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] ) -@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) def test_cmap( graphic_type, colors, uniform_color, cmap, cmap_transform, - alpha, ): # test different ways of passing cmap args fig = fpl.Figure() kwargs = dict() - for kwarg in ["cmap", "cmap_transform", "colors", "uniform_color", "alpha"]: + for kwarg in ["cmap", "cmap_transform", "colors", "uniform_color"]: if locals()[kwarg] is not None: # add to dict of arguments that will be passed kwargs[kwarg] = locals()[kwarg] @@ -250,11 +242,7 @@ def test_cmap( elif graphic_type == "scatter": graphic = fig[0, 0].add_scatter(data=data, **kwargs) - if alpha is None: - alpha = 1.0 - truth = TRUTH_CMAPS[cmap].copy() - truth[:, -1] = alpha # permute if transform is provided if cmap_transform is not None: @@ -275,7 +263,6 @@ def test_cmap( # test changing cmap but not transform graphic.cmap = "viridis" truth = TRUTH_CMAPS["viridis"].copy() - truth[:, -1] = alpha if cmap_transform is not None: truth = truth[cmap_transform] @@ -293,7 +280,7 @@ def test_cmap( cmap_transform_norm /= cmap_transform_norm.max() cmap_transform_norm *= 255 - truth = fpl.utils.get_cmap("viridis", alpha=alpha) + truth = fpl.utils.get_cmap("viridis", alpha=1) truth = np.vstack([truth[val] for val in cmap_transform_norm.astype(int)]) graphic.cmap.transform = cmap_transform @@ -485,3 +472,7 @@ def test_size_space(graphic_type, size_space): graphic.size_space = "world" assert graphic.size_space == "world" assert graphic.world_object.material.size_space == "world" + + +if __name__ == "__main__": + test_cmap("scatter", None, False, "jet", None) \ No newline at end of file From 5a9b8299b7f85d8551fa85b146d49a523e39f999 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 1 Oct 2025 11:38:50 -0400 Subject: [PATCH 12/52] add `ImageVolumeGraphic` (#791) * add volume graphic, basic stuff works * remove a useless file I accidentally added * black * add volume movie * linear interpolation is better * cmap shown for volume image in hlut tooL * volume render modes work * example works * some updates to ImageGraphic for sharing buffesr * update volume and add examples * update example * update add_graphics mixin * bah * separate TextureArrayVolume for volumes, fix indexing for tiling volumes * remove dim kwarg * update interpolation * tests for TextureArrayVolume WIP * remove print * fix example * add example * modify lfs pointer see if this works * better alpha handling * add example to tests * correct docs * add zarr multi channel example * remove debug stuf * volume graphic tests * fixes * volume image graphci features * allow setting only vmin or vmax for images * black * update example * update graphic methods mixin * fix * upate example * better way to detect images * texture array for volume image is 3D * fix test * rename to figure so pygfx gallery scraper picks it up * allow longer time for actions, I guess volumes take a while * rename * update screenshots * for some reason using black via python gives different results than black on commandline * add ImageVolume api docs * replace image_widget_grid yet again... * forgot to commit event tables diff * hlut tool can manage multiple ImageGraphics or ImageVolumeGraphics * remove property that's no longer part of the API * docs * docstring fix * better text on hlut * black * update api docs * update CI timeouts because of new downloaded volume image * update screenshots because hlut line is more gray --- .github/workflows/ci-pygfx-release.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/screenshots.yml | 2 +- .../api/graphic_features/TextureArray.rst | 1 - .../graphic_features/TextureArrayVolume.rst | 39 ++ .../graphic_features/VolumeIsoEmissive.rst | 35 ++ .../graphic_features/VolumeIsoShininess.rst | 35 ++ .../graphic_features/VolumeIsoStepSize.rst | 35 ++ .../graphic_features/VolumeIsoSubStepSize.rst | 35 ++ .../graphic_features/VolumeIsoThreshold.rst | 35 ++ .../api/graphic_features/VolumeRenderMode.rst | 35 ++ .../api/graphic_features/VolumeSlicePlane.rst | 35 ++ docs/source/api/graphic_features/index.rst | 8 + .../api/graphics/ImageVolumeGraphic.rst | 61 +++ docs/source/api/graphics/index.rst | 1 + docs/source/api/layouts/subplot.rst | 1 + docs/source/api/tools/HistogramLUTTool.rst | 3 +- docs/source/conf.py | 1 + docs/source/user_guide/event_tables.rst | 225 +++++++++ examples/image_volume/README.rst | 2 + examples/image_volume/image_volume_4d.py | 115 +++++ examples/image_volume/image_volume_mip.py | 47 ++ .../image_volume_multi_channel.py | 49 ++ .../image_volume_non_orthogonal_slicing.py | 56 +++ .../image_volume/image_volume_render_modes.py | 86 ++++ .../image_volume/image_volume_share_buffer.py | 75 +++ .../image_volume_slicing_animation.py | 64 +++ .../image_volume/image_volume_toy_data.py | 31 ++ .../nb-image-widget-movie-set_data.png | 4 +- .../nb-image-widget-movie-single-0-reset.png | 4 +- .../nb-image-widget-movie-single-0.png | 4 +- .../nb-image-widget-movie-single-279.png | 4 +- ...e-widget-movie-single-50-window-max-33.png | 4 +- ...-widget-movie-single-50-window-mean-13.png | 4 +- ...-widget-movie-single-50-window-mean-33.png | 4 +- ...ge-widget-movie-single-50-window-reset.png | 4 +- .../nb-image-widget-movie-single-50.png | 4 +- .../nb-image-widget-single-gnuplot2.png | 4 +- .../screenshots/nb-image-widget-single.png | 4 +- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 +- ...idget-zfish-frame-50-frame-apply-reset.png | 4 +- ...ge-widget-zfish-frame-50-max-window-13.png | 4 +- ...e-widget-zfish-frame-50-mean-window-13.png | 4 +- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-frame-50.png | 4 +- .../nb-image-widget-zfish-frame-99.png | 4 +- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 +- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 +- ...dget-zfish-grid-frame-50-max-window-13.png | 4 +- ...get-zfish-grid-frame-50-mean-window-13.png | 4 +- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-grid-frame-50.png | 4 +- .../nb-image-widget-zfish-grid-frame-99.png | 4 +- ...e-widget-zfish-grid-init-mean-window-5.png | 4 +- ...fish-grid-set_data-reset-indices-false.png | 4 +- ...zfish-grid-set_data-reset-indices-true.png | 4 +- ...-image-widget-zfish-init-mean-window-5.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 4 +- ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 +- examples/screenshots/image_volume_mip.png | 3 + .../image_volume_multi_channel.png | 3 + .../image_volume_non_orthogonal_slicing.png | 3 + .../screenshots/image_volume_render_modes.png | 3 + .../screenshots/image_volume_share_buffer.png | 3 + examples/screenshots/image_widget.png | 4 +- examples/screenshots/image_widget_grid.png | 4 +- examples/screenshots/image_widget_imgui.png | 4 +- .../screenshots/image_widget_single_video.png | 4 +- examples/screenshots/image_widget_videos.png | 4 +- .../image_widget_viewports_check.png | 4 +- .../screenshots/no-imgui-image_volume_mip.png | 3 + .../no-imgui-image_volume_multi_channel.png | 3 + ...ui-image_volume_non_orthogonal_slicing.png | 3 + examples/tests/testutils.py | 1 + fastplotlib/graphics/__init__.py | 2 + fastplotlib/graphics/_base.py | 43 +- fastplotlib/graphics/features/__init__.py | 20 + fastplotlib/graphics/features/_common.py | 11 + fastplotlib/graphics/features/_image.py | 13 +- fastplotlib/graphics/features/_volume.py | 443 ++++++++++++++++++ fastplotlib/graphics/image.py | 40 +- fastplotlib/graphics/image_volume.py | 421 +++++++++++++++++ fastplotlib/layouts/_graphic_methods_mixin.py | 113 ++++- fastplotlib/tools/_histogram_lut.py | 142 ++++-- .../ui/right_click_menus/_colormap_picker.py | 2 +- fastplotlib/utils/functions.py | 6 +- fastplotlib/widgets/image_widget/_widget.py | 4 +- pyproject.toml | 2 + scripts/generate_add_graphic_methods.py | 10 +- tests/conftest.py | 2 + tests/test_image_volume_graphic.py | 186 ++++++++ tests/test_texture_array_volume.py | 207 ++++++++ 93 files changed, 2776 insertions(+), 184 deletions(-) create mode 100644 docs/source/api/graphic_features/TextureArrayVolume.rst create mode 100644 docs/source/api/graphic_features/VolumeIsoEmissive.rst create mode 100644 docs/source/api/graphic_features/VolumeIsoShininess.rst create mode 100644 docs/source/api/graphic_features/VolumeIsoStepSize.rst create mode 100644 docs/source/api/graphic_features/VolumeIsoSubStepSize.rst create mode 100644 docs/source/api/graphic_features/VolumeIsoThreshold.rst create mode 100644 docs/source/api/graphic_features/VolumeRenderMode.rst create mode 100644 docs/source/api/graphic_features/VolumeSlicePlane.rst create mode 100644 docs/source/api/graphics/ImageVolumeGraphic.rst create mode 100644 examples/image_volume/README.rst create mode 100644 examples/image_volume/image_volume_4d.py create mode 100644 examples/image_volume/image_volume_mip.py create mode 100644 examples/image_volume/image_volume_multi_channel.py create mode 100644 examples/image_volume/image_volume_non_orthogonal_slicing.py create mode 100644 examples/image_volume/image_volume_render_modes.py create mode 100644 examples/image_volume/image_volume_share_buffer.py create mode 100644 examples/image_volume/image_volume_slicing_animation.py create mode 100644 examples/image_volume/image_volume_toy_data.py create mode 100644 examples/screenshots/image_volume_mip.png create mode 100644 examples/screenshots/image_volume_multi_channel.png create mode 100644 examples/screenshots/image_volume_non_orthogonal_slicing.png create mode 100644 examples/screenshots/image_volume_render_modes.png create mode 100644 examples/screenshots/image_volume_share_buffer.png create mode 100644 examples/screenshots/no-imgui-image_volume_mip.png create mode 100644 examples/screenshots/no-imgui-image_volume_multi_channel.png create mode 100644 examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png create mode 100644 fastplotlib/graphics/features/_volume.py create mode 100644 fastplotlib/graphics/image_volume.py create mode 100644 tests/test_image_volume_graphic.py create mode 100644 tests/test_texture_array_volume.py diff --git a/.github/workflows/ci-pygfx-release.yml b/.github/workflows/ci-pygfx-release.yml index 87ed1a113..8c7a64ec4 100644 --- a/.github/workflows/ci-pygfx-release.yml +++ b/.github/workflows/ci-pygfx-release.yml @@ -16,7 +16,7 @@ on: jobs: test-build-full: name: Tests - pygfx release - timeout-minutes: 25 + timeout-minutes: 20 if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 528b62772..d2468783d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ on: jobs: test-build-full: name: Tests - timeout-minutes: 25 + timeout-minutes: 20 if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index cfaf419b8..3f08ce0f1 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -14,7 +14,7 @@ jobs: screenshots: name: Regenerate runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false diff --git a/docs/source/api/graphic_features/TextureArray.rst b/docs/source/api/graphic_features/TextureArray.rst index 73facc5bf..004881282 100644 --- a/docs/source/api/graphic_features/TextureArray.rst +++ b/docs/source/api/graphic_features/TextureArray.rst @@ -23,7 +23,6 @@ Properties TextureArray.buffer TextureArray.col_indices TextureArray.row_indices - TextureArray.shared TextureArray.value Methods diff --git a/docs/source/api/graphic_features/TextureArrayVolume.rst b/docs/source/api/graphic_features/TextureArrayVolume.rst new file mode 100644 index 000000000..2f8599ef7 --- /dev/null +++ b/docs/source/api/graphic_features/TextureArrayVolume.rst @@ -0,0 +1,39 @@ +.. _api.TextureArrayVolume: + +TextureArrayVolume +****************** + +================== +TextureArrayVolume +================== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArrayVolume_api + + TextureArrayVolume + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArrayVolume_api + + TextureArrayVolume.buffer + TextureArrayVolume.col_indices + TextureArrayVolume.row_indices + TextureArrayVolume.value + TextureArrayVolume.zdim_indices + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextureArrayVolume_api + + TextureArrayVolume.add_event_handler + TextureArrayVolume.block_events + TextureArrayVolume.clear_event_handlers + TextureArrayVolume.remove_event_handler + TextureArrayVolume.set_value + diff --git a/docs/source/api/graphic_features/VolumeIsoEmissive.rst b/docs/source/api/graphic_features/VolumeIsoEmissive.rst new file mode 100644 index 000000000..4d7c4bf7d --- /dev/null +++ b/docs/source/api/graphic_features/VolumeIsoEmissive.rst @@ -0,0 +1,35 @@ +.. _api.VolumeIsoEmissive: + +VolumeIsoEmissive +***************** + +================= +VolumeIsoEmissive +================= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoEmissive_api + + VolumeIsoEmissive + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoEmissive_api + + VolumeIsoEmissive.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoEmissive_api + + VolumeIsoEmissive.add_event_handler + VolumeIsoEmissive.block_events + VolumeIsoEmissive.clear_event_handlers + VolumeIsoEmissive.remove_event_handler + VolumeIsoEmissive.set_value + diff --git a/docs/source/api/graphic_features/VolumeIsoShininess.rst b/docs/source/api/graphic_features/VolumeIsoShininess.rst new file mode 100644 index 000000000..0e4ed6dd3 --- /dev/null +++ b/docs/source/api/graphic_features/VolumeIsoShininess.rst @@ -0,0 +1,35 @@ +.. _api.VolumeIsoShininess: + +VolumeIsoShininess +****************** + +================== +VolumeIsoShininess +================== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoShininess_api + + VolumeIsoShininess + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoShininess_api + + VolumeIsoShininess.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoShininess_api + + VolumeIsoShininess.add_event_handler + VolumeIsoShininess.block_events + VolumeIsoShininess.clear_event_handlers + VolumeIsoShininess.remove_event_handler + VolumeIsoShininess.set_value + diff --git a/docs/source/api/graphic_features/VolumeIsoStepSize.rst b/docs/source/api/graphic_features/VolumeIsoStepSize.rst new file mode 100644 index 000000000..91f838d7a --- /dev/null +++ b/docs/source/api/graphic_features/VolumeIsoStepSize.rst @@ -0,0 +1,35 @@ +.. _api.VolumeIsoStepSize: + +VolumeIsoStepSize +***************** + +================= +VolumeIsoStepSize +================= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoStepSize_api + + VolumeIsoStepSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoStepSize_api + + VolumeIsoStepSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoStepSize_api + + VolumeIsoStepSize.add_event_handler + VolumeIsoStepSize.block_events + VolumeIsoStepSize.clear_event_handlers + VolumeIsoStepSize.remove_event_handler + VolumeIsoStepSize.set_value + diff --git a/docs/source/api/graphic_features/VolumeIsoSubStepSize.rst b/docs/source/api/graphic_features/VolumeIsoSubStepSize.rst new file mode 100644 index 000000000..db81fee8a --- /dev/null +++ b/docs/source/api/graphic_features/VolumeIsoSubStepSize.rst @@ -0,0 +1,35 @@ +.. _api.VolumeIsoSubStepSize: + +VolumeIsoSubStepSize +******************** + +==================== +VolumeIsoSubStepSize +==================== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoSubStepSize_api + + VolumeIsoSubStepSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoSubStepSize_api + + VolumeIsoSubStepSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoSubStepSize_api + + VolumeIsoSubStepSize.add_event_handler + VolumeIsoSubStepSize.block_events + VolumeIsoSubStepSize.clear_event_handlers + VolumeIsoSubStepSize.remove_event_handler + VolumeIsoSubStepSize.set_value + diff --git a/docs/source/api/graphic_features/VolumeIsoThreshold.rst b/docs/source/api/graphic_features/VolumeIsoThreshold.rst new file mode 100644 index 000000000..9fa4ab616 --- /dev/null +++ b/docs/source/api/graphic_features/VolumeIsoThreshold.rst @@ -0,0 +1,35 @@ +.. _api.VolumeIsoThreshold: + +VolumeIsoThreshold +****************** + +================== +VolumeIsoThreshold +================== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoThreshold_api + + VolumeIsoThreshold + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoThreshold_api + + VolumeIsoThreshold.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeIsoThreshold_api + + VolumeIsoThreshold.add_event_handler + VolumeIsoThreshold.block_events + VolumeIsoThreshold.clear_event_handlers + VolumeIsoThreshold.remove_event_handler + VolumeIsoThreshold.set_value + diff --git a/docs/source/api/graphic_features/VolumeRenderMode.rst b/docs/source/api/graphic_features/VolumeRenderMode.rst new file mode 100644 index 000000000..8e5c1a56c --- /dev/null +++ b/docs/source/api/graphic_features/VolumeRenderMode.rst @@ -0,0 +1,35 @@ +.. _api.VolumeRenderMode: + +VolumeRenderMode +**************** + +================ +VolumeRenderMode +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeRenderMode_api + + VolumeRenderMode + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeRenderMode_api + + VolumeRenderMode.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeRenderMode_api + + VolumeRenderMode.add_event_handler + VolumeRenderMode.block_events + VolumeRenderMode.clear_event_handlers + VolumeRenderMode.remove_event_handler + VolumeRenderMode.set_value + diff --git a/docs/source/api/graphic_features/VolumeSlicePlane.rst b/docs/source/api/graphic_features/VolumeSlicePlane.rst new file mode 100644 index 000000000..fc58ee222 --- /dev/null +++ b/docs/source/api/graphic_features/VolumeSlicePlane.rst @@ -0,0 +1,35 @@ +.. _api.VolumeSlicePlane: + +VolumeSlicePlane +**************** + +================ +VolumeSlicePlane +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeSlicePlane_api + + VolumeSlicePlane + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VolumeSlicePlane_api + + VolumeSlicePlane.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VolumeSlicePlane_api + + VolumeSlicePlane.add_event_handler + VolumeSlicePlane.block_events + VolumeSlicePlane.clear_event_handlers + VolumeSlicePlane.remove_event_handler + VolumeSlicePlane.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 578d62b45..a2b4aec47 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -18,6 +18,14 @@ Graphic Features ImageVmax ImageInterpolation ImageCmapInterpolation + TextureArrayVolume + VolumeRenderMode + VolumeIsoThreshold + VolumeIsoStepSize + VolumeIsoSubStepSize + VolumeIsoEmissive + VolumeIsoShininess + VolumeSlicePlane TextData FontSize TextFaceColor diff --git a/docs/source/api/graphics/ImageVolumeGraphic.rst b/docs/source/api/graphics/ImageVolumeGraphic.rst new file mode 100644 index 000000000..8adbc7ac7 --- /dev/null +++ b/docs/source/api/graphics/ImageVolumeGraphic.rst @@ -0,0 +1,61 @@ +.. _api.ImageVolumeGraphic: + +ImageVolumeGraphic +****************** + +================== +ImageVolumeGraphic +================== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVolumeGraphic_api + + ImageVolumeGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVolumeGraphic_api + + ImageVolumeGraphic.alpha + ImageVolumeGraphic.alpha_mode + ImageVolumeGraphic.axes + ImageVolumeGraphic.block_events + ImageVolumeGraphic.cmap + ImageVolumeGraphic.cmap_interpolation + ImageVolumeGraphic.data + ImageVolumeGraphic.deleted + ImageVolumeGraphic.emissive + ImageVolumeGraphic.event_handlers + ImageVolumeGraphic.interpolation + ImageVolumeGraphic.mode + ImageVolumeGraphic.name + ImageVolumeGraphic.offset + ImageVolumeGraphic.plane + ImageVolumeGraphic.right_click_menu + ImageVolumeGraphic.rotation + ImageVolumeGraphic.shininess + ImageVolumeGraphic.step_size + ImageVolumeGraphic.substep_size + ImageVolumeGraphic.supported_events + ImageVolumeGraphic.threshold + ImageVolumeGraphic.visible + ImageVolumeGraphic.vmax + ImageVolumeGraphic.vmin + ImageVolumeGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageVolumeGraphic_api + + ImageVolumeGraphic.add_axes + ImageVolumeGraphic.add_event_handler + ImageVolumeGraphic.clear_event_handlers + ImageVolumeGraphic.remove_event_handler + ImageVolumeGraphic.reset_vmin_vmax + ImageVolumeGraphic.rotate + diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index 491013dff..640f76833 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -8,6 +8,7 @@ Graphics LineGraphic ScatterGraphic ImageGraphic + ImageVolumeGraphic TextGraphic LineCollection LineStack diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index e1c55514d..bc2b3aa29 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -47,6 +47,7 @@ Methods Subplot.add_animations Subplot.add_graphic Subplot.add_image + Subplot.add_image_volume Subplot.add_line Subplot.add_line_collection Subplot.add_line_stack diff --git a/docs/source/api/tools/HistogramLUTTool.rst b/docs/source/api/tools/HistogramLUTTool.rst index 128dbb889..429f958e2 100644 --- a/docs/source/api/tools/HistogramLUTTool.rst +++ b/docs/source/api/tools/HistogramLUTTool.rst @@ -27,7 +27,7 @@ Properties HistogramLUTTool.cmap HistogramLUTTool.deleted HistogramLUTTool.event_handlers - HistogramLUTTool.image_graphic + HistogramLUTTool.images HistogramLUTTool.name HistogramLUTTool.offset HistogramLUTTool.right_click_menu @@ -46,7 +46,6 @@ Methods HistogramLUTTool.add_axes HistogramLUTTool.add_event_handler HistogramLUTTool.clear_event_handlers - HistogramLUTTool.disconnect_image_graphic HistogramLUTTool.remove_event_handler HistogramLUTTool.rotate HistogramLUTTool.set_data diff --git a/docs/source/conf.py b/docs/source/conf.py index 59d0a2885..63ded9cca 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -57,6 +57,7 @@ "subsection_order": ExplicitOrder( [ "../../examples/image", + "../../examples/image_volume", "../../examples/heatmap", "../../examples/image_widget", "../../examples/gridplot", diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index 0ae9e974d..d61bff2ee 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -472,6 +472,231 @@ deleted | value | bool | True when graphic was deleted | +----------+------+-------------------------------+ +ImageVolumeGraphic +------------------ + +data +^^^^ + +**event info dict** + ++----------+--------------------------------------+--------------------------------------------------+ +| dict key | type | description | ++==========+======================================+==================================================+ +| key | slice, index, numpy-like fancy index | key at which image data was sliced/fancy indexed | ++----------+--------------------------------------+--------------------------------------------------+ +| value | np.ndarray | float | new data values | ++----------+--------------------------------------+--------------------------------------------------+ + +cmap +^^^^ + +**event info dict** + ++----------+------+---------------+ +| dict key | type | description | ++==========+======+===============+ +| value | str | new cmap name | ++----------+------+---------------+ + +vmin +^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new vmin value | ++----------+-------+----------------+ + +vmax +^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new vmax value | ++----------+-------+----------------+ + +interpolation +^^^^^^^^^^^^^ + +**event info dict** + ++----------+------+--------------------------------------------+ +| dict key | type | description | ++==========+======+============================================+ +| value | str | new interpolation method, nearest | linear | ++----------+------+--------------------------------------------+ + +cmap_interpolation +^^^^^^^^^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------------------------+ +| dict key | type | description | ++==========+======+================================================+ +| value | str | new cmap interpolatio method, nearest | linear | ++----------+------+------------------------------------------------+ + +mode +^^^^ + +**event info dict** + ++----------+------+-----------------------------------------+ +| dict key | type | description | ++==========+======+=========================================+ +| value | str | volume rendering mode that has been set | ++----------+------+-----------------------------------------+ + +threshold +^^^^^^^^^ + +**event info dict** + ++----------+-------+--------------------------+ +| dict key | type | description | ++==========+=======+==========================+ +| value | float | new isosurface threshold | ++----------+-------+--------------------------+ + +step_size +^^^^^^^^^ + +**event info dict** + ++----------+-------+--------------------------+ +| dict key | type | description | ++==========+=======+==========================+ +| value | float | new isosurface step_size | ++----------+-------+--------------------------+ + +substep_size +^^^^^^^^^^^^ + +**event info dict** + ++----------+-------+--------------------------+ +| dict key | type | description | ++==========+=======+==========================+ +| value | float | new isosurface step_size | ++----------+-------+--------------------------+ + +emissive +^^^^^^^^ + +**event info dict** + ++----------+-------------+-------------------------------+ +| dict key | type | description | ++==========+=============+===============================+ +| value | pygfx.Color | new isosurface emissive color | ++----------+-------------+-------------------------------+ + +shininess +^^^^^^^^^ + +**event info dict** + ++----------+------+--------------------------+ +| dict key | type | description | ++==========+======+==========================+ +| value | int | new isosurface shininess | ++----------+------+--------------------------+ + +plane +^^^^^ + +**event info dict** + ++----------+-----------------------------------+-----------------+ +| dict key | type | description | ++==========+===================================+=================+ +| value | tuple[float, float, float, float] | new plane slice | ++----------+-----------------------------------+-----------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + TextGraphic ----------- diff --git a/examples/image_volume/README.rst b/examples/image_volume/README.rst new file mode 100644 index 000000000..6c349ebfa --- /dev/null +++ b/examples/image_volume/README.rst @@ -0,0 +1,2 @@ +Image Volume Examples +===================== diff --git a/examples/image_volume/image_volume_4d.py b/examples/image_volume/image_volume_4d.py new file mode 100644 index 000000000..34bf9b903 --- /dev/null +++ b/examples/image_volume/image_volume_4d.py @@ -0,0 +1,115 @@ +""" +Volume movie +============ + +View 4D data of a volume over time by updating the volume data. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 5s' + +import numpy as np +from scipy.ndimage import gaussian_filter +import fastplotlib as fpl + + +def generate_data( + p=1, + noise=0.5, + T=128, + framerate=10, + firerate=2.0, +): + gamma = np.array([0.9]) + dims = (128, 128, 30) # size of image + sig = (4, 4, 2) # neurons size + bkgrd = 10 + N = 150 # number of neurons + np.random.seed(0) + centers = np.asarray( + [[np.random.randint(s, x - s) for x, s in zip(dims, sig)] for i in range(N)] + ) + Y = np.zeros((T,) + dims, dtype=np.float32) + trueSpikes = np.random.rand(N, T) < firerate / float(framerate) + trueSpikes[:, 0] = 0 + truth = trueSpikes.astype(np.float32) + for i in range(2, T): + if p == 2: + truth[:, i] += gamma[0] * truth[:, i - 1] + gamma[1] * truth[:, i - 2] + else: + truth[:, i] += gamma[0] * truth[:, i - 1] + for i in range(N): + Y[:, centers[i, 0], centers[i, 1], centers[i, 2]] = truth[i] + tmp = np.zeros(dims) + tmp[tuple(np.array(dims) // 2)] = 1.0 + print("gaussing filtering") + z = np.linalg.norm(gaussian_filter(tmp, sig).ravel()) + + print("finishing") + Y = ( + bkgrd + + noise * np.random.randn(*Y.shape) + + 10 * gaussian_filter(Y, (0,) + sig) / z + ) + + return Y + + +voldata = generate_data() + +figure = fpl.Figure(cameras="3d", controller_types="orbit", size=(700, 560)) + +volume = figure[0, 0].add_image_volume( + voldata[0], + vmin=10, + vmax=15, + cmap="gnuplot2", + alpha_mode="add", +) + +hlut = fpl.HistogramLUTTool(voldata, volume) + +figure[0, 0].docks["right"].size = 100 +figure[0, 0].docks["right"].controller.enabled = False +figure[0, 0].docks["right"].add_graphic(hlut) +figure[0, 0].docks["right"].auto_scale(maintain_aspect=False) + +figure.show() + +# load a pre-saved camera state +state = { + "position": np.array([-70, 90, 150]), + "rotation": np.array([-0.09210227, -0.47460177, -0.05001713, 0.87393857]), + "scale": np.array([1.0, 1.0, 1.0]), + "reference_up": np.array([0.0, 1.0, 0.0]), + "fov": 50.0, + "width": 27.605629518746266, + "height": 117.78401927998402, + "depth": 183.4884192530962, + "zoom": 0.75, + "maintain_aspect": True, + "depth_range": None, +} + +figure[0, 0].camera.set_state(state) + + +i = 0 +def update(): + global i + + volume.data = voldata[i] + + i += 1 + if i == voldata.shape[0]: + i = 0 + + +figure.add_animations(update) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_mip.py b/examples/image_volume/image_volume_mip.py new file mode 100644 index 000000000..73ae7803f --- /dev/null +++ b/examples/image_volume/image_volume_mip.py @@ -0,0 +1,47 @@ +""" +Volume Mip mode +=============== + +View a volume using MIP (Maximum Intensity Projection) rendering. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + +voldata = iio.imread("imageio:stent.npz").astype(np.float32) + +figure = fpl.Figure(cameras="3d", controller_types="orbit", size=(700, 560)) + +figure[0, 0].add_image_volume(voldata, mode="mip", alpha_mode="add") + +figure.show() + + +# load a pre-saved camera state +state = { + "position": np.array([-120, 90, 330]), + "rotation": np.array([-0.07280538, -0.41100206, -0.03295049, 0.90812496]), + "scale": np.array([1.0, 1.0, 1.0]), + "reference_up": np.array([0.0, 1.0, 0.0]), + "fov": 50.0, + "width": 128.0, + "height": 128.0, + "depth": 313, + "zoom": 0.75, + "maintain_aspect": True, + "depth_range": None, +} + + +figure[0, 0].camera.set_state(state) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_multi_channel.py b/examples/image_volume/image_volume_multi_channel.py new file mode 100644 index 000000000..6d444e835 --- /dev/null +++ b/examples/image_volume/image_volume_multi_channel.py @@ -0,0 +1,49 @@ +""" +Multi channel volumes +===================== + +Example with multi-channel volume images. Use alpha_mode "add" for additive blending. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +from ome_zarr.io import parse_url +from ome_zarr.reader import Reader + + +# load data +url = "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0062A/6001240_labels.zarr" + +# read the image data +reader = Reader(parse_url(url)) +# nodes may include images, labels etc +nodes = list(reader()) +# first node will be the image pixel data +image_node = nodes[0] + +dask_data = image_node.data + +# use the highest resolution image in the pyramid zarr +voldata = dask_data[0] + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 700) +) + +# add first channel, use cyan colormap +vol_ch0 = figure[0, 0].add_image_volume(voldata[0], cmap="cyan", alpha_mode="add") +# add another channel, use magenta cmap +vol_ch1 = figure[0, 0].add_image_volume(voldata[1], cmap="magenta", alpha_mode="add") + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_non_orthogonal_slicing.py b/examples/image_volume/image_volume_non_orthogonal_slicing.py new file mode 100644 index 000000000..dc74a5e0a --- /dev/null +++ b/examples/image_volume/image_volume_non_orthogonal_slicing.py @@ -0,0 +1,56 @@ +""" +Volume non-orthogonal slicing +============================= + +Perform non-orthogonal slicing of image volumes. + +For an example with UI sliders see the "Volume modes" example. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +voldata = iio.imread("imageio:stent.npz").astype(np.float32) + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 560) +) + +vol = figure[0, 0].add_image_volume(voldata, mode="slice") + +# a plane is defined by ax + by + cz + d = 0 +# the plane property sets (a, b, c, d) +vol.plane = (0, 0.5, 0.5, -70) + +# just a pre-saved camera state to view the plot area +state = { + "position": np.array([-160.0, 105.0, 205.0]), + "rotation": np.array([-0.1, -0.6, -0.07, 0.8]), + "scale": np.array([1., 1., 1.]), + "reference_up": np.array([0., 1., 0.]), + "fov": 50.0, + "width": 128.0, + "height": 128.0, + "depth": 315, + "zoom": 0.75, + "maintain_aspect": True, + "depth_range": None +} + +figure.show() + +figure[0, 0].camera.set_state(state) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_render_modes.py b/examples/image_volume/image_volume_render_modes.py new file mode 100644 index 000000000..d29e3b166 --- /dev/null +++ b/examples/image_volume/image_volume_render_modes.py @@ -0,0 +1,86 @@ +""" +Volume modes +============ + +View a volume using different rendering modes. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +from fastplotlib.ui import EdgeWindow +from fastplotlib.graphics.features import VOLUME_RENDER_MODES +import imageio.v3 as iio +from imgui_bundle import imgui + +voldata = iio.imread("imageio:stent.npz").astype(np.float32) + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 560) +) + +figure[0, 0].add_image_volume(voldata, name="vol-img") + +# add an hlut tool +hlut = fpl.HistogramLUTTool(voldata, figure[0, 0]["vol-img"]) + +figure[0, 0].docks["right"].size = 80 +figure[0, 0].docks["right"].controller.enabled = False +figure[0, 0].docks["right"].add_graphic(hlut) +figure[0, 0].docks["right"].auto_scale(maintain_aspect=False) + + +class GUI(EdgeWindow): + def __init__(self, figure, title="Render options", location="right", size=300): + super().__init__(figure, title=title, location=location, size=size) + + # reference to the graphic for convenience + self.graphic: fpl.ImageVolumeGraphic = self._figure[0, 0]["vol-img"] + + def update(self): + imgui.text("Switch render mode:") + + # add buttons to switch between modes + for mode in VOLUME_RENDER_MODES.keys(): + if imgui.button(mode): + self.graphic.mode = mode + + # add sliders to change iso rendering properties + if self.graphic.mode == "iso": + _, self.graphic.threshold = imgui.slider_float( + "threshold", v=self.graphic.threshold, v_max=255, v_min=1, + ) + _, self.graphic.step_size = imgui.slider_float( + "step_size", v=self.graphic.step_size, v_max=10.0, v_min=0.1, + ) + _, self.graphic.substep_size = imgui.slider_float( + "substep_size", v=self.graphic.substep_size, v_max=10.0, v_min=0.1, + ) + _, self.graphic.emissive = imgui.color_picker3("emissive color", col=self.graphic.emissive.rgb) + + if self.graphic.mode == "slice": + imgui.text("Select plane defined by:\nax + by + cz + d = 0") + _, a = imgui.slider_float("a", v=self.graphic.plane[0], v_min=-1, v_max=1.0) + _, b = imgui.slider_float("b", v=self.graphic.plane[1], v_min=-1, v_max=1.0) + _, c = imgui.slider_float("c", v=self.graphic.plane[2], v_min=-1, v_max=1.0) + + largest_dim = max(self.graphic.data.value.shape) + _, d = imgui.slider_float("d", v=self.graphic.plane[3], v_min=0, v_max=largest_dim * 2) + + self.graphic.plane = (a, b, c, d) + +gui = GUI(figure=figure) +figure.add_gui(gui) + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_share_buffer.py b/examples/image_volume/image_volume_share_buffer.py new file mode 100644 index 000000000..cc9f07915 --- /dev/null +++ b/examples/image_volume/image_volume_share_buffer.py @@ -0,0 +1,75 @@ +""" +Volume share buffers +==================== + +Share the data buffer between two graphics. This example creates one Graphic using MIP rendering, and another graphic +to display a slice of the volume. We can share the data buffer on the GPU between these graphics. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +from imgui_bundle import imgui +import fastplotlib as fpl +from fastplotlib.ui import EdgeWindow +import imageio.v3 as iio +from skimage.filters import gaussian + + +data = iio.imread("imageio:stent.npz") + + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 560), +) + +# MIP rendering is the default `mode` +vol_mip = figure[0, 0].add_image_volume(gaussian(data, sigma=2.0)) + +# make another graphic to show a slice of the volume +vol_slice = figure[0, 0].add_image_volume( + vol_mip.data, # pass the data property from the previous volume so they share the same buffer on the GPU + mode="slice", + plane=(0, -0.5, -0.5, 50), + offset=(150, 0, 0) # place the graphic at x=150 +) + + +class GUI(EdgeWindow): + def __init__(self, figure, title="change data buffer", location="right", size=200): + super().__init__(figure, title=title, location=location, size=size) + self._sigma = 2 + + def update(self): + changed, self._sigma = imgui.slider_int("sigma", v=self._sigma, v_min=0, v_max=5) + + if changed: + vol_mip.data = gaussian(data, sigma=self._sigma) + vol_mip.reset_vmin_vmax() + vol_slice.reset_vmin_vmax() + + imgui.text("Select plane defined by:\nax + by + cz + d = 0") + _, a = imgui.slider_float("a", v=vol_slice.plane[0], v_min=-1, v_max=1.0) + _, b = imgui.slider_float("b", v=vol_slice.plane[1], v_min=-1, v_max=1.0) + _, c = imgui.slider_float("c", v=vol_slice.plane[2], v_min=-1, v_max=1.0) + + largest_dim = max(vol_slice.data.value.shape) + _, d = imgui.slider_float( + "d", v=vol_slice.plane[3], v_min=0, v_max=largest_dim * 2 + ) + + vol_slice.plane = (a, b, c, d) + +gui = GUI(figure) +figure.add_gui(gui) + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_slicing_animation.py b/examples/image_volume/image_volume_slicing_animation.py new file mode 100644 index 000000000..ab671eec6 --- /dev/null +++ b/examples/image_volume/image_volume_slicing_animation.py @@ -0,0 +1,64 @@ +""" +Volume non-orthogonal slicing animation +======================================= + +Perform non-orthogonal slicing of image volumes. + +For an example with UI sliders see the "Volume modes" example. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 8s' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +voldata = iio.imread("imageio:stent.npz").astype(np.float32) + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 560) +) + +vol = figure[0, 0].add_image_volume(voldata, mode="slice") + +# a plane is defined by ax + by + cz + d = 0 +# the plane property sets (a, b, c, d) +vol.plane = (0, 0.5, 0.5, -20) + +# just a pre-saved camera state to view the plot area +state = { + "position": np.array([-110.0, 160.0, 240.0]), + "rotation": np.array([-0.25, -0.5, -0.15, 0.85]), + "scale": np.array([1., 1., 1.]), + "reference_up": np.array([0., 1., 0.]), + "fov": 50.0, + "width": 128.0, + "height": 128.0, + "depth": 315, + "zoom": 0.75, + "maintain_aspect": True, + "depth_range": None +} + +def update(): + # increase d by 1 + vol.plane = (0, 0.5, 0.5, vol.plane[-1] - 1) + if vol.plane[-1] < -200: + vol.plane = (0, 0.5, 0.5, -20) + +figure[0, 0].add_animations(update) + +figure.show() + +figure[0, 0].camera.set_state(state) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_volume/image_volume_toy_data.py b/examples/image_volume/image_volume_toy_data.py new file mode 100644 index 000000000..5c081542d --- /dev/null +++ b/examples/image_volume/image_volume_toy_data.py @@ -0,0 +1,31 @@ +""" +Volume rendering of toy data +============================ + +Volume rendering of toy trig data +""" + +import fastplotlib as fpl +import numpy as np + +n_cols = 100 +n_rows = 100 +z = 50 + +xs = np.linspace(0, 1_000, n_cols) + +sine = np.sin(np.sqrt(xs)) + +data = np.dstack([np.vstack([sine * i for i in range(n_rows)]).astype(np.float32) * j for j in range(z)]) + +figure = fpl.Figure(cameras="3d", controller_types="orbit") + +volume = figure[0, 0].add_image_volume(data) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 17eac72c3..60b8cc2e7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c5d1bee0215d1b5fa2fd5f07c372337a2c0f8d7532092faf189b41b5ae90796 -size 64032 +oid sha256:bfff638ad02e888721d2a9c02d479b8b233798be1e6d8554ff00415386a100d0 +size 63590 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 888490a86..8b79f4286 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5dc016ac3b2cb6553ca67fcf2450eba3334b6280c66b01583adac672100f3f6 -size 115971 +oid sha256:047c77b54a162823efda862dab4fff3fe1d72f7631248aa8ee42cd77af9039f6 +size 115523 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 888490a86..8b79f4286 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5dc016ac3b2cb6553ca67fcf2450eba3334b6280c66b01583adac672100f3f6 -size 115971 +oid sha256:047c77b54a162823efda862dab4fff3fe1d72f7631248aa8ee42cd77af9039f6 +size 115523 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 1b000ac9d..0d13e622a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca0b4a4a366c08cbe2ce612023ed0d46fc4121bfdb397b97c01f0b4fcc846ba9 -size 137759 +oid sha256:568b7e076645c963f6d1936ae3bc2f11cece8e63556bc7dcc0fb5d0251be35d5 +size 137370 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 8e3b3f443..f693dc489 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deb5ef697a6240d66d1fc32364cdb9c30876a809706b4ecc7182c02fdad893c3 -size 124444 +oid sha256:f2a03b0efefb900652eff84473994730509e57cd5c817928b6bee981b0d6967a +size 124179 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 24f0af167..721b14394 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d656095fa6f30f00f4beeefdbbc57403d51a442908cd9c69c4d9366023c51e5 -size 108924 +oid sha256:c2f534ddc07a35ce426b5d0207afe813148023f4f15dcc8bcfe4a383d75ed740 +size 108503 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 14446219c..c7824d57c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97b2cd703d8a465137834e264ba723c58e861c7e82d2675b5bb00b0ba0546c30 -size 101247 +oid sha256:851490e5667065f75cd4daf23f44290796a76a86bc7cc946c87e043530ad23ce +size 100857 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 45bf24ea5..df4c72f61 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b85764ef017fd475f49c6d1b57d4a347ab63aaf4c3e6a36bc1de6d61a678c6df -size 122086 +oid sha256:be73841d2a14b0a20b3fa6b736e787a7f8bb47266420e92ae388a0212d9e654d +size 121745 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 45bf24ea5..df4c72f61 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b85764ef017fd475f49c6d1b57d4a347ab63aaf4c3e6a36bc1de6d61a678c6df -size 122086 +oid sha256:be73841d2a14b0a20b3fa6b736e787a7f8bb47266420e92ae388a0212d9e654d +size 121745 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index d42da0d01..4e3196c9d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e52ce739f941d0cb6353322238e5a3da6cb02b9e8d504a1027e381f3ee74474 -size 225937 +oid sha256:0df95a5c918a26b1c1aff1093b8dd362acbfa3ea6131da430fe67d4252719e7a +size 225472 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index e07679554..499c820db 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b66a299b9b6aae8e2161a1afa789fdf2f4763c681628f9c7bdcf353da2b35656 -size 216922 +oid sha256:a678b0f245c8a15d75ec0ab82a86d6e2b87c8737603b33a0cb057f3726a33a91 +size 216095 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index a91af9da9..90a93008f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a5fe6c2b790dc062ac0aeddce24f462da2d6dcf752d86bd400ccf28f1c912a7 -size 64992 +oid sha256:d73a656a0dfefc16a9faeb78c615e253862e035d546469f15f93f4e711ee18da +size 64766 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 7b2239958..4036fad8a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99ea64fe85c0ff218afb91389a4d38c3e6b184859b4635d237635b914b47ee03 -size 69473 +oid sha256:d3e86c0304cf59171e14bbfcc5f8dbc08c438f6e9b1e4ab0bd47f8643e8f7a95 +size 69225 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 6614a8021..c2b8cbdf2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c5d778b6acd1c97252deccce6ded218380a08fb04f872489838f6da516349b -size 113931 +oid sha256:5504c58509000864a7f126dcfc09d7f2ed80f5dcd372bdfa13e5a359923ac727 +size 113701 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index 70acf36ab..0b97348ac 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f2f1291f3a03ce5100598663baa62a2a68d31c548ccfeb901ba63b0f2edfe77 -size 97498 +oid sha256:fc623177117feca7b7fa9c74a4c612b39272512ad6e2e4c1fe506a101ee39dad +size 97283 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index d7d8a6c43..1d639be5b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5f6e02933fbf04ac8bb97ee959756a4c2701dc7f62d7922c334d507cdac66e2 -size 89628 +oid sha256:334dcbf9b3cd9f2941588458a938a2c8797af5ffab0baf29c72cb244097c4380 +size 89366 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index 7b2239958..4036fad8a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99ea64fe85c0ff218afb91389a4d38c3e6b184859b4635d237635b914b47ee03 -size 69473 +oid sha256:d3e86c0304cf59171e14bbfcc5f8dbc08c438f6e9b1e4ab0bd47f8643e8f7a95 +size 69225 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 6fd1a14ab..e4a01bd38 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3792fa22b1bef03871dab9cd0c8a34d58b3fe9ecb565f17e772a54f0459a7f63 -size 60801 +oid sha256:fa5109589b36c9e1f810ee1e1864dfbce697239a57aa9ab80dc54d37b3bbc8a1 +size 60572 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 5e60750e1..40835dead 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d588c44d786cf667640e1b4664e6c3daaf946fb8373f5afb7ea6dd97ee445a9 -size 86503 +oid sha256:ff68c4dd69efd6be41f16023e4dec4737584eecb3fa19885294b66e52c52a083 +size 85352 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 969404d6a..0f5bd5d1a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87c51655ffee08b1c892b8f7773b7b2137bb52b1457bbfb1c5f4ae342ab508ea -size 104071 +oid sha256:64a3d8391cc9a171b0205a2dc5a647bb5390c5ab03da7afe5c5cf0b82d2769dc +size 103011 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 4c677b40e..d95628db2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57f298f02a1df8fa797e3773a3e976dc3ce6ce67d7c781676582119e6452001b -size 144021 +oid sha256:6aa9f6c95a9025e095efc59afab2c6d47a3307ecea84687753081f6c355943c6 +size 143016 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 05fbce7f7..b609f93e2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3c5adf6bf3f031bd9a9ea6041156905039c9bc56cbffcf0079e08b6cd5ac547 -size 116844 +oid sha256:412f7ed991a71bfdf502fa3a50cad5fe3585153360087d674b28f0edf774dfd5 +size 115844 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 6247c5905..87c0370ba 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42a6cfcb2b1fe8919bf15a4e52fccf65d518d34628ad7c5b5ba9bfabc1a18e36 -size 118412 +oid sha256:96e386d9cb3b0fb3ff9cd57b1a1227c60d09093686e5f71a69abcffb2a565d13 +size 117519 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index b35f45233..0f5bd5d1a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59856df53d455010f8d5c582259ef587ba7c7022e4a923b9d11c6944d82d474f -size 103953 +oid sha256:64a3d8391cc9a171b0205a2dc5a647bb5390c5ab03da7afe5c5cf0b82d2769dc +size 103011 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 128feac14..9c3436b0c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddd3b78f4e2436c688e780b501fec740eb0fe6460deb2122979d11da43ccaf22 -size 101213 +oid sha256:cae0f36ea87b444a4ca66ebe8aec9743d71480f11e4031f45f731cd3c3c6a012 +size 100231 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 2b2d1e8d1..a3dff14c9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24eac0c3c168f4afe74cbb3d9ee95d393664341e35b11eb1b3972446a21616c1 -size 113053 +oid sha256:4a0538e194491b1deaed091da55ec05dac077373287a80298f317bdc536bfe7e +size 111952 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 3eb50089a..9eb7e43a0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc6b1fcd3df0d8dc3446f9570b37016150d353806b2d13f6e777ca0eb4b27f6 -size 104045 +oid sha256:bd255c495da614840d6c3ab75532b897e83174868daa0ee143ed6dc929fdf176 +size 103138 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 260de77d8..abbeef013 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a02745049f9167c2bbb2d44f0959dd9f251254c3e812457b284c6890bda3bc4 -size 105426 +oid sha256:8055763d1d539439434e02c9a6975e4600f4a036e866cea97d58d04a56cd3cda +size 104425 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 31219597e..d16d0afd8 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dae5806f87b217215098f1b74b910e778c0fb3249411564ce2baf59a002c52b -size 77044 +oid sha256:3a0601f179366e572c6284c590a679753ce245bee6533ff65a5b64a45fa87242 +size 76788 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 19724d061..fadaaecc4 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bf8dc7928b28a341d5938b315d143a017de43e907ff91423064d2265f8afd40 -size 112855 +oid sha256:cdffece1879e9245e6438262889c3013cfcfa32b956da9312ca884c0cd912e55 +size 111925 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index ff2aa5de7..4dbf15eda 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c67aa2880595c3cf18e69d6a2886606b039d38eebc1219b765294af01f7ac619 -size 108348 +oid sha256:c4c8eed347524d0fb66141cbed9b804573f1311d5f45e3a7adfee304b6f61436 +size 107239 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index c0ec3fa5d..96e048ee4 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dce2e94163c894b254c5295e54852a988ea063c92ebd38bc3fdf09d78e8d66e9 -size 110236 +oid sha256:1c1365e589d47e15f313d20df5190c4a8be7a59c14f9575161e70a5722e95b25 +size 109170 diff --git a/examples/screenshots/image_volume_mip.png b/examples/screenshots/image_volume_mip.png new file mode 100644 index 000000000..93aa45696 --- /dev/null +++ b/examples/screenshots/image_volume_mip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9910e9b641a41efff7899c1a7561eb5c735f935eac533aeee9863167063c2aaf +size 160929 diff --git a/examples/screenshots/image_volume_multi_channel.png b/examples/screenshots/image_volume_multi_channel.png new file mode 100644 index 000000000..c539f1034 --- /dev/null +++ b/examples/screenshots/image_volume_multi_channel.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f81c9576f42c75e9f6bd280d5e12e2e0daefc0f3b1b77d6419cff1247f6c6c4b +size 194168 diff --git a/examples/screenshots/image_volume_non_orthogonal_slicing.png b/examples/screenshots/image_volume_non_orthogonal_slicing.png new file mode 100644 index 000000000..1db9e6df2 --- /dev/null +++ b/examples/screenshots/image_volume_non_orthogonal_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af3e294b7e7da66ab9ded9ba876028155a4e6e60f8645f333b48fecae0b33b54 +size 50977 diff --git a/examples/screenshots/image_volume_render_modes.png b/examples/screenshots/image_volume_render_modes.png new file mode 100644 index 000000000..cfe46a475 --- /dev/null +++ b/examples/screenshots/image_volume_render_modes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91bb33e51ff719c6a5eec13329cb2a052e83ea57897a09fcdfd8f97a049242cf +size 58720 diff --git a/examples/screenshots/image_volume_share_buffer.png b/examples/screenshots/image_volume_share_buffer.png new file mode 100644 index 000000000..c4fbda272 --- /dev/null +++ b/examples/screenshots/image_volume_share_buffer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7f5a7244fb20ee64246ec07e5785650f4a600480ac5578d6726af84886d2056 +size 42816 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index e0eb67609..f7cae557b 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2cc3c37ee28e16d94b1d437d5ef9ac6c73dfc7f8bf8e39c61d63c8bda5bd26f -size 188762 +oid sha256:172b8929393dee72fbf49dd29fcc2ad5e63eab098d23623c9de296660855223a +size 188287 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index 21d0ebacf..5c0e40831 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ff7ba5c7a1ea220fb49936fa71c92f2fd5c50b3aa51837db0a5b3f5f53f2461 -size 245187 +oid sha256:5ef54ed5ea9898d17f0f62cf06725b4557164e095b853b8170acdc4be1635587 +size 242925 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index 51594b863..d989bfa02 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:851714956ea3727d8b6d66d88fc7d57b08ac39d5af0cb3b3e409e870dfd37e76 -size 174624 +oid sha256:6386b3ef20e0fd87bbb2cfbdee73673620e451310729659d84ffbd64277013b9 +size 173904 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index 51a7d83fd..87066600a 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae426cb3b41874c1f20e3579a539b3229ca164ead978e127366c8d7185544cc4 -size 93730 +oid sha256:a8531303948dfa02de458e10e45e64ba82a7a6c4ab2b582de5dfa9e3ac79a5ad +size 93274 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index c7c45c39a..30fa7c296 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0054d3c7e63d951a892f06f206e2ceb4e96c19d69266f5401cbac7da80c40361 -size 311541 +oid sha256:5c388b89b7d61d7a461918126d7c5dc107013aff0023b951a87d716827e072a1 +size 310421 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png index a0366440c..a70f9ac1d 100644 --- a/examples/screenshots/image_widget_viewports_check.png +++ b/examples/screenshots/image_widget_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bce01da442cabd92e0944215ece40a9887ef2eff5d49320a046893c31d640418 -size 85531 +oid sha256:d4f621b7be8d872e8f29309378fc0c1d4436aac067ceac13ddfe7e8622c5f2d6 +size 82334 diff --git a/examples/screenshots/no-imgui-image_volume_mip.png b/examples/screenshots/no-imgui-image_volume_mip.png new file mode 100644 index 000000000..6b6c28881 --- /dev/null +++ b/examples/screenshots/no-imgui-image_volume_mip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce09f81fa0000514626f2c9c88c1917d992a2139b602d92a7f8b8b5598c52372 +size 170567 diff --git a/examples/screenshots/no-imgui-image_volume_multi_channel.png b/examples/screenshots/no-imgui-image_volume_multi_channel.png new file mode 100644 index 000000000..f79a06e06 --- /dev/null +++ b/examples/screenshots/no-imgui-image_volume_multi_channel.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae5457c576cffabdbb0849f2af396f88ab54729f8ae4446b36c521dccf176f2a +size 204715 diff --git a/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png b/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png new file mode 100644 index 000000000..0a88b96bb --- /dev/null +++ b/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c66511302aecb3ef4fd042c502ebf93674abb9088885f5fc4f5f685f891c6248 +size 58067 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 4c23b3481..546ff120e 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -18,6 +18,7 @@ # examples live in themed sub-folders example_globs = [ "image/*.py", + "image_volume/*.py", "image_widget/*.py", "heatmap/*.py", "scatter/*.py", diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index b458a8c48..a3bbc1b5f 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -2,6 +2,7 @@ from .line import LineGraphic from .scatter import ScatterGraphic from .image import ImageGraphic +from .image_volume import ImageVolumeGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack @@ -11,6 +12,7 @@ "LineGraphic", "ScatterGraphic", "ImageGraphic", + "ImageVolumeGraphic", "TextGraphic", "LineCollection", "LineStack", diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index ab58c7a5c..81694de33 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -55,13 +55,6 @@ class Graphic: _features: dict[str, type] = dict() def __init_subclass__(cls, **kwargs): - # set the type of the graphic in lower case like "image", "line_collection", etc. - cls.type = ( - cls.__name__.lower() - .replace("graphic", "") - .replace("collection", "_collection") - .replace("stack", "_stack") - ) # set of all features cls._features = { @@ -108,14 +101,29 @@ def __init__( alpha_mode: (str), default "auto", The alpha-mode, e.g. 'auto', 'blend', 'weighted_blend', 'solid', or 'dither'. - * 'solid': the points do not have semi-transparent fragments. Writes to the depth buffer. - * 'auto': like 'solid', but allows semi-transparent fragments. - * 'blend': the points are considered transparent, and don't write to the depth buffer. - The points are blended in the order they are drawn. - * 'weighted_blend': like 'blend', but the result does not depend on the order in which points are rendered, - nor is their distance to the camera. - * 'dither': use stochastic transparency. Although the result is a bit noisy, the points distance to the camera - is properly taken into account, which may be better for 3D point clouds. Writes to the depth buffer. + Modes for method “opaque” (overwrites the value in the output texture): + + * “solid”: alpha is ignored. + * “solid_premul”: the alpha is multipled with the color (making it darker). + + Modes for method “blended” (per-fragment blending, a.k.a. compositing): + + * “auto”: classic alpha blending, with depth_write defaulting to True. See note below. + * “blend”: classic alpha blending using the over-operator. depth_write defaults to False. + * “add”: additive blending that adds the fragment color, multiplied by alpha. + * “subtract”: subtractuve blending that removes the fragment color. + * “multiply”: multiplicative blending that multiplies the fragment color. + + Modes for method “weighted” (order independent blending): + + * “weighted_blend”: weighted blended order independent transparency. + * “weighted_solid”: fragments are combined based on alpha, but the final alpha is always 1. Great for e.g. image stitching. + + Modes for method “stochastic” (alpha represents the chance of a fragment being visible): + + * “dither”: stochastic transparency with blue noise. This mode handles order-independent transparency exceptionally well, but it produces results that can look somewhat noisy. + * “bayer”: stochastic transparency with an 8x8 Bayer pattern. + For details see https://docs.pygfx.org/stable/transparency.html @@ -244,6 +252,11 @@ def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo wo.visible = self.visible + if "Image" in self.__class__.__name__: + # Image and ImageVolume use tiling and share one material + self._material.opacity = self.alpha + self._material.alpha_mode = self.alpha_mode + if wo.material is not None: wo.material.opacity = self.alpha wo.material.alpha_mode = self.alpha_mode diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index 086efd546..eb834b674 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -16,6 +16,18 @@ ImageInterpolation, ImageCmapInterpolation, ) +from ._volume import ( + TextureArrayVolume, + VolumeRenderMode, + VolumeIsoThreshold, + VolumeIsoStepSize, + VolumeIsoSubStepSize, + VolumeIsoEmissive, + VolumeIsoShininess, + VolumeSlicePlane, + VOLUME_RENDER_MODES, + create_volume_material_kwargs, +) from ._base import ( GraphicFeature, BufferManager, @@ -54,6 +66,14 @@ "ImageVmax", "ImageInterpolation", "ImageCmapInterpolation", + "TextureArrayVolume", + "VolumeRenderMode", + "VolumeIsoThreshold", + "VolumeIsoStepSize", + "VolumeIsoSubStepSize", + "VolumeIsoEmissive", + "VolumeIsoShininess", + "VolumeSlicePlane", "TextData", "FontSize", "TextFaceColor", diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index 646ee6945..e203be68d 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -1,4 +1,5 @@ import numpy as np +import pygfx from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance @@ -148,6 +149,11 @@ def set_value(self, graphic, value: float): wo = graphic.world_object if wo.material is not None: wo.material.opacity = value + + if "Image" in graphic.__class__.__name__: + # Image and ImageVolume use tiling and share one material + graphic._material.alpha = value + self._value = value event = GraphicFeatureEvent(type="alpha", info={"value": value}) @@ -175,6 +181,11 @@ def set_value(self, graphic, value: str): wo = graphic.world_object if wo.material is not None: wo.alpha_mode = value + + if "Image" in graphic.__class__.__name__: + # Image and ImageVolume use tiling and share one material + graphic._material.alpha_mode = value + self._value = value event = GraphicFeatureEvent(type="alpha_mode", info={"value": value}) diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py index c47a26e6a..559e62c69 100644 --- a/fastplotlib/graphics/features/_image.py +++ b/fastplotlib/graphics/features/_image.py @@ -13,8 +13,13 @@ ) -# manages an array of 8192x8192 Textures representing chunks of an image class TextureArray(GraphicFeature): + """ + Manages an array of Textures representing chunks of an image. + + Creates multiple pygfx.Texture objects based on the GPU's max texture dimension limit. + """ + event_info_spec = [ { "dict key": "key", @@ -70,8 +75,6 @@ def __init__(self, data, isolated_buffer: bool = True): self.buffer[buffer_index] = texture - self._shared: int = 0 - @property def value(self) -> np.ndarray: return self._value @@ -99,10 +102,6 @@ def col_indices(self) -> np.ndarray: """ return self._col_indices - @property - def shared(self) -> int: - return self._shared - def _fix_data(self, data): if data.ndim not in (2, 3): raise ValueError( diff --git a/fastplotlib/graphics/features/_volume.py b/fastplotlib/graphics/features/_volume.py new file mode 100644 index 000000000..fd3c8e745 --- /dev/null +++ b/fastplotlib/graphics/features/_volume.py @@ -0,0 +1,443 @@ +from itertools import product +from math import ceil + +import numpy as np +import pygfx + +from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance + +VOLUME_RENDER_MODES = { + "mip": pygfx.VolumeMipMaterial, + "minip": pygfx.VolumeMinipMaterial, + "iso": pygfx.VolumeIsoMaterial, + "slice": pygfx.VolumeSliceMaterial, +} + + +class TextureArrayVolume(GraphicFeature): + """ + Manages an array of Textures representing chunks of an image. Chunk size is the GPU's max texture limit. + + Creates and manages multiple pygfx.Texture objects. + """ + + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index, numpy-like fancy index", + "description": "key at which image data was sliced/fancy indexed", + }, + { + "dict key": "value", + "type": "np.ndarray | float", + "description": "new data values", + }, + ] + + def __init__(self, data, isolated_buffer: bool = True): + super().__init__() + + data = self._fix_data(data) + + shared = pygfx.renderers.wgpu.get_shared() + + self._texture_size_limit = shared.device.limits["max-texture-dimension-3d"] + + if isolated_buffer: + # useful if data is read-only, example: memmaps + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] + else: + # user's input array is used as the buffer + self._value = data + + # data start indices for each Texture + self._row_indices = np.arange( + 0, + ceil(self.value.shape[1] / self._texture_size_limit) + * self._texture_size_limit, + self._texture_size_limit, + ) + self._col_indices = np.arange( + 0, + ceil(self.value.shape[2] / self._texture_size_limit) + * self._texture_size_limit, + self._texture_size_limit, + ) + + self._zdim_indices = np.arange( + 0, + ceil(self.value.shape[0] / self._texture_size_limit) + * self._texture_size_limit, + self._texture_size_limit, + ) + + shape = (self.zdim_indices.size, self.row_indices.size, self.col_indices.size) + + # buffer will be an array of textures + self._buffer: np.ndarray[pygfx.Texture] = np.empty(shape=shape, dtype=object) + + self._iter = None + + # iterate through each chunk of passed `data` + # create a pygfx.Texture from this chunk + for _, buffer_index, data_slice in self: + texture = pygfx.Texture(self.value[data_slice], dim=3) + + self.buffer[buffer_index] = texture + + @property + def value(self) -> np.ndarray: + """The full array that represents all the data within this TextureArray""" + return self._value + + def set_value(self, graphic, value): + self[:] = value + + @property + def buffer(self) -> np.ndarray[pygfx.Texture]: + """array of buffers that are mapped to the GPU""" + return self._buffer + + @property + def row_indices(self) -> np.ndarray: + """ + row indices that are used to chunk the big data array + into individual Textures on the GPU + """ + return self._row_indices + + @property + def col_indices(self) -> np.ndarray: + """ + column indices that are used to chunk the big data array + into individual Textures on the GPU + """ + return self._col_indices + + @property + def zdim_indices(self) -> np.ndarray: + """ + z dimension indices that are used to chunk the big data array + into individual Textures on the GPU + """ + return self._zdim_indices + + def _fix_data(self, data): + if data.ndim not in (3, 4): + raise ValueError( + "Volume Image data must be 3D with or without an RGB(A) dimension, i.e. " + "it must be of shape [z, rows, cols], [z, rows, cols, 3] or [z, rows, cols, 4]" + ) + + # let's just cast to float32 always + return data.astype(np.float32) + + def __iter__(self): + self._iter = product( + enumerate(self.zdim_indices), + enumerate(self.row_indices), + enumerate(self.col_indices), + ) + + return self + + def __next__( + self, + ) -> tuple[pygfx.Texture, tuple[int, int, int], tuple[slice, slice, slice]]: + """ + Iterate through each Texture within the texture array + + Returns + ------- + Texture, tuple[int, int], tuple[slice, slice] + | Texture: pygfx.Texture + | tuple[int, int]: chunk index, i.e corresponding index of ``self.buffer`` array + | tuple[slice, slice]: data slice of big array in this chunk and Texture + """ + # chunk indices + ( + (chunk_z, data_z_start), + (chunk_row, data_row_start), + (chunk_col, data_col_start), + ) = next(self._iter) + + # indices for to self.buffer for this chunk + chunk_index = (chunk_z, chunk_row, chunk_col) + + # stop indices of big data array for this chunk + z_stop = min(self.value.shape[0], data_z_start + self._texture_size_limit) + row_stop = min(self.value.shape[1], data_row_start + self._texture_size_limit) + col_stop = min(self.value.shape[2], data_col_start + self._texture_size_limit) + + # zdim, row and column slices that slice the data for this chunk from the big data array + data_slice = ( + slice(data_z_start, z_stop), + slice(data_row_start, row_stop), + slice(data_col_start, col_stop), + ) + + # texture for this chunk + texture = self.buffer[chunk_index] + + return texture, chunk_index, data_slice + + def __getitem__(self, item): + return self.value[item] + + @block_reentrance + def __setitem__(self, key, value): + self.value[key] = value + + for texture in self.buffer.ravel(): + texture.update_range((0, 0, 0), texture.size) + + event = GraphicFeatureEvent("data", info={"key": key, "value": value}) + self._call_event_handlers(event) + + def __len__(self): + return self.buffer.size + + +def create_volume_material_kwargs(graphic, mode: str): + kwargs = { + "clim": (graphic.vmin, graphic.vmax), + "map": graphic._texture_map, + "interpolation": graphic.interpolation, + "pick_write": True, + } + + if mode == "iso": + more_kwargs = { + attr: getattr(graphic, attr) + for attr in [ + "threshold", + "step_size", + "substep_size", + "emissive", + "shininess", + ] + } + + elif mode == "slice": + more_kwargs = {"plane": graphic.plane} + else: + more_kwargs = {} + + kwargs.update(more_kwargs) + return kwargs + + +class VolumeRenderMode(GraphicFeature): + """Volume rendering mode, controls world object material""" + + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "volume rendering mode that has been set", + }, + ] + + def __init__(self, value: str): + self._validate(value) + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def _validate(self, value): + if value not in VOLUME_RENDER_MODES.keys(): + raise ValueError( + f"Given render mode: {value} is invalid. Valid render modes are: {VOLUME_RENDER_MODES.keys()}" + ) + + @block_reentrance + def set_value(self, graphic, value: str): + self._validate(value) + + VolumeMaterialCls = VOLUME_RENDER_MODES[value] + + kwargs = create_volume_material_kwargs(graphic, mode=value) + + new_material = VolumeMaterialCls(**kwargs) + # since the world object is a group + for volume_tile in graphic.world_object.children: + volume_tile.material = new_material + + # so we have one place to reference it + graphic._material = new_material + self._value = value + + event = GraphicFeatureEvent(type="mode", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeIsoThreshold(GraphicFeature): + """Isosurface threshold""" + + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new isosurface threshold", + }, + ] + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic._material.threshold = value + self._value = graphic._material.threshold + + event = GraphicFeatureEvent(type="threshold", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeIsoStepSize(GraphicFeature): + """Isosurface step_size""" + + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new isosurface step_size", + }, + ] + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic._material.step_size = value + self._value = graphic._material.step_size + + event = GraphicFeatureEvent(type="step_size", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeIsoSubStepSize(GraphicFeature): + """Isosurface substep_size""" + + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new isosurface step_size", + }, + ] + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic._material.substep_size = value + self._value = graphic._material.substep_size + + event = GraphicFeatureEvent(type="step_size", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeIsoEmissive(GraphicFeature): + """Isosurface emissive color""" + + event_info_spec = [ + { + "dict key": "value", + "type": "pygfx.Color", + "description": "new isosurface emissive color", + }, + ] + + def __init__(self, value: pygfx.Color | str | tuple | np.ndarray): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + @block_reentrance + def set_value(self, graphic, value: pygfx.Color | str | tuple | np.ndarray): + graphic._material.emissive = value + self._value = graphic._material.emissive + + event = GraphicFeatureEvent(type="emissive", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeIsoShininess(GraphicFeature): + """Isosurface shininess""" + + event_info_spec = [ + { + "dict key": "value", + "type": "int", + "description": "new isosurface shininess", + }, + ] + + def __init__(self, value: int): + self._value = value + super().__init__() + + @property + def value(self) -> int: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic._material.shininess = value + self._value = graphic._material.shininess + + event = GraphicFeatureEvent(type="shininess", info={"value": value}) + self._call_event_handlers(event) + + +class VolumeSlicePlane(GraphicFeature): + """Volume plane""" + + event_info_spec = [ + { + "dict key": "value", + "type": "tuple[float, float, float, float]", + "description": "new plane slice", + }, + ] + + def __init__(self, value: tuple[float, float, float, float]): + self._value = value + super().__init__() + + @property + def value(self) -> tuple[float, float, float, float]: + return self._value + + @block_reentrance + def set_value(self, graphic, value: tuple[float, float, float, float]): + graphic._material.plane = value + self._value = graphic._material.plane + + event = GraphicFeatureEvent(type="plane", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 85e34a413..4a33d2c1d 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -88,8 +88,8 @@ class ImageGraphic(Graphic): def __init__( self, data: Any, - vmin: int = None, - vmax: int = None, + vmin: float = None, + vmax: float = None, cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", @@ -105,11 +105,11 @@ def __init__( array-like, usually numpy.ndarray, must support ``memoryview()`` | shape must be ``[n_rows, n_cols]``, ``[n_rows, n_cols, 3]`` for RGB or ``[n_rows, n_cols, 4]`` for RGBA - vmin: int, optional - minimum value for color scaling, calculated from data if not provided + vmin: float, optional + minimum value for color scaling, estimated from data if not provided - vmax: int, optional - maximum value for color scaling, calculated from data if not provided + vmax: float, optional + maximum value for color scaling, estimated from data if not provided cmap: str, optional, default "plasma" colormap to use to display the data. For supported colormaps see the @@ -136,11 +136,20 @@ def __init__( world_object = pygfx.Group() - # texture array that manages the textures on the GPU for displaying this image - self._data = TextureArray(data, isolated_buffer=isolated_buffer) + if isinstance(data, TextureArray): + # share buffer + self._data = data + else: + # create new texture array to manage buffer + # texture array that manages the multiple textures on the GPU that represent this image + self._data = TextureArray(data, isolated_buffer=isolated_buffer) if (vmin is None) or (vmax is None): - vmin, vmax = quick_min_max(data) + _vmin, _vmax = quick_min_max(self.data.value) + if vmin is None: + vmin = _vmin + if vmax is None: + vmax = _vmax # other graphic features self._vmin = ImageVmin(vmin) @@ -172,7 +181,7 @@ def __init__( ) # iterate through each texture chunk and create - # an _ImageTIle, offset the tile using the data indices + # an _ImageTile, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: # create an ImageTile using the texture for this chunk img = _ImageTile( @@ -205,15 +214,16 @@ def data(self, data): self._data[:] = data @property - def cmap(self) -> str: + def cmap(self) -> str | None: """ - Get or set the colormap + Get or set the colormap for grayscale images. Returns ``None`` if image is RGB(A). For supported colormaps see the ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ """ - if self.data.value.ndim > 2: - raise AttributeError("RGB(A) images do not have a colormap property") - return self._cmap.value + if self._cmap is not None: + return self._cmap.value + + return None @cmap.setter def cmap(self, name: str): diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py new file mode 100644 index 000000000..db616b30d --- /dev/null +++ b/fastplotlib/graphics/image_volume.py @@ -0,0 +1,421 @@ +from typing import * + +import numpy as np +import pygfx + +from ..utils import quick_min_max +from ._base import Graphic +from .features import ( + TextureArrayVolume, + ImageCmap, + ImageVmin, + ImageVmax, + ImageInterpolation, + ImageCmapInterpolation, + VolumeRenderMode, + VolumeIsoThreshold, + VolumeIsoStepSize, + VolumeIsoSubStepSize, + VolumeIsoEmissive, + VolumeIsoShininess, + VolumeSlicePlane, + VOLUME_RENDER_MODES, + create_volume_material_kwargs, +) + + +class _VolumeTile(pygfx.Volume): + """ + Similar to pygfx.Volume, only difference is that it modifies the pick_info + by adding the data row start indices that correspond to this chunk of the big Volume + """ + + def __init__( + self, + geometry, + material, + data_slice: tuple[slice, slice, slice], + chunk_index: tuple[int, int, int], + **kwargs, + ): + super().__init__(geometry, material, **kwargs) + + self._data_slice = data_slice + self._chunk_index = chunk_index + + def _wgpu_get_pick_info(self, pick_value): + pick_info = super()._wgpu_get_pick_info(pick_value) + + data_z_start, data_row_start, data_col_start = ( + self.data_slice[0].start, + self.data_slice[1].start, + self.data_slice[2].start, + ) + + # add the actual data row and col start indices + x, y, z = pick_info["index"] + x += data_col_start + y += data_row_start + z += data_z_start + pick_info["index"] = (x, y, z) + + xp, yp, zp = pick_info["voxel_coord"] + xp += data_col_start + yp += data_row_start + zp += data_z_start + pick_info["voxel_coord"] = (xp, yp, zp) + + # add row chunk and col chunk index to pick_info dict + return { + **pick_info, + "data_slice": self.data_slice, + "chunk_index": self.chunk_index, + } + + @property + def data_slice(self) -> tuple[slice, slice, slice]: + return self._data_slice + + @property + def chunk_index(self) -> tuple[int, int, int]: + return self._chunk_index + + +class ImageVolumeGraphic(Graphic): + _features = { + "data": TextureArrayVolume, + "cmap": ImageCmap, + "vmin": ImageVmin, + "vmax": ImageVmax, + "interpolation": ImageInterpolation, + "cmap_interpolation": ImageCmapInterpolation, + "mode": VolumeRenderMode, + "threshold": VolumeIsoThreshold, + "step_size": VolumeIsoStepSize, + "substep_size": VolumeIsoSubStepSize, + "emissive": VolumeIsoEmissive, + "shininess": VolumeIsoShininess, + "plane": VolumeSlicePlane, + } + + def __init__( + self, + data: Any, + mode: str = "mip", + vmin: float = None, + vmax: float = None, + cmap: str = "plasma", + interpolation: str = "linear", + cmap_interpolation: str = "linear", + plane: tuple[float, float, float, float] = (0, 0, -1, 0), + threshold: float = 0.5, + step_size: float = 1.0, + substep_size: float = 0.1, + emissive: str | tuple | np.ndarray = (0, 0, 0), + shininess: int = 30, + isolated_buffer: bool = True, + **kwargs, + ): + """ + Create an ImageVolumeGraphic. + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()``. + Shape must be [n_planes, n_rows, n_cols] for grayscale, or [n_planes, n_rows, n_cols, 3 | 4] for RGB(A) + + mode: str, default "mip" + render mode, one of "mip", "minip", "iso" or "slice" + + vmin: float + lower contrast limit + + vmax: float + upper contrast limit + + cmap: str, default "plasma" + colormap for grayscale volumes + + interpolation: str, default "linear" + interpolation method for sampling pixels + + cmap_interpolation: str, default "linear" + interpolation method for sampling from colormap + + plane: (float, float, float, float), default (0, 0, -1, 0) + Slice volume at this plane. Sets (a, b, c, d) in the equation the defines a plane: ax + by + cz + d = 0. + Used only if `mode` = "slice" + + threshold : float, default 0.5 + The threshold texture value at which the surface is rendered. + Used only if `mode` = "iso" + + step_size : float, default 1.0 + The size of the initial ray marching step for the initial surface finding. Smaller values will result in + more accurate surfaces but slower rendering. + Used only if `mode` = "iso" + + substep_size : float, default 0.1 + The size of the raymarching step for the refined surface finding. Smaller values will result in more + accurate surfaces but slower rendering. + Used only if `mode` = "iso" + + emissive : Color, default (0, 0, 0, 1) + The emissive color of the surface. I.e. the color that the object emits even when not lit by a light + source. This color is added to the final color and unaffected by lighting. The alpha channel is ignored. + Used only if `mode` = "iso" + + shininess : int, default 30 + How shiny the specular highlight is; a higher value gives a sharper highlight. + Used only if `mode` = "iso" + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then set the data, useful if the + data arrays are ready-only such as memmaps. If False, the input array is itself used as the + buffer - useful if the array is large. + + kwargs + additional keyword arguments passed to :class:`.Graphic` + + """ + + valid_modes = VOLUME_RENDER_MODES.keys() + if mode not in valid_modes: + raise ValueError( + f"invalid mode specified: {mode}, valid modes are: {valid_modes}" + ) + + super().__init__(**kwargs) + + world_object = pygfx.Group() + + if isinstance(data, TextureArrayVolume): + # share existing buffer + self._data = data + else: + # create new texture array to manage buffer + # texture array that manages the textures on the GPU that represent this image volume + self._data = TextureArrayVolume(data, isolated_buffer=isolated_buffer) + + if (vmin is None) or (vmax is None): + _vmin, _vmax = quick_min_max(self.data.value) + if vmin is None: + vmin = _vmin + if vmax is None: + vmax = _vmax + + # other graphic features + self._vmin = ImageVmin(vmin) + self._vmax = ImageVmax(vmax) + + self._interpolation = ImageInterpolation(interpolation) + + # TODO: I'm assuming RGB volume images aren't supported??? + # use TextureMap for grayscale images + self._cmap = ImageCmap(cmap) + self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) + + self._texture_map = pygfx.TextureMap( + self._cmap.texture, + filter=self._cmap_interpolation.value, + wrap="clamp-to-edge", + ) + + self._plane = VolumeSlicePlane(plane) + self._threshold = VolumeIsoThreshold(threshold) + self._step_size = VolumeIsoStepSize(step_size) + self._substep_size = VolumeIsoSubStepSize(substep_size) + self._emissive = VolumeIsoEmissive(emissive) + self._shininess = VolumeIsoShininess(shininess) + + material_kwargs = create_volume_material_kwargs(graphic=self, mode=mode) + + VolumeMaterialCls = VOLUME_RENDER_MODES[mode] + + self._material = VolumeMaterialCls(**material_kwargs) + + self._mode = VolumeRenderMode(mode) + + # iterate through each texture chunk and create + # a _VolumeTile, offset the tile using the data indices + for texture, chunk_index, data_slice in self._data: + # create a _VolumeTile using the texture for this chunk + vol = _VolumeTile( + geometry=pygfx.Geometry(grid=texture), + material=self._material, + data_slice=data_slice, # used to parse pick_info + chunk_index=chunk_index, + ) + + # row and column start index for this chunk + data_z_start = data_slice[0].start + data_row_start = data_slice[1].start + data_col_start = data_slice[2].start + + # offset tile position using the indices from the big data array + # that correspond to this chunk + vol.world.z = data_z_start + vol.world.x = data_col_start + vol.world.y = data_row_start + + world_object.add(vol) + + self._set_world_object(world_object) + + @property + def data(self) -> TextureArrayVolume: + """Get or set the image data""" + return self._data + + @data.setter + def data(self, data): + self._data[:] = data + + @property + def mode(self) -> str: + """Get or set the volume rendering mode""" + return self._mode.value + + @mode.setter + def mode(self, mode: str): + self._mode.set_value(self, mode) + + @property + def cmap(self) -> str: + """Get or set colormap name""" + return self._cmap.value + + @cmap.setter + def cmap(self, name: str): + self._cmap.set_value(self, name) + + @property + def vmin(self) -> float: + """Get or set the lower contrast limit""" + return self._vmin.value + + @vmin.setter + def vmin(self, value: float): + self._vmin.set_value(self, value) + + @property + def vmax(self) -> float: + """Get or set the upper contrast limit""" + return self._vmax.value + + @vmax.setter + def vmax(self, value: float): + self._vmax.set_value(self, value) + + @property + def interpolation(self) -> str: + """Get or set the image data interpolation method""" + return self._interpolation.value + + @interpolation.setter + def interpolation(self, value: str): + self._interpolation.set_value(self, value) + + @property + def cmap_interpolation(self) -> str: + """Get or set the cmap interpolation method""" + return self._cmap_interpolation.value + + @cmap_interpolation.setter + def cmap_interpolation(self, value: str): + self._cmap_interpolation.set_value(self, value) + + @property + def plane(self) -> tuple[float, float, float, float]: + """Get or set displayed plane in the volume. Valid only for `slice` render mode.""" + return self._plane.value + + @plane.setter + def plane(self, value: tuple[float, float, float, float]): + if self.mode != "slice": + raise TypeError("`plane` property is only valid for `slice` render mode.") + + self._plane.set_value(self, value) + + @property + def threshold(self) -> float: + """Get or set isosurface threshold, only for `iso` mode""" + return self._threshold.value + + @threshold.setter + def threshold(self, value: float): + if self.mode != "iso": + raise TypeError( + "`threshold` property is only used for `iso` rendering mode" + ) + + self._threshold.set_value(self, value) + + @property + def step_size(self) -> float: + """Get or set isosurface step_size, only for `iso` mode""" + return self._step_size.value + + @step_size.setter + def step_size(self, value: float): + if self.mode != "iso": + raise TypeError( + "`step_size` property is only used for `iso` rendering mode" + ) + + self._step_size.set_value(self, value) + + @property + def substep_size(self) -> float: + """Get or set isosurface substep_size, only for `iso` mode""" + return self._substep_size.value + + @substep_size.setter + def substep_size(self, value: float): + if self.mode != "iso": + raise TypeError( + "`substep_size` property is only used for `iso` rendering mode" + ) + + self._substep_size.set_value(self, value) + + @property + def emissive(self) -> pygfx.Color: + """Get or set isosurface emissive color, only for `iso` mode. Pass a color, RGBA array or pygfx.Color""" + return self._emissive.value + + @emissive.setter + def emissive(self, value: pygfx.Color | str | tuple | np.ndarray): + if self.mode != "iso": + raise TypeError("`emissive` property is only used for `iso` rendering mode") + + self._emissive.set_value(self, value) + + @property + def shininess(self) -> int: + """Get or set isosurface shininess""" + return self._shininess.value + + @shininess.setter + def shininess(self, value: int): + if self.mode != "iso": + raise TypeError( + "`shininess` property is only used for `iso` rendering mode" + ) + + self._shininess.set_value(self, value) + + def reset_vmin_vmax(self): + """ + Reset the vmin, vmax by *estimating* it from the data + + Returns + ------- + None + + """ + + vmin, vmax = quick_min_max(self.data.value) + self.vmin = vmin + self.vmax = vmax diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 1a041547b..96c76f9a8 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -26,8 +26,8 @@ def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: def add_image( self, data: Any, - vmin: int = None, - vmax: int = None, + vmin: float = None, + vmax: float = None, cmap: str = "plasma", interpolation: str = "nearest", cmap_interpolation: str = "linear", @@ -44,11 +44,11 @@ def add_image( array-like, usually numpy.ndarray, must support ``memoryview()`` | shape must be ``[n_rows, n_cols]``, ``[n_rows, n_cols, 3]`` for RGB or ``[n_rows, n_cols, 4]`` for RGBA - vmin: int, optional - minimum value for color scaling, calculated from data if not provided + vmin: float, optional + minimum value for color scaling, estimated from data if not provided - vmax: int, optional - maximum value for color scaling, calculated from data if not provided + vmax: float, optional + maximum value for color scaling, estimated from data if not provided cmap: str, optional, default "plasma" colormap to use to display the data. For supported colormaps see the @@ -83,6 +83,107 @@ def add_image( **kwargs, ) + def add_image_volume( + self, + data: Any, + mode: str = "mip", + vmin: float = None, + vmax: float = None, + cmap: str = "plasma", + interpolation: str = "linear", + cmap_interpolation: str = "linear", + plane: tuple[float, float, float, float] = (0, 0, -1, 0), + threshold: float = 0.5, + step_size: float = 1.0, + substep_size: float = 0.1, + emissive: str | tuple | numpy.ndarray = (0, 0, 0), + shininess: int = 30, + isolated_buffer: bool = True, + **kwargs, + ) -> ImageVolumeGraphic: + """ + + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()``. + Shape must be [n_planes, n_rows, n_cols] for grayscale, or [n_planes, n_rows, n_cols, 3 | 4] for RGB(A) + + mode: str, default "ray" + render mode, one of "mip", "minip", "iso" or "slice" + + vmin: float + lower contrast limit + + vmax: float + upper contrast limit + + cmap: str, default "plasma" + colormap for grayscale volumes + + interpolation: str, default "linear" + interpolation method for sampling pixels + + cmap_interpolation: str, default "linear" + interpolation method for sampling from colormap + + plane: (float, float, float, float), default (0, 0, -1, 0) + Slice volume at this plane. Sets (a, b, c, d) in the equation the defines a plane: ax + by + cz + d = 0. + Used only if `mode` = "slice" + + threshold : float, default 0.5 + The threshold texture value at which the surface is rendered. + Used only if `mode` = "iso" + + step_size : float, default 1.0 + The size of the initial ray marching step for the initial surface finding. Smaller values will result in + more accurate surfaces but slower rendering. + Used only if `mode` = "iso" + + substep_size : float, default 0.1 + The size of the raymarching step for the refined surface finding. Smaller values will result in more + accurate surfaces but slower rendering. + Used only if `mode` = "iso" + + emissive : Color, default (0, 0, 0, 1) + The emissive color of the surface. I.e. the color that the object emits even when not lit by a light + source. This color is added to the final color and unaffected by lighting. The alpha channel is ignored. + Used only if `mode` = "iso" + + shininess : int, default 30 + How shiny the specular highlight is; a higher value gives a sharper highlight. + Used only if `mode` = "iso" + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then set the data, useful if the + data arrays are ready-only such as memmaps. If False, the input array is itself used as the + buffer - useful if the array is large. + + kwargs + additional keyword arguments passed to :class:`.Graphic` + + + """ + return self._create_graphic( + ImageVolumeGraphic, + data, + mode, + vmin, + vmax, + cmap, + interpolation, + cmap_interpolation, + plane, + threshold, + step_size, + substep_size, + emissive, + shininess, + isolated_buffer, + **kwargs, + ) + def add_line_collection( self, data: Union[numpy.ndarray, List[numpy.ndarray]], diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index aeb8dd996..9c6b1b24d 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -1,4 +1,5 @@ from math import ceil +from typing import Sequence import weakref import numpy as np @@ -6,7 +7,7 @@ import pygfx from ..utils import subsample_array -from ..graphics import LineGraphic, ImageGraphic, TextGraphic +from ..graphics import LineGraphic, ImageGraphic, ImageVolumeGraphic, TextGraphic from ..graphics.utils import pause_events from ..graphics._base import Graphic from ..graphics.selectors import LinearRegionSelector @@ -29,28 +30,58 @@ class HistogramLUTTool(Graphic): def __init__( self, data: np.ndarray, - image_graphic: ImageGraphic, + images: ( + ImageGraphic + | ImageVolumeGraphic + | Sequence[ImageGraphic | ImageVolumeGraphic] + ), nbins: int = 100, flank_divisor: float = 5.0, **kwargs, ): """ + HistogramLUT tool that can be used to control the vmin, vmax of ImageGraphics or ImageVolumeGraphics. + If used to control multiple images or image volumes it is assumed that they share a representation of + the same data, and that their histogram, vmin, and vmax are identical. For example, displaying a + ImageVolumeGraphic and several images that represent slices of the same volume data. Parameters ---------- - data - image_graphic + data: np.ndarray + + images: ImageGraphic | ImageVolumeGraphic | tuple[ImageGraphic | ImageVolumeGraphic] + nbins: int, defaut 100. Total number of bins used in the histogram + flank_divisor: float, default 5.0. Fraction of empty histogram bins on the tails of the distribution set `np.inf` for no flanks - kwargs + + kwargs: passed to ``Graphic`` + """ super().__init__(**kwargs) self._nbins = nbins self._flank_divisor = flank_divisor - self._image_graphic = image_graphic + + if isinstance(images, (ImageGraphic, ImageVolumeGraphic)): + images = (images,) + elif isinstance(images, Sequence): + if not all( + [isinstance(ig, (ImageGraphic, ImageVolumeGraphic)) for ig in images] + ): + raise TypeError( + f"`images` argument must be an ImageGraphic, ImageVolumeGraphic, or a " + f"tuple or list or ImageGraphic | ImageVolumeGraphic" + ) + else: + raise TypeError( + f"`images` argument must be an ImageGraphic, ImageVolumeGraphic, or a " + f"tuple or list or ImageGraphic | ImageVolumeGraphic" + ) + + self._images = images self._data = weakref.proxy(data) @@ -60,7 +91,9 @@ def __init__( line_data = np.column_stack([hist_scaled, edges_flanked]) - self._histogram_line = LineGraphic(line_data) + self._histogram_line = LineGraphic( + line_data, colors=(0.8, 0.8, 0.8), alpha_mode="solid", offset=(0, 0, -1) + ) bounds = (edges[0] * self._scale_factor, edges[-1] * self._scale_factor) limits = (edges_flanked[0], edges_flanked[-1]) @@ -77,15 +110,15 @@ def __init__( parent=self._histogram_line, ) + self._vmin = self.images[0].vmin + self._vmax = self.images[0].vmax + # there will be a small difference with the histogram edges so this makes them both line up exactly self._linear_region_selector.selection = ( - self._image_graphic.vmin * self._scale_factor, - self._image_graphic.vmax * self._scale_factor, + self._vmin * self._scale_factor, + self._vmax * self._scale_factor, ) - self._vmin = self.image_graphic.vmin - self._vmax = self.image_graphic.vmax - vmin_str, vmax_str = self._get_vmin_vmax_str() self._text_vmin = TextGraphic( @@ -94,7 +127,8 @@ def __init__( offset=(0, 0, 0), anchor="top-left", outline_color="black", - outline_thickness=1, + outline_thickness=0.5, + alpha_mode="solid", ) self._text_vmin.world_object.material.pick_write = False @@ -105,7 +139,8 @@ def __init__( offset=(0, 0, 0), anchor="bottom-left", outline_color="black", - outline_thickness=1, + outline_thickness=0.5, + alpha_mode="solid", ) self._text_vmax.world_object.material.pick_write = False @@ -130,12 +165,13 @@ def __init__( self._linear_region_handler, "selection" ) - ig_events = _get_image_graphic_events(self.image_graphic) + ig_events = _get_image_graphic_events(self.images[0]) - self.image_graphic.add_event_handler(self._image_cmap_handler, *ig_events) + for ig in self.images: + ig.add_event_handler(self._image_cmap_handler, *ig_events) # colorbar for grayscale images - if self.image_graphic.data.value.ndim != 3: + if self.images[0].cmap is not None: self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) self._colorbar.add_event_handler(self._open_cmap_picker, "click") @@ -162,13 +198,13 @@ def _make_colorbar(self, edges_flanked) -> ImageGraphic: data=colorbar_data, vmin=self.vmin, vmax=self.vmax, - cmap=self.image_graphic.cmap, + cmap=self.images[0].cmap, interpolation="linear", offset=(-55, edges_flanked[0], -1), ) cbar.world_object.world.scale_x = 20 - self._cmap = self.image_graphic.cmap + self._cmap = self.images[0].cmap return cbar @@ -256,8 +292,9 @@ def cmap(self, name: str): if self._colorbar is None: return - with pause_events(self.image_graphic): - self.image_graphic.cmap = name + with pause_events(*self.images): + for ig in self.images: + ig.cmap = name self._cmap = name self._colorbar.cmap = name @@ -268,14 +305,15 @@ def vmin(self) -> float: @vmin.setter def vmin(self, value: float): - with pause_events(self.image_graphic, self._linear_region_selector): + with pause_events(self._linear_region_selector, *self.images): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( value * self._scale_factor, self._linear_region_selector.selection[1], ) - self.image_graphic.vmin = value + for ig in self.images: + ig.vmin = value self._vmin = value if self._colorbar is not None: @@ -291,7 +329,7 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): - with pause_events(self.image_graphic, self._linear_region_selector): + with pause_events(self._linear_region_selector, *self.images): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( @@ -299,7 +337,8 @@ def vmax(self, value: float): value * self._scale_factor, ) - self.image_graphic.vmax = value + for ig in self.images: + ig.vmax = value self._vmax = value if self._colorbar is not None: @@ -326,7 +365,7 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._linear_region_selector.limits = limits self._linear_region_selector.selection = bounds else: - with pause_events(self.image_graphic, self._linear_region_selector): + with pause_events(self._linear_region_selector, *self.images): # don't change the current selection self._linear_region_selector.limits = limits @@ -336,7 +375,7 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._colorbar.clear_event_handlers() self.world_object.remove(self._colorbar.world_object) - if self.image_graphic.data.value.ndim != 3: + if self.images[0].cmap is not None: self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) self._colorbar.add_event_handler(self._open_cmap_picker, "click") @@ -349,34 +388,39 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._plot_area.auto_scale() @property - def image_graphic(self) -> ImageGraphic: - return self._image_graphic - - @image_graphic.setter - def image_graphic(self, graphic): - if not isinstance(graphic, ImageGraphic): + def images(self) -> tuple[ImageGraphic | ImageVolumeGraphic]: + return self._images + + @images.setter + def images(self, images): + if isinstance(images, (ImageGraphic, ImageVolumeGraphic)): + images = (images,) + elif isinstance(images, Sequence): + if not all( + [isinstance(ig, (ImageGraphic, ImageVolumeGraphic)) for ig in images] + ): + raise TypeError( + f"`images` argument must be an ImageGraphic, ImageVolumeGraphic, or a " + f"tuple or list or ImageGraphic | ImageVolumeGraphic" + ) + else: raise TypeError( - f"HistogramLUTTool can only use ImageGraphic types, you have passed: {type(graphic)}" - ) - - if self._image_graphic is not None: - # cleanup events from current image graphic - ig_events = _get_image_graphic_events(self._image_graphic) - self._image_graphic.remove_event_handler( - self._image_cmap_handler, *ig_events + f"`images` argument must be an ImageGraphic, ImageVolumeGraphic, or a " + f"tuple or list or ImageGraphic | ImageVolumeGraphic" ) - self._image_graphic = graphic + if self._images is not None: + for ig in self._images: + # cleanup events from current image graphics + ig_events = _get_image_graphic_events(ig) + ig.remove_event_handler(self._image_cmap_handler, *ig_events) - ig_events = _get_image_graphic_events(self._image_graphic) + self._images = images - self.image_graphic.add_event_handler(self._image_cmap_handler, *ig_events) + ig_events = _get_image_graphic_events(self._images[0]) - def disconnect_image_graphic(self): - ig_events = _get_image_graphic_events(self._image_graphic) - self._image_graphic.remove_event_handler(self._image_cmap_handler, *ig_events) - del self._image_graphic - # self._image_graphic = None + for ig in self.images: + ig.add_event_handler(self._image_cmap_handler, *ig_events) def _open_cmap_picker(self, ev): # check if right click diff --git a/fastplotlib/ui/right_click_menus/_colormap_picker.py b/fastplotlib/ui/right_click_menus/_colormap_picker.py index 199a8ff6d..a80e5b2aa 100644 --- a/fastplotlib/ui/right_click_menus/_colormap_picker.py +++ b/fastplotlib/ui/right_click_menus/_colormap_picker.py @@ -154,7 +154,7 @@ def update(self): self._texture_height = (imgui.get_font_size()) - 2 if imgui.menu_item("Reset vmin-vmax", "", False)[0]: - self._lut_tool.image_graphic.reset_vmin_vmax() + self._lut_tool.images[0].reset_vmin_vmax() # add all the cmap options for cmap_type in COLORMAP_NAMES.keys(): diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index a1d6d476a..a839ed9d0 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -273,15 +273,15 @@ def quick_min_max(data: np.ndarray, max_size=1e6) -> tuple[float, float]: Parameters ---------- - data: np.ndarray or array-like with `min` and `max` attributes + data: np.ndarray or array-like max_size : int, optional - largest array size allowed in the subsampled array. Default is 1e6. + subsamples data array to this max size Returns ------- (float, float) - (min, max) + (min, max) estimate """ if hasattr(data, "min") and hasattr(data, "max"): diff --git a/fastplotlib/widgets/image_widget/_widget.py b/fastplotlib/widgets/image_widget/_widget.py index 650097951..9668b7182 100644 --- a/fastplotlib/widgets/image_widget/_widget.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -562,7 +562,7 @@ def __init__( subplot.add_graphic(ig) if self._histogram_widget: - hlut = HistogramLUTTool(data=d, image_graphic=ig, name="histogram_lut") + hlut = HistogramLUTTool(data=d, images=ig, name="histogram_lut") subplot.docks["right"].add_graphic(hlut) subplot.docks["right"].size = 80 @@ -944,7 +944,7 @@ def set_data( if self._histogram_widget: # set hlut tool to use new graphic - subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic + subplot.docks["right"]["histogram_lut"].images = new_graphic # delete old graphic after setting hlut tool to new graphic # this ensures gc diff --git a/pyproject.toml b/pyproject.toml index debca6d6d..e5d313418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ docs = [ "imageio[ffmpeg]", "matplotlib", "scikit-learn", + "ome-zarr", ] notebook = [ "jupyterlab", @@ -54,6 +55,7 @@ tests = [ "imageio[ffmpeg]", "scikit-learn", "tqdm", + "ome-zarr", ] imgui = ["wgpu[imgui]"] dev = ["fastplotlib[docs,notebook,tests,imgui]"] diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 85e0be669..968c68d2a 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -1,5 +1,6 @@ import inspect import pathlib +import re import black @@ -19,6 +20,8 @@ for name, obj in inspect.getmembers(graphics): if inspect.isclass(obj): + if obj.__name__ == "Graphic": + continue # skip the base class modules.append(obj) @@ -50,10 +53,9 @@ def generate_add_graphics_methods(): for m in modules: cls = m - if cls.__name__ == "Graphic": - # skip base class - continue - method_name = cls.type + cls_name = cls.__name__.replace("Graphic", "") + # from https://stackoverflow.com/a/1176023 + method_name = re.sub(r'(? np.ndarray: + """ + Makes a 2D array where the amplitude of the sine wave + is increasing along the y-direction (along rows), and + the wavelength is increasing along the x-axis (columns) + """ + xs = np.linspace(0, 100, n_cols) + + sine = np.sin(np.sqrt(xs)) + + data = np.dstack( + [ + np.column_stack([sine * i for i in range(n_rows)]).astype(np.float32) * j + for j in range(z) + ] + ) + + return data.T + +def check_texture_array( + data: np.ndarray, + ta: TextureArrayVolume, + buffer_size: int, + buffer_shape: tuple[int, int, int], + zdim_indices_size: int, + row_indices_size: int, + col_indices_size: int, + zdim_indices_values: np.ndarray, + row_indices_values: np.ndarray, + col_indices_values: np.ndarray, +): + + npt.assert_almost_equal(ta.value, data) + + assert ta.buffer.size == buffer_size + assert ta.buffer.shape == buffer_shape + + assert all([isinstance(texture, pygfx.Texture) for texture in ta.buffer.ravel()]) + + assert ta.zdim_indices.size == zdim_indices_size + assert ta.row_indices.size == row_indices_size + assert ta.col_indices.size == col_indices_size + + npt.assert_array_equal(ta.zdim_indices, zdim_indices_values) + npt.assert_array_equal(ta.row_indices, row_indices_values) + npt.assert_array_equal(ta.col_indices, col_indices_values) + + # make sure chunking is correct + for texture, chunk_index, data_slice in ta: + assert ta.buffer[chunk_index] is texture + chunk_z, chunk_row, chunk_col = chunk_index + + data_z_start_index = chunk_z * MAX_TEXTURE_SIZE_3D + data_row_start_index = chunk_row * MAX_TEXTURE_SIZE_3D + data_col_start_index = chunk_col * MAX_TEXTURE_SIZE_3D + + data_z_stop_index = min( + data.shape[0], data_z_start_index + MAX_TEXTURE_SIZE_3D + ) + + data_row_stop_index = min( + data.shape[1], data_row_start_index + MAX_TEXTURE_SIZE_3D + ) + data_col_stop_index = min( + data.shape[2], data_col_start_index + MAX_TEXTURE_SIZE_3D + ) + + zdim_slice = slice(data_z_start_index, data_z_stop_index) + row_slice = slice(data_row_start_index, data_row_stop_index) + col_slice = slice(data_col_start_index, data_col_stop_index) + + assert data_slice == (zdim_slice, row_slice, col_slice) + + +def check_set_slice(data, ta, zdim_slice, row_slice, col_slice): + ta[zdim_slice, row_slice, col_slice] = 1 + npt.assert_almost_equal(ta[zdim_slice, row_slice, col_slice], 1) + + # make sure other vals unchanged + npt.assert_almost_equal(ta[:zdim_slice.start], data[:zdim_slice.start]) + npt.assert_almost_equal(ta[zdim_slice.stop:], data[zdim_slice.stop:]) + + npt.assert_almost_equal(ta[:, :row_slice.start], data[:, :row_slice.start]) + npt.assert_almost_equal(ta[:, row_slice.stop:], data[:, row_slice.stop:]) + + npt.assert_almost_equal(ta[:, :, :col_slice.start], data[:, :, :col_slice.start]) + npt.assert_almost_equal(ta[:, :, col_slice.stop:], data[:, :, col_slice.stop:]) + + +def make_image_volume_graphic(data) -> fpl.ImageVolumeGraphic: + fig = fpl.Figure(cameras="3d") + return fig[0, 0].add_image_volume(data, offset=(0, 0, 0)) + + +def check_image_graphic(texture_array, graphic): + # make sure each ImageTile has the right texture + for (texture, chunk_index, data_slice), img in zip( + texture_array, graphic.world_object.children + ): + assert isinstance(img, _VolumeTile) + assert img.geometry.grid is texture + assert img.world.z == data_slice[0].start + assert img.world.x == data_slice[2].start + assert img.world.y == data_slice[1].start + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_small_texture(test_graphic): + # tests TextureArray with dims that requires only 1 texture + data = make_data(32, 64, 64) + + if test_graphic: + graphic = make_image_volume_graphic(data) + ta = graphic.data + else: + ta = TextureArrayVolume(data) + + check_texture_array( + data=data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1, 1), + zdim_indices_size=1, + row_indices_size=1, + col_indices_size=1, + zdim_indices_values=np.array([0]), + row_indices_values=np.array([0]), + col_indices_values=np.array([0]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(5, 20), slice(10, 40), slice(20, 50)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_texture_at_limit(test_graphic): + # tests TextureArray with data that is 512 x 512 x 512 + data = make_data(MAX_TEXTURE_SIZE_3D, MAX_TEXTURE_SIZE_3D, MAX_TEXTURE_SIZE_3D) + + if test_graphic: + graphic = make_image_volume_graphic(data) + ta = graphic.data + else: + ta = TextureArrayVolume(data) + + check_texture_array( + data=data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1, 1), + zdim_indices_size=1, + row_indices_size=1, + col_indices_size=1, + zdim_indices_values=np.array([0]), + row_indices_values=np.array([0]), + col_indices_values=np.array([0]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(5, 40), slice(10, 100), slice(20, 110)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_high_cols(test_graphic): + data = make_data(10, 100, 300) + + if test_graphic: + graphic = make_image_volume_graphic(data) + ta = graphic.data + else: + ta = TextureArrayVolume(data) + + check_texture_array( + data, + ta=ta, + buffer_size=3, + buffer_shape=(1, 1, 3), + zdim_indices_size=1, + row_indices_size=1, + col_indices_size=3, + zdim_indices_values=np.array([0]), + row_indices_values=np.array([0]), + col_indices_values=np.array([0, MAX_TEXTURE_SIZE_3D, 2 * MAX_TEXTURE_SIZE_3D]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(2, 7), slice(60, 90), slice(100, 180)) From 6a0d3b37eac4658754a8d4766a4175ebf960d754 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 9 Oct 2025 06:49:40 -0400 Subject: [PATCH 13/52] remove a old text editor temp file (#909) --- docs/source/_templates/.class_page_toc.html.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/source/_templates/.class_page_toc.html.swp diff --git a/docs/source/_templates/.class_page_toc.html.swp b/docs/source/_templates/.class_page_toc.html.swp deleted file mode 100644 index d4ed35c1f358cf2a4298189e4e249579be4e6f2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2v2GJV5QaAtNEQj8P|_@iB7uZ`wxK|9jzme3GHEDIdv{}dmG$1~b{!?M9NvJE zjso!-)bIcx@dUgFv*+97Ft$+&ilABP_jq^aW@f*uEKkxsdVRp3clr$1UH7qAUw&+} zR7k6&_D5@N28k4Z4`b(b!mxxQH{nniTc{jMBw?1PCN%l9Od*UdObR6|m@rksn79xl zh;5#B#&)9Cc0mpzKm;}u*g1UK?{!_w!{7nGw|lrboV-MU2oM1xKm>>Y5g-CYfC&6o z1Z=Uz-r|jJ)VJEL?bed5w`7O_5g-CYfCvx)B0vO)01+SpM1Tkofon)WWQ_H<8GDa% z|Nr~n|KIO0_6_wBb&Pt2x`q05o3VGO15^w3b%(Jd)HBpg)KARw8TG;EyoOAuWgi}Ei z?x~L3$YhH1*|E@BJOMk-Gt=bkGelG6OP@$16YLkS#|@-b=DO{^4;7rY(_Bp_`m%|O z=PzOUBX6TCGORjpGOP}k8T#zbMoZj!>$o)^s(Dp(k=J5=9WhsQs;pN<5;;mT8w5$# zcJB+f!4q!mnSw!3h%A$OgpT3`@0Boli4fWSH~nt+99uD98!Zg@2yA@O(xb<&sbB5v B4W|GA From 75ff89210bbb41d7ab2d97c85689a92b25de7b0c Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sat, 11 Oct 2025 01:00:19 +0200 Subject: [PATCH 14/52] Adjust for PyGfx's new ticks (#910) * Adjust for PyGfx's new ticks * put tick_mark in generic dict * new screenshots * bump pygfx --- .../screenshots/no-imgui-nb-astronaut.png | 4 +- .../screenshots/no-imgui-nb-astronaut_RGB.png | 4 +- .../screenshots/no-imgui-nb-camera.png | 4 +- .../screenshots/no-imgui-nb-lines-3d.png | 4 +- .../screenshots/no-imgui-nb-lines-colors.png | 4 +- .../screenshots/no-imgui-nb-lines-data.png | 4 +- .../no-imgui-nb-lines-underlay.png | 4 +- .../screenshots/no-imgui-nb-lines.png | 4 +- .../no-imgui-extent_frac_layout.png | 4 +- .../screenshots/no-imgui-extent_layout.png | 4 +- examples/screenshots/no-imgui-gridplot.png | 4 +- .../no-imgui-gridplot_non_square.png | 4 +- .../no-imgui-gridplot_viewports_check.png | 4 +- examples/screenshots/no-imgui-heatmap.png | 4 +- examples/screenshots/no-imgui-image_cmap.png | 4 +- examples/screenshots/no-imgui-image_rgb.png | 4 +- .../no-imgui-image_rgbvminvmax.png | 4 +- .../screenshots/no-imgui-image_simple.png | 4 +- examples/screenshots/no-imgui-image_small.png | 4 +- .../screenshots/no-imgui-image_vminvmax.png | 4 +- .../screenshots/no-imgui-image_volume_mip.png | 4 +- .../no-imgui-image_volume_multi_channel.png | 4 +- ...ui-image_volume_non_orthogonal_slicing.png | 4 +- examples/screenshots/no-imgui-line.png | 4 +- examples/screenshots/no-imgui-line_cmap.png | 4 +- .../screenshots/no-imgui-line_cmap_more.png | 4 +- .../no-imgui-line_collection_slicing.png | 4 +- .../screenshots/no-imgui-line_colorslice.png | 4 +- .../screenshots/no-imgui-line_dataslice.png | 2 +- examples/screenshots/no-imgui-line_stack.png | 4 +- ...-linear_region_selectors_match_offsets.png | 4 +- .../screenshots/no-imgui-linear_selector.png | 4 +- .../screenshots/no-imgui-rect_frac_layout.png | 4 +- examples/screenshots/no-imgui-rect_layout.png | 4 +- .../no-imgui-scatter_cmap_iris.png | 4 +- .../no-imgui-scatter_colorslice_iris.png | 4 +- .../no-imgui-scatter_dataslice_iris.png | 4 +- .../screenshots/no-imgui-scatter_iris.png | 4 +- .../screenshots/no-imgui-scatter_size.png | 4 +- fastplotlib/graphics/_axes.py | 38 +++++++++++-------- pyproject.toml | 2 +- 41 files changed, 101 insertions(+), 93 deletions(-) diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png index 174b8181d..25a1ebb6b 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9195062f4f4cc37e3975468e8ae397aa9c8064e7f89f2821f6a9ca2b204f0a3 -size 73410 +oid sha256:08ec733c8e28069d226ded849d9dbd2fb2fea0dac91dd4691823a2bf077cc6b0 +size 73878 diff --git a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png index 75ef377aa..f1cc52d2b 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/no-imgui-nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5e46f43c0b440811c626853d5995466347c3a51f49cf129133a8e0ad3c8cea7 -size 71277 +oid sha256:40ed1cf02ff2f8d715a2fe0dfa0589f7f7daee043ab3bd1bfa4279b8356483b4 +size 71445 diff --git a/examples/notebooks/screenshots/no-imgui-nb-camera.png b/examples/notebooks/screenshots/no-imgui-nb-camera.png index aa9ccb37b..be5a96a55 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-camera.png +++ b/examples/notebooks/screenshots/no-imgui-nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07080677bb75b39ee90d86d15172e08423bf764cb48f75100f3d9bc6c1107024 -size 54539 +oid sha256:6a7b1e20fa0f6c70aaf6458c20b2fec6ffff399493ad3d15c7fa706f08adb0c0 +size 54983 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 0af986c10..7f4b745e5 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f3c230460cd4397c24e07e0b671581bb9234254687a7ed839a5500b44ee1485 -size 17783 +oid sha256:0cd5815dc7bac5137d20e9896fe5c8c338d94e3e9e566dfef9b3289aad9d3354 +size 17893 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index 2ca81d14d..38a50ca88 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73872b407bf32309aac7ef7f1169a5cae62e5d6cc5c9ac5df7dcd716fb9da91d -size 31437 +oid sha256:442702ac2d64b42ef67f2a651142e8230969a14823527986ae87dadc061bd3df +size 31674 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 6161902ef..5ba895abd 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26fe3ec674af9d19e08e8ad3246c1ca69bb5ac5a6e4cb23077b8cebf2860ba85 -size 39168 +oid sha256:e684afab41584d2237f4afaca55018925364cd05717045556aa15414fa3d7e44 +size 39368 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index ea3e6170c..066f8ee40 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4db9bf43b9be8055a13fec7cdd6d1c11fe14db4078616e4e19690a96890d5224 -size 52777 +oid sha256:70cdca11587c9b57e226b6d06cdae626c01242e2cf09e55e714319ded52d8be6 +size 53688 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 79c88a22e..494b82301 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8fae40886e496e5377613ab6a08f20d11d41c037f4ec243d57c33948078674d -size 23462 +oid sha256:6a2d67ab6fc37960ddb15e6cf33fbb9db99647b7eafb7be07334f205e29cc1fb +size 23544 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png index fc66f9c05..cf2dfd9d2 100644 --- a/examples/screenshots/no-imgui-extent_frac_layout.png +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:527415b6e9d9e92170471752ba508feb90b50a82e4925228f795c6ddc92b02ca -size 144308 +oid sha256:0b1196c6c708f2c91c79fabedd94de512b2026a68238a7befe7dddda02a817cf +size 144324 diff --git a/examples/screenshots/no-imgui-extent_layout.png b/examples/screenshots/no-imgui-extent_layout.png index 87fac1088..d400171a2 100644 --- a/examples/screenshots/no-imgui-extent_layout.png +++ b/examples/screenshots/no-imgui-extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16b1f946e5678461a64ceaaad967de103e3c6c3706425fc3a4af331fc6874b4d -size 128985 +oid sha256:5ffa8ba36146fefb70ffb7c0555f0068c159bbca93fe3736bb73437c25ca8dd4 +size 128833 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index 4f3ec0fd2..2ce5e1195 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f579888d452a84831dae2551ff26fa3ab56d930b399bd7db5f41c6208f775d22 -size 299163 +oid sha256:ea09532f4c5a0887f4f33604294d20c30e720914c924198fc15d9cae6f185468 +size 298970 diff --git a/examples/screenshots/no-imgui-gridplot_non_square.png b/examples/screenshots/no-imgui-gridplot_non_square.png index 19b1b2088..f6b2b8adb 100644 --- a/examples/screenshots/no-imgui-gridplot_non_square.png +++ b/examples/screenshots/no-imgui-gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:597bcd71e8be59b6c039251cf6414f1600d53296fd07d6abb71dafd9119d1550 -size 219788 +oid sha256:af867dcb5ac5bcd677c31780c07b950493d404f432be7ad7853290ae827cec01 +size 219680 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png index 0db1e0959..95fbe37f5 100644 --- a/examples/screenshots/no-imgui-gridplot_viewports_check.png +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed363583146d44a83f55ad79a08e3ffd01fcbec00c6e8d9939595d87bd21dc4a -size 41628 +oid sha256:ec7c66c05b8faae6cb7d6c233b618964cb1df16cdcd94b6765630488005fcc7b +size 41809 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index adc5f67db..884994481 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa9967b6148bc7a0b7d5e8688492f65147bf263bc3762a6b53f2feb249297fec -size 92602 +oid sha256:92e99fb729e2bd40c88c0569c26df9549ec09333cebf07473c5a4b7f788c5e9e +size 92915 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 6a8019802..26f4b007e 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c128d5e0d522156442b38a407f8622991ff54e7bd668e65b2930febf1c43d2c -size 224933 +oid sha256:0a2cde1f9b872d1697246556ea06d67951e55c99a087c18a0019e0a0803d0cd7 +size 224832 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 5181f0ce5..128e87ac1 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:002b3454abe51b48dfcda3bdd4fc64d69ede8f9315656fe1077e75fef91d39e0 -size 250469 +oid sha256:8c12a73ef1b262a901fb9a7187ba0d0f30eb1a67c817ec82e53115d954243fac +size 250459 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 9d7b91f40..559829eab 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:feda947f567dc4c2d484ca25458520afb8e8eee1ff6e8fb2be32db3b93739d6c -size 43046 +oid sha256:729e00d84f33fd63611d99d786b162f18d5b143b449f4b7a4f0e51a042848da1 +size 43073 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index fdaf65c31..9df136ea9 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c44bb64828621bf1b2074cb5d2409ea81222060ede9d68d7e5354fc8a5e8badb -size 223784 +oid sha256:5be9d8e69952d6ca591916b5c87c43871bcd2a2020546282f23b036c49da8494 +size 223741 diff --git a/examples/screenshots/no-imgui-image_small.png b/examples/screenshots/no-imgui-image_small.png index c346ae19c..31e1421b2 100644 --- a/examples/screenshots/no-imgui-image_small.png +++ b/examples/screenshots/no-imgui-image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a40a4da4f4f83acc4e17223eeb69f3472b07cb9408b8c01a0d786885469e674 -size 11325 +oid sha256:d844ad71bf2afe250fc5d681fc78aedd4b9879593d07aff5aa5092e03141da16 +size 11558 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 9d7b91f40..559829eab 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:feda947f567dc4c2d484ca25458520afb8e8eee1ff6e8fb2be32db3b93739d6c -size 43046 +oid sha256:729e00d84f33fd63611d99d786b162f18d5b143b449f4b7a4f0e51a042848da1 +size 43073 diff --git a/examples/screenshots/no-imgui-image_volume_mip.png b/examples/screenshots/no-imgui-image_volume_mip.png index 6b6c28881..5185b87ef 100644 --- a/examples/screenshots/no-imgui-image_volume_mip.png +++ b/examples/screenshots/no-imgui-image_volume_mip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce09f81fa0000514626f2c9c88c1917d992a2139b602d92a7f8b8b5598c52372 -size 170567 +oid sha256:1320d82fedf7683504377629acc547513ec3fec0643889fef36560cd27e8c986 +size 171432 diff --git a/examples/screenshots/no-imgui-image_volume_multi_channel.png b/examples/screenshots/no-imgui-image_volume_multi_channel.png index f79a06e06..01bc4e566 100644 --- a/examples/screenshots/no-imgui-image_volume_multi_channel.png +++ b/examples/screenshots/no-imgui-image_volume_multi_channel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae5457c576cffabdbb0849f2af396f88ab54729f8ae4446b36c521dccf176f2a -size 204715 +oid sha256:d23e5c3755351bbd37250ae6ecb39374ff2bdfe6599c157c37f65e3ddf9c9198 +size 204892 diff --git a/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png b/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png index 0a88b96bb..be8aaab55 100644 --- a/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png +++ b/examples/screenshots/no-imgui-image_volume_non_orthogonal_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c66511302aecb3ef4fd042c502ebf93674abb9088885f5fc4f5f685f891c6248 -size 58067 +oid sha256:af508ae7f7252e96891a055e130eb2e2ce52417b7c3b25ac402ef86d6dad4c4a +size 59065 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index 061dc4a8d..a9f1cbd3c 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d75db62e1d6e73604741bfd4c3255fcaeb9061256302a867f0d31525e1924fa8 -size 155225 +oid sha256:6f92fdb19a8a4c595e815b7099021896a8f5cf8f4d9342da6cc4aa5168227260 +size 154363 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 2b4ac1fc1..61f550f9f 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77b4423c05e64a2087729106e00ac46157ec949b5074904be7825c8b14d5bd08 -size 36610 +oid sha256:10f15a76991ce45b539c32b858dd1c4e8a8bf8f88be10a6d9887012954f2fd22 +size 36599 diff --git a/examples/screenshots/no-imgui-line_cmap_more.png b/examples/screenshots/no-imgui-line_cmap_more.png index 0fc69ecaa..c977cc430 100644 --- a/examples/screenshots/no-imgui-line_cmap_more.png +++ b/examples/screenshots/no-imgui-line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dbc6f7f47c52c9813562fbe57c5d50635ae447abf3c01fbd4668829bd172305 -size 90316 +oid sha256:0772acc1c85b3c23dfa7bd17c2b9208d7e2705b17c22e7b8c0000b4f0273f9f0 +size 90814 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index 39b0d9c71..4b4511036 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0c5dc9e6feabf2d61748009ae02a1c34fa81fb1bfd41232075177f7bf185a44 -size 72750 +oid sha256:c35085f9253ca5026bcbde45eabad8aedff9af9f67db3a9cf9e61d915e2b1c52 +size 73302 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index a1ac57f50..cb59732e1 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203d8f4f6348e7fee0e374f8721c0cd3ca5e796fba9d15fb757c8b3b6b0e81cc -size 41064 +oid sha256:e9e75cf01d7680ea1a9a178290580ebbe5f58015f7051d486fa0ad999a611dcb +size 41091 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index 795781388..7d9b00227 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7857e80f3a9c1a8e100f2e34c4181cf694efd190be87d0ad39b675d1594bf77 +oid sha256:c52fc239e13555dc466be86de0816a3ed785639a817cb248b7353cf20e33bc93 size 43603 diff --git a/examples/screenshots/no-imgui-line_stack.png b/examples/screenshots/no-imgui-line_stack.png index c8c76c66e..aab512b83 100644 --- a/examples/screenshots/no-imgui-line_stack.png +++ b/examples/screenshots/no-imgui-line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58b9b4b15262cccaa67ddafdecbed8a38e1a3391a73ec4ab372fba58ac534391 -size 55076 +oid sha256:54ec3441c4f71632e04510428cac038a1c88e4c529880804f31feb50eb6c5de6 +size 55293 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 74006a70b..4335083c4 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e35aafe0f4f2527dfa2ef7ac2a80dd55a6e0418ac0427ac9e4bb1ba86b95de8f -size 60559 +oid sha256:d651a7d7d83a1b0afb240efde28acbde71fbc6be17719df11a35e76da318e75e +size 60609 diff --git a/examples/screenshots/no-imgui-linear_selector.png b/examples/screenshots/no-imgui-linear_selector.png index 4152fb23c..425b306e7 100644 --- a/examples/screenshots/no-imgui-linear_selector.png +++ b/examples/screenshots/no-imgui-linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9fc51ee3954815c291d117f5b8e7e80bd3a312207dba5e51d7fffd6e532287b -size 104130 +oid sha256:77400138a435e9e4a884bed699ca814a820d5d2f28ef24d1e0e3dad6483f2302 +size 104087 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png index fc66f9c05..cf2dfd9d2 100644 --- a/examples/screenshots/no-imgui-rect_frac_layout.png +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:527415b6e9d9e92170471752ba508feb90b50a82e4925228f795c6ddc92b02ca -size 144308 +oid sha256:0b1196c6c708f2c91c79fabedd94de512b2026a68238a7befe7dddda02a817cf +size 144324 diff --git a/examples/screenshots/no-imgui-rect_layout.png b/examples/screenshots/no-imgui-rect_layout.png index 87fac1088..d400171a2 100644 --- a/examples/screenshots/no-imgui-rect_layout.png +++ b/examples/screenshots/no-imgui-rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16b1f946e5678461a64ceaaad967de103e3c6c3706425fc3a4af331fc6874b4d -size 128985 +oid sha256:5ffa8ba36146fefb70ffb7c0555f0068c159bbca93fe3736bb73437c25ca8dd4 +size 128833 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index d5280bdb5..5e1ba9e57 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:191963308144dd97037e26f9b26ea97e0af1a1645f929777bd82931925e954c3 -size 43850 +oid sha256:df4c38dce92fc81738a77464a4f0872ce4738d546d0c422938108f54495a3cdb +size 43922 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 7eab4a984..9a8fb0461 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354e8137170a14df5aaa9c1b28ce16e29e48d8e7f103cb2d3dd760cd148e6462 -size 24218 +oid sha256:f4fd239939a34daf48da95e16a90ffe9342a456638284afbc552c9b09aea0182 +size 24292 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index 5a830f181..7debfa887 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1da7bfe315865d3eb481fa8d9b191abf6ab326d73abdd352f750c6c21ff1c99d -size 26311 +oid sha256:2701081a1a9666b1da683ce3b56ae12b11eed3bf3d52e1f253abc330c7caccb9 +size 26403 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index aeb4850eb..ef01b5d5e 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd1cd6c957c83abdd8832fdc47640c5a525af6121c48993c194ecd1780aef770 -size 25669 +oid sha256:413552ed9ff77dc51510266817a09006586a2dcf3c6706d18c032d26d8e8397e +size 25779 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 47b4fe61a..8e2ac0323 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed4c1c689eb446bed9f5ce016161f67e5bd3975f66731ec58ee13626ad9dccb9 -size 42266 +oid sha256:61fbbf5ed203e94bfa2d5e7d6fd62a01200177b7fd2c5499e491935a6eb6586a +size 41926 diff --git a/fastplotlib/graphics/_axes.py b/fastplotlib/graphics/_axes.py index 8063e2819..2102dfd9e 100644 --- a/fastplotlib/graphics/_axes.py +++ b/fastplotlib/graphics/_axes.py @@ -161,26 +161,34 @@ def __init__( ): self._plot_area = plot_area - if x_kwargs is None: - x_kwargs = dict() - - if y_kwargs is None: - y_kwargs = dict() - - if z_kwargs is None: - z_kwargs = dict() + x_kwargs = x_kwargs or {} + y_kwargs = y_kwargs or {} + z_kwargs = z_kwargs or {} + + generic_kwargs = dict( + tick_size=8.0, + line_width=2.0, + tick_marker="tick", # 'tick' for both-sides, 'tick_left' or 'tick_right' for one-sided + color="#fff", + ) - x_kwargs = { - "tick_side": "right", + x_kwargs = dict( + tick_side="right", + **generic_kwargs, **x_kwargs, - } + ) - y_kwargs = {"tick_side": "left", **y_kwargs} + y_kwargs = dict( + tick_side="left", + **generic_kwargs, + **y_kwargs, + ) - z_kwargs = { - "tick_side": "left", + z_kwargs = dict( + tick_side="left", + **generic_kwargs, **z_kwargs, - } + ) # create ruler for each dim self._x = pygfx.Ruler(alpha_mode="solid", **x_kwargs) diff --git a/pyproject.toml b/pyproject.toml index e5d313418..728a1cd1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ keywords = [ requires-python = ">= 3.10" dependencies = [ "numpy>=1.23.0", - "pygfx==0.13", + "pygfx==0.14", "wgpu", # Let pygfx constrain the wgpu version "cmap>=0.1.3", # (this comment keeps this list multiline in VSCode) From e5a818a5e84d92ea718dd96fcdc4f02e6d611c3a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sat, 11 Oct 2025 01:01:48 +0200 Subject: [PATCH 15/52] Refactor z-order to ensure axes are on top (#906) * Only auto-offset images, and push back instead of forward * remove unnecessary use of offset * restore thin line * restore gridplot * new screenshots * add examples * update examples again? * remove screenshots added in wrong plance * better blending for iris scatter example --- examples/gridplot/gridplot.py | 2 +- examples/scatter/scatter_colorslice_iris.py | 3 +- .../screenshots/line_collection_slicing.png | 4 +- .../screenshots/scatter_colorslice_iris.png | 4 +- fastplotlib/graphics/_axes.py | 16 ++++++-- fastplotlib/graphics/image.py | 12 ------ fastplotlib/graphics/line.py | 9 ++--- fastplotlib/graphics/line_collection.py | 6 --- fastplotlib/graphics/scatter.py | 5 +-- fastplotlib/layouts/_plot_area.py | 39 ++++++++++++++----- fastplotlib/legends/legend.py | 5 +-- fastplotlib/utils/enums.py | 7 ++-- 12 files changed, 57 insertions(+), 55 deletions(-) diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py index 5edd6a845..1aa8c8083 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/gridplot/gridplot.py @@ -30,4 +30,4 @@ # See the "JupyterLab and IPython" section in the user guide if __name__ == "__main__": print(__doc__) - fpl.loop.run() + fpl.loop.run() \ No newline at end of file diff --git a/examples/scatter/scatter_colorslice_iris.py b/examples/scatter/scatter_colorslice_iris.py index 425a2f1ff..d9dc3053f 100644 --- a/examples/scatter/scatter_colorslice_iris.py +++ b/examples/scatter/scatter_colorslice_iris.py @@ -23,7 +23,8 @@ data=data[:, :-1], sizes=6, alpha=0.7, - colors=colors # use colors from the list of strings + alpha_mode="weighted_blend", # blend overlapping dots + colors=colors, # use colors from the list of strings ) figure.show() diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index 428e6c33d..9a163bdd4 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a88282f40fb1c02dfe7921793128e9b7abe1bbb85de0e12abc20cb23d3ecc720 -size 73949 +oid sha256:3283a717635f0e39c3da66e705dc988db4054d9210761265bde2f48f8e8c4fce +size 74585 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index d7271ce71..5c5d6d4be 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4f95d45d58779e7afe2b6898621c3e8d46c2dadc04406ed9ffa09c8c84c19b3 -size 25582 +oid sha256:9549c19c4d9ad6804036a4b1018ae438793cb350228d7f00fc0e38f74cd058b9 +size 25893 diff --git a/fastplotlib/graphics/_axes.py b/fastplotlib/graphics/_axes.py index 2102dfd9e..5b4c21682 100644 --- a/fastplotlib/graphics/_axes.py +++ b/fastplotlib/graphics/_axes.py @@ -191,14 +191,22 @@ def __init__( ) # create ruler for each dim - self._x = pygfx.Ruler(alpha_mode="solid", **x_kwargs) - self._y = pygfx.Ruler(alpha_mode="solid", **y_kwargs) - self._z = pygfx.Ruler(alpha_mode="solid", **z_kwargs) + self._x = pygfx.Ruler( + alpha_mode="solid", render_queue=RenderQueue.axes, **x_kwargs + ) + self._y = pygfx.Ruler( + alpha_mode="solid", render_queue=RenderQueue.axes, **y_kwargs + ) + self._z = pygfx.Ruler( + alpha_mode="solid", render_queue=RenderQueue.axes, **z_kwargs + ) # We render the lines and ticks as solid, but enable aa for text for prettier glyphs for ruler in self._x, self._y, self._z: + ruler.line.material.depth_compare = "<=" + ruler.points.material.depth_compare = "<=" + ruler.text.material.depth_compare = "<=" ruler.text.material.alpha_mode = "auto" - ruler.text.material.render_queue = RenderQueue.auto + 50 ruler.text.material.aa = True self._offset = offset diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 4a33d2c1d..1eaf54bb6 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -324,9 +324,6 @@ def add_linear_selector( self._plot_area.add_graphic(selector, center=False) - # place selector above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector def add_linear_region_selector( @@ -402,9 +399,6 @@ def add_linear_region_selector( self._plot_area.add_graphic(selector, center=False) - # place above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector def add_rectangle_selector( @@ -447,9 +441,6 @@ def add_rectangle_selector( self._plot_area.add_graphic(selector, center=False) - # place above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector def add_polygon_selector( @@ -485,7 +476,4 @@ def add_polygon_selector( self._plot_area.add_graphic(selector, center=False) - # place above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index bd3bbc397..f2d862067 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -96,6 +96,7 @@ def __init__( if thickness < 1.1: MaterialCls = pygfx.LineThinMaterial + aa = True else: MaterialCls = pygfx.LineMaterial @@ -110,6 +111,7 @@ def __init__( color=self.colors, pick_write=True, thickness_space=self.size_space, + depth_compare="<=", ) else: material = MaterialCls( @@ -118,6 +120,7 @@ def __init__( color_mode="vertex", pick_write=True, thickness_space=self.size_space, + depth_compare="<=", ) geometry = pygfx.Geometry( positions=self._data.buffer, colors=self._colors.buffer @@ -179,9 +182,6 @@ def add_linear_selector( self._plot_area.add_graphic(selector, center=False) - # place selector above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector def add_linear_region_selector( @@ -238,9 +238,6 @@ def add_linear_region_selector( self._plot_area.add_graphic(selector, center=False) - # place selector below this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) - # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end return selector diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 3d183593a..275cc1e47 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -378,9 +378,6 @@ def add_linear_selector( self._plot_area.add_graphic(selector, center=False) - # place selector above this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - return selector def add_linear_region_selector( @@ -435,9 +432,6 @@ def add_linear_region_selector( self._plot_area.add_graphic(selector, center=False) - # place selector below this graphic - selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) - # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end return selector diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 73095714b..6bff05fa5 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -97,10 +97,7 @@ def __init__( aa = kwargs.get("alpha_mode", "auto") in ("blend", "weighted_blend") geo_kwargs = {"positions": self._data.buffer} - material_kwargs = dict( - pick_write=True, - aa=aa, - ) + material_kwargs = dict(pick_write=True, aa=aa, depth_compare="<=") self._size_space = SizeSpace(size_space) if uniform_color: diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index feda52930..7b5c7952a 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -10,6 +10,7 @@ from ._utils import create_controller from ..graphics._base import Graphic +from ..graphics import ImageGraphic from ..graphics.selectors._base_selector import BaseSelector from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend @@ -394,6 +395,29 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) + def _sort_images_by_depth(self): + """ + In general, we want to avoid setting the offset of a graphic, because the + z-dimension may actually mean something; we cannot know whether the user is + building a 3D scene or not. We could check whether the 3d dimension of line/point data + is all zeros, but maybe this is intended, and *other* graphics in the same scene + may be actually 3D. We could check camera.fov being zero, but maybe the user + switches to a 3D camera later, or uses a 3D orthographic camera. + + The one exception, kindof, is images, which are inherently 2D, and for which + layering helps a lot to get things rendered correctly. So we basically layer the + images, in the order that they were added, pushing older images backwards (away + from the camera). + """ + count = 0 + for graphic in self._graphics: + if isinstance(graphic, ImageGraphic): + count += 1 + auto_depth = -count + user_changed_depth = graphic.offset[2] % 1 > 0.0 # i.e. is not integer + if not user_changed_depth: + graphic.offset = (*graphic.offset[:-1], auto_depth) + def add_graphic(self, graphic: Graphic, center: bool = True): """ Add a Graphic to the scene @@ -416,10 +440,8 @@ def add_graphic(self, graphic: Graphic, center: bool = True): self._add_or_insert_graphic(graphic=graphic, center=center, action="add") - if self.camera.fov == 0: - # for orthographic positions stack objects along the z-axis - # for perspective projections we assume the user wants full 3D control - graphic.offset = (*graphic.offset[:-1], len(self)) + if isinstance(graphic, ImageGraphic): + self._sort_images_by_depth() def insert_graphic( self, @@ -458,17 +480,14 @@ def insert_graphic( graphic=graphic, center=center, action="insert", index=index ) - if self.camera.fov == 0: - # for orthographic positions stack objects along the z-axis - # for perspective projections we assume the user wants full 3D control - if auto_offset: - graphic.offset = (*graphic.offset[:-1], index) + if isinstance(graphic, ImageGraphic): + self._sort_images_by_depth() def _add_or_insert_graphic( self, graphic: Graphic, center: bool = True, - action: str = Literal["insert", "add"], + action: Literal["insert", "add"] = "add", index: int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index b24bcac58..9da836fd7 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -71,11 +71,9 @@ def __init__( # construct Line WorldObject data = np.array([[0, 0, 0], [3, 0, 0]], dtype=np.float32) - material = pygfx.LineMaterial - self._line_world_object = pygfx.Line( geometry=pygfx.Geometry(positions=data), - material=material( + material=pygfx.LineMaterial( alpha_mode="blend", render_queue=RenderQueue.overlay, thickness=8, @@ -112,7 +110,6 @@ def __init__( self._label_world_object.world.x = position[0] + 10 self.world_object.world.y = position[1] - self.world_object.world.z = 2 self.world_object.add_event_handler( partial(self._highlight_graphic, graphic), "click" diff --git a/fastplotlib/utils/enums.py b/fastplotlib/utils/enums.py index 5de3b2e0a..3901b082c 100644 --- a/fastplotlib/utils/enums.py +++ b/fastplotlib/utils/enums.py @@ -9,6 +9,7 @@ class RenderQueue(IntEnum): auto = 2600 transparent = 3000 overlay = 4000 - # For selectors we use a render_queue of 3500, which is at the end of what is considered the group of transparent objects. - # So it's rendered later than the normal scene (2000 - 3000), but before overlays like legends and tooltips. - selector = 3500 + # For axes and selectors we use a higher render_queue, so they get rendered later than + # the graphics. Axes (rulers) have depth_compare '<=' and selectors don't compare depth. + axes = 3400 # still in 'object' group + selector = 3600 # considered in 'overlay' group From 31f0f0365e7782369ff5685f3952695a857e2539 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 17 Oct 2025 10:46:15 -0400 Subject: [PATCH 16/52] remove existing screenshots in regenerate action (#915) --- .github/workflows/screenshots.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 3f08ce0f1..2a9a18d86 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -51,6 +51,9 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | + # delete existing screenshots + rm ./examples/screenshots/*.png + rm ./examples/notebooks/screenshots/*.png # regenerate screenshots RENDERCANVAS_FORCE_OFFSCREEN=1 REGENERATE_SCREENSHOTS=1 pytest -v examples - name: Generate screenshots notebook, exclude image widget From 84094d378681cb43f48a7439b3c6697e37a68f9a Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 19 Oct 2025 02:49:05 -0400 Subject: [PATCH 17/52] thinner selectors (#914) --- examples/selection_tools/linear_selector.py | 3 +- .../selection_tools/linear_selector_image.py | 9 +-- examples/selection_tools/unit_circle.py | 3 + .../graphics/features/_selection_features.py | 18 +++--- .../graphics/selectors/_base_selector.py | 31 ++++++++-- fastplotlib/graphics/selectors/_linear.py | 26 ++++---- .../graphics/selectors/_linear_region.py | 59 ++++++++++++++++--- fastplotlib/tools/_histogram_lut.py | 1 - 8 files changed, 111 insertions(+), 39 deletions(-) diff --git a/examples/selection_tools/linear_selector.py b/examples/selection_tools/linear_selector.py index 65fd8f1b1..8b442db20 100644 --- a/examples/selection_tools/linear_selector.py +++ b/examples/selection_tools/linear_selector.py @@ -2,7 +2,8 @@ Linear Selectors ================ -Example showing how to use a `LinearSelector` with lines and line collections. +Example showing how to use a `LinearSelector` with lines and line collections. The linear selector is the yellow +vertical line. """ # test_example = true diff --git a/examples/selection_tools/linear_selector_image.py b/examples/selection_tools/linear_selector_image.py index 04844b568..657d5ae5e 100644 --- a/examples/selection_tools/linear_selector_image.py +++ b/examples/selection_tools/linear_selector_image.py @@ -2,8 +2,9 @@ Linear Selectors Image ====================== -Example showing how to use a `LinearSelector` to selector rows or columns of an image. The subplot on the right -displays the data for the selector row and column. +Example showing how to use a `LinearSelector` to select rows or columns of an image. The subplot on the right +displays the data for the selector row and column. Move the selectors independently or click the middle mouse +button to move both selectors to the clicked location. """ # test_example = false @@ -24,10 +25,10 @@ image = figure[0, 0].add_image(image_data) # add a row selector -image_row_selector = image.add_linear_selector(axis="y") +image_row_selector = image.add_linear_selector(axis="y", edge_color="cyan") # add column selector -image_col_selector = image.add_linear_selector() +image_col_selector = image.add_linear_selector(edge_color="cyan") # make a line to indicate row data line_image_row = figure[0, 1].add_line(image.data[0]) diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py index 143992c62..b2ba772e4 100644 --- a/examples/selection_tools/unit_circle.py +++ b/examples/selection_tools/unit_circle.py @@ -132,6 +132,9 @@ def set_x_val(ev): sine_selector.add_event_handler(set_x_val, "selection") cosine_selector.add_event_handler(set_x_val, "selection") +# set initial position of the selector so it's not just overlapping the y-axis +sine_selector.selection = 100 + figure.show() diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py index 3052ae3d0..ed18c8287 100644 --- a/fastplotlib/graphics/features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -64,9 +64,9 @@ def set_value(self, selector, value: float): elif self._axis == "y": dim = 1 - for edge in selector._edges: - edge.geometry.positions.data[:, dim] = value - edge.geometry.positions.update_range() + edge = selector._edges[0] + edge.geometry.positions.data[:, dim] = value + edge.geometry.positions.update_range() self._value = value @@ -152,10 +152,10 @@ def set_value(self, selector, value: Sequence[float]): selector.fill.geometry.positions.data[mesh_masks.x_right] = value[1] # change x position of the left edge line - selector.edges[0].geometry.positions.data[:, 0] = value[0] + selector._edges[0].geometry.positions.data[:, 0] = value[0] # change x position of the right edge line - selector.edges[1].geometry.positions.data[:, 0] = value[1] + selector._edges[1].geometry.positions.data[:, 0] = value[1] elif self.axis == "y": # change bottom y position of the fill mesh @@ -165,18 +165,18 @@ def set_value(self, selector, value: Sequence[float]): selector.fill.geometry.positions.data[mesh_masks.y_top] = value[1] # change y position of the bottom edge line - selector.edges[0].geometry.positions.data[:, 1] = value[0] + selector._edges[0].geometry.positions.data[:, 1] = value[0] # change y position of the top edge line - selector.edges[1].geometry.positions.data[:, 1] = value[1] + selector._edges[1].geometry.positions.data[:, 1] = value[1] self._value = value # send changes to GPU selector.fill.geometry.positions.update_range() - selector.edges[0].geometry.positions.update_range() - selector.edges[1].geometry.positions.update_range() + selector._edges[0].geometry.positions.update_range() + selector._edges[1].geometry.positions.update_range() # send event if len(self._event_handlers) < 1: diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 2d2787ac8..e4dbc890b 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -111,6 +111,7 @@ def edge_color(self, color: str | Sequence[float]): def __init__( self, edges: Tuple[Line, ...] = None, + outer_edges: Tuple[Line, ...] = None, fill: Tuple[Mesh, ...] = None, vertices: Tuple[Points, ...] = None, hover_responsive: Tuple[WorldObject, ...] = None, @@ -122,6 +123,9 @@ def __init__( if edges is None: edges = tuple() + if outer_edges is None: + outer_edges = tuple() + if fill is None: fill = tuple() @@ -129,11 +133,15 @@ def __init__( vertices = tuple() self._edges: Tuple[Line, ...] = edges + self._outer_edges: Tuple[Line, ...] = outer_edges self._fill: Tuple[Mesh, ...] = fill self._vertices: Tuple[Points, ...] = vertices self._world_objects: Tuple[WorldObject, ...] = ( - self._edges + self._fill + self._vertices + *self._edges, + *self._outer_edges, + *self._fill, + *self._vertices, ) for wo in self._world_objects: @@ -148,7 +156,7 @@ def __init__( self._hover_colors = {} if hover_responsive is not None: - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: self._original_colors[wo] = wo.material.color self._axis = axis @@ -231,7 +239,7 @@ def _fpl_add_plot_area_hook(self, plot_area): self._plot_area.renderer.add_event_handler(self._move_to_pointer, "click") # mouse hover color events - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: wo.add_event_handler(self._pointer_enter, "pointer_enter") wo.add_event_handler(self._pointer_leave, "pointer_leave") @@ -282,6 +290,12 @@ def _move_start(self, event_source: WorldObject, ev): """ position = self._plot_area.map_screen_to_world(ev) + # if the event source was an outer transparent line, get the + # corresponding inner line since it's just a proxy + if event_source in self._outer_edges: + index = self._outer_edges.index(event_source) + event_source = self._edges[index] + self._move_info = MoveInfo( start_selection=None, start_position=position, @@ -397,9 +411,16 @@ def _pointer_enter(self, ev): return wo = ev.pick_info["world_object"] - if wo not in self._hover_responsive: + if wo not in [*self._hover_responsive, *self._outer_edges]: return + # if it's an outer edge, highlight the corresponding inner edge instead + if wo in self._outer_edges: + # get index + index = self._outer_edges.index(wo) + # now use inner edge + wo = self._edges[index] + if wo in self._edges: self._edge_hovered = True @@ -415,7 +436,7 @@ def _pointer_leave(self, ev): self._edge_hovered = False # reset colors - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: if self._moving: self._hover_colors[wo] = self._original_colors[wo] else: diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 033736a5f..0364305a4 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -78,9 +78,10 @@ def __init__( limits: Sequence[float], axis: str = "x", parent: Graphic = None, - edge_color: str | Sequence[float] | np.ndarray = "w", - thickness: float = 2.5, + edge_color: str | Sequence[float] | np.ndarray = "yellow", + thickness: float = 1.0, arrow_keys_modifier: str = "Shift", + extra_width: float = 14.0, name: str = None, ): """ @@ -111,6 +112,9 @@ def __init__( edge_color: str | tuple | np.ndarray, default "w" color of the selector + extra_width: float, default 14.0 + the width around the selector which is responsive to mouse events, in logical pixels + name: str, optional name of linear selector @@ -141,8 +145,6 @@ def __init__( material = pygfx.LineInfiniteSegmentMaterial - self.colors_outer = pygfx.Color([0.3, 0.3, 0.3, 1.0]) - line_inner = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=line_data), @@ -158,11 +160,12 @@ def __init__( ), ) - self.line_outer = pygfx.Line( - geometry=pygfx.Geometry(positions=line_data), + line_outer = pygfx.Line( + geometry=line_inner.geometry, material=material( - thickness=thickness + 6, - color=self.colors_outer, + thickness=thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + opacity=0, alpha_mode="blend", aa=True, render_queue=RenderQueue.selector, @@ -177,7 +180,7 @@ def __init__( world_object = pygfx.Group() - world_object.add(self.line_outer) + world_object.add(line_outer) world_object.add(line_inner) if axis == "x": @@ -188,8 +191,9 @@ def __init__( # init base selector BaseSelector.__init__( self, - edges=(line_inner, self.line_outer), - hover_responsive=(line_inner, self.line_outer), + edges=(line_inner,), + outer_edges=(line_outer,), + hover_responsive=(line_inner,), arrow_keys_modifier=arrow_keys_modifier, axis=axis, parent=parent, diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index ee6849144..9f5803c93 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -64,9 +64,10 @@ def __init__( parent: Graphic = None, resizable: bool = True, fill_color: str | Sequence[float] = (0, 0, 0.35), - edge_color: str | Sequence[float] = (0.8, 0.6, 0), - edge_thickness: float = 8, + edge_color: str | Sequence[float] = "yellow", + edge_thickness: float = 1.0, arrow_keys_modifier: str = "Shift", + extra_width: float = 14.0, name: str = None, ): """ @@ -113,6 +114,9 @@ def __init__( modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None`` + extra_width: float, default 14.0 + the width around the selector lines which is responsive to mouse events, in logical pixels + name: str, optional name of this selector graphic @@ -215,6 +219,25 @@ def __init__( pick_write=True, ), ) + + line0_outer = pygfx.Line( + pygfx.Geometry( + # share buffer with inner line so they can both be managed together + positions=line0.geometry.positions + ), + pygfx.LineMaterial( + thickness=edge_thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + alpha_mode="blend", + opacity=0, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), + ) + line1 = pygfx.Line( pygfx.Geometry( positions=init_line_data.copy() @@ -232,8 +255,27 @@ def __init__( ), ) - self.edges: tuple[pygfx.Line, pygfx.Line] = (line0, line1) - group.add(*self.edges) + line1_outer = pygfx.Line( + pygfx.Geometry( + # share buffer with inner line so they can both be managed together + positions=line1.geometry.positions + ), + pygfx.LineMaterial( + thickness=edge_thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + alpha_mode="blend", + opacity=0, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), + ) + + edges: tuple[pygfx.Line, pygfx.Line] = (line0, line1) + outer_edges = (line0_outer, line1_outer) + group.add(*edges, *outer_edges) # TODO: if parent offset changes, we should set the selector offset too, use offset evented property # TODO: add check if parent is `None`, will throw error otherwise @@ -253,9 +295,10 @@ def __init__( BaseSelector.__init__( self, - edges=self.edges, + edges=edges, + outer_edges=outer_edges, fill=(self.fill,), - hover_responsive=self.edges, + hover_responsive=edges, arrow_keys_modifier=arrow_keys_modifier, axis=axis, parent=parent, @@ -423,12 +466,12 @@ def _move_graphic(self, move_info: MoveInfo): # if event source was an edge and selector is resizable, # move the edge that caused the event - if move_info.source == self.edges[0]: + if move_info.source == self._edges[0]: # change only left or bottom bound new_min = min(cur_min + delta, cur_max) self._selection.set_value(self, (new_min, cur_max)) - elif move_info.source == self.edges[1]: + elif move_info.source == self._edges[1]: # change only right or top bound new_max = max(cur_max + delta, cur_min) self._selection.set_value(self, (cur_min, new_max)) diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index 9c6b1b24d..7507a7ff2 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -106,7 +106,6 @@ def __init__( size=size, center=origin[0], axis="y", - edge_thickness=8, parent=self._histogram_line, ) From e832ce08365817bc77d23f89979f9927b58e7d6f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 20 Oct 2025 12:57:02 -0400 Subject: [PATCH 18/52] New screenshots :D (#919) * just something so I can make a PR * update screenshots --- examples/notebooks/screenshots/nb-astronaut.png | 4 ++-- examples/notebooks/screenshots/nb-astronaut_RGB.png | 4 ++-- examples/notebooks/screenshots/nb-camera.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-set_data.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-0-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-0.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-279.png | 4 ++-- .../nb-image-widget-movie-single-50-window-max-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-13.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-single-gnuplot2.png | 4 ++-- examples/notebooks/screenshots/nb-image-widget-single.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-5.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-99.png | 2 +- ...-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-5.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-50.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-99.png | 4 ++-- .../nb-image-widget-zfish-grid-init-mean-window-5.png | 4 ++-- ...b-image-widget-zfish-grid-set_data-reset-indices-false.png | 4 ++-- ...nb-image-widget-zfish-grid-set_data-reset-indices-true.png | 4 ++-- .../screenshots/nb-image-widget-zfish-init-mean-window-5.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png | 4 ++-- .../nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 ++-- examples/notebooks/screenshots/nb-lines-3d.png | 4 ++-- examples/notebooks/screenshots/nb-lines-colors.png | 4 ++-- examples/notebooks/screenshots/nb-lines-data.png | 4 ++-- examples/notebooks/screenshots/nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/nb-lines.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-3d.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-colors.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-data.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/no-imgui-nb-lines.png | 2 +- examples/screenshots/extent_frac_layout.png | 4 ++-- examples/screenshots/extent_layout.png | 4 ++-- examples/screenshots/gridplot.png | 4 ++-- examples/screenshots/gridplot_non_square.png | 4 ++-- examples/screenshots/gridplot_viewports_check.png | 4 ++-- examples/screenshots/heatmap.png | 4 ++-- examples/screenshots/image_cmap.png | 2 +- examples/screenshots/image_rgb.png | 4 ++-- examples/screenshots/image_rgbvminvmax.png | 4 ++-- examples/screenshots/image_simple.png | 4 ++-- examples/screenshots/image_small.png | 4 ++-- examples/screenshots/image_vminvmax.png | 4 ++-- examples/screenshots/image_volume_mip.png | 4 ++-- examples/screenshots/image_volume_multi_channel.png | 4 ++-- examples/screenshots/image_volume_non_orthogonal_slicing.png | 4 ++-- examples/screenshots/image_volume_render_modes.png | 4 ++-- examples/screenshots/image_widget.png | 4 ++-- examples/screenshots/image_widget_grid.png | 4 ++-- examples/screenshots/image_widget_imgui.png | 4 ++-- examples/screenshots/image_widget_single_video.png | 4 ++-- examples/screenshots/image_widget_videos.png | 4 ++-- examples/screenshots/image_widget_viewports_check.png | 4 ++-- examples/screenshots/imgui_basic.png | 4 ++-- examples/screenshots/line.png | 4 ++-- examples/screenshots/line_cmap.png | 4 ++-- examples/screenshots/line_cmap_more.png | 4 ++-- examples/screenshots/line_collection_slicing.png | 4 ++-- examples/screenshots/line_colorslice.png | 4 ++-- examples/screenshots/line_dataslice.png | 4 ++-- examples/screenshots/line_stack.png | 4 ++-- .../screenshots/linear_region_selectors_match_offsets.png | 4 ++-- examples/screenshots/linear_selector.png | 4 ++-- examples/screenshots/no-imgui-extent_frac_layout.png | 2 +- examples/screenshots/no-imgui-gridplot.png | 4 ++-- examples/screenshots/no-imgui-gridplot_viewports_check.png | 4 ++-- examples/screenshots/no-imgui-heatmap.png | 4 ++-- examples/screenshots/no-imgui-image_cmap.png | 4 ++-- examples/screenshots/no-imgui-image_rgb.png | 4 ++-- examples/screenshots/no-imgui-image_rgbvminvmax.png | 4 ++-- examples/screenshots/no-imgui-image_simple.png | 4 ++-- examples/screenshots/no-imgui-image_vminvmax.png | 4 ++-- examples/screenshots/no-imgui-image_volume_mip.png | 4 ++-- examples/screenshots/no-imgui-image_volume_multi_channel.png | 4 ++-- examples/screenshots/no-imgui-line.png | 4 ++-- examples/screenshots/no-imgui-line_cmap.png | 2 +- examples/screenshots/no-imgui-line_collection_slicing.png | 4 ++-- examples/screenshots/no-imgui-line_colorslice.png | 4 ++-- examples/screenshots/no-imgui-line_dataslice.png | 4 ++-- .../no-imgui-linear_region_selectors_match_offsets.png | 4 ++-- examples/screenshots/no-imgui-linear_selector.png | 4 ++-- examples/screenshots/no-imgui-rect_frac_layout.png | 2 +- examples/screenshots/no-imgui-scatter_cmap_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_colorslice_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_dataslice_iris.png | 4 ++-- examples/screenshots/no-imgui-scatter_iris.png | 4 ++-- examples/screenshots/rect_frac_layout.png | 4 ++-- examples/screenshots/rect_layout.png | 4 ++-- examples/screenshots/scatter_cmap_iris.png | 4 ++-- examples/screenshots/scatter_colorslice_iris.png | 4 ++-- examples/screenshots/scatter_dataslice_iris.png | 4 ++-- examples/screenshots/scatter_iris.png | 4 ++-- examples/screenshots/scatter_size.png | 2 +- 107 files changed, 207 insertions(+), 207 deletions(-) diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index cebcc4f74..405b26e14 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0f32394238fe9b317c11eea52ded0c663a9a37a4bb9ac3aaf7ec05ba7c39e55 -size 66265 +oid sha256:dbcc22edca9b0dd74da7754a8515c9000d6e651af39135ab90d5a1eaf438d324 +size 66559 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index 6a9c3b26f..0e3d21ef6 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96e2e88b1eed0844bd9f5066d876ec00123cccabbe0f03ceb4dad8c4feb80005 -size 64221 +oid sha256:1b8f9fc3d5e14c1f90c48128be5599d64e994dcce8579bf9276453d68dfdf022 +size 64354 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index f89a7da0f..f58781bb1 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a2280fb566459dd868b930bfe8ddd22ee15998d50f23ef44ec31d065f81353b -size 49947 +oid sha256:e744172658cf49cd7725c8ee1f3e983a3284848f10fc5805b17ae655e439cbda +size 50393 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 60b8cc2e7..181b9cb64 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfff638ad02e888721d2a9c02d479b8b233798be1e6d8554ff00415386a100d0 -size 63590 +oid sha256:29b42b78551ad2e986eb01d996f261ef9499f059ba285cc6e37fb903df090826 +size 64007 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 8b79f4286..c8761fc8a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:047c77b54a162823efda862dab4fff3fe1d72f7631248aa8ee42cd77af9039f6 -size 115523 +oid sha256:c9f6c3d3a76f2fbbbdcf8107e8b2db7900079d252384f7f722649c3ebbe92993 +size 115525 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 8b79f4286..c8761fc8a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:047c77b54a162823efda862dab4fff3fe1d72f7631248aa8ee42cd77af9039f6 -size 115523 +oid sha256:c9f6c3d3a76f2fbbbdcf8107e8b2db7900079d252384f7f722649c3ebbe92993 +size 115525 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 0d13e622a..7ded7f21b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:568b7e076645c963f6d1936ae3bc2f11cece8e63556bc7dcc0fb5d0251be35d5 -size 137370 +oid sha256:45c6c01e5fd26b06af55c0e2b98d8abd40603599dd53be3838479a447e099af8 +size 137111 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index f693dc489..381ea8d70 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2a03b0efefb900652eff84473994730509e57cd5c817928b6bee981b0d6967a -size 124179 +oid sha256:415ef8938bc982678e1402da5ab40e991f600302c13a8beda9690baa672e2289 +size 124095 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 721b14394..451d1060a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2f534ddc07a35ce426b5d0207afe813148023f4f15dcc8bcfe4a383d75ed740 -size 108503 +oid sha256:9439c5a18046ee0a9125de2fd5ecf5b5e605579d844dab9a741a1fd0833d32d3 +size 108554 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index c7824d57c..a4031beb3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:851490e5667065f75cd4daf23f44290796a76a86bc7cc946c87e043530ad23ce -size 100857 +oid sha256:926298948f78d7f205cd95dffc1623f2980a7a3667c45e752daa96b1f64d7cf1 +size 101047 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index df4c72f61..264ab5c2e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be73841d2a14b0a20b3fa6b736e787a7f8bb47266420e92ae388a0212d9e654d -size 121745 +oid sha256:1a682eba89f4456cf381679da67113741bcaace7a540b68b31e6276e6a8f95ce +size 121705 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index df4c72f61..264ab5c2e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be73841d2a14b0a20b3fa6b736e787a7f8bb47266420e92ae388a0212d9e654d -size 121745 +oid sha256:1a682eba89f4456cf381679da67113741bcaace7a540b68b31e6276e6a8f95ce +size 121705 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index 4e3196c9d..147f852bb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0df95a5c918a26b1c1aff1093b8dd362acbfa3ea6131da430fe67d4252719e7a -size 225472 +oid sha256:ae9f5efe64ff9ef3be7cb22444f4afd6430642b653c6b1337167aca4e06c7d81 +size 225650 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 499c820db..4d86e17aa 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a678b0f245c8a15d75ec0ab82a86d6e2b87c8737603b33a0cb057f3726a33a91 -size 216095 +oid sha256:0dc443374648f10112d4afa323a1e2ffff170ebbcbf7b640fc4dc07379f14d5d +size 216362 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 90a93008f..ca40fe5ed 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d73a656a0dfefc16a9faeb78c615e253862e035d546469f15f93f4e711ee18da -size 64766 +oid sha256:015933efd89517a65d73d9c91423e438260b7115fbf55386ad4b35ec72f39cb6 +size 64820 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 4036fad8a..fe1f9d0ad 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3e86c0304cf59171e14bbfcc5f8dbc08c438f6e9b1e4ab0bd47f8643e8f7a95 -size 69225 +oid sha256:97dcdc4ab1a4307507b090dc2e8f7d2c5998828000be4e3b67fcf3941b835473 +size 69210 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index c2b8cbdf2..640bed79b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5504c58509000864a7f126dcfc09d7f2ed80f5dcd372bdfa13e5a359923ac727 -size 113701 +oid sha256:6ba7d7fe7daae9e553f5ca45029822ebb432953c62cf301e9922a752ea438f17 +size 113828 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index 0b97348ac..9198e2ad6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc623177117feca7b7fa9c74a4c612b39272512ad6e2e4c1fe506a101ee39dad -size 97283 +oid sha256:374bbf072443b243c0393d5311fb361a6f089b22e5b60b6a80c90da20ac18a8d +size 97296 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index 1d639be5b..ce3f25a67 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:334dcbf9b3cd9f2941588458a938a2c8797af5ffab0baf29c72cb244097c4380 -size 89366 +oid sha256:eb0c0814509246ca64f9a820dac4617d55fa6be28ec59e787719b653f60d5818 +size 89409 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index 4036fad8a..fe1f9d0ad 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3e86c0304cf59171e14bbfcc5f8dbc08c438f6e9b1e4ab0bd47f8643e8f7a95 -size 69225 +oid sha256:97dcdc4ab1a4307507b090dc2e8f7d2c5998828000be4e3b67fcf3941b835473 +size 69210 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index e4a01bd38..19e84868b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5109589b36c9e1f810ee1e1864dfbce697239a57aa9ab80dc54d37b3bbc8a1 +oid sha256:9d286be8d9ed81b84d2ebfb308287ae7df17dfad4860fde8faf41687958a6f40 size 60572 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 40835dead..20b5c0a50 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff68c4dd69efd6be41f16023e4dec4737584eecb3fa19885294b66e52c52a083 -size 85352 +oid sha256:7c86f37cd4ad4e4a34e402008ec52fbe965d52e79b5ad8210af8813f8e4e3ed3 +size 85103 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 0f5bd5d1a..9e368022a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64a3d8391cc9a171b0205a2dc5a647bb5390c5ab03da7afe5c5cf0b82d2769dc -size 103011 +oid sha256:c6801347572dfd3a4be34cdd18915dc8dd4787764d3c8634b34b66b2c3f130d7 +size 102913 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index d95628db2..486934731 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6aa9f6c95a9025e095efc59afab2c6d47a3307ecea84687753081f6c355943c6 -size 143016 +oid sha256:02e91b1c0a327c6784387d279c34550bd74c5ed7e4c6587a2ef20f6f00204ff1 +size 143096 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index b609f93e2..05a88a73e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:412f7ed991a71bfdf502fa3a50cad5fe3585153360087d674b28f0edf774dfd5 -size 115844 +oid sha256:e30d25ac2af4d897e2e562b31bf1b92d199e34b1da4baad35d110dc8dcb042ba +size 115631 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 87c0370ba..25c537801 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96e386d9cb3b0fb3ff9cd57b1a1227c60d09093686e5f71a69abcffb2a565d13 -size 117519 +oid sha256:9d74bacc8c31e9b7896f46230a7734488c1d1ee5f833584f6be265d04b28860d +size 117556 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 0f5bd5d1a..9e368022a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64a3d8391cc9a171b0205a2dc5a647bb5390c5ab03da7afe5c5cf0b82d2769dc -size 103011 +oid sha256:c6801347572dfd3a4be34cdd18915dc8dd4787764d3c8634b34b66b2c3f130d7 +size 102913 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 9c3436b0c..659eec9de 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cae0f36ea87b444a4ca66ebe8aec9743d71480f11e4031f45f731cd3c3c6a012 -size 100231 +oid sha256:9ec15239b78880daafe3b5c18101019a30cd51db465c515bfc275bce9729eb5a +size 100085 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index a3dff14c9..e73518f67 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a0538e194491b1deaed091da55ec05dac077373287a80298f317bdc536bfe7e -size 111952 +oid sha256:5bc163776b12df7289bcf6e4ed160c843cd7bae12d48c8de90e2fda7039b1755 +size 111929 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 9eb7e43a0..b0d74d7e1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd255c495da614840d6c3ab75532b897e83174868daa0ee143ed6dc929fdf176 -size 103138 +oid sha256:d4a1e4b59103361750bb76e113db7b2a49f508e53c45086c11dcea01b72c40a6 +size 102983 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index abbeef013..a7cba3900 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8055763d1d539439434e02c9a6975e4600f4a036e866cea97d58d04a56cd3cda -size 104425 +oid sha256:db7ca9b37fffdfbebe31a439b12c4b83a73c7893634ddfb676640035b01105da +size 104387 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index d16d0afd8..b288984e2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0601f179366e572c6284c590a679753ce245bee6533ff65a5b64a45fa87242 -size 76788 +oid sha256:fd8b96126d72687ef5ef79990f096faff8317d8be84181ee666677e92b303677 +size 76841 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index fadaaecc4..27b6d6df4 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdffece1879e9245e6438262889c3013cfcfa32b956da9312ca884c0cd912e55 -size 111925 +oid sha256:987eb20fcfdfb95a60e3c5cf8094bbcf4d899e6b1416ce17fc9f924e71330da4 +size 112164 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 4dbf15eda..67500d292 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4c8eed347524d0fb66141cbed9b804573f1311d5f45e3a7adfee304b6f61436 -size 107239 +oid sha256:8eeb0548062ce9498b38b4f876d3d81f7de4f02b1b4fd5a47744007852d546bf +size 107522 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 96e048ee4..1740e97de 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c1365e589d47e15f313d20df5190c4a8be7a59c14f9575161e70a5722e95b25 -size 109170 +oid sha256:33e57f3dba3a865f433ddc4c613f302f7b5b34053b5eb05d0b9c6d5003ef6194 +size 109368 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 4895996d6..e54c6fede 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2924aef7286417e78f023810d1f8cebee85e4ee944537bb28a1d2979cc5eacb5 -size 17957 +oid sha256:e09b9a15d83d3c0ca45a136642f5c9da0fcf2b5fee494a522fbbe9fa6b9db9a5 +size 18097 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index d75d84229..ca21cdd76 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab117d2995b8ec4c9656d7b7101ded007db167592d92b1d7ae2d2751011fb505 -size 30939 +oid sha256:a40192f07f4bce34832aae2c11b28806547e15fccf543e90c09f457954ef422f +size 31081 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 44f6e486b..022d660c8 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c602844bcbd32d94838d5c9c74c4620162bd65bd87bbabe9c60b417fdefdc5ee -size 37158 +oid sha256:c790c3b23101a04bbab300b55d6bfdd1421fb0a0e81c5bff5f75a271e32ccab9 +size 37643 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index 4ad000048..f738b2293 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5895cfb7c24749b54acb26c4903ebd4508b65ef436a6e8de3ac7f40ac03d76f7 -size 50003 +oid sha256:fda16460bb690ef30bfc43a3cbb4336f5bbd190ef592381269cde5f250ae7a36 +size 50977 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index c422fcb80..2adc08fd6 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fec20ab8f0cb4948dc05afff33191cdc81cb54c722900c6b8705a4bb7efef8e8 -size 23473 +oid sha256:ecb09d9358e54a56fdd92c02d8e0f43886babfad5053086c50063bc7c8ff409c +size 23595 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png index 7f4b745e5..f405fb9b0 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cd5815dc7bac5137d20e9896fe5c8c338d94e3e9e566dfef9b3289aad9d3354 -size 17893 +oid sha256:d3ebb5155b5451ff86ac15a141303964c90fee662d4c8291dec80410ebfc6dd8 +size 17898 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png index 38a50ca88..435e1fe96 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:442702ac2d64b42ef67f2a651142e8230969a14823527986ae87dadc061bd3df -size 31674 +oid sha256:31d330b3d1c48156223c3b744a2997ca33db8a86dffae2717e3f2dc231182732 +size 31672 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png index 5ba895abd..a02431728 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-data.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e684afab41584d2237f4afaca55018925364cd05717045556aa15414fa3d7e44 -size 39368 +oid sha256:33aa8cded99e514a582d6fd92a310504ec186760029b398d23eace4a9eb9f0a8 +size 39445 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png index 066f8ee40..426003b01 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70cdca11587c9b57e226b6d06cdae626c01242e2cf09e55e714319ded52d8be6 -size 53688 +oid sha256:2a4af7eb6420b8c25cc6ed173df66f8fd90fafbfa7e17c08e2d8ff028c2a160b +size 53702 diff --git a/examples/notebooks/screenshots/no-imgui-nb-lines.png b/examples/notebooks/screenshots/no-imgui-nb-lines.png index 494b82301..51578e9dd 100644 --- a/examples/notebooks/screenshots/no-imgui-nb-lines.png +++ b/examples/notebooks/screenshots/no-imgui-nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a2d67ab6fc37960ddb15e6cf33fbb9db99647b7eafb7be07334f205e29cc1fb +oid sha256:94c64dfe5bef35e68e1800f5d65e2453ea35378ac44de73958bcc6083d57ea0d size 23544 diff --git a/examples/screenshots/extent_frac_layout.png b/examples/screenshots/extent_frac_layout.png index 3059e78d0..1ee20fa64 100644 --- a/examples/screenshots/extent_frac_layout.png +++ b/examples/screenshots/extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5563fc8bd9f7b24f3f25a98aa1ce6de3afea9b5c1f1cd7cc580fa4866fe796 -size 144210 +oid sha256:23cd21882c3599d7b911c70df62d38aac9bc2d180dbbf05dd40b9561c6c2329a +size 144438 diff --git a/examples/screenshots/extent_layout.png b/examples/screenshots/extent_layout.png index 23303dd28..27eb67784 100644 --- a/examples/screenshots/extent_layout.png +++ b/examples/screenshots/extent_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1716c3f2e1309643a53759e9b5226440c9ac2e6be930f52439305a91c5ca914 -size 117466 +oid sha256:e5316e61ae1fb48acc439f38f77306d17834e8dd295c98a58980a803354651dd +size 117570 diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png index 0f13fd803..6853e1274 100644 --- a/examples/screenshots/gridplot.png +++ b/examples/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0cf92add943b5fafe8615200922714302386277e81cb0643d0024b9f3b31f4a -size 260740 +oid sha256:51c44285622a9abd974a7d5abcc5506778252fbcdf68d874a73aa58e820b9655 +size 260627 diff --git a/examples/screenshots/gridplot_non_square.png b/examples/screenshots/gridplot_non_square.png index 607b7c1d3..4138827bd 100644 --- a/examples/screenshots/gridplot_non_square.png +++ b/examples/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69bb806090ae96978b51f46f36394384db31add4c2b0290936859a3703f25291 -size 198799 +oid sha256:49b726b8a48445936ed6ecd104a7221d11549efe09a75c9deb44679a4fe0b195 +size 198943 diff --git a/examples/screenshots/gridplot_viewports_check.png b/examples/screenshots/gridplot_viewports_check.png index 34fd20c6c..77793538a 100644 --- a/examples/screenshots/gridplot_viewports_check.png +++ b/examples/screenshots/gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5569c549098c0b02d32a76f82897f32f748fc297b20b71b9081da551768d467 -size 45126 +oid sha256:d1b31b17a2ea1d13560752a9881b0698e5edb46d9271d91cce47d2becf267888 +size 46471 diff --git a/examples/screenshots/heatmap.png b/examples/screenshots/heatmap.png index a6cbbb91d..be0f95af9 100644 --- a/examples/screenshots/heatmap.png +++ b/examples/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75cf8ae3241a526044fbcd920f36a77c27752ce06cb920bd2ed6b7b8041f6125 -size 89156 +oid sha256:4ff60beab679b2e922cee1ce2525a65e14f599bf54344a4b3457c57d7afd054f +size 89365 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png index c2b466eca..6576061f8 100644 --- a/examples/screenshots/image_cmap.png +++ b/examples/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3cfe74816efe00318c6a73d2411458a84af486f1d3099ca80807c5fd9fc3d73 +oid sha256:c7c252a2af881092557ec77f00832a55a2a7fc5b7bd6bb940050bb7054daff14 size 209290 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png index 85905def5..79e3f2690 100644 --- a/examples/screenshots/image_rgb.png +++ b/examples/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:152f02896e634a5424ef3045cdb256c05831d76ea37c7b222f42c8009f60aaae -size 234270 +oid sha256:c6842168344b5eda6d33f5a0bfc7524bdada6faee333597f11684ab35ae9bba3 +size 234360 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png index c9453b9ff..b5e5395c3 100644 --- a/examples/screenshots/image_rgbvminvmax.png +++ b/examples/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6da94023f204e3feb6aaa52505c32d5218b111295691e622a1f04300855fa64 -size 43251 +oid sha256:bf2310bab00658375cee4f0c9627c280afa59f9bbae06a1447ba5ae4188885c6 +size 43386 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png index 5ef9fb7a6..0402fd9d7 100644 --- a/examples/screenshots/image_simple.png +++ b/examples/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34e67246f8880636cf31e59c9624d2361880bdc5f539457fa98e484f3605d080 -size 209033 +oid sha256:0fca2ebadff656b3e89e67e9288c4c2eb33b0933c9913ec615d62a167579ccdf +size 209081 diff --git a/examples/screenshots/image_small.png b/examples/screenshots/image_small.png index e67c4a22e..eedd4b5ca 100644 --- a/examples/screenshots/image_small.png +++ b/examples/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1a336451b1eea664734b7331e8e87de6af7cd92f7bfae44fbb91aa05d79d092 -size 12876 +oid sha256:2e8e8a4a4302d1e6bbba477755c11cfa57765a87c0a9c69bc186bb8a7d7ea009 +size 13079 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png index c9453b9ff..b5e5395c3 100644 --- a/examples/screenshots/image_vminvmax.png +++ b/examples/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6da94023f204e3feb6aaa52505c32d5218b111295691e622a1f04300855fa64 -size 43251 +oid sha256:bf2310bab00658375cee4f0c9627c280afa59f9bbae06a1447ba5ae4188885c6 +size 43386 diff --git a/examples/screenshots/image_volume_mip.png b/examples/screenshots/image_volume_mip.png index 93aa45696..200685634 100644 --- a/examples/screenshots/image_volume_mip.png +++ b/examples/screenshots/image_volume_mip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9910e9b641a41efff7899c1a7561eb5c735f935eac533aeee9863167063c2aaf -size 160929 +oid sha256:13a2911b6f0d105d777400305f01edf60243a6d4efd4eea5d23ccb6b1a8eb79d +size 161588 diff --git a/examples/screenshots/image_volume_multi_channel.png b/examples/screenshots/image_volume_multi_channel.png index c539f1034..18ae331de 100644 --- a/examples/screenshots/image_volume_multi_channel.png +++ b/examples/screenshots/image_volume_multi_channel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f81c9576f42c75e9f6bd280d5e12e2e0daefc0f3b1b77d6419cff1247f6c6c4b -size 194168 +oid sha256:66233e1fd5a227a674a730afb12dd1c445b57a5f3b953454715735ecd2f95db8 +size 194287 diff --git a/examples/screenshots/image_volume_non_orthogonal_slicing.png b/examples/screenshots/image_volume_non_orthogonal_slicing.png index 1db9e6df2..7f59f073e 100644 --- a/examples/screenshots/image_volume_non_orthogonal_slicing.png +++ b/examples/screenshots/image_volume_non_orthogonal_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af3e294b7e7da66ab9ded9ba876028155a4e6e60f8645f333b48fecae0b33b54 -size 50977 +oid sha256:e4d0260a8366e7387a164537acae61159ac7c96a80fc0dea2c529fe63fa583eb +size 51823 diff --git a/examples/screenshots/image_volume_render_modes.png b/examples/screenshots/image_volume_render_modes.png index cfe46a475..f478d0e55 100644 --- a/examples/screenshots/image_volume_render_modes.png +++ b/examples/screenshots/image_volume_render_modes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91bb33e51ff719c6a5eec13329cb2a052e83ea57897a09fcdfd8f97a049242cf -size 58720 +oid sha256:022f475b961a962a03f4e83bd2544c5ef92a81d69df72745f66a8c4c8df4a174 +size 58892 diff --git a/examples/screenshots/image_widget.png b/examples/screenshots/image_widget.png index f7cae557b..89638797b 100644 --- a/examples/screenshots/image_widget.png +++ b/examples/screenshots/image_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:172b8929393dee72fbf49dd29fcc2ad5e63eab098d23623c9de296660855223a -size 188287 +oid sha256:4164e333375b56b1e9ea8264df820709f4cea5844460bc544e2b6dda9c26bd9a +size 188062 diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index 5c0e40831..db24f0148 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ef54ed5ea9898d17f0f62cf06725b4557164e095b853b8170acdc4be1635587 -size 242925 +oid sha256:fe4a0dd3f7140a652ff82b49aa2090b8b1fd0641dc45be0a2f1a5f788ce113f2 +size 242559 diff --git a/examples/screenshots/image_widget_imgui.png b/examples/screenshots/image_widget_imgui.png index d989bfa02..908226978 100644 --- a/examples/screenshots/image_widget_imgui.png +++ b/examples/screenshots/image_widget_imgui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6386b3ef20e0fd87bbb2cfbdee73673620e451310729659d84ffbd64277013b9 -size 173904 +oid sha256:683262b7317cc3facd9b5906d3d79c177e2cec8a85dc6143c8cfceda5a6970d3 +size 173641 diff --git a/examples/screenshots/image_widget_single_video.png b/examples/screenshots/image_widget_single_video.png index 87066600a..00f537b89 100644 --- a/examples/screenshots/image_widget_single_video.png +++ b/examples/screenshots/image_widget_single_video.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8531303948dfa02de458e10e45e64ba82a7a6c4ab2b582de5dfa9e3ac79a5ad -size 93274 +oid sha256:205d3a66cc30663459f282bdde263ff1fe6358abcdaa602412e85d746b86ca78 +size 93048 diff --git a/examples/screenshots/image_widget_videos.png b/examples/screenshots/image_widget_videos.png index 30fa7c296..ef858f85f 100644 --- a/examples/screenshots/image_widget_videos.png +++ b/examples/screenshots/image_widget_videos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c388b89b7d61d7a461918126d7c5dc107013aff0023b951a87d716827e072a1 -size 310421 +oid sha256:833063e4af85870420108f51f8c2e4accfb645e2c16eefb2aab6c8d3c951938a +size 310096 diff --git a/examples/screenshots/image_widget_viewports_check.png b/examples/screenshots/image_widget_viewports_check.png index a70f9ac1d..31e30edf3 100644 --- a/examples/screenshots/image_widget_viewports_check.png +++ b/examples/screenshots/image_widget_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4f621b7be8d872e8f29309378fc0c1d4436aac067ceac13ddfe7e8622c5f2d6 -size 82334 +oid sha256:93980b00802158ea9eeb74828adb7ae63a03b8339335e2771a39117470788b19 +size 82255 diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index 5296a04f1..cf98b035c 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3e3fe17f1e35e6a5abb2ed312b1bbdb6b0ec3c3f8fff48be0d28f9385c76e65 -size 35881 +oid sha256:56bfbc50bc1b625d3e1d580a747bfac9b3af44a5cb6df78d75c7cc6c8a0a30ea +size 35926 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png index 19ecfcaf3..1f57f04ea 100644 --- a/examples/screenshots/line.png +++ b/examples/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bace8f1e9179eb9418c80e273dfb290be4229334542092cd5f001fc429c430f8 -size 152561 +oid sha256:10a0c3dadb2b161f51c7dc6efada4c812dddaf17888ead4ddf31f6fd9846c9f4 +size 152396 diff --git a/examples/screenshots/line_cmap.png b/examples/screenshots/line_cmap.png index d998fb0c4..02527b7cd 100644 --- a/examples/screenshots/line_cmap.png +++ b/examples/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4efa1e76e172a57f3a9b90223a8be2ddc48e6c54ea8ec94ce3f83b1fa0b6cc08 -size 37829 +oid sha256:d103a8a054254a093500c16e8feb5f7786b2d304fd0316b7bd9c98a3a7c34949 +size 37964 diff --git a/examples/screenshots/line_cmap_more.png b/examples/screenshots/line_cmap_more.png index 2b61ed446..8962cbb6b 100644 --- a/examples/screenshots/line_cmap_more.png +++ b/examples/screenshots/line_cmap_more.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e74c93cbf4e88d6eb13f599b2b70ca15c739d0ba0b9e1670431f4f5078e3fce -size 89687 +oid sha256:546dc7884850f620c789b103ab3fceb2d154cd78fafea427d6f380b1ec6c1160 +size 90061 diff --git a/examples/screenshots/line_collection_slicing.png b/examples/screenshots/line_collection_slicing.png index 9a163bdd4..b5f4db0bf 100644 --- a/examples/screenshots/line_collection_slicing.png +++ b/examples/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3283a717635f0e39c3da66e705dc988db4054d9210761265bde2f48f8e8c4fce -size 74585 +oid sha256:651dd9c28fb6f4548384571bb011a4e2dc8591d6da1cc0f3fb0d5defe0fcd498 +size 75078 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png index c34889af7..ea11d63aa 100644 --- a/examples/screenshots/line_colorslice.png +++ b/examples/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65593126a03d7368c746e5f1669109de167a16d1277f74562ce742095047af5d -size 43146 +oid sha256:9dd1fab17becdae1b118f9a19e32a4f5b2ca9945da0db00394faef26fa9a6c46 +size 43271 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png index 10a4fa573..b1db2cf6f 100644 --- a/examples/screenshots/line_dataslice.png +++ b/examples/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bee8eadc3a297dd1d666910d43ed14cc368577b94b5d8ef9f6443819eb2f1134 -size 44689 +oid sha256:ed1d8c7d20dc80433d7bf167eb769e96d49e47ceaf380b7e3602e7c8331e2b99 +size 44842 diff --git a/examples/screenshots/line_stack.png b/examples/screenshots/line_stack.png index fd68e397c..f3ab2474b 100644 --- a/examples/screenshots/line_stack.png +++ b/examples/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32aa5e6f7c47a99e2720267095c09184a3706fcfaa549661137358047ba6eab8 -size 55274 +oid sha256:79825955b81f94f70625ee0845911bd13bd2726667a6b2db2896df1852aab944 +size 55457 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index 99830c8f4..8bffafba1 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:558a8d46400d51abd319e73e7953bda0ec36c99a18db0bdfcdefdf96dd77d630 -size 62670 +oid sha256:6243ceed25fbb7932c3b5f8a6400c6049bdb074a7dddfab5ed620e75c1e39680 +size 62841 diff --git a/examples/screenshots/linear_selector.png b/examples/screenshots/linear_selector.png index dfa96362b..122960918 100644 --- a/examples/screenshots/linear_selector.png +++ b/examples/screenshots/linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b7973ea6abdbbeb5feb9fdeb3c94ed7f0e4930c8b98f304f2938329678686a7 -size 105205 +oid sha256:fe3b3ea8beb0d4a50f24a748842e4b3eebc1ebbb7824caf3625c55f38a553c80 +size 106794 diff --git a/examples/screenshots/no-imgui-extent_frac_layout.png b/examples/screenshots/no-imgui-extent_frac_layout.png index cf2dfd9d2..e19263a19 100644 --- a/examples/screenshots/no-imgui-extent_frac_layout.png +++ b/examples/screenshots/no-imgui-extent_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b1196c6c708f2c91c79fabedd94de512b2026a68238a7befe7dddda02a817cf +oid sha256:c2994279094daf35ec203df4b7951e496aaf12767d570f81a792df1172a412a8 size 144324 diff --git a/examples/screenshots/no-imgui-gridplot.png b/examples/screenshots/no-imgui-gridplot.png index 2ce5e1195..752afa712 100644 --- a/examples/screenshots/no-imgui-gridplot.png +++ b/examples/screenshots/no-imgui-gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea09532f4c5a0887f4f33604294d20c30e720914c924198fc15d9cae6f185468 -size 298970 +oid sha256:ac8a61bb685f201dfcabe48c8241dbb41c5f824213dd8399117cad8e2f178906 +size 298976 diff --git a/examples/screenshots/no-imgui-gridplot_viewports_check.png b/examples/screenshots/no-imgui-gridplot_viewports_check.png index 95fbe37f5..61f0c5148 100644 --- a/examples/screenshots/no-imgui-gridplot_viewports_check.png +++ b/examples/screenshots/no-imgui-gridplot_viewports_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec7c66c05b8faae6cb7d6c233b618964cb1df16cdcd94b6765630488005fcc7b -size 41809 +oid sha256:8e5f030bb76739370c581715277e3da47d38ade0a041eec9b5e0d9ff9e311115 +size 41800 diff --git a/examples/screenshots/no-imgui-heatmap.png b/examples/screenshots/no-imgui-heatmap.png index 884994481..105370f32 100644 --- a/examples/screenshots/no-imgui-heatmap.png +++ b/examples/screenshots/no-imgui-heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92e99fb729e2bd40c88c0569c26df9549ec09333cebf07473c5a4b7f788c5e9e -size 92915 +oid sha256:86ba296fc5282f664a9057eb29f99c20601c025fd937540c95f60465510ece96 +size 92928 diff --git a/examples/screenshots/no-imgui-image_cmap.png b/examples/screenshots/no-imgui-image_cmap.png index 26f4b007e..e1c95ee70 100644 --- a/examples/screenshots/no-imgui-image_cmap.png +++ b/examples/screenshots/no-imgui-image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a2cde1f9b872d1697246556ea06d67951e55c99a087c18a0019e0a0803d0cd7 -size 224832 +oid sha256:e1851126cacf5482954f5ce3933cbcbc3253f2ff1cc6c103de69eb42cec9061e +size 224864 diff --git a/examples/screenshots/no-imgui-image_rgb.png b/examples/screenshots/no-imgui-image_rgb.png index 128e87ac1..4d5efaccc 100644 --- a/examples/screenshots/no-imgui-image_rgb.png +++ b/examples/screenshots/no-imgui-image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c12a73ef1b262a901fb9a7187ba0d0f30eb1a67c817ec82e53115d954243fac -size 250459 +oid sha256:61092536672c350a27f6b41a6ea9da1d5154dead93a244b2c7fc8550870c69a8 +size 250491 diff --git a/examples/screenshots/no-imgui-image_rgbvminvmax.png b/examples/screenshots/no-imgui-image_rgbvminvmax.png index 559829eab..ca8659db1 100644 --- a/examples/screenshots/no-imgui-image_rgbvminvmax.png +++ b/examples/screenshots/no-imgui-image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:729e00d84f33fd63611d99d786b162f18d5b143b449f4b7a4f0e51a042848da1 -size 43073 +oid sha256:1d5ff797c68119cbe7ea5c6ec03aa95c38b80ffce3d48c15c1f77fad2a858d42 +size 43103 diff --git a/examples/screenshots/no-imgui-image_simple.png b/examples/screenshots/no-imgui-image_simple.png index 9df136ea9..e008ba01f 100644 --- a/examples/screenshots/no-imgui-image_simple.png +++ b/examples/screenshots/no-imgui-image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5be9d8e69952d6ca591916b5c87c43871bcd2a2020546282f23b036c49da8494 -size 223741 +oid sha256:aae7bfe952402c9a0d4e9ace56736eea1e9ea7900ab90289bc62605ba33aca11 +size 223774 diff --git a/examples/screenshots/no-imgui-image_vminvmax.png b/examples/screenshots/no-imgui-image_vminvmax.png index 559829eab..ca8659db1 100644 --- a/examples/screenshots/no-imgui-image_vminvmax.png +++ b/examples/screenshots/no-imgui-image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:729e00d84f33fd63611d99d786b162f18d5b143b449f4b7a4f0e51a042848da1 -size 43073 +oid sha256:1d5ff797c68119cbe7ea5c6ec03aa95c38b80ffce3d48c15c1f77fad2a858d42 +size 43103 diff --git a/examples/screenshots/no-imgui-image_volume_mip.png b/examples/screenshots/no-imgui-image_volume_mip.png index 5185b87ef..f35dd7c33 100644 --- a/examples/screenshots/no-imgui-image_volume_mip.png +++ b/examples/screenshots/no-imgui-image_volume_mip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1320d82fedf7683504377629acc547513ec3fec0643889fef36560cd27e8c986 -size 171432 +oid sha256:39c5da8bb512f2200f53a183030ee5071ee4bdd0462a2ab5a79979816b3c8086 +size 171440 diff --git a/examples/screenshots/no-imgui-image_volume_multi_channel.png b/examples/screenshots/no-imgui-image_volume_multi_channel.png index 01bc4e566..b8e106ecb 100644 --- a/examples/screenshots/no-imgui-image_volume_multi_channel.png +++ b/examples/screenshots/no-imgui-image_volume_multi_channel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d23e5c3755351bbd37250ae6ecb39374ff2bdfe6599c157c37f65e3ddf9c9198 -size 204892 +oid sha256:30b0f3b18dfeff123d5287012347fec09a9ab16277b5e6d2beb9039699da9954 +size 204901 diff --git a/examples/screenshots/no-imgui-line.png b/examples/screenshots/no-imgui-line.png index a9f1cbd3c..3ba59c9d7 100644 --- a/examples/screenshots/no-imgui-line.png +++ b/examples/screenshots/no-imgui-line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f92fdb19a8a4c595e815b7099021896a8f5cf8f4d9342da6cc4aa5168227260 -size 154363 +oid sha256:8fe7456b142edf45e8f51f82ea7a80af2152a9d22b485d77804e56bc1768412f +size 154361 diff --git a/examples/screenshots/no-imgui-line_cmap.png b/examples/screenshots/no-imgui-line_cmap.png index 61f550f9f..ede3a56f3 100644 --- a/examples/screenshots/no-imgui-line_cmap.png +++ b/examples/screenshots/no-imgui-line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10f15a76991ce45b539c32b858dd1c4e8a8bf8f88be10a6d9887012954f2fd22 +oid sha256:82312d1eebf0f31ba3d9d409f15e8e5dafb5fe4ccce64e7cb941f0780861f73d size 36599 diff --git a/examples/screenshots/no-imgui-line_collection_slicing.png b/examples/screenshots/no-imgui-line_collection_slicing.png index 4b4511036..7be9a32fd 100644 --- a/examples/screenshots/no-imgui-line_collection_slicing.png +++ b/examples/screenshots/no-imgui-line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c35085f9253ca5026bcbde45eabad8aedff9af9f67db3a9cf9e61d915e2b1c52 -size 73302 +oid sha256:7c7e6d0a301ab18640ae98867610549af33ef2b9e89b76d306a5e0d276906e99 +size 73997 diff --git a/examples/screenshots/no-imgui-line_colorslice.png b/examples/screenshots/no-imgui-line_colorslice.png index cb59732e1..4e6168bd4 100644 --- a/examples/screenshots/no-imgui-line_colorslice.png +++ b/examples/screenshots/no-imgui-line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9e75cf01d7680ea1a9a178290580ebbe5f58015f7051d486fa0ad999a611dcb -size 41091 +oid sha256:424040bc42e702b0adc4da95c854edbfbb8c15b598fb9254d4daa9590e9ea0ad +size 41087 diff --git a/examples/screenshots/no-imgui-line_dataslice.png b/examples/screenshots/no-imgui-line_dataslice.png index 7d9b00227..e755fc270 100644 --- a/examples/screenshots/no-imgui-line_dataslice.png +++ b/examples/screenshots/no-imgui-line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c52fc239e13555dc466be86de0816a3ed785639a817cb248b7353cf20e33bc93 -size 43603 +oid sha256:cf7cca31763750345014ee4314e915e2cd76245ae9d489a05a9983235a932df2 +size 43566 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 4335083c4..cec12c158 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d651a7d7d83a1b0afb240efde28acbde71fbc6be17719df11a35e76da318e75e -size 60609 +oid sha256:bbdc5f9047465d256ee1358bf9f1ecc679f1f7484b50f369442d64bf7f18da3a +size 60305 diff --git a/examples/screenshots/no-imgui-linear_selector.png b/examples/screenshots/no-imgui-linear_selector.png index 425b306e7..1580027a8 100644 --- a/examples/screenshots/no-imgui-linear_selector.png +++ b/examples/screenshots/no-imgui-linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77400138a435e9e4a884bed699ca814a820d5d2f28ef24d1e0e3dad6483f2302 -size 104087 +oid sha256:6490694a4c7f266e62e953b3430a30511f3761077a961ce2547ab6d758ad9180 +size 105556 diff --git a/examples/screenshots/no-imgui-rect_frac_layout.png b/examples/screenshots/no-imgui-rect_frac_layout.png index cf2dfd9d2..e19263a19 100644 --- a/examples/screenshots/no-imgui-rect_frac_layout.png +++ b/examples/screenshots/no-imgui-rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b1196c6c708f2c91c79fabedd94de512b2026a68238a7befe7dddda02a817cf +oid sha256:c2994279094daf35ec203df4b7951e496aaf12767d570f81a792df1172a412a8 size 144324 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index 5e1ba9e57..d2b7fe814 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df4c38dce92fc81738a77464a4f0872ce4738d546d0c422938108f54495a3cdb -size 43922 +oid sha256:52f8ca8276e89b37811d9c6e63479adafebc336035e7fe4d6c0bc2e7020737d3 +size 44151 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 9a8fb0461..014376df7 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4fd239939a34daf48da95e16a90ffe9342a456638284afbc552c9b09aea0182 -size 24292 +oid sha256:341b2b24ec810ccc6dde388c8ecef42a6022b78270087c2a1c85b22d79b9bc84 +size 28191 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index 7debfa887..1134126b9 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2701081a1a9666b1da683ce3b56ae12b11eed3bf3d52e1f253abc330c7caccb9 -size 26403 +oid sha256:e6e09668ba3aa56f5b5b77fa9749982e89e5a974af8f11364896c3811f8629e8 +size 26590 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index ef01b5d5e..5f25e6247 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:413552ed9ff77dc51510266817a09006586a2dcf3c6706d18c032d26d8e8397e -size 25779 +oid sha256:b0f086b794b91cec140f2bf63decc31cdff1798f62aa9ac3b5fab0763361ab8a +size 25954 diff --git a/examples/screenshots/rect_frac_layout.png b/examples/screenshots/rect_frac_layout.png index 3059e78d0..1ee20fa64 100644 --- a/examples/screenshots/rect_frac_layout.png +++ b/examples/screenshots/rect_frac_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa5563fc8bd9f7b24f3f25a98aa1ce6de3afea9b5c1f1cd7cc580fa4866fe796 -size 144210 +oid sha256:23cd21882c3599d7b911c70df62d38aac9bc2d180dbbf05dd40b9561c6c2329a +size 144438 diff --git a/examples/screenshots/rect_layout.png b/examples/screenshots/rect_layout.png index 23303dd28..27eb67784 100644 --- a/examples/screenshots/rect_layout.png +++ b/examples/screenshots/rect_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1716c3f2e1309643a53759e9b5226440c9ac2e6be930f52439305a91c5ca914 -size 117466 +oid sha256:e5316e61ae1fb48acc439f38f77306d17834e8dd295c98a58980a803354651dd +size 117570 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index e8c87cdfc..57faa7feb 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b71e5397b6eebff7ad540463515e0cba15cf8d4a9c63a2303d737c8f199c7d8 -size 45051 +oid sha256:5fdce90b719794c08edb2210dfedb0a8229438435cfe502e569c27e3d7dba230 +size 45582 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index 5c5d6d4be..4ac153675 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9549c19c4d9ad6804036a4b1018ae438793cb350228d7f00fc0e38f74cd058b9 -size 25893 +oid sha256:f207f84a7ff2acc7cf28e798dc01f063c0d3a9a53db103561e01e038ce45cf97 +size 29644 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index 65599a790..d59514086 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a39a4a3f705c3dff50c3dad0a70d3d8412e5a5672169c7a68b90abb6030861bc -size 27533 +oid sha256:b89bf9a1563fdbc0076a058e2d7d21a5d508c7579f6c1c46871de1c608949af3 +size 27833 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index b74c04a06..be2872c3c 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:862d8d1c5025add59f37206b88184eee8977ef6f9ada4518013c8270459288d7 -size 26977 +oid sha256:62cc26b015cd588b7a40146aa464efc451d8acaee39e1f0031a586b37f1304fe +size 27352 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index c0867d92c..ca413760a 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f67e1ad8120b2077002459917e99cd3e80a9b1ccc55e76923f2d86c791c8100d +oid sha256:20fa7dd6e09d8c4deccf3cf7166c73d36f194c7a4f6cb24fc605e9571e886cce size 44273 From de63805d51b7606198053b9bbedf9b84badf9682 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 21 Oct 2025 08:40:43 -0400 Subject: [PATCH 19/52] fewer github actions for CI (#925) --- .github/workflows/ci-pygfx-release.yml | 11 ++++++----- .github/workflows/ci.yml | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-pygfx-release.yml b/.github/workflows/ci-pygfx-release.yml index 8c7a64ec4..e1c7f09cd 100644 --- a/.github/workflows/ci-pygfx-release.yml +++ b/.github/workflows/ci-pygfx-release.yml @@ -1,4 +1,4 @@ -name: CI +name: CI-pygfx-release on: push: @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.11", "3.12", "3.13"] + python: ["3.11", "3.13"] imgui_dep: ["imgui", ""] notebook_dep: ["notebook", ""] os: ["ubuntu-latest", "macos-latest"] @@ -63,19 +63,20 @@ jobs: run: | pytest -v tests/ - name: Test examples + if: ${{ matrix.python == '3.13' }} env: RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v examples/ - name: Test examples notebooks, exclude ImageWidget notebook - if: ${{ matrix.notebook_dep == 'notebook' }} + if: ${{ matrix.notebook_dep == 'notebook' && matrix.python == '3.13' }} env: FASTPLOTLIB_NB_TESTS: 1 # test notebooks, exclude ImageWidget notebooks run: pytest --nbmake $(find ./examples/notebooks/ -maxdepth 1 -type f -name "*.ipynb" ! -name "image_widget*.ipynb" -print | xargs) - - name: Test ImageWidget notebooks + - name: Test ImageWidget notebook # test image widget notebooks only if imgui is installed - if: ${{ matrix.notebook_dep == 'notebook' && matrix.imgui_dep == 'imgui' }} + if: ${{ matrix.notebook_dep == 'notebook' && matrix.imgui_dep == 'imgui' && matrix.python == '3.13' }} env: FASTPLOTLIB_NB_TESTS: 1 run: pytest --nbmake $(find ./examples/notebooks/ -maxdepth 1 -type f -name "image_widget*.ipynb" -print | xargs) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2468783d..621870329 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI-pygfx-main on: push: @@ -15,13 +15,13 @@ on: jobs: test-build-full: - name: Tests + name: Tests - pygfx main timeout-minutes: 20 if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false matrix: - python: ["3.11", "3.12", "3.13"] + python: ["3.11", "3.13"] imgui_dep: ["imgui", ""] notebook_dep: ["notebook", ""] os: ["ubuntu-latest", "macos-latest"] @@ -69,19 +69,20 @@ jobs: run: | pytest -v tests/ - name: Test examples + if: ${{ matrix.python == '3.13' }} env: RENDERCANVAS_FORCE_OFFSCREEN: 1 run: | pytest -v examples/ - name: Test examples notebooks, exclude ImageWidget notebook - if: ${{ matrix.notebook_dep == 'notebook' }} + if: ${{ matrix.notebook_dep == 'notebook' && matrix.python == '3.13' }} env: FASTPLOTLIB_NB_TESTS: 1 # test notebooks, exclude ImageWidget notebooks run: pytest --nbmake $(find ./examples/notebooks/ -maxdepth 1 -type f -name "*.ipynb" ! -name "image_widget*.ipynb" -print | xargs) - - name: Test ImageWidget notebooks + - name: Test ImageWidget notebook # test image widget notebooks only if imgui is installed - if: ${{ matrix.notebook_dep == 'notebook' && matrix.imgui_dep == 'imgui' }} + if: ${{ matrix.notebook_dep == 'notebook' && matrix.imgui_dep == 'imgui' && matrix.python == '3.13' }} env: FASTPLOTLIB_NB_TESTS: 1 run: pytest --nbmake $(find ./examples/notebooks/ -maxdepth 1 -type f -name "image_widget*.ipynb" -print | xargs) From 522c296b28d5107e4e53a847d20f61cdbc9ba46d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 21 Oct 2025 08:42:58 -0400 Subject: [PATCH 20/52] skip existing arrays in ImageWidget.set_data() (#924) --- fastplotlib/widgets/image_widget/_widget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fastplotlib/widgets/image_widget/_widget.py b/fastplotlib/widgets/image_widget/_widget.py index 9668b7182..715fe3489 100644 --- a/fastplotlib/widgets/image_widget/_widget.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -929,6 +929,11 @@ def set_data( for i, (new_array, current_array, subplot) in enumerate( zip(new_data, self._data, self.figure) ): + # if the new array is the same as the existing array, skip + # this allows setting just a subset of the arrays in the ImageWidget + if new_data is self._data[i]: + continue + # check last two dims (x and y) to see if data shape is changing old_data_shape = self._data[i].shape[-self.n_img_dims[i] :] self._data[i] = new_array From 8b128d34008a315f8b990ad149fe09d4bbf21d67 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 22 Oct 2025 18:36:05 -0400 Subject: [PATCH 21/52] add animation func getter and `clear_animations()` method (#896) * add animation func getter and clear() method * requested changes * fix kwarg name * requested changes * update api docs --- docs/source/api/layouts/figure.rst | 2 ++ docs/source/api/layouts/imgui_figure.rst | 2 ++ docs/source/api/layouts/subplot.rst | 2 ++ fastplotlib/layouts/_figure.py | 36 ++++++++++++++++++++++++ fastplotlib/layouts/_plot_area.py | 36 ++++++++++++++++++++++++ 5 files changed, 78 insertions(+) diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index d191fe8ce..e306710be 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: Figure_api + Figure.animations Figure.cameras Figure.canvas Figure.controllers @@ -38,6 +39,7 @@ Methods Figure.add_animations Figure.add_subplot Figure.clear + Figure.clear_animations Figure.close Figure.export Figure.export_numpy diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index e1922a9f4..959a98743 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: ImguiFigure_api + ImguiFigure.animations ImguiFigure.cameras ImguiFigure.canvas ImguiFigure.controllers @@ -42,6 +43,7 @@ Methods ImguiFigure.add_gui ImguiFigure.add_subplot ImguiFigure.clear + ImguiFigure.clear_animations ImguiFigure.close ImguiFigure.export ImguiFigure.export_numpy diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index bc2b3aa29..0183c7e63 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: Subplot_api + Subplot.animations Subplot.axes Subplot.background_color Subplot.camera @@ -57,6 +58,7 @@ Methods Subplot.center_graphic Subplot.center_scene Subplot.clear + Subplot.clear_animations Subplot.delete_graphic Subplot.get_figure Subplot.insert_graphic diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a4e219757..8fd5dc666 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -543,6 +543,11 @@ def show_tooltips(self) -> bool: """show/hide tooltips for all graphics""" return self._show_tooltips + @property + def animations(self) -> dict[str, list[callable]]: + """Returns a dictionary of 'pre' and 'post' animation functions.""" + return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} + @show_tooltips.setter def show_tooltips(self, val: bool): self._show_tooltips = val @@ -765,6 +770,37 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) + def clear_animations(self, removal: str = None): + """ + Remove animation functions. + + Parameters + ---------- + removal: str, default ``None`` + The type of animation functions to clear. One of 'pre' or 'post'. If `None`, removes all animation + functions. + """ + if removal is None: + # remove all + for func in self._animate_funcs_pre: + self._animate_funcs_pre.remove(func) + + for func in self._animate_funcs_post: + self._animate_funcs_post.remove(func) + elif removal == "pre": + # only pre + for func in self._animate_funcs_pre: + self._animate_funcs_pre.remove(func) + elif removal == "post": + # only post + for func in self._animate_funcs_post: + self._animate_funcs_post.remove(func) + else: + raise ValueError( + f"Animation type: {removal} must be one of 'pre' or 'post'. To remove all animation " + f"functions, pass `type=None`" + ) + def clear(self): """Clear all Subplots""" for subplot in self: diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 7b5c7952a..3c5027caf 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -274,6 +274,11 @@ def background_color(self, colors: str | tuple[float]): """1, 2, or 4 colors, each color must be acceptable by pygfx.Color""" self._background_material.set_colors(*colors) + @property + def animations(self) -> dict[str, list[callable]]: + """Returns a dictionary of 'pre' and 'post' animation functions.""" + return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} + def map_screen_to_world( self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False ) -> np.ndarray | None: @@ -395,6 +400,37 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) + def clear_animations(self, removal: str = None): + """ + Remove animation functions. + + Parameters + ---------- + removal: str, default ``None`` + The type of animation functions to clear. One of 'pre' or 'post'. If `None`, removes all animation + functions. + """ + if removal is None: + # remove all + for func in self._animate_funcs_pre: + self._animate_funcs_pre.remove(func) + + for func in self._animate_funcs_post: + self._animate_funcs_post.remove(func) + elif removal == "pre": + # only pre + for func in self._animate_funcs_pre: + self._animate_funcs_pre.remove(func) + elif removal == "post": + # only post + for func in self._animate_funcs_post: + self._animate_funcs_post.remove(func) + else: + raise ValueError( + f"Animation type: {removal} must be one of 'pre' or 'post'. To remove all animation " + f"functions, pass `type=None`" + ) + def _sort_images_by_depth(self): """ In general, we want to avoid setting the offset of a graphic, because the From 12849813ac880e3304951fb0739c6e569a81d721 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 22 Oct 2025 20:28:21 -0400 Subject: [PATCH 22/52] Scatter markers and other scatter features (#913) * cleanup * start per-vertex markers * vectorized marker code * more scatter stuff * progress * progress * GraphicFeatures.property_name is now an instance attribute instead of a class attribute * all point features implemented, not a single thing tested yet * docstring * some reorganization of scatter and line related features * docstring * fix * update fastplotlib/layouts/_graphic_methods_mixin.py * basics work * modify iris example to use markers * scatter tests, black * add asterisk to test * sprites example * update examples * add scatter validation example * fixes * fix * more tests * more fixes and tests * markers buffer manager tests * docstring * rotation fixes and tests * add a useful assert to positions data buffer manager test * point_rotations tests * need float32 else all tests fail lulz * black * skip graphic features that are None * fix * fix * update scatter screenshot * update scatter API docs * update event tables * increase tolerance because of mac * I guess let's keep the higher 0.2 threshold * remove renamed feature docs * finish sentence --- .../source/api/graphic_features/EdgeWidth.rst | 35 ++ .../graphic_features/PointsSizesFeature.rst | 37 -- .../api/graphic_features/UniformEdgeColor.rst | 35 ++ .../api/graphic_features/UniformMarker.rst | 35 ++ .../api/graphic_features/UniformRotations.rst | 35 ++ .../api/graphic_features/VertexCmap.rst | 1 - .../api/graphic_features/VertexColors.rst | 1 - .../api/graphic_features/VertexMarkers.rst | 37 ++ .../api/graphic_features/VertexPointSizes.rst | 36 ++ .../api/graphic_features/VertexPositions.rst | 1 - .../api/graphic_features/VertexRotations.rst | 36 ++ docs/source/api/graphic_features/index.rst | 12 +- docs/source/api/graphics/ScatterGraphic.rst | 7 + docs/source/user_guide/event_tables.rst | 138 ++++- examples/notebooks/test_gc.ipynb | 5 + examples/scatter/scatter_image_as_points.py | 55 ++ examples/scatter/scatter_iris.py | 30 +- examples/scatter/scatter_validate.py | 77 +++ examples/screenshots/imgui_basic.png | 4 +- .../no-imgui-scatter_cmap_iris.png | 4 +- .../no-imgui-scatter_colorslice_iris.png | 4 +- .../no-imgui-scatter_dataslice_iris.png | 4 +- .../no-imgui-scatter_image_as_points.png | 3 + .../screenshots/no-imgui-scatter_iris.png | 4 +- .../screenshots/no-imgui-scatter_size.png | 4 +- .../screenshots/no-imgui-scatter_validate.png | 3 + examples/screenshots/scatter_cmap_iris.png | 4 +- .../screenshots/scatter_colorslice_iris.png | 4 +- .../screenshots/scatter_dataslice_iris.png | 4 +- .../screenshots/scatter_image_as_points.png | 3 + examples/screenshots/scatter_iris.png | 4 +- examples/screenshots/scatter_size.png | 4 +- examples/screenshots/scatter_validate.png | 3 + examples/tests/test_examples.py | 2 +- fastplotlib/graphics/_base.py | 7 + fastplotlib/graphics/_positions_base.py | 5 +- fastplotlib/graphics/features/__init__.py | 26 +- fastplotlib/graphics/features/_base.py | 16 +- fastplotlib/graphics/features/_common.py | 60 +- fastplotlib/graphics/features/_image.py | 36 +- fastplotlib/graphics/features/_line.py | 28 + .../graphics/features/_positions_graphics.py | 184 +----- fastplotlib/graphics/features/_scatter.py | 574 ++++++++++++++++++ .../graphics/features/_selection_features.py | 16 +- fastplotlib/graphics/features/_text.py | 20 +- fastplotlib/graphics/features/_volume.py | 34 +- fastplotlib/graphics/scatter.py | 341 ++++++++++- fastplotlib/layouts/_graphic_methods_mixin.py | 102 +++- scripts/generate_add_graphic_methods.py | 1 + tests/conftest.py | 8 +- tests/test_common_features.py | 8 +- tests/test_figure.py | 50 +- tests/test_image_volume_graphic.py | 8 +- tests/test_markers_buffer_manager.py | 143 +++++ tests/test_point_rotations_buffer_manager.py | 120 ++++ tests/test_positions_data_buffer_manager.py | 1 + tests/test_positions_graphics.py | 45 +- tests/test_scatter_graphic.py | 278 +++++++++ tests/test_sizes_buffer_manager.py | 6 +- tests/test_texture_array_volume.py | 17 +- tests/utils.py | 51 ++ 61 files changed, 2382 insertions(+), 474 deletions(-) create mode 100644 docs/source/api/graphic_features/EdgeWidth.rst delete mode 100644 docs/source/api/graphic_features/PointsSizesFeature.rst create mode 100644 docs/source/api/graphic_features/UniformEdgeColor.rst create mode 100644 docs/source/api/graphic_features/UniformMarker.rst create mode 100644 docs/source/api/graphic_features/UniformRotations.rst create mode 100644 docs/source/api/graphic_features/VertexMarkers.rst create mode 100644 docs/source/api/graphic_features/VertexPointSizes.rst create mode 100644 docs/source/api/graphic_features/VertexRotations.rst create mode 100644 examples/scatter/scatter_image_as_points.py create mode 100644 examples/scatter/scatter_validate.py create mode 100644 examples/screenshots/no-imgui-scatter_image_as_points.png create mode 100644 examples/screenshots/no-imgui-scatter_validate.png create mode 100644 examples/screenshots/scatter_image_as_points.png create mode 100644 examples/screenshots/scatter_validate.png create mode 100644 fastplotlib/graphics/features/_line.py create mode 100644 fastplotlib/graphics/features/_scatter.py create mode 100644 tests/test_markers_buffer_manager.py create mode 100644 tests/test_point_rotations_buffer_manager.py create mode 100644 tests/test_scatter_graphic.py diff --git a/docs/source/api/graphic_features/EdgeWidth.rst b/docs/source/api/graphic_features/EdgeWidth.rst new file mode 100644 index 000000000..ba912dc2a --- /dev/null +++ b/docs/source/api/graphic_features/EdgeWidth.rst @@ -0,0 +1,35 @@ +.. _api.EdgeWidth: + +EdgeWidth +********* + +========= +EdgeWidth +========= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: EdgeWidth_api + + EdgeWidth + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: EdgeWidth_api + + EdgeWidth.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: EdgeWidth_api + + EdgeWidth.add_event_handler + EdgeWidth.block_events + EdgeWidth.clear_event_handlers + EdgeWidth.remove_event_handler + EdgeWidth.set_value + diff --git a/docs/source/api/graphic_features/PointsSizesFeature.rst b/docs/source/api/graphic_features/PointsSizesFeature.rst deleted file mode 100644 index f3f78b74b..000000000 --- a/docs/source/api/graphic_features/PointsSizesFeature.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _api.PointsSizesFeature: - -PointsSizesFeature -****************** - -================== -PointsSizesFeature -================== -.. currentmodule:: fastplotlib.graphics.features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: PointsSizesFeature_api - - PointsSizesFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: PointsSizesFeature_api - - PointsSizesFeature.buffer - PointsSizesFeature.shared - PointsSizesFeature.value - -Methods -~~~~~~~ -.. autosummary:: - :toctree: PointsSizesFeature_api - - PointsSizesFeature.add_event_handler - PointsSizesFeature.block_events - PointsSizesFeature.clear_event_handlers - PointsSizesFeature.remove_event_handler - PointsSizesFeature.set_value - diff --git a/docs/source/api/graphic_features/UniformEdgeColor.rst b/docs/source/api/graphic_features/UniformEdgeColor.rst new file mode 100644 index 000000000..26489e6d7 --- /dev/null +++ b/docs/source/api/graphic_features/UniformEdgeColor.rst @@ -0,0 +1,35 @@ +.. _api.UniformEdgeColor: + +UniformEdgeColor +**************** + +================ +UniformEdgeColor +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformEdgeColor_api + + UniformEdgeColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformEdgeColor_api + + UniformEdgeColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformEdgeColor_api + + UniformEdgeColor.add_event_handler + UniformEdgeColor.block_events + UniformEdgeColor.clear_event_handlers + UniformEdgeColor.remove_event_handler + UniformEdgeColor.set_value + diff --git a/docs/source/api/graphic_features/UniformMarker.rst b/docs/source/api/graphic_features/UniformMarker.rst new file mode 100644 index 000000000..56b6c2fa4 --- /dev/null +++ b/docs/source/api/graphic_features/UniformMarker.rst @@ -0,0 +1,35 @@ +.. _api.UniformMarker: + +UniformMarker +************* + +============= +UniformMarker +============= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformMarker_api + + UniformMarker + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformMarker_api + + UniformMarker.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformMarker_api + + UniformMarker.add_event_handler + UniformMarker.block_events + UniformMarker.clear_event_handlers + UniformMarker.remove_event_handler + UniformMarker.set_value + diff --git a/docs/source/api/graphic_features/UniformRotations.rst b/docs/source/api/graphic_features/UniformRotations.rst new file mode 100644 index 000000000..f834dbe20 --- /dev/null +++ b/docs/source/api/graphic_features/UniformRotations.rst @@ -0,0 +1,35 @@ +.. _api.UniformRotations: + +UniformRotations +**************** + +================ +UniformRotations +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformRotations_api + + UniformRotations + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformRotations_api + + UniformRotations.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformRotations_api + + UniformRotations.add_event_handler + UniformRotations.block_events + UniformRotations.clear_event_handlers + UniformRotations.remove_event_handler + UniformRotations.set_value + diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst index fd1013620..57b9d6311 100644 --- a/docs/source/api/graphic_features/VertexCmap.rst +++ b/docs/source/api/graphic_features/VertexCmap.rst @@ -22,7 +22,6 @@ Properties VertexCmap.buffer VertexCmap.name - VertexCmap.shared VertexCmap.transform VertexCmap.value diff --git a/docs/source/api/graphic_features/VertexColors.rst b/docs/source/api/graphic_features/VertexColors.rst index d09da7a18..b72b7564a 100644 --- a/docs/source/api/graphic_features/VertexColors.rst +++ b/docs/source/api/graphic_features/VertexColors.rst @@ -21,7 +21,6 @@ Properties :toctree: VertexColors_api VertexColors.buffer - VertexColors.shared VertexColors.value Methods diff --git a/docs/source/api/graphic_features/VertexMarkers.rst b/docs/source/api/graphic_features/VertexMarkers.rst new file mode 100644 index 000000000..bea8dd346 --- /dev/null +++ b/docs/source/api/graphic_features/VertexMarkers.rst @@ -0,0 +1,37 @@ +.. _api.VertexMarkers: + +VertexMarkers +************* + +============= +VertexMarkers +============= +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexMarkers_api + + VertexMarkers + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexMarkers_api + + VertexMarkers.buffer + VertexMarkers.value + VertexMarkers.value_int + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexMarkers_api + + VertexMarkers.add_event_handler + VertexMarkers.block_events + VertexMarkers.clear_event_handlers + VertexMarkers.remove_event_handler + VertexMarkers.set_value + diff --git a/docs/source/api/graphic_features/VertexPointSizes.rst b/docs/source/api/graphic_features/VertexPointSizes.rst new file mode 100644 index 000000000..07f195f6d --- /dev/null +++ b/docs/source/api/graphic_features/VertexPointSizes.rst @@ -0,0 +1,36 @@ +.. _api.VertexPointSizes: + +VertexPointSizes +**************** + +================ +VertexPointSizes +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPointSizes_api + + VertexPointSizes + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPointSizes_api + + VertexPointSizes.buffer + VertexPointSizes.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexPointSizes_api + + VertexPointSizes.add_event_handler + VertexPointSizes.block_events + VertexPointSizes.clear_event_handlers + VertexPointSizes.remove_event_handler + VertexPointSizes.set_value + diff --git a/docs/source/api/graphic_features/VertexPositions.rst b/docs/source/api/graphic_features/VertexPositions.rst index d181f07b9..95480e1d4 100644 --- a/docs/source/api/graphic_features/VertexPositions.rst +++ b/docs/source/api/graphic_features/VertexPositions.rst @@ -21,7 +21,6 @@ Properties :toctree: VertexPositions_api VertexPositions.buffer - VertexPositions.shared VertexPositions.value Methods diff --git a/docs/source/api/graphic_features/VertexRotations.rst b/docs/source/api/graphic_features/VertexRotations.rst new file mode 100644 index 000000000..97cf5f4e2 --- /dev/null +++ b/docs/source/api/graphic_features/VertexRotations.rst @@ -0,0 +1,36 @@ +.. _api.VertexRotations: + +VertexRotations +*************** + +=============== +VertexRotations +=============== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexRotations_api + + VertexRotations + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexRotations_api + + VertexRotations.buffer + VertexRotations.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexRotations_api + + VertexRotations.add_event_handler + VertexRotations.block_events + VertexRotations.clear_event_handlers + VertexRotations.remove_event_handler + VertexRotations.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index a2b4aec47..d008b5202 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -6,12 +6,18 @@ Graphic Features VertexColors UniformColor - UniformSize SizeSpace - Thickness VertexPositions - PointsSizesFeature VertexCmap + Thickness + VertexMarkers + UniformMarker + UniformEdgeColor + EdgeWidth + UniformRotations + VertexRotations + VertexPointSizes + UniformSize TextureArray ImageCmap ImageVmin diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 48d30d01f..7f4336abe 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -28,9 +28,16 @@ Properties ScatterGraphic.colors ScatterGraphic.data ScatterGraphic.deleted + ScatterGraphic.edge_colors + ScatterGraphic.edge_width ScatterGraphic.event_handlers + ScatterGraphic.image + ScatterGraphic.markers + ScatterGraphic.mode ScatterGraphic.name ScatterGraphic.offset + ScatterGraphic.point_rotation_mode + ScatterGraphic.point_rotations ScatterGraphic.right_click_menu ScatterGraphic.rotation ScatterGraphic.size_space diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index d61bff2ee..c1f5b89e0 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -39,11 +39,11 @@ colors **event info dict** -+----------+-------------------+-----------------+ -| dict key | type | description | -+==========+===================+=================+ -| value | np.ndarray [RGBA] | new color value | -+----------+-------------------+-----------------+ ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ cmap ^^^^ @@ -217,11 +217,11 @@ colors **event info dict** -+----------+-------------------+-----------------+ -| dict key | type | description | -+==========+===================+=================+ -| value | np.ndarray [RGBA] | new color value | -+----------+-------------------+-----------------+ ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ cmap ^^^^ @@ -236,6 +236,80 @@ cmap | value | str | new cmap to set at given slice | +----------+-------+--------------------------------+ +markers +^^^^^^^ + +**event info dict** + ++----------+----------------------------------------------+------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which markers were indexed/sliced | ++----------+----------------------------------------------+------------------------------------------------+ +| value | str | np.ndarray[str] | new marker values for points that were changed | ++----------+----------------------------------------------+------------------------------------------------+ + +markers +^^^^^^^ + +**event info dict** + ++----------+------------+------------------+ +| dict key | type | description | ++==========+============+==================+ +| value | str | None | new marker value | ++----------+------------+------------------+ + +edge_colors +^^^^^^^^^^^ + +**event info dict** + ++----------+--------------------------------------------------+----------------+ +| dict key | type | description | ++==========+==================================================+================+ +| value | str | np.ndarray | pygfx.Color | Sequence[float] | new edge_color | ++----------+--------------------------------------------------+----------------+ + +edge_colors +^^^^^^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +edge_width +^^^^^^^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new edge_width | ++----------+-------+----------------+ + +image +^^^^^ + +**event info dict** + ++----------+--------------------------------------+--------------------------------------------------+ +| dict key | type | description | ++==========+======================================+==================================================+ +| key | slice, index, numpy-like fancy index | key at which image data was sliced/fancy indexed | ++----------+--------------------------------------+--------------------------------------------------+ +| value | np.ndarray | float | new data values | ++----------+--------------------------------------+--------------------------------------------------+ + size_space ^^^^^^^^^^ @@ -247,6 +321,30 @@ size_space | value | str | 'screen' | 'world' | 'model' | +----------+------+------------------------------+ +point_rotations +^^^^^^^^^^^^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new edge_width | ++----------+-------+----------------+ + +point_rotations +^^^^^^^^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+==================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which point rotations were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------+ +| value | int | float | array-like | new rotation values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------+ + name ^^^^ @@ -868,11 +966,11 @@ colors **event info dict** -+----------+-------------------+-----------------+ -| dict key | type | description | -+==========+===================+=================+ -| value | np.ndarray [RGBA] | new color value | -+----------+-------------------+-----------------+ ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ cmap ^^^^ @@ -1022,11 +1120,11 @@ colors **event info dict** -+----------+-------------------+-----------------+ -| dict key | type | description | -+==========+===================+=================+ -| value | np.ndarray [RGBA] | new color value | -+----------+-------------------+-----------------+ ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ cmap ^^^^ diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb index df08e7a2d..92000f27e 100644 --- a/examples/notebooks/test_gc.ipynb +++ b/examples/notebooks/test_gc.ipynb @@ -120,6 +120,11 @@ "\n", "for g in objects:\n", " for feature in g._features:\n", + " if not hasattr(g, f\"_{feature}\"):\n", + " continue\n", + "\n", + " if getattr(g, f\"_{feature}\") is None:\n", + " continue # not in the right mode to support this feature\n", " g.add_event_handler(feature_changed_handler, feature)" ] }, diff --git a/examples/scatter/scatter_image_as_points.py b/examples/scatter/scatter_image_as_points.py new file mode 100644 index 000000000..aeae30bd0 --- /dev/null +++ b/examples/scatter/scatter_image_as_points.py @@ -0,0 +1,55 @@ +""" +Scatter image as points +======================= + +Display a scatter using an image as the points. These are also called sprites. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +xs = np.linspace(0, 2 * np.pi, 10) + +# make sine and cosine data +sine = np.column_stack([xs, np.sin(xs)]) +cosine = np.column_stack([xs, np.cos(xs)]) + +# a simple image to display as the points +array = np.array([ + [1, 0, 1], + [0, 1, 0], + [1, 1, 1], +]) + +# load an image of Almar's cat +wikkie = np.flipud(iio.imread("imageio:wikkie.png")) + +figure = fpl.Figure(size=(700, 350)) + +scatter = figure[0, 0].add_scatter( + data=sine, + mode="image", # mode must be "image", otherwise the `image` arg is ignored and markers are used + image=array, + cmap="jet", # the image is multiplied by the scatter point colors if provided + sizes=25, +) + +scatter2 = figure[0, 0].add_scatter( + data=cosine, + mode="image", + image=wikkie, # if an RGB(A) image is provided and no colors are provided, then the image is shown as-is + sizes=40, +) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/scatter/scatter_iris.py b/examples/scatter/scatter_iris.py index 03b0ee67e..b9df16026 100644 --- a/examples/scatter/scatter_iris.py +++ b/examples/scatter/scatter_iris.py @@ -6,24 +6,36 @@ """ # test_example = true -# sphinx_gallery_pygfx_docs = 'hidden' +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np -from pathlib import Path -import sys +from sklearn.cluster import AgglomerativeClustering +from sklearn import datasets + figure = fpl.Figure(size=(700, 560)) -current_file = Path(sys.argv[0]).resolve() +data, target = datasets.load_iris(return_X_y=True) +data = data[:, :2] # use only first 2 features + +# map target class to scatter point marker +markers_map = {0: "o", 1: "s", 2: "+"} +markers = list(map(markers_map.get, target)) -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) +agg = AgglomerativeClustering(n_clusters=3) +agg.fit_predict(data) -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points +clusters_labels = agg.labels_ -scatter = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter = figure[0, 0].add_scatter( + data=data, + sizes=10, + alpha=0.7, + cmap="tab10", + cmap_transform=clusters_labels, + markers=markers, +) figure.show() diff --git a/examples/scatter/scatter_validate.py b/examples/scatter/scatter_validate.py new file mode 100644 index 000000000..abddffee0 --- /dev/null +++ b/examples/scatter/scatter_validate.py @@ -0,0 +1,77 @@ +""" +Scatter validation +================== + +Example that shows some scatter plot features for test validation. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + +xs = np.linspace(0, 2 * np.pi, 10) + +# make sine and cosine data +sine = np.column_stack([xs, np.sin(xs)]) +cosine = np.column_stack([xs, np.cos(xs)]) + +# a simple image to display as the points +array = np.array([ + [1, 0, 1], + [0, 1, 0], + [1, 1, 1], +]) + +# load an image of Almar's cat +wikkie = np.flipud(iio.imread("imageio:wikkie.png")) + +figure = fpl.Figure( + size=(700, 560) +) + +figure[0, 0].add_scatter(sine) + +# combinations of per-point markers, colors and edge colors +figure[0, 0].add_scatter( + sine, + colors=["magenta"] * 3 + ["cyan"] * 3 + ["yellow"] * 3 + ["purple"], + uniform_edge_color=False, + edge_colors=["w"] * 3 + ["orange"] * 3 + ["blue"] * 3 + ["green"], + markers=list("osD+x^v<>*"), + edge_width=2.0, + sizes=20, + uniform_size=True, +) + + +# per-point rotations +figure[0, 0].add_scatter( + sine, + markers="^", + sizes=20, + point_rotation_mode="vertex", + point_rotations=xs, + uniform_size=True, + offset=(0, 1, 0) +) + + +# point sizes +figure[0, 0].add_scatter( + sine, + markers="s", + sizes=xs * 5, + offset=(0, 2, 0) +) + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/screenshots/imgui_basic.png b/examples/screenshots/imgui_basic.png index cf98b035c..32e1b52c2 100644 --- a/examples/screenshots/imgui_basic.png +++ b/examples/screenshots/imgui_basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56bfbc50bc1b625d3e1d580a747bfac9b3af44a5cb6df78d75c7cc6c8a0a30ea -size 35926 +oid sha256:5194566726b85eb2e5cfbe04785f86698f0bfe1f0bd4cc39bbca1102f5da655b +size 35790 diff --git a/examples/screenshots/no-imgui-scatter_cmap_iris.png b/examples/screenshots/no-imgui-scatter_cmap_iris.png index d2b7fe814..60ef9f37c 100644 --- a/examples/screenshots/no-imgui-scatter_cmap_iris.png +++ b/examples/screenshots/no-imgui-scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52f8ca8276e89b37811d9c6e63479adafebc336035e7fe4d6c0bc2e7020737d3 -size 44151 +oid sha256:4dccc0e78ec14b320491155fe4d3bed0b0acebc6bf25ca1d569ebffd84e74cf1 +size 42745 diff --git a/examples/screenshots/no-imgui-scatter_colorslice_iris.png b/examples/screenshots/no-imgui-scatter_colorslice_iris.png index 014376df7..baa7189b6 100644 --- a/examples/screenshots/no-imgui-scatter_colorslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:341b2b24ec810ccc6dde388c8ecef42a6022b78270087c2a1c85b22d79b9bc84 -size 28191 +oid sha256:c337c47f63837df1ee22801321e092341d00e241e3624916d4a5bd7a99280419 +size 24092 diff --git a/examples/screenshots/no-imgui-scatter_dataslice_iris.png b/examples/screenshots/no-imgui-scatter_dataslice_iris.png index 1134126b9..4b8f048fe 100644 --- a/examples/screenshots/no-imgui-scatter_dataslice_iris.png +++ b/examples/screenshots/no-imgui-scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6e09668ba3aa56f5b5b77fa9749982e89e5a974af8f11364896c3811f8629e8 -size 26590 +oid sha256:a40aa8b1b9a57aaf41bbf0d79f7772d34851558a34a47cb1c30847035d0302a4 +size 24451 diff --git a/examples/screenshots/no-imgui-scatter_image_as_points.png b/examples/screenshots/no-imgui-scatter_image_as_points.png new file mode 100644 index 000000000..4ed688e7d --- /dev/null +++ b/examples/screenshots/no-imgui-scatter_image_as_points.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d8d71ee1382ce87b25afeeb2759d1e8474699f5c86a93cffacb5c7b22e787a0 +size 58639 diff --git a/examples/screenshots/no-imgui-scatter_iris.png b/examples/screenshots/no-imgui-scatter_iris.png index 5f25e6247..5cec5446d 100644 --- a/examples/screenshots/no-imgui-scatter_iris.png +++ b/examples/screenshots/no-imgui-scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0f086b794b91cec140f2bf63decc31cdff1798f62aa9ac3b5fab0763361ab8a -size 25954 +oid sha256:d473448543c094e30fb4fbf602c6a5a84995ac671b1fc05a34bd1a4ee9cb1734 +size 30225 diff --git a/examples/screenshots/no-imgui-scatter_size.png b/examples/screenshots/no-imgui-scatter_size.png index 8e2ac0323..cf5b140d7 100644 --- a/examples/screenshots/no-imgui-scatter_size.png +++ b/examples/screenshots/no-imgui-scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61fbbf5ed203e94bfa2d5e7d6fd62a01200177b7fd2c5499e491935a6eb6586a -size 41926 +oid sha256:bf652dda41ab68d04fd98bb1a1569a2d704333181d0018957403c238ab8e8e4d +size 43341 diff --git a/examples/screenshots/no-imgui-scatter_validate.png b/examples/screenshots/no-imgui-scatter_validate.png new file mode 100644 index 000000000..59919b51e --- /dev/null +++ b/examples/screenshots/no-imgui-scatter_validate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a2646bb946948ba5ce9af0e0f0adaca745c39a2770ca27af7fcd0830350fd1b +size 19108 diff --git a/examples/screenshots/scatter_cmap_iris.png b/examples/screenshots/scatter_cmap_iris.png index 57faa7feb..74fae9f55 100644 --- a/examples/screenshots/scatter_cmap_iris.png +++ b/examples/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fdce90b719794c08edb2210dfedb0a8229438435cfe502e569c27e3d7dba230 -size 45582 +oid sha256:641ff08c68450320aaa31f53035d69938e4e0347273e576fc2847e3eb050e1f5 +size 44445 diff --git a/examples/screenshots/scatter_colorslice_iris.png b/examples/screenshots/scatter_colorslice_iris.png index 4ac153675..821784a26 100644 --- a/examples/screenshots/scatter_colorslice_iris.png +++ b/examples/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f207f84a7ff2acc7cf28e798dc01f063c0d3a9a53db103561e01e038ce45cf97 -size 29644 +oid sha256:6df93416cee2dc13b5435ef56a0dcfe02e3b8d087ca28077693c0cf270881bf5 +size 25438 diff --git a/examples/screenshots/scatter_dataslice_iris.png b/examples/screenshots/scatter_dataslice_iris.png index d59514086..55f376161 100644 --- a/examples/screenshots/scatter_dataslice_iris.png +++ b/examples/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b89bf9a1563fdbc0076a058e2d7d21a5d508c7579f6c1c46871de1c608949af3 -size 27833 +oid sha256:7c49cb755a020fedc9de46256357321a2bc4170a57ba96030cdbd4f0abffec6d +size 25765 diff --git a/examples/screenshots/scatter_image_as_points.png b/examples/screenshots/scatter_image_as_points.png new file mode 100644 index 000000000..d2a3e1821 --- /dev/null +++ b/examples/screenshots/scatter_image_as_points.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69d84b6e7da872fc6cce3838fb1eb2ad87fe5d846962ef264d8390904ca53abb +size 60287 diff --git a/examples/screenshots/scatter_iris.png b/examples/screenshots/scatter_iris.png index be2872c3c..8c6f8d402 100644 --- a/examples/screenshots/scatter_iris.png +++ b/examples/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62cc26b015cd588b7a40146aa464efc451d8acaee39e1f0031a586b37f1304fe -size 27352 +oid sha256:5d78bbf60c03baf857413ca0fa0c189a5c9cdf90a7c0a1bc443b6ea7fa4d8f6b +size 31881 diff --git a/examples/screenshots/scatter_size.png b/examples/screenshots/scatter_size.png index ca413760a..9da829f65 100644 --- a/examples/screenshots/scatter_size.png +++ b/examples/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20fa7dd6e09d8c4deccf3cf7166c73d36f194c7a4f6cb24fc605e9571e886cce -size 44273 +oid sha256:8351d9c26f034e04d5ce7b205337e48b6e827a0ee9ecd52bf69935af7d79f9af +size 47048 diff --git a/examples/screenshots/scatter_validate.png b/examples/screenshots/scatter_validate.png new file mode 100644 index 000000000..1ce5e0f1d --- /dev/null +++ b/examples/screenshots/scatter_validate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680542f32e591dbf0426e5f550e26ab7a031aae52a6303528ccde7fae524124e +size 20288 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 105325b3d..dd3a3260d 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -163,7 +163,7 @@ def test_example_screenshots(module, prep_environment): rgb = normalize_image(rgb) ref_img = normalize_image(ref_img) - similar, rmse = image_similarity(rgb, ref_img, threshold=0.05) + similar, rmse = image_similarity(rgb, ref_img) update_diffs(module.stem, similar, rgb, ref_img) assert similar, ( diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 81694de33..a4f3e9a67 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -335,6 +335,13 @@ def decorator(_callback): if t in self._features.keys(): # fpl feature event feature = getattr(self, f"_{t}") + + if feature is None: + # feature is None in the graphic's current mode, probably is a scatter graphic + raise AttributeError( + f"{self} does not have the passed feature: '{t}' in its current mode." + ) + feature.add_event_handler(_callback_wrapper) else: # wrap pygfx event diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 143c4cc85..73520cc84 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Sequence import numpy as np @@ -9,7 +9,6 @@ VertexColors, UniformColor, VertexCmap, - PointsSizesFeature, SizeSpace, ) @@ -36,7 +35,7 @@ def colors(self) -> VertexColors | pygfx.Color: return self._colors.value @colors.setter - def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str]): + def colors(self, value: str | np.ndarray | Sequence[float] | Sequence[str]): if isinstance(self._colors, VertexColors): self._colors[:] = value diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index eb834b674..95e4d321f 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -1,13 +1,21 @@ from ._positions_graphics import ( VertexColors, UniformColor, - UniformSize, SizeSpace, - Thickness, VertexPositions, - PointsSizesFeature, VertexCmap, ) +from ._line import Thickness +from ._scatter import ( + VertexMarkers, + UniformMarker, + UniformEdgeColor, + EdgeWidth, + UniformRotations, + VertexRotations, + VertexPointSizes, + UniformSize, +) from ._image import ( TextureArray, ImageCmap, @@ -54,12 +62,18 @@ __all__ = [ "VertexColors", "UniformColor", - "UniformSize", "SizeSpace", - "Thickness", "VertexPositions", - "PointsSizesFeature", "VertexCmap", + "Thickness", + "VertexMarkers", + "UniformMarker", + "UniformEdgeColor", + "EdgeWidth", + "UniformRotations", + "VertexRotations", + "VertexPointSizes", + "UniformSize", "TextureArray", "ImageCmap", "ImageVmin", diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 153f6aad2..5dec9f1e5 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -49,7 +49,8 @@ def __init__(self, type: str, info: dict): class GraphicFeature: - def __init__(self, **kwargs): + def __init__(self, property_name: str, **kwargs): + self._property_name = property_name self._event_handlers = list() self._block_events = False @@ -139,10 +140,9 @@ def __init__( data: NDArray | pygfx.Buffer, buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", isolated_buffer: bool = True, - texture_dim: int = 2, **kwargs, ): - super().__init__() + super().__init__(**kwargs) if isolated_buffer and not isinstance(data, pygfx.Resource): # useful if data is read-only, example: memmaps bdata = np.zeros(data.shape, dtype=data.dtype) @@ -157,9 +157,6 @@ def __init__( self._buffer = data elif buffer_type == "buffer": self._buffer = pygfx.Buffer(bdata) - elif buffer_type == "texture": - # TODO: placeholder, not currently used since TextureArray is used specifically for Image graphics - self._buffer = pygfx.Texture(bdata, dim=texture_dim) else: raise ValueError( "`data` must be a pygfx.Buffer instance or `buffer_type` must be one of: 'buffer' or 'texture'" @@ -167,8 +164,6 @@ def __init__( self._event_handlers: list[callable] = list() - self._shared: int = 0 - @property def value(self) -> np.ndarray: """numpy array object representing the data managed by this buffer""" @@ -183,11 +178,6 @@ def buffer(self) -> pygfx.Buffer | pygfx.Texture: """managed buffer""" return self._buffer - @property - def shared(self) -> int: - """Number of graphics that share this buffer""" - return self._shared - @property def __array_interface__(self): raise BufferError( diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index e203be68d..b2b99cc49 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -1,20 +1,20 @@ +from typing import Sequence + import numpy as np -import pygfx from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance class Name(GraphicFeature): - property_name = "name" event_info_spec = [ {"dict key": "value", "type": "str", "description": "user provided name"}, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "name"): """Graphic name""" self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> str: @@ -30,12 +30,11 @@ def set_value(self, graphic, value: str): self._value = value - event = GraphicFeatureEvent(type="name", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class Offset(GraphicFeature): - property_name = "offset" event_info_spec = [ { "dict key": "value", @@ -44,7 +43,9 @@ class Offset(GraphicFeature): }, ] - def __init__(self, value: np.ndarray | list | tuple): + def __init__( + self, value: np.ndarray | Sequence[float], property_name: str = "offset" + ): """Offset position of the graphic, [x, y, z]""" self._validate(value) @@ -53,7 +54,7 @@ def __init__(self, value: np.ndarray | list | tuple): # set values self._value[:] = value - super().__init__() + super().__init__(property_name=property_name) def _validate(self, value): if not len(value) == 3: @@ -64,7 +65,7 @@ def value(self) -> np.ndarray: return self._value @block_reentrance - def set_value(self, graphic, value: np.ndarray | list | tuple): + def set_value(self, graphic, value: np.ndarray | Sequence[float]): self._validate(value) value = np.asarray(value) @@ -76,12 +77,11 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): # set value of existing feature value array self._value[:] = value - event = GraphicFeatureEvent(type="offset", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class Rotation(GraphicFeature): - property_name = "offset" event_info_spec = [ { "dict key": "value", @@ -90,7 +90,9 @@ class Rotation(GraphicFeature): }, ] - def __init__(self, value: np.ndarray | list | tuple): + def __init__( + self, value: np.ndarray | Sequence[float], property_name: str = "rotation" + ): """Graphic rotation quaternion""" self._validate(value) @@ -98,7 +100,7 @@ def __init__(self, value: np.ndarray | list | tuple): self._value = np.zeros(4) self._value[:] = value - super().__init__() + super().__init__(property_name=property_name) def _validate(self, value): if not len(value) == 4: @@ -111,7 +113,7 @@ def value(self) -> np.ndarray: return self._value @block_reentrance - def set_value(self, graphic, value: np.ndarray | list | tuple): + def set_value(self, graphic, value: np.ndarray | Sequence[float]): self._validate(value) value = np.asarray(value) @@ -124,21 +126,20 @@ def set_value(self, graphic, value: np.ndarray | list | tuple): # set value of existing feature value array self._value[:] = value - event = GraphicFeatureEvent(type="rotation", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class Alpha(GraphicFeature): """The alpha value (i.e. opacity) of a graphic.""" - property_name = "alpha" event_info_spec = [ {"dict key": "value", "type": "float", "description": "new alpha value"}, ] - def __init__(self, value: float): + def __init__(self, value: float, property_name: str = "alpha"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> float: @@ -156,21 +157,20 @@ def set_value(self, graphic, value: float): self._value = value - event = GraphicFeatureEvent(type="alpha", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class AlphaMode(GraphicFeature): """The alpha-mode value of a graphic (i.e. how alpha is handled by the renderer).""" - property_name = "alpha_mode" event_info_spec = [ {"dict key": "value", "type": "str", "description": "new alpha mode"}, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "alpha_mode"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> str: @@ -188,21 +188,20 @@ def set_value(self, graphic, value: str): self._value = value - event = GraphicFeatureEvent(type="alpha_mode", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class Visible(GraphicFeature): """Access or change the visibility.""" - property_name = "visible" event_info_spec = [ {"dict key": "value", "type": "bool", "description": "new visibility bool"}, ] - def __init__(self, value: bool): + def __init__(self, value: bool, property_name: str = "visible"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> bool: @@ -213,7 +212,7 @@ def set_value(self, graphic, value: bool): graphic.world_object.visible = value self._value = value - event = GraphicFeatureEvent(type="visible", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -222,7 +221,6 @@ class Deleted(GraphicFeature): Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted """ - property_name = "deleted" event_info_spec = [ { "dict key": "value", @@ -231,9 +229,9 @@ class Deleted(GraphicFeature): }, ] - def __init__(self, value: bool): + def __init__(self, value: bool, property_name: str = "deleted"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> bool: @@ -242,5 +240,5 @@ def value(self) -> bool: @block_reentrance def set_value(self, graphic, value: bool): self._value = value - event = GraphicFeatureEvent(type="deleted", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py index 559e62c69..648f79bc8 100644 --- a/fastplotlib/graphics/features/_image.py +++ b/fastplotlib/graphics/features/_image.py @@ -33,8 +33,8 @@ class TextureArray(GraphicFeature): }, ] - def __init__(self, data, isolated_buffer: bool = True): - super().__init__() + def __init__(self, data, isolated_buffer: bool = True, property_name: str = "data"): + super().__init__(property_name=property_name) data = self._fix_data(data) @@ -154,7 +154,9 @@ def __setitem__(self, key, value): for texture in self.buffer.ravel(): texture.update_range((0, 0, 0), texture.size) - event = GraphicFeatureEvent("data", info={"key": key, "value": value}) + event = GraphicFeatureEvent( + self._property_name, info={"key": key, "value": value} + ) self._call_event_handlers(event) def __len__(self): @@ -172,9 +174,9 @@ class ImageVmin(GraphicFeature): }, ] - def __init__(self, value: float): + def __init__(self, value: float, property_name: str = "vmin"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> float: @@ -186,7 +188,7 @@ def set_value(self, graphic, value: float): graphic._material.clim = (value, vmax) self._value = value - event = GraphicFeatureEvent(type="vmin", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -201,9 +203,9 @@ class ImageVmax(GraphicFeature): }, ] - def __init__(self, value: float): + def __init__(self, value: float, property_name: str = "vmax"): self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> float: @@ -215,7 +217,7 @@ def set_value(self, graphic, value: float): graphic._material.clim = (vmin, value) self._value = value - event = GraphicFeatureEvent(type="vmax", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -230,10 +232,10 @@ class ImageCmap(GraphicFeature): }, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "cmap"): self._value = value self.texture = get_cmap_texture(value) - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> str: @@ -246,7 +248,7 @@ def set_value(self, graphic, value: str): graphic._material.map.texture.update_range((0, 0, 0), size=(256, 1, 1)) self._value = value - event = GraphicFeatureEvent(type="cmap", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -261,10 +263,10 @@ class ImageInterpolation(GraphicFeature): }, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "interpolation"): self._validate(value) self._value = value - super().__init__() + super().__init__(property_name=property_name) def _validate(self, value): if value not in ["nearest", "linear"]: @@ -296,10 +298,10 @@ class ImageCmapInterpolation(GraphicFeature): }, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "cmap_interpolation"): self._validate(value) self._value = value - super().__init__() + super().__init__(property_name=property_name) def _validate(self, value): if value not in ["nearest", "linear"]: @@ -320,5 +322,5 @@ def set_value(self, graphic, value: str): graphic._material.map.mag_filter = value self._value = value - event = GraphicFeatureEvent(type="cmap_interpolation", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_line.py b/fastplotlib/graphics/features/_line.py new file mode 100644 index 000000000..792cb7832 --- /dev/null +++ b/fastplotlib/graphics/features/_line.py @@ -0,0 +1,28 @@ +from ._base import ( + GraphicFeature, + GraphicFeatureEvent, + block_reentrance, +) + + +class Thickness(GraphicFeature): + event_info_spec = [ + {"dict key": "value", "type": "float", "description": "new thickness value"}, + ] + + def __init__(self, value: float, property_name: str = "thickness"): + self._value = value + super().__init__(property_name=property_name) + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + value = float(value) + graphic.world_object.material.thickness = value + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py index 21202cdf3..ae57e77d7 100644 --- a/fastplotlib/graphics/features/_positions_graphics.py +++ b/fastplotlib/graphics/features/_positions_graphics.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Sequence import numpy as np import pygfx @@ -17,7 +17,6 @@ class VertexColors(BufferManager): - property_name = "colors" event_info_spec = [ { "dict key": "key", @@ -38,16 +37,17 @@ class VertexColors(BufferManager): def __init__( self, - colors: str | np.ndarray | tuple[float] | list[float] | list[str], + colors: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], n_colors: int, isolated_buffer: bool = True, + property_name: str = "colors", ): """ Manages the vertex color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` Parameters ---------- - colors: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float + colors: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str] specify colors as a single human-readable string, RGBA array, or an iterable of strings or RGBA arrays @@ -57,13 +57,15 @@ def __init__( """ data = parse_colors(colors, n_colors) - super().__init__(data=data, isolated_buffer=isolated_buffer) + super().__init__( + data=data, isolated_buffer=isolated_buffer, property_name=property_name + ) @block_reentrance def __setitem__( self, key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], - user_value: str | np.ndarray | tuple[float] | list[float] | list[str], + user_value: str | pygfx.Color | np.ndarray | Sequence[float] | Sequence[str], ): user_key = key @@ -137,7 +139,7 @@ def __setitem__( "user_value": user_value, } - event = GraphicFeatureEvent("colors", info=event_info) + event = GraphicFeatureEvent(self._property_name, info=event_info) self._call_event_handlers(event) def __len__(self): @@ -145,63 +147,41 @@ def __len__(self): class UniformColor(GraphicFeature): - property_name = "colors" event_info_spec = [ { "dict key": "value", - "type": "np.ndarray [RGBA]", + "type": "str | pygfx.Color | np.ndarray | Sequence[float]", "description": "new color value", }, ] - def __init__(self, value: str | np.ndarray | tuple | list | pygfx.Color): + def __init__( + self, + value: str | pygfx.Color | np.ndarray | Sequence[float], + property_name: str = "colors", + ): """Manages uniform color for line or scatter material""" self._value = pygfx.Color(value) - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> pygfx.Color: return self._value @block_reentrance - def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): + def set_value( + self, graphic, value: str | pygfx.Color | np.ndarray | Sequence[float] + ): value = pygfx.Color(value) graphic.world_object.material.color = value self._value = value - event = GraphicFeatureEvent(type="colors", info={"value": value}) - self._call_event_handlers(event) - - -class UniformSize(GraphicFeature): - property_name = "sizes" - event_info_spec = [ - {"dict key": "value", "type": "float", "description": "new size value"}, - ] - - def __init__(self, value: int | float): - """Manages uniform size for scatter material""" - - self._value = float(value) - super().__init__() - - @property - def value(self) -> float: - return self._value - - @block_reentrance - def set_value(self, graphic, value: float | int): - value = float(value) - graphic.world_object.material.size = value - self._value = value - - event = GraphicFeatureEvent(type="sizes", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class SizeSpace(GraphicFeature): - property_name = "size_space" event_info_spec = [ { "dict key": "value", @@ -210,11 +190,11 @@ class SizeSpace(GraphicFeature): }, ] - def __init__(self, value: str): + def __init__(self, value: str, property_name: str = "size_space"): """Manages the coordinate space for scatter/line graphic""" self._value = value - super().__init__() + super().__init__(property_name=property_name) @property def value(self) -> str: @@ -233,12 +213,11 @@ def set_value(self, graphic, value: str): graphic.world_object.material.size_space = value self._value = value - event = GraphicFeatureEvent(type="size_space", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) class VertexPositions(BufferManager): - property_name = "data" event_info_spec = [ { "dict key": "key", @@ -252,14 +231,18 @@ class VertexPositions(BufferManager): }, ] - def __init__(self, data: Any, isolated_buffer: bool = True): + def __init__( + self, data: Any, isolated_buffer: bool = True, property_name: str = "data" + ): """ Manages the vertex positions buffer shown in the graphic. Supports fancy indexing if the data array also supports it. """ data = self._fix_data(data) - super().__init__(data, isolated_buffer=isolated_buffer) + super().__init__( + data, isolated_buffer=isolated_buffer, property_name=property_name + ) def _fix_data(self, data): # data = to_gpu_supported_dtype(data) @@ -293,115 +276,13 @@ def __setitem__( # determine offset and size for GPU upload self._update_range(key) - self._emit_event("data", key, value) - - def __len__(self): - return len(self.buffer.data) - - -class PointsSizesFeature(BufferManager): - property_name = "sizes" - event_info_spec = [ - { - "dict key": "key", - "type": "slice, index (int) or numpy-like fancy index", - "description": "key at which point sizes were indexed/sliced", - }, - { - "dict key": "value", - "type": "int | float | array-like", - "description": "new size values for points that were changed", - }, - ] - - def __init__( - self, - sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], - n_datapoints: int, - isolated_buffer: bool = True, - ): - """ - Manages sizes buffer of scatter points. - """ - sizes = self._fix_sizes(sizes, n_datapoints) - super().__init__(data=sizes, isolated_buffer=isolated_buffer) - - def _fix_sizes( - self, - sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], - n_datapoints: int, - ): - if np.issubdtype(type(sizes), np.number): - # single value given - sizes = np.full( - n_datapoints, sizes, dtype=np.float32 - ) # force it into a float to avoid weird gpu errors - - elif isinstance( - sizes, (np.ndarray, tuple, list) - ): # if it's not a ndarray already, make it one - sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 - if (sizes.ndim != 1) or (sizes.size != n_datapoints): - raise ValueError( - f"sequence of `sizes` must be 1 dimensional with " - f"the same length as the number of datapoints" - ) - - else: - raise TypeError( - "sizes must be a single , , or a sequence (array, list, tuple) of int" - "or float with the length equal to the number of datapoints" - ) - - if np.count_nonzero(sizes < 0) > 1: - raise ValueError( - "All sizes must be positive numbers greater than or equal to 0.0." - ) - - return sizes - - @block_reentrance - def __setitem__( - self, - key: int | slice | np.ndarray[int | bool] | list[int | bool], - value: int | float | np.ndarray | list[int | float] | tuple[int | float], - ): - # this is a very simple 1D buffer, no parsing required, directly set buffer - self.buffer.data[key] = value - self._update_range(key) - - self._emit_event("sizes", key, value) + self._emit_event(self._property_name, key, value) def __len__(self): return len(self.buffer.data) -class Thickness(GraphicFeature): - property_name = "thickness" - event_info_spec = [ - {"dict key": "value", "type": "float", "description": "new thickness value"}, - ] - - def __init__(self, value: float): - self._value = value - super().__init__() - - @property - def value(self) -> float: - return self._value - - @block_reentrance - def set_value(self, graphic, value: float): - value = float(value) - graphic.world_object.material.thickness = value - self._value = value - - event = GraphicFeatureEvent(type="thickness", info={"value": value}) - self._call_event_handlers(event) - - class VertexCmap(BufferManager): - property_name = "cmap" event_info_spec = [ { "dict key": "key", @@ -420,13 +301,14 @@ def __init__( vertex_colors: VertexColors, cmap_name: str | None, transform: np.ndarray | None, + property_name: str = "colors", ): """ Sliceable colormap feature, manages a VertexColors instance and provides a way to set colormaps with arbitrary transforms """ - super().__init__(data=vertex_colors.buffer) + super().__init__(data=vertex_colors.buffer, property_name=property_name) self._vertex_colors = vertex_colors self._cmap_name = cmap_name @@ -478,7 +360,7 @@ def __setitem__(self, key: slice, cmap_name): # TODO: should we block vertex_colors from emitting an event? # Because currently this will result in 2 emitted events, one # for cmap and another from the colors - self._emit_event("cmap", key, cmap_name) + self._emit_event(self._property_name, key, cmap_name) @property def name(self) -> str: diff --git a/fastplotlib/graphics/features/_scatter.py b/fastplotlib/graphics/features/_scatter.py new file mode 100644 index 000000000..16671ef89 --- /dev/null +++ b/fastplotlib/graphics/features/_scatter.py @@ -0,0 +1,574 @@ +from typing import Sequence + +import numpy as np +import pygfx + +from ._base import ( + GraphicFeature, + BufferManager, + GraphicFeatureEvent, + block_reentrance, +) + + +marker_names = { + # MPL + "o": "circle", + "s": "square", + "D": "diamond", + "+": "plus", + "x": "cross", + "^": "triangle_up", + "<": "triangle_left", + ">": "triangle_right", + "v": "triangle_down", + "*": "asterisk6", + # Unicode + "●": "circle", + "○": "ring", + "■": "square", + "♦": "diamond", + "♥": "heart", + "♠": "spade", + "♣": "club", + "✳": "asterisk6", + "▲": "triangle_up", + "▼": "triangle_down", + "◀": "triangle_left", + "▶": "triangle_right", + # Emojis (these may look like their plaintext variants in your editor) + "❤️": "heart", + "♠️": "spade", + "♣️": "club", + "♦️": "diamond", + "💎": "diamond", + "💍": "ring", + "✳️": "asterisk6", + "📍": "pin", +} + + +def user_input_to_marker(name): + resolved_name = marker_names.get(name, name).lower() + if resolved_name not in pygfx.MarkerShape: + raise ValueError( + f"markers must be a string in: {list(pygfx.MarkerShape) + list(marker_names.keys())}, not {name!r}" + ) + + return resolved_name + + +def validate_user_markers_array(markers): + # make sure all markers are valid + # need to validate before converting to ints because + # we can't use control flow in the vectorized function + unique_values = np.unique(markers) + for m in unique_values: + user_input_to_marker(m) + + +# fast vectorized function to convert array of user markers to the standardized strings +# TODO: can probably use search-sorted for this too +vectorized_user_markers_to_std_markers = np.vectorize(marker_names.get, otypes=[" array of int +# see: https://github.com/pygfx/pygfx/issues/1215 +# Prepare for searchsorted +def init_searchsorted(markers_mapping): + keys = np.array(list(markers_mapping.keys())) + vals = np.array(list(markers_mapping.values())) + + order = np.argsort(keys) + keys = keys[order] + vals = vals[order] + + return keys, vals + + +marker_int_searchsorted_keys, marker_int_searchsorted_vals = init_searchsorted( + marker_int_mapping +) + + +def searchsorted_markers_to_int_array(markers_str_array: np.ndarray[str]): + # Vectorized lookup + indices = np.searchsorted(marker_int_searchsorted_keys, markers_str_array) + return marker_int_searchsorted_vals[indices] + + +class VertexMarkers(BufferManager): + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which markers were indexed/sliced", + }, + { + "dict key": "value", + "type": "str | np.ndarray[str]", + "description": "new marker values for points that were changed", + }, + ] + + def __init__( + self, + markers: str | Sequence[str] | np.ndarray, + n_datapoints: int, + property_name: str = "markers", + ): + """ + Manages the markers buffer for the scatter points. Supports fancy indexing. + """ + + # first validate then allocate buffers + + if isinstance(markers, str): + markers = user_input_to_marker(markers) + + elif isinstance(markers, (tuple, list, np.ndarray)): + validate_user_markers_array(markers) + + # allocate buffers + markers_int_array = np.zeros(n_datapoints, dtype=np.int32) + + marker_str_length = max(map(len, list(pygfx.MarkerShape))) + + self._markers_readable_array = np.empty( + n_datapoints, dtype=f" np.ndarray[str]: + """numpy array of per-vertex marker shapes in human-readable form""" + return self._markers_readable_array + + @property + def value_int(self) -> np.ndarray[np.int32]: + """numpy array of the actual int32 buffer that represents per-vertex marker shapes on the GPU""" + return self.buffer.data + + def _set_markers_arrays(self, key, value, n_markers): + if isinstance(value, str): + # set markers at these indices to this value + m = user_input_to_marker(value) + self._markers_readable_array[key] = m + self.value_int[key] = marker_int_mapping[m] + + elif isinstance(value, (np.ndarray, list, tuple)): + if n_markers != len(value): + raise IndexError( + f"Must provide one marker value, or an array/list/tuple of marker values with the same length " + f"as the slice. You have provided the slice: {key}, which refers to {n_markers} markers, but " + f"provided {len(value)} new marker values. You must provide 1 or {n_markers} values." + ) + + validate_user_markers_array(value) + + new_markers_human_readable = vectorized_user_markers_to_std_markers(value) + new_markers_int = searchsorted_markers_to_int_array( + new_markers_human_readable + ) + + self._markers_readable_array[key] = new_markers_human_readable + self.value_int[key] = new_markers_int + else: + raise TypeError( + "new markers value must be a str, Sequence or np.ndarray of new marker values" + ) + + @block_reentrance + def __setitem__( + self, + key: int | slice | list[int | bool] | np.ndarray[int | bool], + value: str | Sequence[str] | np.ndarray[str], + ): + if isinstance(key, int): + if key >= self.value.size: + raise IndexError(f"index : {key} out of bounds: {self.value.size}") + + if not isinstance(value, str): + # only a single marker should be provided if changing one at one index + raise TypeError( + f"you must provide a marker value if providing a single index, " + f"you have passed index: {key} and value: {value}" + ) + + m = user_input_to_marker(value) + self._markers_readable_array[key] = m + self.value_int[key] = marker_int_mapping[m] + + elif isinstance(key, slice): + # find the number of new markers by converting slice to range and then parse markers + start, stop, step = key.indices(self.value.size) + + n_markers = len(range(start, stop, step)) + self._set_markers_arrays(key, value, n_markers) + + elif isinstance(key, (list, np.ndarray)): + key = np.asarray(key) # convert to array if list + + if key.dtype == bool: + # make sure len is same + if not key.size == self.buffer.data.shape[0]: + raise IndexError( + f"Length of array for fancy indexing must match number of datapoints.\n" + f"There are {len(self.buffer.data.shape[0])} datapoints and you have passed " + f"a bool array of size: {key.size}" + ) + + n_markers = np.count_nonzero(key) + self._set_markers_arrays(key, value, n_markers) + + # if it's an array of int + elif np.issubdtype(key.dtype, np.integer): + if key.size > self.buffer.data.shape[0]: + raise IndexError( + f"Length of array for fancy indexing must be <= n_datapoints. " + f"There are: {self.buffer.data.shape[0]} datapoints, you have passed an " + f"integer array for fancy indexing of size: {key.size}" + ) + n_markers = key.size + self._set_markers_arrays(key, value, n_markers) + + else: + # fancy indexing doesn't make sense with non-integer types + raise TypeError( + f"can only using integer or booleans arrays for fancy indexing, your array is of type: {key.dtype}" + ) + + else: + raise TypeError( + f"Can only set markers by slicing/indexing using the one of the following types: " + f"int | slice | list[int | bool] | np.ndarray[int | bool], you have passed" + f"sliced using the following type: {type(key)}" + ) + + # _update_range handles parsing the key to + # determine offset and size for GPU upload + self._update_range(key) + + self._emit_event(self._property_name, key, value) + + def __len__(self): + return len(self.buffer.data) + + +class UniformMarker(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str | None", + "description": "new marker value", + }, + ] + + def __init__(self, marker: str, property_name: str = "markers"): + """Manages evented uniform buffer for scatter marker""" + + self._value = user_input_to_marker(marker) + super().__init__(property_name=property_name) + + @property + def value(self) -> str: + return self._value + + @block_reentrance + def set_value(self, graphic, value: str): + value = user_input_to_marker(value) + graphic.world_object.material.marker = value + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +class UniformEdgeColor(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str | np.ndarray | pygfx.Color | Sequence[float]", + "description": "new edge_color", + }, + ] + + def __init__( + self, + edge_color: str | np.ndarray | pygfx.Color | Sequence[float], + property_name: str = "edge_colors", + ): + """Manages evented uniform buffer for scatter marker edge_color""" + + self._value = pygfx.Color(edge_color) + super().__init__(property_name=property_name) + + @property + def value(self) -> pygfx.Color: + return self._value + + @block_reentrance + def set_value( + self, graphic, value: str | np.ndarray | pygfx.Color | Sequence[float] + ): + graphic.world_object.material.edge_color = pygfx.Color(value) + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +class EdgeWidth(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new edge_width", + }, + ] + + def __init__(self, edge_width: float, property_name: str = "edge_width"): + """Manages evented uniform buffer for scatter marker edge_width""" + + self._value = edge_width + super().__init__(property_name=property_name) + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic.world_object.material.edge_width = value + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +class UniformRotations(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new edge_width", + }, + ] + + def __init__(self, edge_width: float, property_name: str = "point_rotations"): + """Manages evented uniform buffer for scatter marker rotation""" + + self._value = edge_width + super().__init__(property_name=property_name) + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float): + graphic.world_object.material.rotations = value + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +class VertexRotations(BufferManager): + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which point rotations were indexed/sliced", + }, + { + "dict key": "value", + "type": "int | float | array-like", + "description": "new rotation values for points that were changed", + }, + ] + + def __init__( + self, + rotations: int | float | np.ndarray | Sequence[int | float], + n_datapoints: int, + isolated_buffer: bool = True, + property_name: str = "point_rotations", + ): + """ + Manages rotations buffer of scatter points. + """ + sizes = self._fix_sizes(rotations, n_datapoints) + super().__init__( + data=sizes, isolated_buffer=isolated_buffer, property_name=property_name + ) + + def _fix_sizes( + self, + sizes: int | float | np.ndarray | Sequence[int | float], + n_datapoints: int, + ): + if np.issubdtype(type(sizes), np.number): + # single value given + sizes = np.full( + n_datapoints, sizes, dtype=np.float32 + ) # force it into a float to avoid weird gpu errors + + elif isinstance( + sizes, (np.ndarray, tuple, list) + ): # if it's not a ndarray already, make it one + sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != n_datapoints): + raise ValueError( + f"sequence of `rotations` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) + + else: + raise TypeError( + "`rotations` must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints" + ) + + return sizes + + @block_reentrance + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | list[int | bool], + value: int | float | np.ndarray | Sequence[int | float], + ): + # this is a very simple 1D buffer, no parsing required, directly set buffer + self.buffer.data[key] = value + self._update_range(key) + + self._emit_event(self._property_name, key, value) + + def __len__(self): + return len(self.buffer.data) + + +class VertexPointSizes(BufferManager): + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which point sizes were indexed/sliced", + }, + { + "dict key": "value", + "type": "int | float | array-like", + "description": "new size values for points that were changed", + }, + ] + + def __init__( + self, + sizes: int | float | np.ndarray | Sequence[int | float], + n_datapoints: int, + isolated_buffer: bool = True, + property_name: str = "sizes", + ): + """ + Manages sizes buffer of scatter points. + """ + sizes = self._fix_sizes(sizes, n_datapoints) + super().__init__( + data=sizes, isolated_buffer=isolated_buffer, property_name=property_name + ) + + def _fix_sizes( + self, + sizes: int | float | np.ndarray | Sequence[int | float], + n_datapoints: int, + ): + if np.issubdtype(type(sizes), np.number): + # single value given + sizes = np.full( + n_datapoints, sizes, dtype=np.float32 + ) # force it into a float to avoid weird gpu errors + + elif isinstance( + sizes, (np.ndarray, tuple, list) + ): # if it's not a ndarray already, make it one + sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != n_datapoints): + raise ValueError( + f"sequence of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) + + else: + raise TypeError( + "sizes must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints" + ) + + if np.count_nonzero(sizes < 0) > 1: + raise ValueError( + "All sizes must be positive numbers greater than or equal to 0.0." + ) + + return sizes + + @block_reentrance + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | list[int | bool], + value: int | float | np.ndarray | Sequence[int | float], + ): + # this is a very simple 1D buffer, no parsing required, directly set buffer + self.buffer.data[key] = value + self._update_range(key) + + self._emit_event(self._property_name, key, value) + + def __len__(self): + return len(self.buffer.data) + + +class UniformSize(GraphicFeature): + event_info_spec = [ + {"dict key": "value", "type": "float", "description": "new size value"}, + ] + + def __init__(self, value: int | float, property_name: str = "sizes"): + """Manages uniform size for scatter material""" + + self._value = float(value) + super().__init__(property_name=property_name) + + @property + def value(self) -> float: + return self._value + + @block_reentrance + def set_value(self, graphic, value: float | int): + value = float(value) + graphic.world_object.material.size = value + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py index ed18c8287..654b3d4c6 100644 --- a/fastplotlib/graphics/features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -40,7 +40,7 @@ def __init__(self, axis: str, value: float, limits: tuple[float, float]): """ - super().__init__() + super().__init__(property_name="selection") self._axis = axis self._limits = limits @@ -70,7 +70,7 @@ def set_value(self, selector, value: float): self._value = value - event = GraphicFeatureEvent("selection", {"value": value}) + event = GraphicFeatureEvent(self._property_name, {"value": value}) event.get_selected_index = selector.get_selected_index self._call_event_handlers(event) @@ -99,7 +99,7 @@ class LinearRegionSelectionFeature(GraphicFeature): ] def __init__(self, value: tuple[int, int], axis: str, limits: tuple[float, float]): - super().__init__() + super().__init__(property_name="selection") self._axis = axis self._limits = limits @@ -182,7 +182,7 @@ def set_value(self, selector, value: Sequence[float]): if len(self._event_handlers) < 1: return - event = GraphicFeatureEvent("selection", {"value": self.value}) + event = GraphicFeatureEvent(self._property_name, {"value": self.value}) event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data @@ -220,7 +220,7 @@ def __init__( value: tuple[float, float, float, float], limits: tuple[float, float, float, float], ): - super().__init__() + super().__init__(property_name="selection") self._limits = limits self._value = tuple(int(v) for v in value) @@ -335,7 +335,7 @@ def set_value(self, selector, value: Sequence[float]): if len(self._event_handlers) < 1: return - event = GraphicFeatureEvent("selection", {"value": self.value}) + event = GraphicFeatureEvent(self._property_name, {"value": self.value}) event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data @@ -371,7 +371,7 @@ def __init__( value: Sequence[tuple[float]], limits: tuple[float, float, float, float], ): - super().__init__() + super().__init__(property_name="selection") self._limits = limits self._value = np.asarray(value).reshape(-1, 3).astype(float) @@ -438,7 +438,7 @@ def set_value(self, selector, value: Sequence[tuple[float]]): if len(self._event_handlers) < 1: return - event = GraphicFeatureEvent("selection", {"value": self.value}) + event = GraphicFeatureEvent(self._property_name, {"value": self.value}) event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data diff --git a/fastplotlib/graphics/features/_text.py b/fastplotlib/graphics/features/_text.py index d8e5e95e8..ed0485d3a 100644 --- a/fastplotlib/graphics/features/_text.py +++ b/fastplotlib/graphics/features/_text.py @@ -16,7 +16,7 @@ class TextData(GraphicFeature): def __init__(self, value: str): self._value = value - super().__init__() + super().__init__(property_name="text") @property def value(self) -> str: @@ -27,7 +27,7 @@ def set_value(self, graphic, value: str): graphic.world_object.set_text(value) self._value = value - event = GraphicFeatureEvent(type="text", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -42,7 +42,7 @@ class FontSize(GraphicFeature): def __init__(self, value: float | int): self._value = value - super().__init__() + super().__init__(property_name="font_size") @property def value(self) -> float | int: @@ -53,7 +53,7 @@ def set_value(self, graphic, value: float | int): graphic.world_object.font_size = value self._value = graphic.world_object.font_size - event = GraphicFeatureEvent(type="font_size", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -68,7 +68,7 @@ class TextFaceColor(GraphicFeature): def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): self._value = pygfx.Color(value) - super().__init__() + super().__init__(property_name="face_color") @property def value(self) -> pygfx.Color: @@ -80,7 +80,7 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float graphic.world_object.material.color = value self._value = graphic.world_object.material.color - event = GraphicFeatureEvent(type="face_color", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -95,7 +95,7 @@ class TextOutlineColor(GraphicFeature): def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): self._value = pygfx.Color(value) - super().__init__() + super().__init__(property_name="outline_color") @property def value(self) -> pygfx.Color: @@ -107,7 +107,7 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float graphic.world_object.material.outline_color = value self._value = graphic.world_object.material.outline_color - event = GraphicFeatureEvent(type="outline_color", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -122,7 +122,7 @@ class TextOutlineThickness(GraphicFeature): def __init__(self, value: float): self._value = value - super().__init__() + super().__init__(property_name="outline_thickness") @property def value(self) -> float: @@ -133,5 +133,5 @@ def set_value(self, graphic, value: float): graphic.world_object.material.outline_thickness = value self._value = graphic.world_object.material.outline_thickness - event = GraphicFeatureEvent(type="outline_thickness", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_volume.py b/fastplotlib/graphics/features/_volume.py index fd3c8e745..ec4c4052a 100644 --- a/fastplotlib/graphics/features/_volume.py +++ b/fastplotlib/graphics/features/_volume.py @@ -35,7 +35,7 @@ class TextureArrayVolume(GraphicFeature): ] def __init__(self, data, isolated_buffer: bool = True): - super().__init__() + super().__init__(property_name="data") data = self._fix_data(data) @@ -192,7 +192,9 @@ def __setitem__(self, key, value): for texture in self.buffer.ravel(): texture.update_range((0, 0, 0), texture.size) - event = GraphicFeatureEvent("data", info={"key": key, "value": value}) + event = GraphicFeatureEvent( + self._property_name, info={"key": key, "value": value} + ) self._call_event_handlers(event) def __len__(self): @@ -242,7 +244,7 @@ class VolumeRenderMode(GraphicFeature): def __init__(self, value: str): self._validate(value) self._value = value - super().__init__() + super().__init__(property_name="mode") @property def value(self) -> str: @@ -271,7 +273,7 @@ def set_value(self, graphic, value: str): graphic._material = new_material self._value = value - event = GraphicFeatureEvent(type="mode", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -288,7 +290,7 @@ class VolumeIsoThreshold(GraphicFeature): def __init__(self, value: float): self._value = value - super().__init__() + super().__init__(property_name="threshold") @property def value(self) -> float: @@ -299,7 +301,7 @@ def set_value(self, graphic, value: float): graphic._material.threshold = value self._value = graphic._material.threshold - event = GraphicFeatureEvent(type="threshold", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -316,7 +318,7 @@ class VolumeIsoStepSize(GraphicFeature): def __init__(self, value: float): self._value = value - super().__init__() + super().__init__(property_name="step_size") @property def value(self) -> float: @@ -327,7 +329,7 @@ def set_value(self, graphic, value: float): graphic._material.step_size = value self._value = graphic._material.step_size - event = GraphicFeatureEvent(type="step_size", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -344,7 +346,7 @@ class VolumeIsoSubStepSize(GraphicFeature): def __init__(self, value: float): self._value = value - super().__init__() + super().__init__(property_name="substep_size") @property def value(self) -> float: @@ -355,7 +357,7 @@ def set_value(self, graphic, value: float): graphic._material.substep_size = value self._value = graphic._material.substep_size - event = GraphicFeatureEvent(type="step_size", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -372,7 +374,7 @@ class VolumeIsoEmissive(GraphicFeature): def __init__(self, value: pygfx.Color | str | tuple | np.ndarray): self._value = pygfx.Color(value) - super().__init__() + super().__init__(property_name="emissive") @property def value(self) -> pygfx.Color: @@ -383,7 +385,7 @@ def set_value(self, graphic, value: pygfx.Color | str | tuple | np.ndarray): graphic._material.emissive = value self._value = graphic._material.emissive - event = GraphicFeatureEvent(type="emissive", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -400,7 +402,7 @@ class VolumeIsoShininess(GraphicFeature): def __init__(self, value: int): self._value = value - super().__init__() + super().__init__(property_name="shininess") @property def value(self) -> int: @@ -411,7 +413,7 @@ def set_value(self, graphic, value: float): graphic._material.shininess = value self._value = graphic._material.shininess - event = GraphicFeatureEvent(type="shininess", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) @@ -428,7 +430,7 @@ class VolumeSlicePlane(GraphicFeature): def __init__(self, value: tuple[float, float, float, float]): self._value = value - super().__init__() + super().__init__(property_name="plane") @property def value(self) -> tuple[float, float, float, float]: @@ -439,5 +441,5 @@ def set_value(self, graphic, value: tuple[float, float, float, float]): graphic._material.plane = value self._value = graphic._material.plane - event = GraphicFeatureEvent(type="plane", info={"value": value}) + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 6bff05fa5..4df4f7557 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,36 +5,58 @@ from ._positions_base import PositionsGraphic from .features import ( - PointsSizesFeature, + VertexPointSizes, UniformSize, SizeSpace, VertexPositions, VertexColors, UniformColor, VertexCmap, + VertexMarkers, + UniformMarker, + UniformEdgeColor, + EdgeWidth, + UniformRotations, + VertexRotations, + TextureArray, ) class ScatterGraphic(PositionsGraphic): _features = { "data": VertexPositions, - "sizes": (PointsSizesFeature, UniformSize), + "sizes": (VertexPointSizes, UniformSize), "colors": (VertexColors, UniformColor), "cmap": (VertexCmap, None), + "markers": (VertexMarkers, UniformMarker, None), + "edge_colors": (UniformEdgeColor, VertexColors, None), + "edge_width": (EdgeWidth, None), + "image": (TextureArray, None), "size_space": SizeSpace, + "point_rotations": (UniformRotations, VertexRotations, None), } def __init__( self, data: Any, - colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + colors: str | np.ndarray | Sequence[float] | Sequence[str] = "w", uniform_color: bool = False, cmap: str = None, cmap_transform: np.ndarray = None, - isolated_buffer: bool = True, + mode: Literal["markers", "simple", "gaussian", "image"] = "markers", + markers: str | np.ndarray | Sequence[str] = "o", + uniform_marker: bool = False, + custom_sdf: str = None, + edge_colors: str | np.ndarray | pygfx.Color | Sequence[float] = "black", + uniform_edge_color: bool = True, + edge_width: float = 1.0, + image: np.ndarray = None, + point_rotations: float | np.ndarray = 0, + point_rotation_mode: Literal["uniform", "vertex", "curve"] = "uniform", sizes: float | np.ndarray | Sequence[float] = 1, uniform_size: bool = False, size_space: str = "screen", + isolated_buffer: bool = True, **kwargs, ): """ @@ -62,9 +84,66 @@ def __init__( cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap - isolated_buffer: bool, default True - whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. + mode: one of: "markers", "simple", "gaussian", "image", default "markers" + The scatter points mode, cannot be changed after the graphic has been created. + + * markers: represent points with various or custom markers, default + * simple: all scatters points are simple circles + * gaussian: each point is a gaussian blob + * image: use an image for each point, pass an array to the `image` kwarg, these are also called sprites + + markers: None | str | np.ndarray | Sequence[str], default "o" + The shape of the markers when `mode` is "markers" + + Supported values: + + * A string from pygfx.MarkerShape enum + * Matplotlib compatible characters: "osD+x^v<>*". + * Unicode symbols: "●○■♦♥♠♣✳▲▼◀▶". + * Emojis: "❤️♠️♣️♦️💎💍✳️📍". + * A string containing the value "custom". In this case, the WGSL + code defined by ``custom_sdf`` will be used. + + uniform_marker: bool, default False + Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use + the same marker for all points and want to save GPU RAM. + + custom_sdf: str = None, + The SDF code for the marker shape when the marker is set to custom. + Can be used when `mode` is "markers". + + Negative values are inside the shape, positive values are outside the + shape. + + The SDF's takes in two parameters `coords: vec2` and `size: f32`. + The first is a WGSL coordinate and `size` is the overall size of + the texture. The returned value should be the signed distance from + any edge of the shape. Distances (positive and negative) that are + less than half the `edge_width` in absolute terms will be colored + with the `edge_color`. Other negative distances will be colored by + `colors`. + + edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" + edge color of the markers, used when `mode` is "markers" + + uniform_edge_color: bool, default True + Set the same edge color for all markers. Useful for saving GPU RAM. + + edge_width: float = 1.0, + Width of the marker edges. used when `mode` is "markers". + + image: ArrayLike, optional + renders an image at the scatter points, also known as sprites. + The image color is multiplied with the point's "normal" color. + + point_rotations: float | ArrayLike = 0, + The rotation of the scatter points in radians. Default 0. A single float rotation value can be set on all + points, or an array of rotation values can be used to set per-point rotations + + point_rotation_mode: one of: "uniform" | "vertex" | "curve", default "uniform" + * uniform: set the same rotation for every point, useful to save GPU RAM + * vertex: set per-vertex rotations + * curve: The rotation follows the curve of the line defined by the points (in screen space) sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points @@ -74,7 +153,11 @@ def __init__( save GPU VRAM when all points have the same size. size_space: str, default "screen" - coordinate space in which the size is expressed ("screen", "world", "model") + coordinate space in which the size is expressed, one of ("screen", "world", "model") + + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. kwargs passed to :class:`.Graphic` @@ -94,40 +177,260 @@ def __init__( n_datapoints = self.data.value.shape[0] + geo_kwargs = {"positions": self._data.buffer} + aa = kwargs.get("alpha_mode", "auto") in ("blend", "weighted_blend") - geo_kwargs = {"positions": self._data.buffer} - material_kwargs = dict(pick_write=True, aa=aa, depth_compare="<=") + material_kwargs = dict( + pick_write=True, + aa=aa, + depth_compare="<=", + ) + + self._markers: VertexMarkers | UniformMarker | None = None + self._edge_colors: UniformEdgeColor | VertexColors | None = None + self._edge_width: EdgeWidth | None = None + self._point_rotations: VertexRotations | UniformRotations | None = None + self._image: TextureArray | None = None + + # material cannot be changed after the ScatterGraphic is created + self._mode = mode + match self.mode: + case "markers": + # default + material = pygfx.PointsMarkerMaterial + + if uniform_marker: + if not isinstance(markers, str): + raise TypeError( + "must pass a single marker if uniform_marker is True" + ) + + self._markers = UniformMarker(markers) + + material_kwargs["marker_mode"] = pygfx.MarkerMode.uniform + material_kwargs["marker"] = self._markers.value + else: + material_kwargs["marker_mode"] = pygfx.MarkerMode.vertex + + self._markers = VertexMarkers(markers, n_datapoints) + + geo_kwargs["markers"] = self._markers.buffer + + if edge_colors is None: + # interpret as no edge color + edge_colors = (0, 0, 0, 0) + + if uniform_edge_color: + if not isinstance(edge_colors, (str, pygfx.Color)): + if len(edge_colors) not in [3, 4]: + raise TypeError( + f"if `uniform_edge_color` is True, then `edge_color` must be a str, pygfx.Color, " + f"or an RGB(A) tuple, list, array representation of a single color. You have passed: " + f"{edge_colors}" + ) + + self._edge_colors = UniformEdgeColor(edge_colors) + material_kwargs["edge_color"] = self._edge_colors.value + material_kwargs["edge_color_mode"] = pygfx.ColorMode.uniform + else: + self._edge_colors = VertexColors( + edge_colors, n_datapoints, property_name="edge_colors" + ) + material_kwargs["edge_color_mode"] = pygfx.ColorMode.vertex + geo_kwargs["edge_colors"] = self._edge_colors.buffer + + self._edge_width = EdgeWidth(edge_width) + material_kwargs["edge_width"] = self._edge_width.value + material_kwargs["custom_sdf"] = custom_sdf + + case "simple": + # basic points material + material = pygfx.PointsMaterial + + case "gaussian": + material = pygfx.PointsGaussianBlobMaterial + + case "image": + material = pygfx.PointsSpriteMaterial + # sprites should actually only be one texture, but we don't + # want to create a new buffer manager just for sprites. + # If someone is creating scatter plots with images of size + # larger than the typical limit of 16384, I'm very curious + # to know what they're trying to visualize + shared = pygfx.renderers.wgpu.get_shared() + limit = shared.device.limits["max-texture-dimension-2d"] + if any([dim > limit for dim in image.shape]): + raise BufferError( + f"Scatter point image dimension is greater than the device texture limit." + f"Your device limit is: {limit} but your image shape is: {image.shape}" + ) + + # create texture array with normalized image + self._image = TextureArray( + image / np.nanmax(image), property_name="image" + ) + + material_kwargs["sprite"] = self._image.buffer[0, 0] + self._size_space = SizeSpace(size_space) if uniform_color: - material_kwargs["color_mode"] = "uniform" + material_kwargs["color_mode"] = pygfx.ColorMode.uniform material_kwargs["color"] = self.colors else: - material_kwargs["color_mode"] = "vertex" + material_kwargs["color_mode"] = pygfx.ColorMode.vertex geo_kwargs["colors"] = self.colors.buffer if uniform_size: - material_kwargs["size_mode"] = "uniform" + material_kwargs["size_mode"] = pygfx.SizeMode.uniform self._sizes = UniformSize(sizes) material_kwargs["size"] = self.sizes else: - material_kwargs["size_mode"] = "vertex" - self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) + material_kwargs["size_mode"] = pygfx.SizeMode.vertex + self._sizes = VertexPointSizes(sizes, n_datapoints=n_datapoints) geo_kwargs["sizes"] = self.sizes.buffer + match point_rotation_mode: + case pygfx.enums.RotationMode.vertex: + self._point_rotations = VertexRotations( + point_rotations, n_datapoints=n_datapoints + ) + geo_kwargs["rotations"] = self._point_rotations.buffer + + case pygfx.enums.RotationMode.uniform: + self._point_rotations = UniformRotations(point_rotations) + + case pygfx.enums.RotationMode.curve: + pass # nothing special for curve rotation mode + + case _: + raise ValueError( + f"`point_rotation_mode` must be one of: {pygfx.enums.RotationMode}, " + f"you have passed: {point_rotation_mode}" + ) + + material_kwargs["rotation_mode"] = point_rotation_mode material_kwargs["size_space"] = self.size_space + world_object = pygfx.Points( pygfx.Geometry(**geo_kwargs), - material=pygfx.PointsMaterial(**material_kwargs), + material=material(**material_kwargs), ) self._set_world_object(world_object) @property - def sizes(self) -> PointsSizesFeature | float: + def mode(self) -> str: + """scatter point display mode""" + return self._mode + + @property + def markers(self) -> str | VertexMarkers | None: + """markers if mode is 'marker'""" + if isinstance(self._markers, VertexMarkers): + return self._markers + elif isinstance(self._markers, UniformMarker): + return self._markers.value + + @markers.setter + def markers(self, value: str | np.ndarray[str] | Sequence[str]): + if self.mode != "markers": + raise AttributeError( + f"scatter plot is: {self.mode}. The mode must be 'markers' to set the markers" + ) + if isinstance(self._markers, VertexMarkers): + self._markers[:] = value + elif isinstance(self._markers, UniformMarker): + self._markers.set_value(self, value) + + @property + def edge_colors(self) -> str | pygfx.Color | VertexColors | None: + """edge_colors if mode is 'marker'""" + + if isinstance(self._edge_colors, VertexColors): + return self._edge_colors + + elif isinstance(self._edge_colors, UniformEdgeColor): + return self._edge_colors.value + + @edge_colors.setter + def edge_colors(self, value: str | np.ndarray | Sequence[str] | Sequence[float]): + if self.mode != "markers": + raise AttributeError( + f"scatter plot is: {self.mode}. The mode must be 'markers' to set the edge_colors" + ) + + if isinstance(self._edge_colors, VertexColors): + self._edge_colors[:] = value + + elif isinstance(self._edge_colors, UniformEdgeColor): + self._edge_colors.set_value(self, value) + + @property + def edge_width(self) -> float | None: + """Get or set the edge_width if mode is 'markers'""" + if self._edge_width is None: + return None + + return self._edge_width.value + + @edge_width.setter + def edge_width(self, value: float): + if self.mode != "markers": + raise AttributeError( + f"scatter plot is: {self.mode}. The mode must be 'markers' to set the edge_width" + ) + + self._edge_width.set_value(self, value) + + @property + def point_rotation_mode(self) -> str: + """point rotation mode, read-only, one of 'uniform', 'vertex', or 'curve'""" + return self.world_object.material.rotation_mode + + @property + def point_rotations(self) -> VertexRotations | float | None: + """rotation of each point, in radians, if `point_rotation_mode` is 'uniform' or 'vertex'""" + + if isinstance(self._point_rotations, VertexRotations): + return self._point_rotations + + elif isinstance(self._point_rotations, UniformRotations): + return self._point_rotations.value + + @point_rotations.setter + def point_rotations(self, value: float | np.ndarray[float]): + if self.point_rotation_mode not in ["uniform", "vertex"]: + raise AttributeError( + f"point_rotation_mode is: {self.point_rotation_mode}. " + f"it be 'uniform' or 'vertex' to set the `point_rotations`" + ) + + if isinstance(self._point_rotations, VertexRotations): + self._point_rotations[:] = value + + elif isinstance(self._point_rotations, UniformRotations): + self._point_rotations.set_value(self, value) + + @property + def image(self) -> TextureArray | None: + """Get or set the image data, returns None if scatter plot mode is not 'image'""" + return self._image + + @image.setter + def image(self, data): + if self.mode != "image": + raise AttributeError( + f"scatter plot is: {self.mode}. The mode must be 'image' to set the image" + ) + + self._image[:] = data + + @property + def sizes(self) -> VertexPointSizes | float: """Get or set the scatter point size(s)""" - if isinstance(self._sizes, PointsSizesFeature): + if isinstance(self._sizes, VertexPointSizes): return self._sizes elif isinstance(self._sizes, UniformSize): @@ -135,7 +438,7 @@ def sizes(self) -> PointsSizesFeature | float: @sizes.setter def sizes(self, value): - if isinstance(self._sizes, PointsSizesFeature): + if isinstance(self._sizes, VertexPointSizes): self._sizes[:] = value elif isinstance(self._sizes, UniformSize): diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 96c76f9a8..62f824227 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -4,6 +4,8 @@ import numpy +import pygfx + from ..graphics import * from ..graphics._base import Graphic @@ -103,6 +105,7 @@ def add_image_volume( ) -> ImageVolumeGraphic: """ + Create an ImageVolumeGraphic. Parameters ---------- @@ -110,7 +113,7 @@ def add_image_volume( array-like, usually numpy.ndarray, must support ``memoryview()``. Shape must be [n_planes, n_rows, n_cols] for grayscale, or [n_planes, n_rows, n_cols, 3 | 4] for RGB(A) - mode: str, default "ray" + mode: str, default "mip" render mode, one of "mip", "minip", "iso" or "slice" vmin: float @@ -432,14 +435,26 @@ def add_line_stack( def add_scatter( self, data: Any, - colors: str | numpy.ndarray | tuple[float] | list[float] | list[str] = "w", + colors: Union[str, numpy.ndarray, Sequence[float], Sequence[str]] = "w", uniform_color: bool = False, cmap: str = None, cmap_transform: numpy.ndarray = None, - isolated_buffer: bool = True, + mode: Literal["markers", "simple", "gaussian", "image"] = "markers", + markers: Union[str, numpy.ndarray, Sequence[str]] = "o", + uniform_marker: bool = False, + custom_sdf: str = None, + edge_colors: Union[ + str, pygfx.utils.color.Color, numpy.ndarray, Sequence[float] + ] = "black", + uniform_edge_color: bool = True, + edge_width: float = 1.0, + image: numpy.ndarray = None, + point_rotations: float | numpy.ndarray = 0, + point_rotation_mode: Literal["uniform", "vertex", "curve"] = "uniform", sizes: Union[float, numpy.ndarray, Sequence[float]] = 1, uniform_size: bool = False, size_space: str = "screen", + isolated_buffer: bool = True, **kwargs, ) -> ScatterGraphic: """ @@ -468,9 +483,66 @@ def add_scatter( cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap - isolated_buffer: bool, default True - whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. + mode: one of: "markers", "simple", "gaussian", "image", default "markers" + The scatter points mode, cannot be changed after the graphic has been created. + + * markers: represent points with various or custom markers, default + * simple: all scatters points are simple circles + * gaussian: each point is a gaussian blob + * image: use an image for each point, pass an array to the `image` kwarg, these are also called sprites + + markers: None | str | np.ndarray | Sequence[str], default "o" + The shape of the markers when `mode` is "markers" + + Supported values: + + * A string from pygfx.MarkerShape enum + * Matplotlib compatible characters: "osD+x^v<>". + * Unicode symbols: "●○■♦♥♠♣✳▲▼◀▶". + * Emojis: "❤️♠️♣️♦️💎💍✳️📍". + * A string containing the value "custom". In this case, the WGSL + code defined by ``custom_sdf`` will be used. + + uniform_marker: bool, default False + Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use + the same marker for all points and want to save GPU RAM. + + custom_sdf: str = None, + The SDF code for the marker shape when the marker is set to custom. + Can be used when `mode` is "markers". + + Negative values are inside the shape, positive values are outside the + shape. + + The SDF's takes in two parameters `coords: vec2` and `size: f32`. + The first is a WGSL coordinate and `size` is the overall size of + the texture. The returned value should be the signed distance from + any edge of the shape. Distances (positive and negative) that are + less than half the `edge_width` in absolute terms will be colored + with the `edge_color`. Other negative distances will be colored by + `colors`. + + edge_colors: str | np.ndarray | pygfx.Color | Sequence[float], default "black" + edge color of the markers, used when `mode` is "markers" + + uniform_edge_color: bool, default True + Set the same edge color for all markers. Useful for saving GPU RAM. + + edge_width: float = 1.0, + Width of the marker edges. used when `mode` is "markers". + + image: ArrayLike, optional + renders an image at the scatter points, also known as sprites. + The image color is multiplied with the point's "normal" color. + + point_rotations: float | ArrayLike = 0, + The rotation of the scatter points in radians. Default 0. A single float rotation value can be set on all + points, or an array of rotation values can be used to set per-point rotations + + point_rotation_mode: one of: "uniform" | "vertex" | "curve", default "uniform" + * uniform: set the same rotation for every point, useful to save GPU RAM + * vertex: set per-vertex rotations + * curve: The rotation follows the curve of the line defined by the points (in screen space) sizes: float or iterable of float, optional, default 1.0 sizes of the scatter points @@ -480,7 +552,11 @@ def add_scatter( save GPU VRAM when all points have the same size. size_space: str, default "screen" - coordinate space in which the size is expressed ("screen", "world", "model") + coordinate space in which the size is expressed, one of ("screen", "world", "model") + + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. kwargs passed to :class:`.Graphic` @@ -494,10 +570,20 @@ def add_scatter( uniform_color, cmap, cmap_transform, - isolated_buffer, + mode, + markers, + uniform_marker, + custom_sdf, + edge_colors, + uniform_edge_color, + edge_width, + image, + point_rotations, + point_rotation_mode, sizes, uniform_size, size_space, + isolated_buffer, **kwargs, ) diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 968c68d2a..46433180f 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -33,6 +33,7 @@ def generate_add_graphics_methods(): f.write("from typing import *\n\n") f.write("import numpy\n\n") + f.write("import pygfx\n\n") f.write("from ..graphics import *\n") f.write("from ..graphics._base import Graphic\n\n") diff --git a/tests/conftest.py b/tests/conftest.py index 29ac02fcb..761b0762e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,5 +6,9 @@ def pytest_sessionstart(session): - pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension-2d": MAX_TEXTURE_SIZE}) - pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension-3d": MAX_TEXTURE_SIZE_3D}) + pygfx.renderers.wgpu.set_wgpu_limits( + **{"max-texture-dimension-2d": MAX_TEXTURE_SIZE} + ) + pygfx.renderers.wgpu.set_wgpu_limits( + **{"max-texture-dimension-3d": MAX_TEXTURE_SIZE_3D} + ) diff --git a/tests/test_common_features.py b/tests/test_common_features.py index 5671478a7..aea016aae 100644 --- a/tests/test_common_features.py +++ b/tests/test_common_features.py @@ -4,7 +4,13 @@ import pytest import fastplotlib as fpl -from fastplotlib.graphics.features import GraphicFeatureEvent, Name, Offset, Rotation, Visible +from fastplotlib.graphics.features import ( + GraphicFeatureEvent, + Name, + Offset, + Rotation, + Visible, +) def make_graphic(kind: str, **kwargs): diff --git a/tests/test_figure.py b/tests/test_figure.py index 520091009..d5d4087e3 100644 --- a/tests/test_figure.py +++ b/tests/test_figure.py @@ -175,28 +175,16 @@ def test_set_controllers_from_existing_controllers(): def test_subplot_names(): # names must be unique with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", "4", "4", "5"] - ) + fpl.Figure(shape=(2, 3), names=["1", "2", "3", "4", "4", "5"]) with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=["1", "2", None, "4", "4", "5"] - ) + fpl.Figure(shape=(2, 3), names=["1", "2", None, "4", "4", "5"]) with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=[None, "2", None, "4", "4", "5"] - ) + fpl.Figure(shape=(2, 3), names=[None, "2", None, "4", "4", "5"]) # len(names) <= n_subplots - fig = fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", "4", "5", "6"] - ) + fig = fpl.Figure(shape=(2, 3), names=["1", "2", "3", "4", "5", "6"]) assert fig[0, 0].name == "1" assert fig[0, 1].name == "2" @@ -205,10 +193,7 @@ def test_subplot_names(): assert fig[1, 1].name == "5" assert fig[1, 2].name == "6" - fig = fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", None, "5", "6"] - ) + fig = fpl.Figure(shape=(2, 3), names=["1", "2", "3", None, "5", "6"]) assert fig[0, 0].name == "1" assert fig[0, 1].name == "2" @@ -217,10 +202,7 @@ def test_subplot_names(): assert fig[1, 1].name == "5" assert fig[1, 2].name == "6" - fig = fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", None, "5", None] - ) + fig = fpl.Figure(shape=(2, 3), names=["1", "2", "3", None, "5", None]) assert fig[0, 0].name == "1" assert fig[0, 1].name == "2" @@ -230,10 +212,7 @@ def test_subplot_names(): assert fig[1, 2].name is None # if fewer subplot names are given than n_sublots, pad with Nones - fig = fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", "4"] - ) + fig = fpl.Figure(shape=(2, 3), names=["1", "2", "3", "4"]) assert fig[0, 0].name == "1" assert fig[0, 1].name == "2" @@ -244,19 +223,10 @@ def test_subplot_names(): # raise if len(names) > n_subplots with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", "4", "5", "6", "7"] - ) + fpl.Figure(shape=(2, 3), names=["1", "2", "3", "4", "5", "6", "7"]) with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=["1", "2", "3", "4", None, "6", "7"] - ) + fpl.Figure(shape=(2, 3), names=["1", "2", "3", "4", None, "6", "7"]) with pytest.raises(ValueError): - fpl.Figure( - shape=(2, 3), - names=["1", None, "3", "4", None, "6", "7"] - ) + fpl.Figure(shape=(2, 3), names=["1", None, "3", "4", None, "6", "7"]) diff --git a/tests/test_image_volume_graphic.py b/tests/test_image_volume_graphic.py index f6c6c0641..3cb574e78 100644 --- a/tests/test_image_volume_graphic.py +++ b/tests/test_image_volume_graphic.py @@ -51,8 +51,12 @@ def check_set_slice( data_values[:, : col_slice.start], data[:, : col_slice.start] ) npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :]) - npt.assert_almost_equal(data_values[:, :, : zpl_slice.start], data[:, :, : zpl_slice.start]) - npt.assert_almost_equal(data_values[:, :, zpl_slice.stop :], data[:, :, zpl_slice.stop :]) + npt.assert_almost_equal( + data_values[:, :, : zpl_slice.start], data[:, :, : zpl_slice.start] + ) + npt.assert_almost_equal( + data_values[:, :, zpl_slice.stop :], data[:, :, zpl_slice.stop :] + ) global EVENT_RETURN_VALUE assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) diff --git a/tests/test_markers_buffer_manager.py b/tests/test_markers_buffer_manager.py new file mode 100644 index 000000000..65ead392e --- /dev/null +++ b/tests/test_markers_buffer_manager.py @@ -0,0 +1,143 @@ +import numpy as np +from numpy import testing as npt +import pytest + +import fastplotlib as fpl +import pygfx +from fastplotlib.graphics.features import GraphicFeatureEvent, VertexMarkers +from fastplotlib.graphics.features._scatter import marker_names, vectorized_user_markers_to_std_markers + +from .utils import ( + generate_slice_indices, + generate_positions_spiral_data, +) + + +EVENT_RETURN_VALUE: GraphicFeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +MARKERS1 = list("osD+x^v<>*") +MARKERS2 = list(">+vx*")) +def test_uniform_markers(marker): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + scatter = fig[0, 0].add_scatter(data, markers=marker, uniform_marker=True) + + marker_full_name = marker_names.get(marker) + + assert isinstance(scatter.world_object.material, pygfx.PointsMarkerMaterial) + assert scatter.world_object.material.marker_mode == pygfx.MarkerMode.uniform + assert isinstance(scatter._markers, UniformMarker) + + assert scatter.markers == marker_full_name + assert scatter.world_object.material.marker == marker_full_name + + # test changes and event + scatter.add_event_handler(event_handler, "markers") + scatter.markers = "o" + assert scatter.markers == pygfx.MarkerShape.circle + assert scatter.world_object.material.marker == pygfx.MarkerShape.circle + + check_event(scatter, "markers", pygfx.MarkerShape.circle) + + +@pytest.mark.parametrize("to_type", [list, tuple, np.array]) +@pytest.mark.parametrize("uniform_marker", [True, False]) +def test_incompatible_marker_args(to_type, uniform_marker): + markers = ["o"] * 3 + ["s"] * 3 + ["+"] * 3 + ["x"] + + markers = to_type(markers) + + data = generate_positions_spiral_data("xyz") + + fig = fpl.Figure() + + if uniform_marker: + with pytest.raises(TypeError): + scatter = fig[0, 0].add_scatter(data, markers=markers, uniform_marker=True) + + else: + scatter = fig[0, 0].add_scatter(data, markers=markers, uniform_marker=False) + assert isinstance(scatter._markers, VertexMarkers) + assert scatter.world_object.material.marker_mode == pygfx.MarkerMode.vertex + + +def test_uniform_custom_sdf(): + lower_right_triangle_sdf = """ + // hardcode square root of 2 + let m_sqrt_2 = 1.4142135; + + // given a distance from an origin point, this defines the hypotenuse of a lower right triangle + let distance = (-coord.x + coord.y) / m_sqrt_2; + + // return distance for this position + return distance * size; + """ + + data = generate_positions_spiral_data("xyz") + + fig = fpl.Figure() + + scatter = fig[0, 0].add_scatter( + data, markers="custom", uniform_marker=True, custom_sdf=lower_right_triangle_sdf + ) + + assert scatter.markers == "custom" + assert scatter.world_object.material.marker == "custom" + assert scatter.world_object.material.custom_sdf == lower_right_triangle_sdf + +# test with both list[str] and 2D numpy array inputs as colors +@pytest.mark.parametrize("edge_colors",[generate_color_inputs("multi")[0], generate_color_inputs("multi")[1]]) +def test_edge_colors(edge_colors): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + scatter = fig[0, 0].add_scatter( + data=data, + edge_colors=edge_colors, + uniform_edge_color=False, + ) + + assert isinstance(scatter._edge_colors, VertexColors) + + npt.assert_almost_equal(scatter.edge_colors.value, MULTI_COLORS_TRUTH) + + assert ( + scatter.edge_colors.buffer is scatter.world_object.geometry.edge_colors + ) + + # test changes, don't need to test extensively here since it's tested in the main VertexColors test + new_colors, array = generate_color_inputs("multi2") + scatter.edge_colors = new_colors + npt.assert_almost_equal(scatter.edge_colors.value, array) + + +@pytest.mark.parametrize("edge_color", ["r", (1, 0, 0), [1, 0, 0], np.array([1, 0, 0])]) +def test_uniform_edge_colors(edge_color): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + scatter = fig[0, 0].add_scatter( + data=data, edge_colors=edge_color, uniform_edge_color=True + ) + + assert isinstance(scatter._edge_colors, UniformEdgeColor) + assert scatter.edge_colors == pygfx.Color(edge_color) + assert scatter.world_object.material.edge_color == pygfx.Color(edge_color) + + # test changes and event + scatter.add_event_handler(event_handler, "edge_colors") + scatter.edge_colors = "g" + + assert scatter.edge_colors == pygfx.Color("g") + assert scatter.world_object.material.edge_color == pygfx.Color("g") + + check_event(scatter, "edge_colors", pygfx.Color("g")) + + +@pytest.mark.parametrize("edge_colors", [generate_color_inputs("multi")[0],generate_color_inputs("multi")[1]]) +@pytest.mark.parametrize("uniform_edge_color", [False, True]) +def test_incompatible_edge_colors_args(edge_colors, uniform_edge_color): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + if uniform_edge_color: + with pytest.raises(TypeError): + scatter = fig[0, 0].add_scatter( + data=data, + edge_colors=edge_colors, + uniform_edge_color=uniform_edge_color, + ) + + +@pytest.mark.parametrize("edge_width", [0.0, 0.5, 1.0, 5.0]) +def test_edge_width(edge_width): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + scatter = fig[0, 0].add_scatter( + data=data, + edge_width=edge_width, + ) + + assert isinstance(scatter._edge_width, EdgeWidth) + assert scatter.world_object.material.edge_width == edge_width + assert scatter.edge_width == edge_width + + # test changes and events + scatter.add_event_handler(event_handler, "edge_width") + scatter.edge_width = 2.0 + + npt.assert_almost_equal(scatter.edge_width, 2.0) + npt.assert_almost_equal(scatter.world_object.material.edge_width, 2.0) + + check_event(scatter, "edge_width", 2.0) + + +def test_uniform_rotation(): + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + + scatter = fig[0, 0].add_scatter( + data=data, + point_rotations=np.pi / 2, + ) + + assert scatter.point_rotation_mode == "uniform" + npt.assert_almost_equal(scatter.point_rotations, np.pi / 2) + + # test changes and events + scatter.add_event_handler(event_handler, "point_rotations") + scatter.point_rotations = np.pi / 3 + + npt.assert_almost_equal(scatter.point_rotations, np.pi / 3) + + check_event(scatter, "point_rotations", np.pi / 3) + + +def test_sprite(): + image = np.array( + [ + [1, 0, 1], + [0, 1, 0], + [1, 1, 1], + ] + ) + + data = generate_positions_spiral_data("xyz") + + fig = fpl.Figure() + + scatter = fig[0, 0].add_scatter( + data=data, + mode="image", + image=image, + ) + + # make sure the image is a fpl TextureArray + assert isinstance(scatter.image, TextureArray) + # make sure the sprite is the TextureArray buffer, i.e. a pygfx.Texture + assert scatter.world_object.material.sprite is scatter.image.buffer[0, 0] + assert scatter.image.buffer.size == 1 + + npt.assert_almost_equal(scatter.image.value, image) + npt.assert_almost_equal(scatter.image.buffer[0, 0].data, image) + + # test changes and event + + image2 = np.array( + [ + [0, 1, 0], + [1, 0, 1], + [0, 1, 0] + ] + ) + + scatter.add_event_handler(event_handler, "image") + + scatter.image = image2 + npt.assert_almost_equal(scatter.image.buffer[0, 0].data, image2) + + assert EVENT_RETURN_VALUE.graphic is scatter + assert EVENT_RETURN_VALUE.target is scatter.world_object + assert EVENT_RETURN_VALUE.type == "image" + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], image2) diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py index 2f55eab27..d1260e27c 100644 --- a/tests/test_sizes_buffer_manager.py +++ b/tests/test_sizes_buffer_manager.py @@ -2,7 +2,7 @@ from numpy import testing as npt import pytest -from fastplotlib.graphics.features import PointsSizesFeature +from fastplotlib.graphics.features import VertexPointSizes from .utils import generate_slice_indices @@ -28,7 +28,7 @@ def generate_data(input_type: str) -> np.ndarray | float: @pytest.mark.parametrize("data", [generate_data(v) for v in ["float", "sine"]]) def test_create_buffer(data): - sizes = PointsSizesFeature(data, n_datapoints=10) + sizes = VertexPointSizes(data, n_datapoints=10) if isinstance(data, float): npt.assert_almost_equal(sizes[:], generate_data("float")) @@ -50,7 +50,7 @@ def test_slice(slice_method: dict, user_input: str): size = slice_method["size"] others = slice_method["others"] - sizes = PointsSizesFeature(data, n_datapoints=10) + sizes = VertexPointSizes(data, n_datapoints=10) match user_input: case "float": diff --git a/tests/test_texture_array_volume.py b/tests/test_texture_array_volume.py index 0cc425230..f2d28501b 100644 --- a/tests/test_texture_array_volume.py +++ b/tests/test_texture_array_volume.py @@ -31,6 +31,7 @@ def make_data(z: int, n_rows: int, n_cols: int) -> np.ndarray: return data.T + def check_texture_array( data: np.ndarray, ta: TextureArrayVolume, @@ -68,9 +69,7 @@ def check_texture_array( data_row_start_index = chunk_row * MAX_TEXTURE_SIZE_3D data_col_start_index = chunk_col * MAX_TEXTURE_SIZE_3D - data_z_stop_index = min( - data.shape[0], data_z_start_index + MAX_TEXTURE_SIZE_3D - ) + data_z_stop_index = min(data.shape[0], data_z_start_index + MAX_TEXTURE_SIZE_3D) data_row_stop_index = min( data.shape[1], data_row_start_index + MAX_TEXTURE_SIZE_3D @@ -91,14 +90,14 @@ def check_set_slice(data, ta, zdim_slice, row_slice, col_slice): npt.assert_almost_equal(ta[zdim_slice, row_slice, col_slice], 1) # make sure other vals unchanged - npt.assert_almost_equal(ta[:zdim_slice.start], data[:zdim_slice.start]) - npt.assert_almost_equal(ta[zdim_slice.stop:], data[zdim_slice.stop:]) + npt.assert_almost_equal(ta[: zdim_slice.start], data[: zdim_slice.start]) + npt.assert_almost_equal(ta[zdim_slice.stop :], data[zdim_slice.stop :]) - npt.assert_almost_equal(ta[:, :row_slice.start], data[:, :row_slice.start]) - npt.assert_almost_equal(ta[:, row_slice.stop:], data[:, row_slice.stop:]) + npt.assert_almost_equal(ta[:, : row_slice.start], data[:, : row_slice.start]) + npt.assert_almost_equal(ta[:, row_slice.stop :], data[:, row_slice.stop :]) - npt.assert_almost_equal(ta[:, :, :col_slice.start], data[:, :, :col_slice.start]) - npt.assert_almost_equal(ta[:, :, col_slice.stop:], data[:, :, col_slice.stop:]) + npt.assert_almost_equal(ta[:, :, : col_slice.start], data[:, :, : col_slice.start]) + npt.assert_almost_equal(ta[:, :, col_slice.stop :], data[:, :, col_slice.stop :]) def make_image_volume_graphic(data) -> fpl.ImageVolumeGraphic: diff --git a/tests/utils.py b/tests/utils.py index bc9a092c8..6da080433 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -148,6 +148,23 @@ def generate_color_inputs( array = np.vstack([pygfx.Color(c) for c in s]) return [s, array] + if name == "multi2": + # a second multi option + s = [ + "g", + "r", + "cyan", + "magenta", + "b", + "black", + "white", + "purple", + "yellow", + "pink", + ] + array = np.vstack([pygfx.Color(c) for c in s]) + return [s, array] + color = pygfx.Color(name) s = name @@ -172,3 +189,37 @@ def generate_color_inputs( [1.0, 0.6470588445663452, 0.0, 1.0], ] ) + + +TRUTH_CMAPS = { + "jet": np.array( + [ + [0.0, 0.0, 0.5, 1.0], + [0.0, 0.0, 0.99910873, 1.0], + [0.0, 0.37843138, 1.0, 1.0], + [0.0, 0.8333333, 1.0, 1.0], + [0.30044276, 1.0, 0.66729915, 1.0], + [0.65464896, 1.0, 0.31309298, 1.0], + [1.0, 0.90123457, 0.0, 1.0], + [1.0, 0.4945534, 0.0, 1.0], + [1.0, 0.08787218, 0.0, 1.0], + [0.5, 0.0, 0.0, 1.0], + ], + dtype=np.float32, + ), + "viridis": np.array( + [ + [0.267004, 0.004874, 0.329415, 1.0], + [0.281412, 0.155834, 0.469201, 1.0], + [0.244972, 0.287675, 0.53726, 1.0], + [0.190631, 0.407061, 0.556089, 1.0], + [0.147607, 0.511733, 0.557049, 1.0], + [0.119483, 0.614817, 0.537692, 1.0], + [0.20803, 0.718701, 0.472873, 1.0], + [0.421908, 0.805774, 0.35191, 1.0], + [0.699415, 0.867117, 0.175971, 1.0], + [0.993248, 0.906157, 0.143936, 1.0], + ], + dtype=np.float32, + ), +} From 262c68a6d3767d12a94cf0b5172b0de13e77ce4c Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 23 Oct 2025 09:35:28 -0400 Subject: [PATCH 23/52] Fix docstring (#930) * fix docstring * fix docstring * add docs _gallery to .gitignore * no edge color for spinning spiral example --- .gitignore | 1 + examples/scatter/spinning_spiral.py | 2 +- fastplotlib/graphics/scatter.py | 3 +-- fastplotlib/layouts/_graphic_methods_mixin.py | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index c599d5f8c..bea3faf0d 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,4 @@ dmypy.json .vscode/ examples/desktop/diffs/*.png +docs/source/_gallery/ diff --git a/examples/scatter/spinning_spiral.py b/examples/scatter/spinning_spiral.py index 80e893301..89e74eaec 100644 --- a/examples/scatter/spinning_spiral.py +++ b/examples/scatter/spinning_spiral.py @@ -34,7 +34,7 @@ canvas_kwargs={"max_fps": 500, "vsync": False} ) -spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", alpha=0.5, sizes=sizes) +spiral = figure[0, 0].add_scatter(data, cmap="viridis_r", edge_colors=None, alpha=0.5, sizes=sizes) # pre-generate normally distributed data to jitter the points before each render jitter = np.random.normal(scale=0.001, size=n * 3).reshape((n, 3)) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 4df4f7557..a2e696a82 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -101,8 +101,7 @@ def __init__( * Matplotlib compatible characters: "osD+x^v<>*". * Unicode symbols: "●○■♦♥♠♣✳▲▼◀▶". * Emojis: "❤️♠️♣️♦️💎💍✳️📍". - * A string containing the value "custom". In this case, the WGSL - code defined by ``custom_sdf`` will be used. + * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. uniform_marker: bool, default False Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 62f824227..a38eaa8eb 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -497,11 +497,10 @@ def add_scatter( Supported values: * A string from pygfx.MarkerShape enum - * Matplotlib compatible characters: "osD+x^v<>". + * Matplotlib compatible characters: "osD+x^v<>*". * Unicode symbols: "●○■♦♥♠♣✳▲▼◀▶". * Emojis: "❤️♠️♣️♦️💎💍✳️📍". - * A string containing the value "custom". In this case, the WGSL - code defined by ``custom_sdf`` will be used. + * A string containing the value "custom". In this case, WGSL code defined by ``custom_sdf`` will be used. uniform_marker: bool, default False Use the same marker for all points. Only valid when `mode` is "markers". Useful if you need to use From 61b0c0111106c5e41d3a194e37d3f818f202d691 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 3 Nov 2025 19:24:28 -0500 Subject: [PATCH 24/52] VectorsGraphic (#929) * start vector fields * basics of vector field works * add vector field to api docs * update event tables * add vector field to subsection_order * update vector field, almost there * vector field done and working * cleanup * update docstring * update * black * black * update * update .gitignore * restore stuff in .gitignore * test examples * update min pygfx version * ome zarr being annoying * remove multi-channel from screenshot tests, only show code in nb * don't run multi channel example * rename to VectorsGraphic since we will use field for something else * interactive electric field example * update name * black * update name * wrong sign, now correct * update api docs * Apply suggestion from @clewis7 Co-authored-by: Caitlin Lewis * fix render order and blending, add comment * rename * docstrings, comments * checks * clarify example * cleanup --------- Co-authored-by: Caitlin Lewis --- .gitignore | 2 + .../api/graphic_features/VectorDirections.rst | 35 ++ .../api/graphic_features/VectorPositions.rst | 35 ++ docs/source/api/graphic_features/index.rst | 2 + docs/source/api/graphics/VectorsGraphic.rst | 49 ++ docs/source/api/graphics/index.rst | 1 + docs/source/api/layouts/subplot.rst | 1 + docs/source/conf.py | 1 + docs/source/user_guide/event_tables.rst | 102 +++++ .../image_volume_multi_channel.py | 11 +- .../image_volume_multi_channel.png | 3 - .../no-imgui-image_volume_multi_channel.png | 3 - examples/tests/testutils.py | 1 + examples/vectors/README.rst | 2 + .../vectors_interact_electric_charges.py | 182 ++++++++ examples/vectors/vectors_simple.py | 43 ++ examples/vectors/vectors_swirl.py | 47 ++ fastplotlib/graphics/__init__.py | 2 + fastplotlib/graphics/_vectors.py | 430 ++++++++++++++++++ fastplotlib/graphics/features/__init__.py | 8 + fastplotlib/graphics/features/_vectors.py | 187 ++++++++ fastplotlib/layouts/_graphic_methods_mixin.py | 55 +++ pyproject.toml | 2 +- scripts/generate_add_graphic_methods.py | 2 +- 24 files changed, 1192 insertions(+), 14 deletions(-) create mode 100644 docs/source/api/graphic_features/VectorDirections.rst create mode 100644 docs/source/api/graphic_features/VectorPositions.rst create mode 100644 docs/source/api/graphics/VectorsGraphic.rst delete mode 100644 examples/screenshots/image_volume_multi_channel.png delete mode 100644 examples/screenshots/no-imgui-image_volume_multi_channel.png create mode 100644 examples/vectors/README.rst create mode 100644 examples/vectors/vectors_interact_electric_charges.py create mode 100644 examples/vectors/vectors_simple.py create mode 100644 examples/vectors/vectors_swirl.py create mode 100644 fastplotlib/graphics/_vectors.py create mode 100644 fastplotlib/graphics/features/_vectors.py diff --git a/.gitignore b/.gitignore index bea3faf0d..950f261c0 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/source/sg_execution_times.rst # PyBuilder target/ @@ -134,5 +135,6 @@ dmypy.json # vs code .vscode/ +# diffs from visual regression tests examples/desktop/diffs/*.png docs/source/_gallery/ diff --git a/docs/source/api/graphic_features/VectorDirections.rst b/docs/source/api/graphic_features/VectorDirections.rst new file mode 100644 index 000000000..99e47b4a1 --- /dev/null +++ b/docs/source/api/graphic_features/VectorDirections.rst @@ -0,0 +1,35 @@ +.. _api.VectorDirections: + +VectorDirections +**************** + +================ +VectorDirections +================ +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VectorDirections_api + + VectorDirections + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VectorDirections_api + + VectorDirections.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VectorDirections_api + + VectorDirections.add_event_handler + VectorDirections.block_events + VectorDirections.clear_event_handlers + VectorDirections.remove_event_handler + VectorDirections.set_value + diff --git a/docs/source/api/graphic_features/VectorPositions.rst b/docs/source/api/graphic_features/VectorPositions.rst new file mode 100644 index 000000000..939c00e00 --- /dev/null +++ b/docs/source/api/graphic_features/VectorPositions.rst @@ -0,0 +1,35 @@ +.. _api.VectorPositions: + +VectorPositions +*************** + +=============== +VectorPositions +=============== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VectorPositions_api + + VectorPositions + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VectorPositions_api + + VectorPositions.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VectorPositions_api + + VectorPositions.add_event_handler + VectorPositions.block_events + VectorPositions.clear_event_handlers + VectorPositions.remove_event_handler + VectorPositions.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index d008b5202..5c5c2b464 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -32,6 +32,8 @@ Graphic Features VolumeIsoEmissive VolumeIsoShininess VolumeSlicePlane + VectorPositions + VectorDirections TextData FontSize TextFaceColor diff --git a/docs/source/api/graphics/VectorsGraphic.rst b/docs/source/api/graphics/VectorsGraphic.rst new file mode 100644 index 000000000..4a629f5db --- /dev/null +++ b/docs/source/api/graphics/VectorsGraphic.rst @@ -0,0 +1,49 @@ +.. _api.VectorsGraphic: + +VectorsGraphic +************** + +============== +VectorsGraphic +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VectorsGraphic_api + + VectorsGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VectorsGraphic_api + + VectorsGraphic.alpha + VectorsGraphic.alpha_mode + VectorsGraphic.axes + VectorsGraphic.block_events + VectorsGraphic.deleted + VectorsGraphic.directions + VectorsGraphic.event_handlers + VectorsGraphic.name + VectorsGraphic.offset + VectorsGraphic.positions + VectorsGraphic.right_click_menu + VectorsGraphic.rotation + VectorsGraphic.supported_events + VectorsGraphic.visible + VectorsGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VectorsGraphic_api + + VectorsGraphic.add_axes + VectorsGraphic.add_event_handler + VectorsGraphic.clear_event_handlers + VectorsGraphic.remove_event_handler + VectorsGraphic.rotate + diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index 640f76833..ac47a7dfd 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -9,6 +9,7 @@ Graphics ScatterGraphic ImageGraphic ImageVolumeGraphic + VectorsGraphic TextGraphic LineCollection LineStack diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 0183c7e63..4e40e8d08 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -54,6 +54,7 @@ Methods Subplot.add_line_stack Subplot.add_scatter Subplot.add_text + Subplot.add_vectors Subplot.auto_scale Subplot.center_graphic Subplot.center_scene diff --git a/docs/source/conf.py b/docs/source/conf.py index 63ded9cca..74a1fbaf9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -66,6 +66,7 @@ "../../examples/line", "../../examples/line_collection", "../../examples/scatter", + "../../examples/vectors", "../../examples/text", "../../examples/events", "../../examples/selection_tools", diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index c1f5b89e0..8e942830e 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -795,6 +795,108 @@ deleted | value | bool | True when graphic was deleted | +----------+------+-------------------------------+ +VectorsGraphic +-------------- + +positions +^^^^^^^^^ + +**event info dict** + ++----------+------------+----------------------+ +| dict key | type | description | ++==========+============+======================+ +| value | np.ndarray | new vector positions | ++----------+------------+----------------------+ + +directions +^^^^^^^^^^ + +**event info dict** + ++----------+------------+-----------------------+ +| dict key | type | description | ++==========+============+=======================+ +| value | np.ndarray | new vector directions | ++----------+------------+-----------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + TextGraphic ----------- diff --git a/examples/image_volume/image_volume_multi_channel.py b/examples/image_volume/image_volume_multi_channel.py index 6d444e835..01fc27ac6 100644 --- a/examples/image_volume/image_volume_multi_channel.py +++ b/examples/image_volume/image_volume_multi_channel.py @@ -5,8 +5,9 @@ Example with multi-channel volume images. Use alpha_mode "add" for additive blending. """ -# test_example = true -# sphinx_gallery_pygfx_docs = 'screenshot' +# test_example = false +# run_example = false +# sphinx_gallery_pygfx_docs = 'code' import fastplotlib as fpl from ome_zarr.io import parse_url @@ -18,10 +19,8 @@ # read the image data reader = Reader(parse_url(url)) -# nodes may include images, labels etc -nodes = list(reader()) -# first node will be the image pixel data -image_node = nodes[0] +# first node is image data +image_node = next(reader()) dask_data = image_node.data diff --git a/examples/screenshots/image_volume_multi_channel.png b/examples/screenshots/image_volume_multi_channel.png deleted file mode 100644 index 18ae331de..000000000 --- a/examples/screenshots/image_volume_multi_channel.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66233e1fd5a227a674a730afb12dd1c445b57a5f3b953454715735ecd2f95db8 -size 194287 diff --git a/examples/screenshots/no-imgui-image_volume_multi_channel.png b/examples/screenshots/no-imgui-image_volume_multi_channel.png deleted file mode 100644 index b8e106ecb..000000000 --- a/examples/screenshots/no-imgui-image_volume_multi_channel.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:30b0f3b18dfeff123d5287012347fec09a9ab16277b5e6d2beb9039699da9954 -size 204901 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 546ff120e..7b70defdb 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,6 +24,7 @@ "scatter/*.py", "line/*.py", "line_collection/*.py", + "vectors/*.py" "gridplot/*.py", "window_layouts/*.py", "events/*.py", diff --git a/examples/vectors/README.rst b/examples/vectors/README.rst new file mode 100644 index 000000000..a35457409 --- /dev/null +++ b/examples/vectors/README.rst @@ -0,0 +1,2 @@ +Vector Examples +=============== diff --git a/examples/vectors/vectors_interact_electric_charges.py b/examples/vectors/vectors_interact_electric_charges.py new file mode 100644 index 000000000..4aa1d41b6 --- /dev/null +++ b/examples/vectors/vectors_interact_electric_charges.py @@ -0,0 +1,182 @@ +""" +Static Electric Field +===================== + +Interactively move the charges around by clicking and dragging the mouse to see +the static field with the charges at their new positions. This is just computing +static fields, no electrodynamics or magnetic field effects are taken into account. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + + +# based on vacuum permittivity, 1/4πε from wikipedia: https://en.wikipedia.org/wiki/Coulomb%27s_law#Coulomb_constant +k_e = 8.98755 * 10**9 + + +def coulombs_law(q: float, r: np.ndarray) -> np.ndarray[float, float]: + """ + Compute force on a unit charge at a distance ``r`` from a particle of charge ``q``. + Broadcasts over ``r`` array. + + q: charge in coulombs + r: 2D array of distance vectors, shape [n, 2] + + Returns force vector at each distance ``r`` provided, shape [n, 2] + """ + r_cap = r / np.linalg.norm(r, ord=2, axis=1)[:, None] + F = k_e * ((q * r_cap) / ((np.linalg.norm(r, ord=2, axis=1))**2)[:, None]) + + return F + + +figure = fpl.Figure(size=(700, 750)) + +# positions of 3 particles in a 2d plane +positions = np.array([ + [3, 3], + [8, 5], + [4, 8], +]) + +# charges of the 3 particles +charges = np.array([ + 3.5 * 10**-10, + 1 * 10**-10, + -3.5 * 10**-10, +]) + +# red to indicate positive charge, blue to indicate negative charge +colors = ["r", "r", "b"] + +# scatter point to indicate particle positions +particles = figure[0, 0].add_scatter( + data=positions, + colors=colors, + sizes=1, + edge_width=0.05, + uniform_edge_color=False, + alpha=0.7, + size_space="model", + metadata={"charges": charges}, # you can store anything as arbitrary metadata + alpha_mode="blend", +) + +xs = np.linspace(0, 10, num=20) +ys = np.linspace(0, 10, num=20) + +x, y = np.meshgrid(xs, ys) + +# display vectors at these positions in the field +field_positions = np.column_stack([x.ravel(), y.ravel()]) + +# allocate array to store direction of the field at every position due to the charge of the 3 particles +# i.e., the force felt by a unit charge at a given position in the field +field_directions = np.zeros(field_positions.shape, dtype=np.float32) + +vectors = figure[0, 0].add_vectors( + positions=field_positions, + directions=field_directions, + alpha=0.7, + alpha_mode="blend", +) + + +def update_field(): + """update the static field w.r.t. the new positions of the particles""" + + # get force vectors due to each charge and add them up + force_vectors_total = np.zeros(field_positions.shape) + + for i in range(particles.data.value.shape[0]): + force_vectors = coulombs_law( + q=particles.metadata["charges"][i], # force due to one of the charges + r=field_positions - particles.data[:, :-1][i] + ) + + force_vectors_total = force_vectors_total + force_vectors + + # zero out when the force is too large to display + # large vectors will otherwise take up the entire plot area + force_vectors_total[np.linalg.norm(force_vectors_total, axis=1, ord=2) > 3.5] = 0 + + # update the graphic + vectors.directions = force_vectors_total + + +update_field() + +# render particles on top of field +particles.world_object.material.render_queue = vectors.world_object.material.render_queue + 1 + +# interactivity code, very similar to the "Drag points" example +is_moving = False +particle_index = None +# interact with particles by moving them with mouse +@particles.add_event_handler("pointer_down") +def start_drag(ev: pygfx.PointerEvent): + global is_moving + global particle_index + + if ev.button != 1: # check for left mouse button + return + + is_moving = True + particle_index = ev.pick_info["vertex_index"] + # set edge color to indicate this particle has been selected + particles.edge_colors[particle_index] = "y" + + +@figure.renderer.add_event_handler("pointer_move") +def move_point(ev): + global is_moving + global particle_index + + # if not moving, return + if not is_moving: + return + + # pause controller so mouse events move the scatter and not the camera + with figure[0, 0].controller.pause(): + # map x, y from screen space to world space + pos = figure[0, 0].map_screen_to_world(ev) + + if pos is None: + # end movement + is_moving = False + particle_index = None + return + + # change scatter data + particles.data[particle_index, :-1] = pos[:-1] + # update field + update_field() + + +@figure.renderer.add_event_handler("pointer_up") +def end_drag(ev: pygfx.PointerEvent): + global is_moving + global particle_index + + # end movement + if is_moving: + # reset color + particles.edge_colors[particle_index] = "k" + + is_moving = False + particle_index = None + + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/vectors/vectors_simple.py b/examples/vectors/vectors_simple.py new file mode 100644 index 000000000..e26d6da71 --- /dev/null +++ b/examples/vectors/vectors_simple.py @@ -0,0 +1,43 @@ +""" +Simple Vectors +============== + +Simple example with vectors. Similar to matplotlib quiver. + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 700)) + +start, stop, step = 0, 2 * np.pi, 0.2 + +# get uniform x, y positions +x, y = np.meshgrid(np.arange(start, stop, step), np.arange(start, stop, step)) + +# vectors, u and v are x and y components indicating directions +u = np.cos(x) +v = np.sin(y) + +# positions of each vector as [n_points, 2] array +positions = np.column_stack([x.ravel(), y.ravel()]) +# directions of each vector as a [n_points, 2] array +directions = np.column_stack([u.ravel(), v.ravel()]) + + +vectors = figure[0, 0].add_vectors( + positions=positions, + directions=directions, +) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/vectors/vectors_swirl.py b/examples/vectors/vectors_swirl.py new file mode 100644 index 000000000..fcfcb86b0 --- /dev/null +++ b/examples/vectors/vectors_swirl.py @@ -0,0 +1,47 @@ +""" +Swirling vectors +================ + +Example showing swirling vectors. Similar to matplotlib quiver. + +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(cameras="3d", controller_types="orbit", size=(700, 700)) + +start, stop, step = -1, 1, 0.3 + +# Make the grid +x, y, z = np.meshgrid( + np.arange(start, stop, step), + np.arange(start, stop, step), + np.arange(start, stop, step), +) + +# Make the direction data for the arrows +u = np.sin(np.pi * x) * np.cos(np.pi * y) * np.cos(np.pi * z) +v = -np.cos(np.pi * x) * np.sin(np.pi * y) * np.cos(np.pi * z) +w = np.sqrt(2.0 / 3.0) * np.cos(np.pi * x) * np.cos(np.pi * y) * np.sin(np.pi * z) + +positions = np.column_stack([x.ravel(), y.ravel(), z.ravel()]) +directions = np.column_stack([u.ravel(), v.ravel(), w.ravel()]) + + +vectors = figure[0, 0].add_vectors( + positions=positions, + directions=directions, +) + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index a3bbc1b5f..46051479d 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -3,6 +3,7 @@ from .scatter import ScatterGraphic from .image import ImageGraphic from .image_volume import ImageVolumeGraphic +from ._vectors import VectorsGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack @@ -13,6 +14,7 @@ "ScatterGraphic", "ImageGraphic", "ImageVolumeGraphic", + "VectorsGraphic", "TextGraphic", "LineCollection", "LineStack", diff --git a/fastplotlib/graphics/_vectors.py b/fastplotlib/graphics/_vectors.py new file mode 100644 index 000000000..6f761bd49 --- /dev/null +++ b/fastplotlib/graphics/_vectors.py @@ -0,0 +1,430 @@ +from typing import Sequence + +import pygfx +from pygfx.geometries.utils import merge as merge_geometries +import pylinalg as la +import numpy as np + +from ._base import Graphic +from .features import ( + VectorPositions, + VectorDirections, +) + + +class VectorsGraphic(Graphic): + _features = { + "positions": VectorPositions, + "directions": VectorDirections, + } + + def __init__( + self, + positions: np.ndarray | Sequence[float], + directions: np.ndarray | Sequence[float], + color: str | Sequence[float] | np.ndarray = "w", + size: float = None, + vector_shape_options: dict = None, + **kwargs, + ): + """ + Create graphic that draw vectors. Similar to matplotlib quiver. + + Parameters + ---------- + positions: np.ndarray | Sequence[float] + positions of the vectors, array-like, shape must be [n, 2] or [n, 3] where n is the number of vectors. + + directions: np.ndarray | Sequence[float] + directions of the vectors, array-like, shape must be [n, 2] or [n, 3] where n is the number of vectors. + + spacing: float + average distance between pairs of nearest-neighbor vectors, used for scaling + + color: str | pygfx.Color | Sequence[float] | np.ndarray, default "w" + color of the vectors + + size: float or None + Size of a vector of magnitude 1 in world space for display purpose. + Estimated from density if not provided. + + vector_shape_options: dict + dict with the following fields that directly describes the shape of the vector arrows. + Overrides ``size`` argument. + + * cone_radius + * cone_height + * stalk_radius + * stalk_height + + **kwargs + passed to :class:`.Graphic` + + """ + + super().__init__(**kwargs) + + # TODO: once it's possible to constructor instanced objects with a shared buffer I can do this + # if isinstance(positions, VectorPositions): + # self._positions = positions + # else: + # self._positions = VectorPositions(positions) + # + # if isinstance(directions, VectorDirections): + # self._directions = directions + # else: + # self._directions = VectorDirections(directions) + + positions = np.asarray(positions) + directions = np.asarray(directions) + + if positions.shape != directions.shape: + raise ValueError( + f"positions.shape != directions.shape: {positions.shape} != {directions.shape}\n" + f"They must be of the same shape" + ) + + self._positions = VectorPositions(positions) + self._directions = VectorDirections(directions) + + if vector_shape_options is not None: + required = {"cone_radius", "cone_height", "stalk_radius", "stalk_height"} + if set(vector_shape_options.keys()) != required: + raise KeyError( + f"`vector_shape_options` must be a dict with the following keys: {required}.\n" + f"You have passed: {vector_shape_options}" + ) + shape_options = vector_shape_options + else: + if size is None: + # guess from density + # sort xs and then take unique to get the density along x, same for y and z + x_density = np.diff(np.unique(np.sort(self._positions[:, 0]))).mean() + y_density = np.diff(np.unique(np.sort(self._positions[:, 1]))).mean() + densities = [x_density, y_density] + + # if z is not basically zero + if not np.allclose( + np.diff(np.unique(np.sort(self._positions[:, 2]))), 0.0 + ): + z_density = np.diff(np.unique(np.sort(positions[:, 2]))).mean() + densities.append(z_density) + + mean_density = np.mean(densities) + + size = mean_density + + cone_height = size / 2 + stalk_height = size / 2 + + cone_radius = size / 10 + stalk_radius = cone_radius / 8 + + shape_options = { + "cone_radius": cone_radius, + "cone_height": cone_height, + "stalk_radius": stalk_radius, + "stalk_height": stalk_height, + } + + geometry = create_vector_geometry(color=color, **shape_options) + material = pygfx.MeshBasicMaterial() + + n_vectors = self._positions.value.shape[0] + + world_object = pygfx.InstancedMesh(geometry, material, n_vectors) + + magnitudes = np.linalg.norm(self.directions[:], axis=1, ord=2) + + for i in range(n_vectors): + # get quaternion to rotate vector to new direction + rotation = la.quat_from_vecs( + self._directions.init_direction, self._directions[i] + ) + # get the new transform + transform = la.mat_compose( + self._positions.value[i], rotation, magnitudes[i] + ) + # set the buffer + world_object.instance_buffer.data["matrix"][i] = transform.T + + world_object.instance_buffer.update_full() + + self._set_world_object(world_object) + + @property + def positions(self) -> VectorPositions: + """Vector positions""" + return self._positions + + @positions.setter + def positions(self, new_positions): + self._positions.set_value(self, new_positions) + + @property + def directions(self) -> VectorDirections: + """Vector directions""" + return self._directions + + @directions.setter + def directions(self, new_directions): + self._directions.set_value(self, new_directions) + + +# mesh code copied and adapted from pygfx +def generate_torso( + radius_bottom, + radius_top, + height, + radial_segments, + height_segments, + theta_start, + theta_length, + z_offset=0.0, +): + """copied from pygfx, generates the mesh for a cylinder with the given parameters""" + # compute POSITIONS assuming x-y horizontal plane and z up axis + + # radius for each vertex ring from bottom to top + n_rings = height_segments + 1 + radii = np.linspace(radius_bottom, radius_top, num=n_rings, dtype=np.float32) + + # height for each vertex ring from bottom to top + half_height = height / 2 + heights = np.linspace(-half_height, half_height, num=n_rings, dtype=np.float32) + + # to enable texture mapping to fully wrap around the cylinder, + # we can't close the geometry and need a degenerate vertex + n_vertices = radial_segments + 1 + + # xy coordinates on unit circle for a single vertex ring + theta = np.linspace( + theta_start, theta_start + theta_length, num=n_vertices, dtype=np.float32 + ) + ring_xy = np.column_stack([np.cos(theta), np.sin(theta)]) + + # put all the rings together + positions = np.empty((n_rings, n_vertices, 3), dtype=np.float32) + positions[..., :2] = ring_xy[None, ...] * radii[:, None, None] + positions[..., 2] = heights[:, None] - z_offset + + # the NORMALS are the same for every ring, so compute for only one ring + # and then repeat + slope = (radius_bottom - radius_top) / height + ring_normals = np.empty(positions.shape[1:], dtype=np.float32) + ring_normals[..., :2] = ring_xy + ring_normals[..., 2] = slope + ring_normals /= np.linalg.norm(ring_normals, axis=-1)[:, None] + normals = np.empty_like(positions) + normals[:] = ring_normals[None, ...] + + # the TEXTURE COORDS + # u maps 0..1 to theta_start..theta_start+theta_length + # v maps 0..1 to -height/2..height/2 + ring_u = (theta - theta_start) / theta_length + ring_v = (heights / height) + 0.5 + texcoords = np.empty((n_rings, n_vertices, 2), dtype=np.float32) + texcoords[..., 0] = ring_u[None, :] + texcoords[..., 1] = ring_v[:, None] + + # the face INDEX + # the amount of vertices + indices = np.arange(n_rings * n_vertices, dtype=np.uint32).reshape( + (n_rings, n_vertices) + ) + # for every panel (height_segments, radial_segments) there is a quad (2, 3) + index = np.empty((height_segments, radial_segments, 2, 3), dtype=np.uint32) + # create a grid of initial indices for the panels + index[:, :, 0, 0] = indices[ + np.arange(height_segments)[:, None], np.arange(radial_segments)[None, :] + ] + # the remainder of the indices for every panel are relative + index[:, :, 0, 1] = index[:, :, 0, 0] + 1 + index[:, :, 0, 2] = index[:, :, 0, 0] + n_vertices + index[:, :, 1, 0] = index[:, :, 0, 0] + n_vertices + 1 + index[:, :, 1, 1] = index[:, :, 1, 0] - 1 + index[:, :, 1, 2] = index[:, :, 1, 0] - n_vertices + + return ( + positions.reshape((-1, 3)), + normals.reshape((-1, 3)), + texcoords.reshape((-1, 2)), + index.flatten(), + ) + + +def generate_cap(radius, height, radial_segments, theta_start, theta_length, up=True): + """copied from pygfx, generates the mesh for a circular cap with the given parameters""" + # compute POSITIONS assuming x-y horizontal plane and z up axis + + # to enable texture mapping to fully wrap around the cylinder, + # we can't close the geometry and need a degenerate vertex + n_vertices = radial_segments + 1 + + # xy coordinates on unit circle for vertex ring + theta = np.linspace( + theta_start, theta_start + theta_length, num=n_vertices, dtype=np.float32 + ) + ring_xy = np.column_stack([np.cos(theta), np.sin(theta)]) + + # put the vertices together, inserting a center vertex at the start + positions = np.empty((1 + n_vertices, 3), dtype=np.float32) + positions[0, :2] = [0.0, 0.0] + positions[1:, :2] = ring_xy * radius + positions[..., 2] = height + + # the NORMALS + normals = np.zeros_like(positions, dtype=np.float32) + sign = int(up) * 2.0 - 1.0 + normals[..., 2] = sign + + # the TEXTURE COORDS + # uv etches out a circle from the [0..1, 0..1] range + # direction is reversed for up=False + texcoords = np.empty((1 + n_vertices, 2), dtype=np.float32) + texcoords[0] = [0.5, 0.5] + texcoords[1:, 0] = ring_xy[:, 0] * 0.5 + 0.5 + texcoords[1:, 1] = ring_xy[:, 1] * 0.5 * sign + 0.5 + + # the face INDEX + indices = np.arange(n_vertices) + 1 + # for every radial segment there is a triangle (3) + index = np.empty((radial_segments, 3), dtype=np.uint32) + # create a grid of initial indices for the panels + index[:, 0] = indices[np.arange(radial_segments)] + # the remainder of the indices for every panel are relative + index[:, 1 + int(up)] = n_vertices + index[:, 2 - int(up)] = index[:, 0] + 1 + + return ( + positions, + normals, + texcoords, + index.flatten(), + ) + + +def create_vector_geometry( + color: str | pygfx.Color | Sequence[float] | np.ndarray = "w", + cone_cap_color: str | pygfx.Color | Sequence[float] | np.ndarray | None = None, + cone_radius: float = 1.0, + cone_height: float = 0.5, + stalk_radius: float = 0.3, + stalk_height: float = 0.5, + segments: int = 12, +): + """ + Generate the mesh for a vector pointing in the direction [0, 0, 1], a unit vector in the +z direction. + + Parameters + ---------- + color: + color of the vector + + cone_cap_color: + color of the cone cap, by default it will use a darker version of the provided vector color from above. + + cone_radius: + radius of the bottom of the cone segment of the vector + + cone_height: + height of the cone segment of the vector + + stalk_radius: + radius of the vector's stalk + + stalk_height: + height of the vector's stalk + + segments: + number of mesh segments, more looks nicers but is also more expennsive to render, 12 looks good enough. + + """ + + radius_top = 0 # radius top = 0 means the cylinder becomes a cone + + radial_segments = segments + + height_segments = 1 + theta_start = 0.0 + theta_length = np.pi * 2 + + # create cone + cone = generate_torso( + cone_radius, + radius_top, + cone_height, + radial_segments, + height_segments, + theta_start, + theta_length, + ) + + groups = [cone] + + cone_cap_start_ix = len(cone[0]) + + # create bottom cap + cone_cap = generate_cap( + cone_radius, + -cone_height / 2, + radial_segments, + theta_start, + theta_length, + up=False, + ) + + cone_cap_stop_ix = cone_cap_start_ix + len(cone_cap[0]) + + groups.append(cone_cap) + + stalk = generate_torso( + stalk_radius, + stalk_radius, + stalk_height, + radial_segments, + height_segments, + theta_start, + theta_length, + z_offset=cone_height, + ) + + groups.append(stalk) + + stalk_cap = generate_cap( + stalk_radius, + -stalk_radius / 2, + radial_segments, + theta_start, + theta_length, + up=False, + ) + + groups.append(stalk_cap) + + merged = merge_geometries(groups) + + positions, normals, texcoords, indices = merged + + color = np.array(pygfx.Color(color).rgb, dtype=np.float32) + + # color the cone cap in a different color + if cone_cap_color is not None: + cone_cap_color = np.array(pygfx.Color(cone_cap_color).rgb, dtype=np.float32) + else: + # make the cone cap a slightly darker version of the cone color + cone_cap_color = (color - np.array([0.25, 0.25, 0.25], dtype=np.float32)).clip( + 0 + ) + + colors = np.repeat([color], repeats=len(positions), axis=0) + + colors[cone_cap_start_ix:cone_cap_stop_ix, :] = cone_cap_color + + return pygfx.Geometry( + indices=indices.reshape((-1, 3)), + positions=positions, + normals=normals, + texcoords=texcoords, + colors=colors, + ) diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index 95e4d321f..f745f10c8 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -36,6 +36,12 @@ VOLUME_RENDER_MODES, create_volume_material_kwargs, ) + +from ._vectors import ( + VectorPositions, + VectorDirections, +) + from ._base import ( GraphicFeature, BufferManager, @@ -88,6 +94,8 @@ "VolumeIsoEmissive", "VolumeIsoShininess", "VolumeSlicePlane", + "VectorPositions", + "VectorDirections", "TextData", "FontSize", "TextFaceColor", diff --git a/fastplotlib/graphics/features/_vectors.py b/fastplotlib/graphics/features/_vectors.py new file mode 100644 index 000000000..9c86d25fc --- /dev/null +++ b/fastplotlib/graphics/features/_vectors.py @@ -0,0 +1,187 @@ +import numpy as np +import pylinalg as la + +from ._base import ( + GraphicFeature, + GraphicFeatureEvent, + block_reentrance, +) + + +# it doesn't make sense to modify just a portion of a vector field, I can't think of a use case. +# so we only allow setting the entire buffer, but allow getting portions of it +class VectorPositions(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new vector positions", + }, + ] + + def __init__( + self, + positions: np.ndarray, + isolated_buffer: bool = True, + property_name: str = "positions", + ): + """ + Manages vector field positions by managing the translation elements of the mesh instance transform matrix buffer + """ + + positions = np.asarray(positions, dtype=np.float32) + if positions.ndim != 2: + raise ValueError( + f"vector field positions must be of shape [n, 2] or [n, 3]" + ) + + if positions.shape[1] == 2: + positions = np.column_stack( + [ + positions[:, 0], + positions[:, 1], + np.zeros(positions.shape[0], dtype=np.float32), + ] + ) + + elif positions.shape[1] == 3: + pass + + else: + raise ValueError( + f"vector field positions must be of shape [n, 2] or [n, 3]" + ) + + self._positions = positions + + super().__init__(property_name=property_name) + + @property + def value(self) -> np.ndarray: + return self._positions + + def __getitem__(self, item): + return self.value[item] + + def __setitem__(self, key, value): + raise NotImplementedError( + "cannot set individual slices of vector positions, must set all positions" + ) + + @block_reentrance + def set_value(self, graphic, value: np.ndarray): + if value.shape[0] != self._positions.shape[0]: + raise ValueError( + f"number of vector positions in passed array != number of vectors in graphic: " + f"{value.shape[0]} != {self._positions.shape[0]}" + ) + + if value.shape[1] == 2: + # assume 2d + self._positions[:, :-1] = value + + else: + self._positions[:] = value + + for i in range(self._positions.shape[0]): + # only need to update the translation vector + graphic.world_object.instance_buffer.data["matrix"][i][3, 0:3] = ( + self._positions[i] + ) + + graphic.world_object.instance_buffer.update_full() + + event = GraphicFeatureEvent(type="positions", info={"value": value}) + self._call_event_handlers(event) + + +class VectorDirections(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new vector directions", + }, + ] + + # vector is always pointing in [0, 0, 1] when mesh is initialized + init_direction = np.array([0, 0, 1]) + init_direction.flags.writeable = False + + def __init__( + self, + directions: np.ndarray, + isolated_buffer: bool = True, + property_name: str = "directions", + ): + """Manages vector field positions by managing the mesh instance buffer's full transform matrix""" + + directions = np.asarray(directions, dtype=np.float32) + if directions.ndim != 2: + raise ValueError( + f"vector field directions must be of shape [n, 2] or [n, 3]" + ) + + if directions.shape[1] == 2: + directions = np.column_stack( + [ + directions[:, 0], + directions[:, 1], + np.zeros(directions.shape[0], dtype=np.float32), + ] + ) + + elif directions.shape[1] == 3: + pass + + else: + raise ValueError( + f"vector field directions must be of shape [n, 2] or [n, 3]" + ) + + self._directions = directions + + super().__init__(property_name=property_name) + + @property + def value(self) -> np.ndarray: + return self._directions + + def __getitem__(self, item): + return self.value[item] + + def __setitem__(self, key, value): + raise NotImplementedError( + "cannot set individual slices of vector directions, must set all directions" + ) + + @block_reentrance + def set_value(self, graphic, value: np.ndarray): + if value.shape[0] != self._directions.shape[0]: + raise ValueError( + f"number of vector directions in passed array != number of vectors in graphic: " + f"{value.shape[0]} != {self._directions.shape[0]}" + ) + + if value.shape[1] == 2: + # assume 2d + self._directions[:, :-1] = value + + else: + self._directions[:] = value + + # vector determines the size of the vector + magnitudes = np.linalg.norm(self._directions, axis=1, ord=2) + + for i in range(self._directions.shape[0]): + # get quaternion to rotate vector to new direction + rotation = la.quat_from_vecs(self.init_direction, self._directions[i]) + # get the new transform + transform = la.mat_compose(graphic.positions[i], rotation, magnitudes[i]) + # set the buffer + graphic.world_object.instance_buffer.data["matrix"][i] = transform.T + + graphic.world_object.instance_buffer.update_full() + + event = GraphicFeatureEvent(type="directions", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a38eaa8eb..e7ff99a1d 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -649,3 +649,58 @@ def add_text( anchor, **kwargs, ) + + def add_vectors( + self, + positions: Union[numpy.ndarray, Sequence[float]], + directions: Union[numpy.ndarray, Sequence[float]], + color: Union[str, Sequence[float], numpy.ndarray] = "w", + size: float = None, + vector_shape_options: dict = None, + **kwargs, + ) -> VectorsGraphic: + """ + + Create graphic that draw vectors. Similar to matplotlib quiver. + + Parameters + ---------- + positions: np.ndarray | Sequence[float] + positions of the vectors, array-like, shape must be [n, 2] or [n, 3] where n is the number of vectors. + + directions: np.ndarray | Sequence[float] + directions of the vectors, array-like, shape must be [n, 2] or [n, 3] where n is the number of vectors. + + spacing: float + average distance between pairs of nearest-neighbor vectors, used for scaling + + color: str | pygfx.Color | Sequence[float] | np.ndarray, default "w" + color of the vectors + + size: float or None + Size of a vector of magnitude 1 in world space for display purpose. + Estimated from density if not provided. + + vector_shape_options: dict + dict with the following fields that directly describes the shape of the vector arrows. + Overrides ``size`` argument. + + * cone_radius + * cone_height + * stalk_radius + * stalk_height + + **kwargs + passed to :class:`.Graphic` + + + """ + return self._create_graphic( + VectorsGraphic, + positions, + directions, + color, + size, + vector_shape_options, + **kwargs, + ) diff --git a/pyproject.toml b/pyproject.toml index 728a1cd1a..9ec1d6ce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ keywords = [ requires-python = ">= 3.10" dependencies = [ "numpy>=1.23.0", - "pygfx==0.14", + "pygfx==0.15", "wgpu", # Let pygfx constrain the wgpu version "cmap>=0.1.3", # (this comment keeps this list multiline in VSCode) diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 46433180f..865eab27f 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -56,7 +56,7 @@ def generate_add_graphics_methods(): cls = m cls_name = cls.__name__.replace("Graphic", "") # from https://stackoverflow.com/a/1176023 - method_name = re.sub(r'(? Date: Mon, 3 Nov 2025 21:23:05 -0500 Subject: [PATCH 25/52] bump version to 0.6.0 (#934) --- fastplotlib/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/_version.py b/fastplotlib/_version.py index e6ac4d62d..1cd4ca99b 100644 --- a/fastplotlib/_version.py +++ b/fastplotlib/_version.py @@ -10,7 +10,7 @@ # This is the reference version number, to be bumped before each release. # The build system detects this definition when building a distribution. -__version__ = "0.5.1" +__version__ = "0.6.0" # Allow using nearly the same code in different projects project_name = "fastplotlib" From bd5101e14fc1f5e87389f576edf6f837d2f15a39 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 14 Nov 2025 07:53:47 -0500 Subject: [PATCH 26/52] update pygfx pin (#939) * update pygfx pin * Fix screenshot logic * new pin --------- Co-authored-by: Almar Klein --- examples/tests/test_examples.py | 38 +++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index dd3a3260d..caf7b5e82 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -100,29 +100,10 @@ def test_example_screenshots(module, prep_environment): # import the example module example = import_from_path(module.stem, module) - if fpl.IMGUI: - # there doesn't seem to be a resize event for the manual offscreen canvas - example.figure.imgui_renderer._backend.io.display_size = ( - example.figure.canvas.get_logical_size() - ) - # run this once so any edge widgets set their sizes and therefore the subplots get the correct rect - # hacky but it works for now - example.figure.imgui_renderer.render() - - example.figure._fpl_reset_layout() - # render each subplot - for subplot in example.figure: - subplot.viewport.render(subplot.scene, subplot.camera) - - # flush pygfx renderer - example.figure.renderer.flush() - - if fpl.IMGUI: - # render imgui - example.figure.imgui_renderer.render() - - # render a frame - img = np.asarray(example.figure.renderer.target.draw()) + # Render a frame. Twice, because layouts don't settle in one go? + m1 = example.figure.renderer.target.draw() + m2 = example.figure.renderer.target.draw() + img = np.asarray(m2) # check if _something_ was rendered assert img is not None and img.size > 0 @@ -164,7 +145,6 @@ def test_example_screenshots(module, prep_environment): ref_img = normalize_image(ref_img) similar, rmse = image_similarity(rgb, ref_img) - update_diffs(module.stem, similar, rgb, ref_img) assert similar, ( f"diff {rmse} above threshold for {module.stem}, see " @@ -206,7 +186,9 @@ def get_diffs_rgba(slicer): if __name__ == "__main__": - os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" - os.environ["PYGFX_DEFAULT_PPAA"] = "none" - test_examples_run("simple") - test_example_screenshots("simple") + # Need to apply these env vars *before* running this script, + # since FPL selects its backend on import. + # os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true" + # os.environ["PYGFX_DEFAULT_PPAA"] = "none" + + test_example_screenshots(examples_to_test[0], None) diff --git a/pyproject.toml b/pyproject.toml index 9ec1d6ce9..73dfd7ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ keywords = [ requires-python = ">= 3.10" dependencies = [ "numpy>=1.23.0", - "pygfx==0.15", + "pygfx==0.15.3", "wgpu", # Let pygfx constrain the wgpu version "cmap>=0.1.3", # (this comment keeps this list multiline in VSCode) From 3e8b8d5963572acc4c236f4f98b9325cc14a7f8e Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 15 Nov 2025 09:22:13 -0500 Subject: [PATCH 27/52] Bump (#942) --- fastplotlib/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/_version.py b/fastplotlib/_version.py index 1cd4ca99b..6ce66cd9a 100644 --- a/fastplotlib/_version.py +++ b/fastplotlib/_version.py @@ -10,7 +10,7 @@ # This is the reference version number, to be bumped before each release. # The build system detects this definition when building a distribution. -__version__ = "0.6.0" +__version__ = "0.6.1" # Allow using nearly the same code in different projects project_name = "fastplotlib" From c924c66803be4f57220bfb1eaf13c1ac9800676d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 15 Nov 2025 09:25:08 -0500 Subject: [PATCH 28/52] better plot scaling logic (#938) * better plot scaling logic for when a subplot controller manages multiple cameras * skip scaling in empty subplots --- fastplotlib/layouts/_plot_area.py | 80 ++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 3c5027caf..8146a00de 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -607,14 +607,33 @@ def center_scene(self, *, zoom: float = 1.0): if not len(self._fpl_graphics_scene.children) > 0: return - # scale all cameras associated with this controller - # else it looks wonky - for camera in self.controller.cameras: - camera.show_object(self._fpl_graphics_scene) + if self.parent.__class__.__name__.endswith("Figure"): + # always use figure._subplots.ravel() in internal fastplotlib code + # otherwise if we use `for subplot in figure`, this could conflict + # with a user's iterator where they are doing `for subplot in figure` !!! + for subplot in self.parent._subplots.ravel(): + # scale all cameras associated with this controller + if subplot.camera in self.controller.cameras: + # skip if the scene is empty + if len(subplot._fpl_graphics_scene.children) < 1: + continue + + # center the camera in the other subplot w.r.t. the scene in that other subplot! + self._auto_center_scene( + subplot.camera, subplot._fpl_graphics_scene, zoom + ) + else: + # just change for this plot area + # this is probably a dock area + self._auto_center_scene(self.camera, self._fpl_graphics_scene, zoom) - # camera.show_object can cause the camera width and height to increase so apply a zoom to compensate - # probably because camera.show_object uses bounding sphere - camera.zoom = zoom + def _auto_center_scene( + self, camera: pygfx.PerspectiveCamera, scene: pygfx.Scene, zoom: float + ): + camera.show_object(scene) + # camera.show_object can cause the camera width and height to increase so apply a zoom to compensate + # probably because camera.show_object uses bounding sphere + camera.zoom = zoom def auto_scale( self, @@ -642,16 +661,43 @@ def auto_scale( self.center_scene() if maintain_aspect is None: # if not provided keep current setting + # use the same maintain apsect for all other cameras that this controller manages + # I think this make sense for most use cases, even when the other controllers are + # only managing one or 2 axes maintain_aspect = self.camera.maintain_aspect - # scale all cameras associated with this controller else it looks wonky - for camera in self.controller.cameras: - camera.maintain_aspect = maintain_aspect + if self.parent.__class__.__name__.endswith("Figure"): + # always use figure._subplots.ravel() in internal fastplotlib code + # otherwise if we use `for subplot in figure`, this could conflict + # with a user's iterator where they are doing `for subplot in figure` !!! + for subplot in self.parent._subplots.ravel(): + # skip if the scene is empty + if len(subplot._fpl_graphics_scene.children) < 1: + continue - if len(self._fpl_graphics_scene.children) > 0: - width, height, depth = np.ptp( - self._fpl_graphics_scene.get_world_bounding_box(), axis=0 + # scale the camera in the other subplot w.r.t. the scene in that other subplot! + if subplot.camera in self.controller.cameras: + camera = subplot.camera + self._auto_scale_scene( + camera, subplot._fpl_graphics_scene, zoom, maintain_aspect + ) + else: + # just change for this plot area, this is probably a dock area + self._auto_scale_scene( + self.camera, self._fpl_graphics_scene, zoom, maintain_aspect ) + + def _auto_scale_scene( + self, + camera: pygfx.PerspectiveCamera, + scene: pygfx.Scene, + zoom: float, + maintain_aspect: bool, + ): + camera.maintain_aspect = maintain_aspect + + if len(scene.children) > 0: + width, height, depth = np.ptp(scene.get_world_bounding_box(), axis=0) else: width, height, depth = (1, 1, 1) @@ -661,12 +707,10 @@ def auto_scale( if height < 0.01: height = 1 - # scale all cameras associated with this controller else it looks wonky - for camera in self.controller.cameras: - camera.width = width - camera.height = height + camera.width = width + camera.height = height - camera.zoom = zoom + camera.zoom = zoom def remove_graphic(self, graphic: Graphic): """ From f1db32faae68e9c647d7d661724c8e1380e7210d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 16 Nov 2025 08:25:25 -0500 Subject: [PATCH 29/52] proper z layering for images (#944) --- fastplotlib/layouts/_plot_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 8146a00de..606f83909 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -446,7 +446,7 @@ def _sort_images_by_depth(self): from the camera). """ count = 0 - for graphic in self._graphics: + for graphic in reversed(self._graphics): if isinstance(graphic, ImageGraphic): count += 1 auto_depth = -count From 7b4c68746ddb7028fb32ef3adb239506be5210dc Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 16 Nov 2025 12:33:45 -0500 Subject: [PATCH 30/52] fix linear and linear region selectors offset when parent is None (#945) --- fastplotlib/graphics/selectors/_linear.py | 11 +++++++---- fastplotlib/graphics/selectors/_linear_region.py | 15 +++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 0364305a4..0c956d57b 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -183,10 +183,13 @@ def __init__( world_object.add(line_outer) world_object.add(line_inner) - if axis == "x": - offset = (parent.offset[0], 0, 0) - elif axis == "y": - offset = (0, parent.offset[1], 0) + if parent is None: + offset = (0, 0, 0) + else: + if axis == "x": + offset = (parent.offset[0], 0, 0) + elif axis == "y": + offset = (0, parent.offset[1], 0) # init base selector BaseSelector.__init__( diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 9f5803c93..70a8dffa8 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -277,12 +277,15 @@ def __init__( outer_edges = (line0_outer, line1_outer) group.add(*edges, *outer_edges) - # TODO: if parent offset changes, we should set the selector offset too, use offset evented property - # TODO: add check if parent is `None`, will throw error otherwise - if axis == "x": - offset = (parent.offset[0], center + parent.offset[1], 0) - elif axis == "y": - offset = (center + parent.offset[1], parent.offset[1], 0) + if parent is None: + offset = (0, 0, 0) + else: + # TODO: if parent offset changes, we should set the selector offset too, use offset evented property + # TODO: add check if parent is `None`, will throw error otherwise + if axis == "x": + offset = (parent.offset[0], center + parent.offset[1], 0) + elif axis == "y": + offset = (center + parent.offset[1], parent.offset[1], 0) # set the initial bounds of the selector # compensate for any offset from the parent graphic From 9abe236407b175d5849a66b1e89bdf388177e6e9 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 4 Dec 2025 04:23:14 +0100 Subject: [PATCH 31/52] Polygon buffer also shrinks when it can (#958) * Polygon buffer also shrinks when it can. * fix for zero * remove unused import --------- Co-authored-by: kushalkolar --- examples/guis/sine_cosine_funcs.py | 1 - fastplotlib/graphics/features/_selection_features.py | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/guis/sine_cosine_funcs.py b/examples/guis/sine_cosine_funcs.py index f7dd064cf..935f9a5a1 100644 --- a/examples/guis/sine_cosine_funcs.py +++ b/examples/guis/sine_cosine_funcs.py @@ -9,7 +9,6 @@ # test_example = false # sphinx_gallery_pygfx_docs = 'screenshot' -import glfw import numpy as np import fastplotlib as fpl from fastplotlib.ui import EdgeWindow diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py index 654b3d4c6..9b30dd70c 100644 --- a/fastplotlib/graphics/features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -416,12 +416,14 @@ def set_value(self, selector, value: Sequence[tuple[float]]): geometry = selector.geometry - # Need larger buffer? - if len(value) > geometry.positions.nitems: - arr = np.zeros((geometry.positions.nitems * 2, 3), np.float32) + # Need larger (or smaller) buffer? Scale up/down with factors of 2. + need_position_size = 2 ** int(np.ceil(np.log2(max(8, len(value))))) + if need_position_size != geometry.positions.nitems: + arr = np.zeros((need_position_size, 3), np.float32) geometry.positions = gfx.Buffer(arr) - if len(indices) > geometry.indices.nitems: - arr = np.zeros((geometry.indices.nitems * 2, 3), np.int32) + need_indices_size = 2 ** int(np.ceil(np.log2(max(8, len(indices))))) + if need_indices_size != geometry.indices.nitems: + arr = np.zeros((need_indices_size, 3), np.int32) geometry.indices = gfx.Buffer(arr) geometry.positions.data[: len(value)] = value From db49c620bfe100bd8daa2b7ed3579982bcf2187e Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 4 Dec 2025 15:01:22 +0100 Subject: [PATCH 32/52] Add support for mesh and surface (#953) * Add support for mesh and surface * restore line example * Implement image-surface example * Fix 3D camera for mesh examples * Just remove selector logic for now * new examples in gallery * mesh with graphic features (#954) * mesh with gfeatures * surface graphic works * update, add PolygonGraphic, works * black * for docs and test screenshots * black * polygon data updating works * update * black * more examples * update add_graphic mixin * code review changes * fix * better polygon example * remove unused import * update graphics mixin * docs * black * don't need to set limit for docs gen * add missing comma * dont run terrain example * new screenshots * remove (i expect) unnecessary check * lights as properties * just use VertexPositions * update PolyData * I need to use precommit * update api docs --------- Co-authored-by: Kushal Kolar --- docs/source/api/graphic_features/MeshCmap.rst | 35 ++ .../api/graphic_features/MeshIndices.rst | 36 ++ .../api/graphic_features/SurfaceData.rst | 35 ++ docs/source/api/graphic_features/index.rst | 3 + docs/source/api/graphics/MeshGraphic.rst | 55 ++ docs/source/api/graphics/PolygonGraphic.rst | 56 +++ docs/source/api/graphics/SurfaceGraphic.rst | 56 +++ docs/source/api/graphics/index.rst | 3 + docs/source/api/layouts/subplot.rst | 5 + docs/source/conf.py | 14 +- docs/source/user_guide/event_tables.rst | 399 +++++++++++++++ examples/mesh/README.rst | 2 + examples/mesh/image_surface.py | 37 ++ examples/mesh/mesh.py | 36 ++ examples/mesh/polygon_animation.py | 76 +++ examples/mesh/polygons.py | 61 +++ examples/mesh/surface_earth.py | 95 ++++ examples/mesh/surface_ellipsoid.py | 55 ++ examples/mesh/surface_gaussian.py | 45 ++ examples/mesh/surface_height.py | 35 ++ examples/mesh/surface_ripple.py | 62 +++ examples/mesh/surface_sphere_ripple.py | 81 +++ examples/mesh/surface_terrain.py | 36 ++ examples/screenshots/image_surface.png | 3 + examples/screenshots/mesh.png | 3 + .../screenshots/no-imgui-image_surface.png | 3 + examples/screenshots/no-imgui-mesh.png | 3 + .../screenshots/no-imgui-surface_gaussian.png | 3 + .../screenshots/no-imgui-surface_height.png | 3 + .../screenshots/no-imgui-vectors_simple.png | 3 + .../screenshots/no-imgui-vectors_swirl.png | 3 + examples/screenshots/surface_gaussian.png | 3 + examples/screenshots/surface_height.png | 3 + examples/screenshots/vectors_simple.png | 3 + examples/screenshots/vectors_swirl.png | 3 + examples/tests/testutils.py | 3 +- fastplotlib/graphics/__init__.py | 4 + fastplotlib/graphics/features/__init__.py | 14 +- fastplotlib/graphics/features/_base.py | 6 + fastplotlib/graphics/features/_mesh.py | 284 +++++++++++ .../{_positions_graphics.py => _positions.py} | 2 - fastplotlib/graphics/features/utils.py | 5 +- fastplotlib/graphics/mesh.py | 473 ++++++++++++++++++ fastplotlib/layouts/_graphic_methods_mixin.py | 196 ++++++++ fastplotlib/layouts/_plot_area.py | 18 + 45 files changed, 2345 insertions(+), 14 deletions(-) create mode 100644 docs/source/api/graphic_features/MeshCmap.rst create mode 100644 docs/source/api/graphic_features/MeshIndices.rst create mode 100644 docs/source/api/graphic_features/SurfaceData.rst create mode 100644 docs/source/api/graphics/MeshGraphic.rst create mode 100644 docs/source/api/graphics/PolygonGraphic.rst create mode 100644 docs/source/api/graphics/SurfaceGraphic.rst create mode 100644 examples/mesh/README.rst create mode 100644 examples/mesh/image_surface.py create mode 100644 examples/mesh/mesh.py create mode 100644 examples/mesh/polygon_animation.py create mode 100644 examples/mesh/polygons.py create mode 100644 examples/mesh/surface_earth.py create mode 100644 examples/mesh/surface_ellipsoid.py create mode 100644 examples/mesh/surface_gaussian.py create mode 100644 examples/mesh/surface_height.py create mode 100644 examples/mesh/surface_ripple.py create mode 100644 examples/mesh/surface_sphere_ripple.py create mode 100644 examples/mesh/surface_terrain.py create mode 100644 examples/screenshots/image_surface.png create mode 100644 examples/screenshots/mesh.png create mode 100644 examples/screenshots/no-imgui-image_surface.png create mode 100644 examples/screenshots/no-imgui-mesh.png create mode 100644 examples/screenshots/no-imgui-surface_gaussian.png create mode 100644 examples/screenshots/no-imgui-surface_height.png create mode 100644 examples/screenshots/no-imgui-vectors_simple.png create mode 100644 examples/screenshots/no-imgui-vectors_swirl.png create mode 100644 examples/screenshots/surface_gaussian.png create mode 100644 examples/screenshots/surface_height.png create mode 100644 examples/screenshots/vectors_simple.png create mode 100644 examples/screenshots/vectors_swirl.png create mode 100644 fastplotlib/graphics/features/_mesh.py rename fastplotlib/graphics/features/{_positions_graphics.py => _positions.py} (99%) create mode 100644 fastplotlib/graphics/mesh.py diff --git a/docs/source/api/graphic_features/MeshCmap.rst b/docs/source/api/graphic_features/MeshCmap.rst new file mode 100644 index 000000000..865ac13d9 --- /dev/null +++ b/docs/source/api/graphic_features/MeshCmap.rst @@ -0,0 +1,35 @@ +.. _api.MeshCmap: + +MeshCmap +******** + +======== +MeshCmap +======== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: MeshCmap_api + + MeshCmap + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: MeshCmap_api + + MeshCmap.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: MeshCmap_api + + MeshCmap.add_event_handler + MeshCmap.block_events + MeshCmap.clear_event_handlers + MeshCmap.remove_event_handler + MeshCmap.set_value + diff --git a/docs/source/api/graphic_features/MeshIndices.rst b/docs/source/api/graphic_features/MeshIndices.rst new file mode 100644 index 000000000..6005ca0c0 --- /dev/null +++ b/docs/source/api/graphic_features/MeshIndices.rst @@ -0,0 +1,36 @@ +.. _api.MeshIndices: + +MeshIndices +*********** + +=========== +MeshIndices +=========== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: MeshIndices_api + + MeshIndices + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: MeshIndices_api + + MeshIndices.buffer + MeshIndices.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: MeshIndices_api + + MeshIndices.add_event_handler + MeshIndices.block_events + MeshIndices.clear_event_handlers + MeshIndices.remove_event_handler + MeshIndices.set_value + diff --git a/docs/source/api/graphic_features/SurfaceData.rst b/docs/source/api/graphic_features/SurfaceData.rst new file mode 100644 index 000000000..87828d226 --- /dev/null +++ b/docs/source/api/graphic_features/SurfaceData.rst @@ -0,0 +1,35 @@ +.. _api.SurfaceData: + +SurfaceData +*********** + +=========== +SurfaceData +=========== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: SurfaceData_api + + SurfaceData + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: SurfaceData_api + + SurfaceData.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: SurfaceData_api + + SurfaceData.add_event_handler + SurfaceData.block_events + SurfaceData.clear_event_handlers + SurfaceData.remove_event_handler + SurfaceData.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 5c5c2b464..cd11544be 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -9,6 +9,9 @@ Graphic Features SizeSpace VertexPositions VertexCmap + MeshIndices + MeshCmap + SurfaceData Thickness VertexMarkers UniformMarker diff --git a/docs/source/api/graphics/MeshGraphic.rst b/docs/source/api/graphics/MeshGraphic.rst new file mode 100644 index 000000000..5e2c5dac5 --- /dev/null +++ b/docs/source/api/graphics/MeshGraphic.rst @@ -0,0 +1,55 @@ +.. _api.MeshGraphic: + +MeshGraphic +*********** + +=========== +MeshGraphic +=========== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: MeshGraphic_api + + MeshGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: MeshGraphic_api + + MeshGraphic.alpha + MeshGraphic.alpha_mode + MeshGraphic.axes + MeshGraphic.block_events + MeshGraphic.clim + MeshGraphic.cmap + MeshGraphic.colors + MeshGraphic.deleted + MeshGraphic.event_handlers + MeshGraphic.indices + MeshGraphic.mapcoords + MeshGraphic.mode + MeshGraphic.name + MeshGraphic.offset + MeshGraphic.plane + MeshGraphic.positions + MeshGraphic.right_click_menu + MeshGraphic.rotation + MeshGraphic.supported_events + MeshGraphic.visible + MeshGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: MeshGraphic_api + + MeshGraphic.add_axes + MeshGraphic.add_event_handler + MeshGraphic.clear_event_handlers + MeshGraphic.remove_event_handler + MeshGraphic.rotate + diff --git a/docs/source/api/graphics/PolygonGraphic.rst b/docs/source/api/graphics/PolygonGraphic.rst new file mode 100644 index 000000000..f9446f425 --- /dev/null +++ b/docs/source/api/graphics/PolygonGraphic.rst @@ -0,0 +1,56 @@ +.. _api.PolygonGraphic: + +PolygonGraphic +************** + +============== +PolygonGraphic +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: PolygonGraphic_api + + PolygonGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: PolygonGraphic_api + + PolygonGraphic.alpha + PolygonGraphic.alpha_mode + PolygonGraphic.axes + PolygonGraphic.block_events + PolygonGraphic.clim + PolygonGraphic.cmap + PolygonGraphic.colors + PolygonGraphic.data + PolygonGraphic.deleted + PolygonGraphic.event_handlers + PolygonGraphic.indices + PolygonGraphic.mapcoords + PolygonGraphic.mode + PolygonGraphic.name + PolygonGraphic.offset + PolygonGraphic.plane + PolygonGraphic.positions + PolygonGraphic.right_click_menu + PolygonGraphic.rotation + PolygonGraphic.supported_events + PolygonGraphic.visible + PolygonGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: PolygonGraphic_api + + PolygonGraphic.add_axes + PolygonGraphic.add_event_handler + PolygonGraphic.clear_event_handlers + PolygonGraphic.remove_event_handler + PolygonGraphic.rotate + diff --git a/docs/source/api/graphics/SurfaceGraphic.rst b/docs/source/api/graphics/SurfaceGraphic.rst new file mode 100644 index 000000000..385ce2432 --- /dev/null +++ b/docs/source/api/graphics/SurfaceGraphic.rst @@ -0,0 +1,56 @@ +.. _api.SurfaceGraphic: + +SurfaceGraphic +************** + +============== +SurfaceGraphic +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: SurfaceGraphic_api + + SurfaceGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: SurfaceGraphic_api + + SurfaceGraphic.alpha + SurfaceGraphic.alpha_mode + SurfaceGraphic.axes + SurfaceGraphic.block_events + SurfaceGraphic.clim + SurfaceGraphic.cmap + SurfaceGraphic.colors + SurfaceGraphic.data + SurfaceGraphic.deleted + SurfaceGraphic.event_handlers + SurfaceGraphic.indices + SurfaceGraphic.mapcoords + SurfaceGraphic.mode + SurfaceGraphic.name + SurfaceGraphic.offset + SurfaceGraphic.plane + SurfaceGraphic.positions + SurfaceGraphic.right_click_menu + SurfaceGraphic.rotation + SurfaceGraphic.supported_events + SurfaceGraphic.visible + SurfaceGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: SurfaceGraphic_api + + SurfaceGraphic.add_axes + SurfaceGraphic.add_event_handler + SurfaceGraphic.clear_event_handlers + SurfaceGraphic.remove_event_handler + SurfaceGraphic.rotate + diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index ac47a7dfd..bac85e6c1 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -10,6 +10,9 @@ Graphics ImageGraphic ImageVolumeGraphic VectorsGraphic + MeshGraphic + SurfaceGraphic + PolygonGraphic TextGraphic LineCollection LineStack diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 4e40e8d08..93db00a2e 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -20,12 +20,14 @@ Properties .. autosummary:: :toctree: Subplot_api + Subplot.ambient_light Subplot.animations Subplot.axes Subplot.background_color Subplot.camera Subplot.canvas Subplot.controller + Subplot.directional_light Subplot.docks Subplot.frame Subplot.graphics @@ -52,7 +54,10 @@ Methods Subplot.add_line Subplot.add_line_collection Subplot.add_line_stack + Subplot.add_mesh + Subplot.add_polygon Subplot.add_scatter + Subplot.add_surface Subplot.add_text Subplot.add_vectors Subplot.auto_scale diff --git a/docs/source/conf.py b/docs/source/conf.py index 74a1fbaf9..8547e9ae7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,15 +10,12 @@ os.environ["WGPU_FORCE_OFFSCREEN"] = "1" import fastplotlib -import pygfx from pygfx.utils.gallery_scraper import find_examples_for_gallery from pathlib import Path import sys from sphinx_gallery.sorting import ExplicitOrder import imageio.v3 as iio -MAX_TEXTURE_SIZE = 2048 -pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension-2d": MAX_TEXTURE_SIZE}) ROOT_DIR = Path(__file__).parents[1].parents[0] # repo root EXAMPLES_DIR = Path.joinpath(ROOT_DIR, "examples") @@ -44,7 +41,7 @@ "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_design", - "sphinx_gallery.gen_gallery" + "sphinx_gallery.gen_gallery", ] sphinx_gallery_conf = { @@ -65,6 +62,7 @@ "../../examples/controllers", "../../examples/line", "../../examples/line_collection", + "../../examples/mesh", "../../examples/scatter", "../../examples/vectors", "../../examples/text", @@ -77,9 +75,9 @@ "../../examples/qt", ] ), - "ignore_pattern": r'__init__\.py', + "ignore_pattern": r"__init__\.py", "nested_sections": False, - "thumbnail_size": (250, 250) + "thumbnail_size": (250, 250), } extra_conf = find_examples_for_gallery(EXAMPLES_DIR) @@ -107,7 +105,7 @@ "check_switcher": True, "switcher": { "json_url": "http://www.fastplotlib.org/_static/switcher.json", - "version_match": release + "version_match": release, }, "icon_links": [ { @@ -115,7 +113,7 @@ "url": "https://github.com/fastplotlib/fastplotlib", "icon": "fa-brands fa-github", } - ] + ], } html_static_path = ["_static"] diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index 8e942830e..ba53c3411 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -897,6 +897,405 @@ deleted | value | bool | True when graphic was deleted | +----------+------+-------------------------------+ +MeshGraphic +----------- + +positions +^^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+========================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex positions data were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------------+ +| value | int | float | array-like | new data values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------------+ + +indices +^^^^^^^ + +**event info dict** + ++----------+----------------------------------------------+-------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+=================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex indices were indexed/sliced | ++----------+----------------------------------------------+-------------------------------------------------+ +| value | int | float | array-like | new data values for indices that were changed | ++----------+----------------------------------------------+-------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+------------------------------------------------------------+-------------+ +| dict key | type | description | ++==========+============================================================+=============+ +| value | str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | new cmap | ++----------+------------------------------------------------------------+-------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +SurfaceGraphic +-------------- + +data +^^^^ + +**event info dict** + ++----------+------------+------------------+ +| dict key | type | description | ++==========+============+==================+ +| value | np.ndarray | new surface data | ++----------+------------+------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+------------------------------------------------------------+-------------+ +| dict key | type | description | ++==========+============================================================+=============+ +| value | str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | new cmap | ++----------+------------------------------------------------------------+-------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +PolygonGraphic +-------------- + +data +^^^^ + +**event info dict** + ++----------+------------+------------------+ +| dict key | type | description | ++==========+============+==================+ +| value | np.ndarray | new surface data | ++----------+------------+------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+--------------------------------------------------+-----------------+ +| dict key | type | description | ++==========+==================================================+=================+ +| value | str | pygfx.Color | np.ndarray | Sequence[float] | new color value | ++----------+--------------------------------------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+------------------------------------------------------------+-------------+ +| dict key | type | description | ++==========+============================================================+=============+ +| value | str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | new cmap | ++----------+------------------------------------------------------------+-------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +alpha +^^^^^ + +**event info dict** + ++----------+-------+-----------------+ +| dict key | type | description | ++==========+=======+=================+ +| value | float | new alpha value | ++----------+-------+-----------------+ + +alpha_mode +^^^^^^^^^^ + +**event info dict** + ++----------+------+----------------+ +| dict key | type | description | ++==========+======+================+ +| value | str | new alpha mode | ++----------+------+----------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + TextGraphic ----------- diff --git a/examples/mesh/README.rst b/examples/mesh/README.rst new file mode 100644 index 000000000..99e569fed --- /dev/null +++ b/examples/mesh/README.rst @@ -0,0 +1,2 @@ +Mesh Examples +============= diff --git a/examples/mesh/image_surface.py b/examples/mesh/image_surface.py new file mode 100644 index 000000000..fce3c4958 --- /dev/null +++ b/examples/mesh/image_surface.py @@ -0,0 +1,37 @@ +""" +Image surface +============= + +Example showing an image as a surface. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import imageio.v3 as iio +import fastplotlib as fpl +import scipy.ndimage + +im = iio.imread("imageio:astronaut.png") + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + + +# Create the height map from the image +z = im.mean(axis=2) +z = scipy.ndimage.gaussian_filter(z, 5) # 2nd arg is sigma + +mesh = figure[0, 0].add_surface(z, cmap=im) +mesh.world_object.local.scale_y = -1 + + +figure[0, 0].axes.grids.xy.visible = True +figure[0, 0].camera.show_object(mesh.world_object, (1, 2, -1), up=(0, 0, 1)) +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/mesh.py b/examples/mesh/mesh.py new file mode 100644 index 000000000..4c8de088d --- /dev/null +++ b/examples/mesh/mesh.py @@ -0,0 +1,36 @@ +""" +Simple mesh +=========== + +Example showing a simple mesh +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import pygfx as gfx + + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + + +# Load geometry using Pygfx's geometry util +geo = gfx.geometries.torus_knot_geometry() +positions = geo.positions.data +indices = geo.indices.data + +mesh = fpl.MeshGraphic(positions, indices, colors="magenta") + +figure[0, 0].add_graphic(mesh) +figure[0, 0].axes.grids.xy.visible = True +figure[0, 0].camera.show_object(mesh.world_object, (1, 1, -1), up=(0, 0, 1)) + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/polygon_animation.py b/examples/mesh/polygon_animation.py new file mode 100644 index 000000000..6d4bc7bf0 --- /dev/null +++ b/examples/mesh/polygon_animation.py @@ -0,0 +1,76 @@ +""" +Polygon animation +================= + +Polygon animation example that changes the polygon data. Random points are generated by sampling from a +2D gaussian and a polygon is updated to visualize a convex hull for the sampled points. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 8s' + +import numpy as np +from scipy.spatial import ConvexHull +import fastplotlib as fpl + + +def points_to_hull(points) -> np.ndarray: + hull = ConvexHull(points, qhull_options="Qs") + return points[hull.vertices] + + +figure = fpl.Figure(size=(700, 560)) + + +cov = np.array([[1, 0], [0, 1]]) + +# sample points from a 2d gaussian +samples1 = np.random.multivariate_normal((0, 0), cov, size=20) +samples2 = np.random.multivariate_normal((5, 0), cov, size=50) + +# add the convex hull as a polygon +polygon1 = figure[0, 0].add_polygon( + points_to_hull(samples1), colors="cyan", alpha=0.7, alpha_mode="blend" +) +# add the sampled points +scatter1 = figure[0, 0].add_scatter( + samples1, sizes=8, colors="blue", alpha=0.7, alpha_mode="blend" +) + +# add the second gaussian and convex hull polygon +polygon2 = figure[0, 0].add_polygon( + points_to_hull(samples2), colors="magenta", alpha=0.7, alpha_mode="blend" +) +scatter2 = figure[0, 0].add_scatter( + samples2, sizes=8, colors="r", alpha=0.7, alpha_mode="blend" +) + + +def animate(): + # set new scatter data + scatter1.data[:, :-1] += np.random.normal(0, 0.05, size=samples1.size).reshape( + samples1.shape + ) + # set convex hull with new polygon vertices + polygon1.data = points_to_hull(scatter1.data[:, :-1]) + + # set the other scatter and polygon + scatter2.data[:, :-1] += np.random.normal(0, 0.05, size=samples2.size).reshape( + samples2.shape + ) + polygon2.data = points_to_hull(scatter2.data[:, :-1]) + + +figure.show() +figure[0, 0].camera.width = 10 +figure[0, 0].camera.height = 10 + +figure.add_animations(animate) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/polygons.py b/examples/mesh/polygons.py new file mode 100644 index 000000000..616c2e0fb --- /dev/null +++ b/examples/mesh/polygons.py @@ -0,0 +1,61 @@ +""" +Polygons +======== + +An example with polygons. + +""" + +# test_example = True +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np +from cmap import Colormap + +figure = fpl.Figure(size=(700, 560)) + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points, endpoint=False) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + np.asarray(center)[None] + + +# define vertices for some polygons +circle_data = make_circle(center=(0, 0), radius=5) +octogon_data = make_circle(center=(15, 0), radius=7, n_points=8) +rectangle_data = np.array([[10, 10], [20, 10], [20, 15], [10, 15]]) +triangle_data = np.array( + [ + [-5, 8], + [5, 8], + [0, 15], + [-5, 8], + ] +) + +# add polygons +figure[0, 0].add_polygon(circle_data, name="circle") +figure[0, 0].add_polygon( + octogon_data, + colors=Colormap("jet").lut(8), # set vertex colors from jet cmap + name="octogon" +) +figure[0, 0].add_polygon( + rectangle_data, + colors=["r", "r", "cyan", "y"], # manually specify vertex colors + name="rectangle" +) +figure[0, 0].add_polygon(triangle_data, colors="m") + +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_earth.py b/examples/mesh/surface_earth.py new file mode 100644 index 000000000..c2e137bc8 --- /dev/null +++ b/examples/mesh/surface_earth.py @@ -0,0 +1,95 @@ +""" +Earth sphere animation +====================== + +Example showing how to create a sphere with an image of the Earth and rotate it around its 23.44° axis of rotation +with respect to the ecliptic (the xz plane in the visualization). + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 8s' + +import fastplotlib as fpl +import numpy as np +import imageio.v3 as iio +import pylinalg as la + + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + +# create a sphere from spherical coordinates +# see this for reference: https://mathworld.wolfram.com/SphericalCoordinates.html +# phi and theta are swapped in this example w.r.t. the wolfram alpha description +radius = 10 +nx = 101 +phi = np.linspace(0, np.pi * 2, num=nx, dtype=np.float32) +ny = 51 +theta = np.linspace(0, np.pi, num=ny, dtype=np.float32) + +phi_grid, theta_grid = np.meshgrid(phi, theta) + +# convert to cartesian coordinates +theta_grid_sin = np.sin(theta_grid) +x = radius * np.cos(phi_grid) * theta_grid_sin * -1 +y = radius * np.cos(theta_grid) +z = radius * np.sin(phi_grid) * theta_grid_sin + +# get texture coords to map the image onto the mesh positions +u = phi_grid / (np.pi * 2) +v = 1 - (theta_grid / np.pi) +texcoords = np.dstack([u, v]).reshape(-1, 2) + +# get an image of the earth from nasa +image = iio.imread( + "https://svs.gsfc.nasa.gov/vis/a000000/a003600/a003615/flat_earth_Largest_still.0330.jpg" +) +# images coordinate systems are typically inverted in y, so flip the image +image = np.ascontiguousarray(np.flipud(image)) + +# create a sphere +sphere = figure[0, 0].add_surface( + np.dstack([x, y, z]), + mode="phong", + colors="magenta", + cmap=image, + mapcoords=texcoords, +) + +# display xz plane as a grid +figure[0, 0].axes.grids.xz.visible = True +figure.show() + +# view from top right angle +figure[0, 0].camera.show_object(sphere.world_object, (-0.5, -0.25, -1), up=(0, 1, 0)) +figure[0, 0].camera.zoom = 1.25 + +# create quaternion for 23.44 degrees axial tilt +axial_tilt = la.quat_from_euler((np.radians(23.44), 0), order="XY") + +# a line to indicate the axial tilt +figure[0, 0].add_line( + np.array([[0, -20, 0], [0, 20, 0]]), rotation=axial_tilt, colors="magenta" +) + +rot = 1 + + +def rotate(): + # rotate by 1 degree + global rot + rot += 1 + rot_quat = la.quat_from_euler((0, np.radians(rot)), order="XY") + + # apply rotation w.r.t. axial tilt + sphere.rotation = la.quat_mul(axial_tilt, rot_quat) + + +figure[0, 0].add_animations(rotate) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_ellipsoid.py b/examples/mesh/surface_ellipsoid.py new file mode 100644 index 000000000..6d7cdae7b --- /dev/null +++ b/examples/mesh/surface_ellipsoid.py @@ -0,0 +1,55 @@ +""" +Ellipsoid surface +================= + +Simple example of a sphere surface mesh with a colormap indicating z values. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + +# create an ellipsoid from spherical coordinates +# see this for reference: https://mathworld.wolfram.com/SphericalCoordinates.html +# phi and theta are swapped in this example w.r.t. the wolfram alpha description +radius = 10 + +nx = 101 +phi = np.linspace(0, np.pi * 2, num=nx, dtype=np.float32) +ny = 51 +theta = np.linspace(0, np.pi, num=ny, dtype=np.float32) + +phi_grid, theta_grid = np.meshgrid(phi, theta) + +# convert to cartesian coordinates +theta_grid_sin = np.sin(theta_grid) +x = radius * np.cos(phi_grid) * theta_grid_sin * -1 +y = radius * np.cos(theta_grid) + +# elongate along z axis +z = radius * 2 * np.sin(phi_grid) * theta_grid_sin + +sphere = figure[0, 0].add_surface( + np.dstack([x, y, z]), + mode="phong", + cmap="bwr", # by default, providing a colormap name will map the colors to z values +) + +# display xz plane as a grid +figure[0, 0].axes.grids.xy.visible = True +figure.show() + +# view from top right angle +figure[0, 0].camera.show_object(sphere.world_object, (1, 1, -1), up=(0, 0, 1)) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_gaussian.py b/examples/mesh/surface_gaussian.py new file mode 100644 index 000000000..6a9fb0f1d --- /dev/null +++ b/examples/mesh/surface_gaussian.py @@ -0,0 +1,45 @@ +""" +Gaussian kernel as a surface +============================ + +Example showing a gaussian kernel as a surface mesh +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np + + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + + +def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1): + return ( + 1.0 + / (2.0 * np.pi * sx * sy) + * np.exp( + -((x - mx) ** 2.0 / (2.0 * sx**2.0) + (y - my) ** 2.0 / (2.0 * sy**2.0)) + ) + ) + + +r = np.linspace(0, 10, num=200) +x, y = np.meshgrid(r, r) +z = gaus2d(x, y, mx=5, my=5, sx=1, sy=1) * 50 + +mesh = figure[0, 0].add_surface( + np.dstack([x, y, z]), mode="phong", cmap="jet" +) + +# figure[0, 0].axes.grids.xy.visible = True +figure[0, 0].camera.show_object(mesh.world_object, (-2, 2, -2), up=(0, 0, 1)) +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_height.py b/examples/mesh/surface_height.py new file mode 100644 index 000000000..1e1db7ffe --- /dev/null +++ b/examples/mesh/surface_height.py @@ -0,0 +1,35 @@ +""" +Simple surface +============== + +Example showing a surface mesh +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np +import pygfx as gfx + + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + + +t = np.linspace(0, 6, 100).astype(np.float32) +x = np.sin(t) +y = np.cos(t * 2) +z = (x.reshape(1, -1) * x.reshape(-1, 1)) * 50 # 100x100 + +surface = figure[0, 0].add_surface(z, cmap="bwr") + +# figure[0, 0].axes.grids.xy.visible = True +figure[0, 0].camera.show_object(surface.world_object, (-2, 2, -3), up=(0, 0, 1)) +figure.show() + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_ripple.py b/examples/mesh/surface_ripple.py new file mode 100644 index 000000000..ac556bd1b --- /dev/null +++ b/examples/mesh/surface_ripple.py @@ -0,0 +1,62 @@ +""" +Surface animation +================= + +Example of a surface ripple animation by setting the z-height data on every render. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 6s' + +import fastplotlib as fpl +import numpy as np + + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + + +def create_ripple(shape=(100, 100), phase=0.0, freq=np.pi / 4, ampl=1.0): + m, n = shape + y, x = np.ogrid[-m / 2 : m / 2, -n / 2 : n / 2] + r = np.sqrt(x**2 + y**2) + z = (ampl * np.sin(freq * r + phase)) / np.sqrt(r + 1) + + return z * 8 + + +z = create_ripple() + +# set the clim vmax +max_z = create_ripple(phase=(np.pi / 4) - (np.pi / 2)).max() + +surface = figure[0, 0].add_surface( + z, mode="basic", cmap="viridis", clim=(-max_z, max_z) +) + +figure[0, 0].camera.show_object(surface.world_object, (-1, 3, -1), up=(0, 0, 1)) +figure.show() + +figure[0, 0].camera.zoom = 1.15 + +phase = 0.0 + + +def animate(): + global phase + + z = create_ripple(phase=phase) + + surface.data = z + + phase -= 0.1 + + +figure[0, 0].add_animations(animate) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_sphere_ripple.py b/examples/mesh/surface_sphere_ripple.py new file mode 100644 index 000000000..6caa03465 --- /dev/null +++ b/examples/mesh/surface_sphere_ripple.py @@ -0,0 +1,81 @@ +""" +Sphere ripple animation +======================= + +Example of a sphere with a ripple effect by setting the data on every render. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 6s' + +import fastplotlib as fpl +import numpy as np + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + +# create an ellipsoid from spherical coordinates +# see this for reference: https://mathworld.wolfram.com/SphericalCoordinates.html +# phi and theta are swapped in this example w.r.t. the wolfram alpha description +radius = 10 +nx = 250 +phi = np.linspace(0, np.pi * 2, num=nx, dtype=np.float32) +ny = 250 +theta = np.linspace(0, np.pi, num=ny, dtype=np.float32) + +phi_grid, theta_grid = np.meshgrid(phi, theta) + +# convert to cartesian coordinates +theta_grid_sin = np.sin(theta_grid) +x = radius * np.cos(phi_grid) * theta_grid_sin * -1 +y = radius * np.cos(theta_grid) + +ripple_amplitude = 1.0 +ripple_frequency = 20.0 +ripple = ripple_amplitude * np.sin(ripple_frequency * theta_grid) + +z_ref = radius * np.sin(phi_grid) * theta_grid_sin +z = z_ref * (1 + ripple / radius) + +sphere = figure[0, 0].add_surface( + np.dstack([x, y, z]), + mode="phong", + colors="red", + cmap="jet", +) + +# display xz plane as a grid +figure[0, 0].axes.grids.xy.visible = True +figure.show() + +figure[0, 0].camera.show_object(sphere.world_object, (10, 1, -1), up=(0, 0, 1)) +figure[0, 0].camera.zoom = 1.3 + + +start = 0 + + +def animate(): + global start + theta = np.linspace(start, start + np.pi, num=ny, dtype=np.float32) + _, theta_grid = np.meshgrid(phi, theta) + ripple = ripple_amplitude * np.sin(ripple_frequency * theta_grid) + + z = z_ref * (1 + ripple / radius) + + sphere.data = np.dstack([x, y, z]) + + start += 0.005 + + if start > np.pi * 2: + start = 0 + + +figure[0, 0].add_animations(animate) + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/mesh/surface_terrain.py b/examples/mesh/surface_terrain.py new file mode 100644 index 000000000..f747a708c --- /dev/null +++ b/examples/mesh/surface_terrain.py @@ -0,0 +1,36 @@ +""" +Elevation map of the earth +========================== + +Surface graphic showing elevation map of the earth +""" + +# run_example = false +# sphinx_gallery_pygfx_docs = 'code' + +import imageio.v3 as iio +import fastplotlib as fpl +import numpy as np + +# grayscale image of the earth where the pixel value indicates elevation +elevation = iio.imread("https://neo.gsfc.nasa.gov/archive/bluemarble/bmng/topography/srtm_ramp2.world.5400x2700.jpg").astype(np.float32) +elevation /= 2 + +figure = fpl.Figure(size=(700, 560), cameras="3d", controller_types="orbit") + +mesh = figure[0, 0].add_surface(elevation, cmap="terrain") +mesh.world_object.local.scale_y = -1 + + +figure[0, 0].axes.grids.xy.visible = True +figure[0, 0].camera.show_object(mesh.world_object, (-4, 2, -1), up=(0, 0, 1)) +figure.show() + +figure[0, 0].camera.zoom = 2.5 + + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/screenshots/image_surface.png b/examples/screenshots/image_surface.png new file mode 100644 index 000000000..86300a7d4 --- /dev/null +++ b/examples/screenshots/image_surface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0a74e7a23147dc7c50b085c9beb7e1d41d012546606b586692b3b4968947569 +size 301999 diff --git a/examples/screenshots/mesh.png b/examples/screenshots/mesh.png new file mode 100644 index 000000000..8a2d5c219 --- /dev/null +++ b/examples/screenshots/mesh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a040db1c5159f0e8e9b3dfb61b7076909481d7f3c21b25722cd0b50c14c30b2d +size 320096 diff --git a/examples/screenshots/no-imgui-image_surface.png b/examples/screenshots/no-imgui-image_surface.png new file mode 100644 index 000000000..5ebc655d1 --- /dev/null +++ b/examples/screenshots/no-imgui-image_surface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0703c47bee63b8170100fbe58a72b41a4c40525adf2ed2c16fa2d860c627ed21 +size 311054 diff --git a/examples/screenshots/no-imgui-mesh.png b/examples/screenshots/no-imgui-mesh.png new file mode 100644 index 000000000..5a83fc871 --- /dev/null +++ b/examples/screenshots/no-imgui-mesh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:675e4b8201f6dc77d1f7bd5269ad948b45bbdcfb400d764c771a89e8528b974f +size 325110 diff --git a/examples/screenshots/no-imgui-surface_gaussian.png b/examples/screenshots/no-imgui-surface_gaussian.png new file mode 100644 index 000000000..849d4d9cb --- /dev/null +++ b/examples/screenshots/no-imgui-surface_gaussian.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ccd13d12890895cc70bf4b9e071ea75f6a23a90f885ac6986ac6fb1fd6d544b +size 33510 diff --git a/examples/screenshots/no-imgui-surface_height.png b/examples/screenshots/no-imgui-surface_height.png new file mode 100644 index 000000000..789783464 --- /dev/null +++ b/examples/screenshots/no-imgui-surface_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8662ea400572e3a9730b42d915c093040ed2694c0f02439438898570ca41666 +size 51219 diff --git a/examples/screenshots/no-imgui-vectors_simple.png b/examples/screenshots/no-imgui-vectors_simple.png new file mode 100644 index 000000000..02f067c08 --- /dev/null +++ b/examples/screenshots/no-imgui-vectors_simple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eb8ba74def34c750e876c1811800157606d71423fa27c5f3e338b66513a30ee +size 129275 diff --git a/examples/screenshots/no-imgui-vectors_swirl.png b/examples/screenshots/no-imgui-vectors_swirl.png new file mode 100644 index 000000000..63300917b --- /dev/null +++ b/examples/screenshots/no-imgui-vectors_swirl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e41489d3fffefe5b4217879d8fd166205e81b91f7e88c2437ae21113b3937c1 +size 72255 diff --git a/examples/screenshots/surface_gaussian.png b/examples/screenshots/surface_gaussian.png new file mode 100644 index 000000000..8e9a414c4 --- /dev/null +++ b/examples/screenshots/surface_gaussian.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a40c79782f8498d4c03f0f6f09583c8a7d139a73a8457b30158d5625d77792ba +size 32108 diff --git a/examples/screenshots/surface_height.png b/examples/screenshots/surface_height.png new file mode 100644 index 000000000..56a6a2c9b --- /dev/null +++ b/examples/screenshots/surface_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1f9bb4570725a7876f5296a69e9325b81e1e58633c46ae502adc0dc6ad00aca +size 50123 diff --git a/examples/screenshots/vectors_simple.png b/examples/screenshots/vectors_simple.png new file mode 100644 index 000000000..332b37812 --- /dev/null +++ b/examples/screenshots/vectors_simple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bf6bdfb4530a434417480bcd10713ccdc43db3a9c554da4be8e333760a8984d +size 126670 diff --git a/examples/screenshots/vectors_swirl.png b/examples/screenshots/vectors_swirl.png new file mode 100644 index 000000000..ab6f298e9 --- /dev/null +++ b/examples/screenshots/vectors_swirl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec512fd733f25df5706055efbdb077db5fa034349c5611a86a20f3055b0d8123 +size 71012 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 7b70defdb..aad729c7a 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -24,7 +24,8 @@ "scatter/*.py", "line/*.py", "line_collection/*.py", - "vectors/*.py" + "vectors/*.py", + "mesh/*.py", "gridplot/*.py", "window_layouts/*.py", "events/*.py", diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 46051479d..3d01e4a35 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -4,6 +4,7 @@ from .image import ImageGraphic from .image_volume import ImageVolumeGraphic from ._vectors import VectorsGraphic +from .mesh import MeshGraphic, SurfaceGraphic, PolygonGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack @@ -15,6 +16,9 @@ "ImageGraphic", "ImageVolumeGraphic", "VectorsGraphic", + "MeshGraphic", + "SurfaceGraphic", + "PolygonGraphic", "TextGraphic", "LineCollection", "LineStack", diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index f745f10c8..cf99d376d 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -1,10 +1,19 @@ -from ._positions_graphics import ( +from ._positions import ( VertexColors, UniformColor, SizeSpace, VertexPositions, VertexCmap, ) +from ._mesh import ( + MeshIndices, + MeshCmap, + SurfaceData, + PolygonData, + resolve_cmap_mesh, + surface_data_to_mesh, + triangulate_polygon, +) from ._line import Thickness from ._scatter import ( VertexMarkers, @@ -71,6 +80,9 @@ "SizeSpace", "VertexPositions", "VertexCmap", + "MeshIndices", + "MeshCmap", + "SurfaceData", "Thickness", "VertexMarkers", "UniformMarker", diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 5dec9f1e5..779310476 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -289,6 +289,12 @@ def _update_range( # the first dimension corresponding to n_datapoints key: int | np.ndarray[int | bool] | slice = key[0] + if isinstance(key, slice): + if key == slice(None): + # directly update full, don't need to figure out chunks + self.buffer.update_full() + return + offset, size = self._parse_offset_size(key, upper_bound) self.buffer.update_range(offset=offset, size=size) diff --git a/fastplotlib/graphics/features/_mesh.py b/fastplotlib/graphics/features/_mesh.py new file mode 100644 index 000000000..7355acb4e --- /dev/null +++ b/fastplotlib/graphics/features/_mesh.py @@ -0,0 +1,284 @@ +from typing import Any, Sequence + +import numpy as np +import pygfx + +from ._base import ( + GraphicFeature, + GraphicFeatureEvent, + to_gpu_supported_dtype, + block_reentrance, +) + +from ._positions import VertexPositions +from ...utils.functions import get_cmap +from ...utils.triangulation import triangulate + + +def resolve_cmap_mesh(cmap) -> pygfx.TextureMap | None: + """Turn a user-provided in a pygfx.TextureMap, supporting 1D, 2D and 3D data.""" + + if cmap is None: + pygfx_cmap = None + elif isinstance(cmap, pygfx.TextureMap): + pygfx_cmap = cmap + elif isinstance(cmap, pygfx.Texture): + pygfx_cmap = pygfx.TextureMap(cmap) + elif isinstance(cmap, (str, dict)): + pygfx_cmap = pygfx.cm.create_colormap(get_cmap(cmap)) + else: + map = np.asarray(cmap) + if map.ndim == 2: # 1D plus color + pygfx_cmap = pygfx.cm.create_colormap(cmap) + else: + tex = pygfx.Texture(map, dim=map.ndim - 1) + pygfx_cmap = pygfx.TextureMap(tex) + + return pygfx_cmap + + +class MeshIndices(VertexPositions): + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which vertex indices were indexed/sliced", + }, + { + "dict key": "value", + "type": "int | float | array-like", + "description": "new data values for indices that were changed", + }, + ] + + def __init__( + self, data: Any, isolated_buffer: bool = True, property_name: str = "indices" + ): + """ + Manages the vertex indices buffer shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + + data = self._fix_data(data) + super().__init__( + data, isolated_buffer=isolated_buffer, property_name=property_name + ) + + def _fix_data(self, data): + if data.ndim != 2 or data.shape[1] not in (3, 4): + raise ValueError( + f"indices must be of shape: [n_vertices, 3] or [n_vertices, 4], " + f"you passed an array of shape: {data.shape}" + ) + return data.astype("i4") + + +class MeshCmap(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray", + "description": "new cmap", + }, + ] + + def __init__( + self, + value: str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None, + property_name: str = "cmap", + ): + """Manages a mesh colormap""" + + self._value = value + super().__init__(property_name=property_name) + + @property + def value( + self, + ) -> str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None: + return self._value + + @block_reentrance + def set_value( + self, + graphic, + value: str | dict | pygfx.TextureMap | pygfx.Texture | np.ndarray | None, + ): + graphic.world_object.material.map = resolve_cmap_mesh(value) + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +def surface_data_to_mesh(data: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """ + surface data to mesh positions and indices + + expects data that is of shape: [m, n, 3] or [m, n] + """ + + data = np.asarray(data) + + if data.ndim == 2: + # "image" of z values passed + # [m, n] -> [n_vertices, 3] + y = ( + np.arange(data.shape[0]) + .reshape(data.shape[0], 1) + .repeat(data.shape[1], axis=1) + ) + x = ( + np.arange(data.shape[1]) + .reshape(1, data.shape[1]) + .repeat(data.shape[0], axis=0) + ) + positions = np.column_stack((x.ravel(), y.ravel(), data.ravel())) + else: + if data.ndim != 3: + raise ValueError( + f"expect data that is of shape: [m, n, 3], [m, n]\n" + f"you passed: {data.shape}" + ) + if data.shape[2] != 3: + raise ValueError( + f"expect data that is of shape: [m, n, 3], [m, n]\n" + f"you passed: {data.shape}" + ) + + # [m, n, 3] -> [n_vertices, 3] + positions = data.reshape(-1, 3) + + # Create faces + w = data.shape[1] + i = np.arange(data.shape[0] - 1) + j = np.arange(w - 1) + + j, i = np.meshgrid(j, i, indexing="ij") + start = j.ravel() + w * i.ravel() + + indices = np.column_stack([start, start + 1, start + w + 1, start + w]) + + return positions, indices + + +class SurfaceData(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new surface data", + }, + ] + + def __init__(self, value: np.ndarray | Sequence, property_name: str = "data"): + self._value = np.asarray(value, dtype=np.float32) + super().__init__(property_name=property_name) + + @property + def value(self) -> np.ndarray: + return self._value + + @block_reentrance + def set_value(self, graphic, value: np.ndarray): + positions, indices = surface_data_to_mesh(value) + + graphic.positions = positions + graphic.indices = indices + + # if cmap is a 1D texture we need to set the texcoords again using new z values + if graphic.world_object.material.map is not None: + if graphic.world_object.material.map.texture.dim == 1: + mapcoords = positions[:, 2] + + if graphic.clim is None: + clim = mapcoords.min(), mapcoords.max() + else: + clim = graphic.clim + mapcoords = (mapcoords - clim[0]) / (clim[1] - clim[0]) + graphic.mapcoords = mapcoords + + self._value = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + +def triangulate_polygon(data: np.ndarray | Sequence): + """vertices of shape [n_vertices , 2] -> positions, indices""" + data = np.asarray(data, dtype=np.float32) + + err_msg = ( + f"polygon vertex data must be of shape [n_vertices, 2], you passed: {data}" + ) + + if data.ndim != 2: + raise ValueError(err_msg) + if data.shape[1] != 2: + raise ValueError(err_msg) + + if len(data) >= 3: + indices = triangulate(data) + else: + indices = np.arange((0, 3), np.int32) + + data = np.column_stack([data, np.zeros(data.shape[0], dtype=np.float32)]) + + return data, indices + + +class PolygonData(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new polygon vertex data", + }, + ] + + def __init__(self, value: np.ndarray, property_name: str = "data"): + self._value = np.asarray(value, dtype=np.float32) + super().__init__(property_name=property_name) + + @property + def value(self) -> np.ndarray: + return self._value + + @block_reentrance + def set_value(self, graphic, value: np.ndarray | Sequence): + value = np.asarray(value, dtype=np.float32) + + positions, indices = triangulate_polygon(value) + + geometry = graphic.world_object.geometry + + # Need larger (or smaller) buffer? Scale up/down with factors of 2. + need_position_size = 2 ** int(np.ceil(np.log2(max(8, len(positions))))) + if need_position_size != geometry.positions.nitems: + arr = np.zeros((need_position_size, 3), np.float32) + geometry.positions = pygfx.Buffer(arr) + need_indices_size = 2 ** int(np.ceil(np.log2(max(8, len(indices))))) + if need_indices_size != geometry.indices.nitems: + arr = np.zeros((need_indices_size, 3), np.int32) + geometry.indices = pygfx.Buffer(arr) + + geometry.positions.data[: len(positions)] = positions + geometry.positions.data[len(positions) :] = ( + positions[-1] if len(positions) else (0, 0, 0) + ) + geometry.positions.draw_range = 0, len(positions) + geometry.positions.update_full() + + geometry.indices.data[: len(indices)] = indices + geometry.indices.data[len(indices) :] = 0 + geometry.indices.draw_range = 0, len(indices) + geometry.indices.update_full() + + # send event + if len(self._event_handlers) < 1: + return + + event = GraphicFeatureEvent(self._property_name, {"value": self.value}) + + # calls any events + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/features/_positions_graphics.py b/fastplotlib/graphics/features/_positions.py similarity index 99% rename from fastplotlib/graphics/features/_positions_graphics.py rename to fastplotlib/graphics/features/_positions.py index ae57e77d7..295d22417 100644 --- a/fastplotlib/graphics/features/_positions_graphics.py +++ b/fastplotlib/graphics/features/_positions.py @@ -245,8 +245,6 @@ def __init__( ) def _fix_data(self, data): - # data = to_gpu_supported_dtype(data) - if data.ndim == 1: # if user provides a 1D array, assume these are y-values data = np.column_stack([np.arange(data.size, dtype=data.dtype), data]) diff --git a/fastplotlib/graphics/features/utils.py b/fastplotlib/graphics/features/utils.py index 408610e1e..aa4022052 100644 --- a/fastplotlib/graphics/features/utils.py +++ b/fastplotlib/graphics/features/utils.py @@ -34,8 +34,9 @@ def parse_colors( elif colors.ndim == 2: if not (colors.shape[1] in (3, 4) and colors.shape[0] == n_colors): raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGB or RGBA arrays for each datapoint in the shape [n_datapoints, 3] or [n_datapoints, 4]" + f"Valid array color arguments must be a single RGBA array or a stack of " + f"RGB or RGBA arrays for each datapoint in the shape [n_datapoints, 3] or [n_datapoints, 4].\n" + f"n_datapoints is: {n_colors}, you passed a colors array of shape: {colors.shape}" ) data = colors else: diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py new file mode 100644 index 000000000..2e5a11851 --- /dev/null +++ b/fastplotlib/graphics/mesh.py @@ -0,0 +1,473 @@ +from typing import Sequence, Any, Literal + +import numpy as np + +import pygfx + +from ._positions_base import Graphic +from .features import ( + VertexPositions, + MeshIndices, + MeshCmap, + SurfaceData, + surface_data_to_mesh, + VertexColors, + UniformColor, + resolve_cmap_mesh, + VolumeSlicePlane, + PolygonData, + triangulate_polygon, +) + + +class MeshGraphic(Graphic): + _features = { + "positions": VertexPositions, + "indices": MeshIndices, + "colors": (VertexColors, UniformColor), + "cmap": MeshCmap, + } + + def __init__( + self, + positions: Any, + indices: Any, + mode: Literal["basic", "phong", "slice"] = "phong", + plane: tuple[float, float, float, float] = (0.0, 0.0, 1.0, 0.0), + colors: str | np.ndarray | Sequence = "w", + mapcoords: Any = None, + cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray = None, + clim: tuple[float, float] = None, + isolated_buffer: bool = True, + **kwargs, + ): + """ + Create a mesh Graphic. + + Parameters + ---------- + positions: array-like + The 3D positions of the vertices. + + indices: array-like + The indices into the positions that make up the triangles. Each 3 + subsequent indices form a triangle. + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + * slice: display a slice of the mesh at the specified ``plane`` + + plane: (float, float, float, float), default (0., 0., 1., 0.) + Slice mesh at this plane. Sets (a, b, c, d) in the equation the defines a plane: ax + by + cz + d = 0. + Used only if `mode` = "slice". The plane is defined in world space. + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value, mapped to [0..1]. + If ``mapcoords`` and ``cmap`` are given, they are used instead of ``colors``. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + An image can also be used, this is basically a 2D colormap. + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer - useful if the + array is large. In almost all cases this should be ``True``. + + **kwargs + passed to :class:`.Graphic` + + """ + + super().__init__(**kwargs) + + if isinstance(positions, VertexPositions): + self._positions = positions + else: + self._positions = VertexPositions( + positions, isolated_buffer=isolated_buffer, property_name="positions" + ) + + if isinstance(positions, MeshIndices): + self._indices = indices + else: + self._indices = MeshIndices( + indices, isolated_buffer=isolated_buffer, property_name="indices" + ) + + self._cmap = MeshCmap(cmap) + + # Apply contrast limits. Would be nice if Pygfx mesh material had clim too! But + # for now we apply it as a pre-processing step. + if clim is None and mapcoords is not None: + clim = mapcoords.min(), mapcoords.max() + + if mapcoords is not None: + mapcoords = (mapcoords - clim[0]) / (clim[1] - clim[0]) + self._mapcoords = pygfx.Buffer(np.asarray(mapcoords, dtype=np.float32)) + else: + self._mapcoords = None + + self._clim = clim + + uniform_color = "w" + per_vertex_colors = False + + if cmap is None: + if colors is None: + uniform_color = "w" + self._colors = UniformColor(uniform_color) + elif isinstance(colors, str) or isinstance(colors, tuple): + uniform_color = colors + self._colors = UniformColor(uniform_color) + elif isinstance(colors, VertexColors): + per_vertex_colors = True + self._colors = colors + else: + per_vertex_colors = True + self._colors = VertexColors( + colors, n_colors=self._positions.value.shape[0] + ) + + geometry = pygfx.Geometry( + positions=self._positions.buffer, indices=self._indices._buffer + ) + + valid_modes = ["basic", "phong", "slice"] + if mode not in valid_modes: + raise ValueError(f"mode must be one of: {valid_modes}\nYou passed: {mode}") + self._mode = mode + + material_cls = getattr(pygfx, f"Mesh{mode.capitalize()}Material") + + if mode == "slice": + self._plane = VolumeSlicePlane(plane) + add_kwargs = {"plane": self._plane.value} + else: + # for basic and phong, maybe later we can add more of the properties + add_kwargs = {} + + material = material_cls( + color_mode="uniform", + color=uniform_color, + pick_write=True, + **add_kwargs, + ) + + # Set all the data + if per_vertex_colors: + geometry.colors = self._colors.buffer + if self._mapcoords is not None: + geometry.texcoords = self._mapcoords + if cmap is not None: + material.map = resolve_cmap_mesh(cmap) + + # Decide on color mode + # uniform = None #: Use the uniform color (usually ``material.color``). + # vertex = None #: Use the per-vertex color specified in the geometry (usually ``geometry.colors``). + # face = None #: Use the per-face color specified in the geometry (usually ``geometry.colors``). + # vertex_map = None #: Use per-vertex texture coords (``geometry.texcoords``), and sample these in ``material.map``. + # face_map = None #: Use per-face texture coords (``geometry.texcoords``), and sample these in ``material.map``. + if mapcoords is not None and cmap is not None: + material.color_mode = "vertex_map" + elif per_vertex_colors: + material.color_mode = "vertex" + else: + material.color_mode = "uniform" + + world_object: pygfx.Mesh = pygfx.Mesh(geometry=geometry, material=material) + + self._set_world_object(world_object) + + @property + def mode(self) -> Literal["basic", "phong", "slice"]: + """get mesh rendering mode""" + return self._mode + + @property + def positions(self) -> VertexPositions: + """Get or set the vertex positions""" + return self._positions + + @positions.setter + def positions(self, new_positions): + self._positions[:] = new_positions + + @property + def indices(self) -> MeshIndices: + """Get or set the vertex indices""" + return self._indices + + @indices.setter + def indices(self, mew_indices): + self._indices[:] = mew_indices + + @property + def mapcoords(self) -> np.ndarray | None: + """get or set the mapcoords""" + if self._mapcoords is not None: + return self._mapcoords.data + + @mapcoords.setter + def mapcoords(self, new_mapcoords: np.ndarray | None): + if new_mapcoords is None: + self.world_object.geometry.texcoords = None + self._mapcoords = None + return + + if new_mapcoords.shape == self._mapcoords.data.shape: + self._mapcoords.data[:] = new_mapcoords + self._mapcoords.update_full() + else: + # allocate new buffer + self._mapcoords = pygfx.Buffer(np.asarray(new_mapcoords, dtype=np.float32)) + self.world_object.geometry.texcoords = self._mapcoords + + @property + def clim(self) -> tuple[float, float] | None: + """get or set the colormap limits""" + return self._clim + + @clim.setter + def clim(self, new_clim: tuple[float, float]): + if len(new_clim) != 2: + raise ValueError("clim must be a: tuple[float, float]") + + self._clim = tuple(new_clim) + + self.mapcoords = (self.mapcoords - self.clim[0]) / (self.clim[1] - self.clim[0]) + + @property + def colors(self) -> VertexColors | pygfx.Color: + """Get or set the colors""" + if isinstance(self._colors, VertexColors): + return self._colors + + elif isinstance(self._colors, UniformColor): + return self._colors.value + + @colors.setter + def colors(self, value: str | np.ndarray | Sequence[float] | Sequence[str]): + if isinstance(self._colors, VertexColors): + self._colors[:] = value + + elif isinstance(self._colors, UniformColor): + self._colors.set_value(self, value) + + @property + def cmap(self) -> str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray | None: + """get or set the cmap""" + if self._cmap is not None: + return self._cmap.value + + @cmap.setter + def cmap( + self, + new_cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray | None, + ): + self._cmap.set_value(self, new_cmap) + + @property + def plane(self) -> tuple[float, float, float, float] | None: + """Get or set the current slice plane. Valid only for ``"slice"`` render mode.""" + if self.mode != "slice": + return + + return self._plane.value + + @plane.setter + def plane(self, value: tuple[float, float, float, float]): + if self.mode != "slice": + raise TypeError("`plane` property is only valid for `slice` render mode.") + + self._plane.set_value(self, value) + + +class SurfaceGraphic(MeshGraphic): + _features = { + "data": SurfaceData, + "colors": (VertexColors, UniformColor), + "cmap": MeshCmap, + } + + def __init__( + self, + data: np.ndarray, + mode: Literal["basic", "phong", "slice"] = "phong", + colors: str | np.ndarray | Sequence = "w", + mapcoords: Any = None, + cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray = None, + clim: tuple[float, float] | None = None, + **kwargs, + ): + """ + Create a Surface mesh Graphic + + Parameters + ---------- + data: array-like + A height-map (an image where the values indicate height, i.e. z values). + Can also be a [m, n, 3] to explicitly specify the x and y values in addition to the z values. + [m, n, 3] is a dstack of (x, y, z) values that form a grid on the xy plane. + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value (mapped to [0..1] using ``clim``). + If not given, they will be the depth (z-coordinate) of the surface. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + + clim: tuple[float, float] + The colormap limits. If the mapcoords has values between e.g. 5 and 90, you want to set the clim + to e.g. (5, 90) or (0, 100) to determine how the values map onto the colormap. + + **kwargs + passed to :class:`.Graphic` + + """ + + self._data = SurfaceData(data) + + positions, indices = surface_data_to_mesh(data) + + cmap_tex_view = resolve_cmap_mesh(cmap) + if (cmap_tex_view is not None) and (mapcoords is None): + if cmap_tex_view.texture.dim == 1: # 1d + mapcoords = positions[:, 2] + + elif cmap_tex_view.texture.dim == 2: + mapcoords = np.column_stack((positions[:, 0], positions[:, 1])).astype( + np.float32 + ) + + super().__init__( + positions, + indices, + mode=mode, + colors=colors, + mapcoords=mapcoords, + cmap=cmap, + clim=clim, + **kwargs, + ) + + @property + def data(self) -> np.ndarray: + """get or set the surface data""" + return self._data.value + + @data.setter + def data(self, new_data: np.ndarray): + self._data.set_value(self, new_data) + + +class PolygonGraphic(MeshGraphic): + _features = { + "data": SurfaceData, + "colors": (VertexColors, UniformColor), + "cmap": MeshCmap, + } + + def __init__( + self, + data: np.ndarray, + mode: Literal["basic", "phong"] = "basic", + colors: str | np.ndarray | Sequence = "w", + mapcoords: Any = None, + cmap: str | dict | pygfx.Texture | pygfx.TextureMap | np.ndarray = None, + clim: tuple[float, float] | None = None, + **kwargs, + ): + """ + Create a polygon mesh graphic. + + The data are always in the 'xy' plane. Set a rotation to display the polygon in another plane or in 3D space. + + Parameters + ---------- + data: array-like + The polygon vertices, must be of shape: [n_vertices, 2] + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value (mapped to [0..1] using ``clim``). + If not given, they will be the depth (z-coordinate) of the surface. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + + clim: tuple[float, float] + The colormap limits. If the mapcoords has values between e.g. 5 and 90, you want to set the clim + to e.g. (5, 90) or (0, 100) to determine how the values map onto the colormap. + + **kwargs + passed to :class:`.Graphic` + """ + + positions, indices = triangulate_polygon(data) + + self._data = PolygonData(positions) + + super().__init__( + positions, + indices, + mode=mode, + colors=colors, + mapcoords=mapcoords, + cmap=cmap, + clim=clim, + **kwargs, + ) + + @property + def data(self) -> np.ndarray: + """get or set the polygon vertex data""" + return self._data.value + + @data.setter + def data(self, new_data: np.ndarray | Sequence): + self._data.set_value(self, new_data) + + @property + def clim(self) -> tuple[float, float] | None: + """get or set the colormap limits""" + return self._clim + + @clim.setter + def clim(self, new_clim: tuple[float, float]): + if len(new_clim) != 2: + raise ValueError("clim must be a: tuple[float, float]") + + self._clim = tuple(new_clim) + + self.mapcoords = (self.mapcoords - self.clim[0]) / (self.clim[1] - self.clim[0]) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index e7ff99a1d..06a4c7517 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -432,6 +432,144 @@ def add_line_stack( **kwargs, ) + def add_mesh( + self, + positions: Any, + indices: Any, + mode: Literal["basic", "phong", "slice"] = "phong", + plane: tuple[float, float, float, float] = (0.0, 0.0, 1.0, 0.0), + colors: Union[str, numpy.ndarray, Sequence] = "w", + mapcoords: Any = None, + cmap: ( + str + | dict + | pygfx.resources._texture.Texture + | pygfx.resources._texturemap.TextureMap + | numpy.ndarray + ) = None, + clim: tuple[float, float] = None, + isolated_buffer: bool = True, + **kwargs, + ) -> MeshGraphic: + """ + + Create a mesh Graphic. + + Parameters + ---------- + positions: array-like + The 3D positions of the vertices. + + indices: array-like + The indices into the positions that make up the triangles. Each 3 + subsequent indices form a triangle. + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + * slice: display a slice of the mesh at the specified ``plane`` + + plane: (float, float, float, float), default (0., 0., 1., 0.) + Slice mesh at this plane. Sets (a, b, c, d) in the equation the defines a plane: ax + by + cz + d = 0. + Used only if `mode` = "slice". The plane is defined in world space. + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value, mapped to [0..1]. + If ``mapcoords`` and ``cmap`` are given, they are used instead of ``colors``. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + An image can also be used, this is basically a 2D colormap. + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer - useful if the + array is large. In almost all cases this should be ``True``. + + **kwargs + passed to :class:`.Graphic` + + + """ + return self._create_graphic( + MeshGraphic, + positions, + indices, + mode, + plane, + colors, + mapcoords, + cmap, + clim, + isolated_buffer, + **kwargs, + ) + + def add_polygon( + self, + data: numpy.ndarray, + mode: Literal["basic", "phong"] = "basic", + colors: Union[str, numpy.ndarray, Sequence] = "w", + mapcoords: Any = None, + cmap: ( + str + | dict + | pygfx.resources._texture.Texture + | pygfx.resources._texturemap.TextureMap + | numpy.ndarray + ) = None, + clim: tuple[float, float] | None = None, + **kwargs, + ) -> PolygonGraphic: + """ + + Create a polygon mesh graphic. + + The data are always in the 'xy' plane. Set a rotation to display the polygon in another plane or in 3D space. + + Parameters + ---------- + data: array-like + The polygon vertices, must be of shape: [n_vertices, 2] + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value (mapped to [0..1] using ``clim``). + If not given, they will be the depth (z-coordinate) of the surface. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + + clim: tuple[float, float] + The colormap limits. If the mapcoords has values between e.g. 5 and 90, you want to set the clim + to e.g. (5, 90) or (0, 100) to determine how the values map onto the colormap. + + **kwargs + passed to :class:`.Graphic` + + """ + return self._create_graphic( + PolygonGraphic, data, mode, colors, mapcoords, cmap, clim, **kwargs + ) + def add_scatter( self, data: Any, @@ -586,6 +724,64 @@ def add_scatter( **kwargs, ) + def add_surface( + self, + data: numpy.ndarray, + mode: Literal["basic", "phong", "slice"] = "phong", + colors: Union[str, numpy.ndarray, Sequence] = "w", + mapcoords: Any = None, + cmap: ( + str + | dict + | pygfx.resources._texture.Texture + | pygfx.resources._texturemap.TextureMap + | numpy.ndarray + ) = None, + clim: tuple[float, float] | None = None, + **kwargs, + ) -> SurfaceGraphic: + """ + + Create a Surface mesh Graphic + + Parameters + ---------- + data: array-like + A height-map (an image where the values indicate height, i.e. z values). + Can also be a [m, n, 3] to explicitly specify the x and y values in addition to the z values. + [m, n, 3] is a dstack of (x, y, z) values that form a grid on the xy plane. + + mode: one of "basic", "phong", "slice", default "phong" + * basic: illuminate mesh with only ambient lighting + * phong: phong lighting model, good for most use cases, see https://en.wikipedia.org/wiki/Phong_shading + + colors: str, array, or iterable, default "w" + A uniform color, or the per-position colors. + + mapcoords: array-like + The per-position coordinates to which to apply the colormap (a.k.a. texcoords). + These can e.g. be some domain-specific value (mapped to [0..1] using ``clim``). + If not given, they will be the depth (z-coordinate) of the surface. + + cmap: str, optional + Apply a colormap to the mesh, this overrides any argument passed to + "colors". For supported colormaps see the ``cmap`` library + catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + Both 1D and 2D colormaps are supported, though the mapcoords has to match the dimensionality. + + clim: tuple[float, float] + The colormap limits. If the mapcoords has values between e.g. 5 and 90, you want to set the clim + to e.g. (5, 90) or (0, 100) to determine how the values map onto the colormap. + + **kwargs + passed to :class:`.Graphic` + + + """ + return self._create_graphic( + SurfaceGraphic, data, mode, colors, mapcoords, cmap, clim, **kwargs + ) + def add_text( self, text: str, diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 606f83909..01721780c 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -117,6 +117,12 @@ def __init__( self._background = pygfx.Background(None, self._background_material) self.scene.add(self._background) + self._ambient_light = pygfx.AmbientLight() + self._directional_light = pygfx.DirectionalLight() + + self.scene.add(self._ambient_light) + self.scene.add(self._camera.add(self._directional_light)) + def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" if obj is None: @@ -166,6 +172,8 @@ def camera(self, new_camera: str | pygfx.PerspectiveCamera): # user wants to set completely new camera, remove current camera from controller if isinstance(new_camera, pygfx.PerspectiveCamera): self.controller.remove_camera(self._camera) + # add directional light to new camera + new_camera.add(self._directional_light) # add new camera to controller self.controller.add_camera(new_camera) @@ -274,6 +282,16 @@ def background_color(self, colors: str | tuple[float]): """1, 2, or 4 colors, each color must be acceptable by pygfx.Color""" self._background_material.set_colors(*colors) + @property + def ambient_light(self) -> pygfx.AmbientLight: + """the ambient lighting in the scene""" + return self._ambient_light + + @property + def directional_light(self) -> pygfx.DirectionalLight: + """the directional lighting on the camera in the scene""" + return self._directional_light + @property def animations(self) -> dict[str, list[callable]]: """Returns a dictionary of 'pre' and 'post' animation functions.""" From 68dceee6d63334d098c0100854e0fbc9ab4cc3c6 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 16 Dec 2025 22:15:35 -0500 Subject: [PATCH 33/52] remove dim length checks in `iw.set_data()` (#965) * remove dim length checks in iw.set_data() * black --- fastplotlib/widgets/image_widget/_widget.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fastplotlib/widgets/image_widget/_widget.py b/fastplotlib/widgets/image_widget/_widget.py index 715fe3489..86a01b083 100644 --- a/fastplotlib/widgets/image_widget/_widget.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -966,10 +966,6 @@ def set_data( ] if max_lengths[scroll_dim] == np.inf: max_lengths[scroll_dim] = new_length - elif max_lengths[scroll_dim] != new_length: - raise ValueError( - f"New arrays have differing values along dim {scroll_dim}" - ) self._dims_max_bounds[scroll_dim] = max_lengths[scroll_dim] From 359770a4c79d680ea486a2433612f02b8b3b7b85 Mon Sep 17 00:00:00 2001 From: Flynn <75346097+FlynnOConnell@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:05:08 -0500 Subject: [PATCH 34/52] center links and badges (#969) --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 227d5bfb8..41c1ba72e 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,20 @@ --- -[![CI](https://github.com/fastplotlib/fastplotlib/actions/workflows/ci.yml/badge.svg)](https://github.com/fastplotlib/fastplotlib/actions/workflows/ci.yml) -[![PyPI version](https://badge.fury.io/py/fastplotlib.svg)](https://badge.fury.io/py/fastplotlib) -[![Deploy docs](https://github.com/fastplotlib/fastplotlib/actions/workflows/docs-deploy.yml/badge.svg)](https://fastplotlib.org/ver/dev/) -[![DOI](https://zenodo.org/badge/485481453.svg)](https://zenodo.org/doi/10.5281/zenodo.13365890) - -[**Installation**](https://github.com/fastplotlib/fastplotlib#installation) | -[**GPU Drivers**](https://github.com/kushalkolar/fastplotlib#graphics-drivers) | -[**Documentation**](https://github.com/fastplotlib/fastplotlib#documentation) | -[**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | -[**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) +

+ CI + PyPI version + Deploy docs + DOI +

+ +

+ Installation | + GPU Drivers | + Documentation | + Examples | + Contributing +

Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that utilizes [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! `fastplotlib` is an expressive plotting library that enables rapid prototyping for large scale exploratory scientific visualization. From 7075e46f79d23da43b2ca4658c15b5c8380c63d6 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 14 Jan 2026 12:18:46 -0500 Subject: [PATCH 35/52] cursor tool and better tooltips (#947) * new cursor tool, basics work * lint and update api docs * add custom tooltip to figure * example WIP * manual picking, world obj -> graphic mapping * fix * fix gc * stuff * cursor and tooltips refactor, basically works * much simpler tooltip and cursor * move TextBox and Tooltip, update API docs * cursor manages tooltips * update examples * black * cleanup * docstrings * update example * update example * add image space transforms examples * finish space transforms examples * typing * black * docstrings * update examples * textbox constructor takes args, docstrings * True -> true * update names * update cursor examples * new ground truth screenshots for transforms * black * update * revert persist kwarg in PlotArea animations stuff * fix * message for image volumes * Update fastplotlib/graphics/_base.py Co-authored-by: Kushal Kolar --------- Co-authored-by: clewis7 --- docs/source/api/graphic_features/Scale.rst | 35 ++ docs/source/api/graphic_features/index.rst | 1 + docs/source/api/graphics/Graphic.rst | 5 + docs/source/api/graphics/ImageGraphic.rst | 5 + .../api/graphics/ImageVolumeGraphic.rst | 5 + docs/source/api/graphics/LineCollection.rst | 5 + docs/source/api/graphics/LineGraphic.rst | 5 + docs/source/api/graphics/LineStack.rst | 5 + docs/source/api/graphics/MeshGraphic.rst | 5 + docs/source/api/graphics/PolygonGraphic.rst | 5 + docs/source/api/graphics/ScatterGraphic.rst | 5 + docs/source/api/graphics/SurfaceGraphic.rst | 5 + docs/source/api/graphics/TextGraphic.rst | 5 + docs/source/api/graphics/VectorsGraphic.rst | 5 + docs/source/api/layouts/figure.rst | 2 - docs/source/api/layouts/imgui_figure.rst | 2 - docs/source/api/layouts/subplot.rst | 3 + .../api/selectors/LinearRegionSelector.rst | 5 + docs/source/api/selectors/LinearSelector.rst | 5 + .../api/selectors/RectangleSelector.rst | 5 + docs/source/api/tools/Cursor.rst | 42 ++ docs/source/api/tools/HistogramLUTTool.rst | 5 + docs/source/api/tools/TextBox.rst | 38 ++ docs/source/api/tools/Tooltip.rst | 10 +- docs/source/api/tools/index.rst | 2 + docs/source/conf.py | 1 + docs/source/user_guide/event_tables.rst | 154 +++++++ docs/source/user_guide/guide.rst | 22 +- examples/line_collection/line_collection.py | 2 +- examples/line_collection/line_stack.py | 20 - examples/mesh/surface_ripple.py | 3 + examples/misc/cursor_transform.py | 54 +++ examples/misc/cursors.py | 48 ++ examples/misc/cursors_marker.py | 47 ++ examples/misc/tooltips.py | 54 --- examples/misc/tooltips_custom.py | 14 +- .../screenshots/no-imgui-rotation_image.png | 3 + .../screenshots/no-imgui-rotation_line.png | 3 + .../screenshots/no-imgui-scaling_image.png | 3 + .../screenshots/no-imgui-scaling_line.png | 3 + .../screenshots/no-imgui-translate_image.png | 3 + .../screenshots/no-imgui-translate_line.png | 3 + .../no-imgui-translation_scaling_image.png | 3 + .../no-imgui-translation_scaling_line.png | 3 + ...gui-translation_scaling_rotation_image.png | 3 + ...mgui-translation_scaling_rotation_line.png | 3 + examples/screenshots/rotation_image.png | 3 + examples/screenshots/rotation_line.png | 3 + examples/screenshots/scaling_image.png | 3 + examples/screenshots/scaling_line.png | 3 + examples/screenshots/translate_image.png | 3 + examples/screenshots/translate_line.png | 3 + .../screenshots/translation_scaling_image.png | 3 + .../screenshots/translation_scaling_line.png | 3 + .../translation_scaling_rotation_image.png | 3 + .../translation_scaling_rotation_line.png | 3 + examples/spaces_transforms/README.rst | 2 + examples/spaces_transforms/rotation_image.py | 94 ++++ examples/spaces_transforms/rotation_line.py | 89 ++++ examples/spaces_transforms/scaling_image.py | 94 ++++ examples/spaces_transforms/scaling_line.py | 89 ++++ examples/spaces_transforms/translate_image.py | 95 ++++ examples/spaces_transforms/translate_line.py | 90 ++++ .../translation_scaling_image.py | 99 +++++ .../translation_scaling_line.py | 94 ++++ .../translation_scaling_rotation_image.py | 102 +++++ .../translation_scaling_rotation_line.py | 99 +++++ examples/tests/testutils.py | 1 + fastplotlib/graphics/_base.py | 150 ++++++- fastplotlib/graphics/_collection_base.py | 13 +- fastplotlib/graphics/_positions_base.py | 8 + fastplotlib/graphics/_vectors.py | 12 +- fastplotlib/graphics/features/__init__.py | 3 +- fastplotlib/graphics/features/_common.py | 49 ++ fastplotlib/graphics/image.py | 22 + fastplotlib/graphics/image_volume.py | 15 + fastplotlib/graphics/mesh.py | 21 + .../graphics/selectors/_base_selector.py | 2 + fastplotlib/graphics/text.py | 2 + fastplotlib/layouts/_figure.py | 83 ++-- fastplotlib/layouts/_imgui_figure.py | 2 - fastplotlib/layouts/_plot_area.py | 139 +++++- fastplotlib/tools/__init__.py | 5 +- fastplotlib/tools/_cursor.py | 420 ++++++++++++++++++ fastplotlib/tools/_histogram_lut.py | 2 + .../tools/{_tooltip.py => _textbox.py} | 223 +++++----- 86 files changed, 2514 insertions(+), 299 deletions(-) create mode 100644 docs/source/api/graphic_features/Scale.rst create mode 100644 docs/source/api/tools/Cursor.rst create mode 100644 docs/source/api/tools/TextBox.rst create mode 100644 examples/misc/cursor_transform.py create mode 100644 examples/misc/cursors.py create mode 100644 examples/misc/cursors_marker.py delete mode 100644 examples/misc/tooltips.py create mode 100644 examples/screenshots/no-imgui-rotation_image.png create mode 100644 examples/screenshots/no-imgui-rotation_line.png create mode 100644 examples/screenshots/no-imgui-scaling_image.png create mode 100644 examples/screenshots/no-imgui-scaling_line.png create mode 100644 examples/screenshots/no-imgui-translate_image.png create mode 100644 examples/screenshots/no-imgui-translate_line.png create mode 100644 examples/screenshots/no-imgui-translation_scaling_image.png create mode 100644 examples/screenshots/no-imgui-translation_scaling_line.png create mode 100644 examples/screenshots/no-imgui-translation_scaling_rotation_image.png create mode 100644 examples/screenshots/no-imgui-translation_scaling_rotation_line.png create mode 100644 examples/screenshots/rotation_image.png create mode 100644 examples/screenshots/rotation_line.png create mode 100644 examples/screenshots/scaling_image.png create mode 100644 examples/screenshots/scaling_line.png create mode 100644 examples/screenshots/translate_image.png create mode 100644 examples/screenshots/translate_line.png create mode 100644 examples/screenshots/translation_scaling_image.png create mode 100644 examples/screenshots/translation_scaling_line.png create mode 100644 examples/screenshots/translation_scaling_rotation_image.png create mode 100644 examples/screenshots/translation_scaling_rotation_line.png create mode 100644 examples/spaces_transforms/README.rst create mode 100644 examples/spaces_transforms/rotation_image.py create mode 100644 examples/spaces_transforms/rotation_line.py create mode 100644 examples/spaces_transforms/scaling_image.py create mode 100644 examples/spaces_transforms/scaling_line.py create mode 100644 examples/spaces_transforms/translate_image.py create mode 100644 examples/spaces_transforms/translate_line.py create mode 100644 examples/spaces_transforms/translation_scaling_image.py create mode 100644 examples/spaces_transforms/translation_scaling_line.py create mode 100644 examples/spaces_transforms/translation_scaling_rotation_image.py create mode 100644 examples/spaces_transforms/translation_scaling_rotation_line.py create mode 100644 fastplotlib/tools/_cursor.py rename fastplotlib/tools/{_tooltip.py => _textbox.py} (55%) diff --git a/docs/source/api/graphic_features/Scale.rst b/docs/source/api/graphic_features/Scale.rst new file mode 100644 index 000000000..b0ef07a79 --- /dev/null +++ b/docs/source/api/graphic_features/Scale.rst @@ -0,0 +1,35 @@ +.. _api.Scale: + +Scale +***** + +===== +Scale +===== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Scale_api + + Scale.add_event_handler + Scale.block_events + Scale.clear_event_handlers + Scale.remove_event_handler + Scale.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index cd11544be..71268ddab 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -48,6 +48,7 @@ Graphic Features Name Offset Rotation + Scale Alpha AlphaMode Visible diff --git a/docs/source/api/graphics/Graphic.rst b/docs/source/api/graphics/Graphic.rst index da6424e3e..f94892949 100644 --- a/docs/source/api/graphics/Graphic.rst +++ b/docs/source/api/graphics/Graphic.rst @@ -30,7 +30,9 @@ Properties Graphic.offset Graphic.right_click_menu Graphic.rotation + Graphic.scale Graphic.supported_events + Graphic.tooltip_format Graphic.visible Graphic.world_object @@ -42,6 +44,9 @@ Methods Graphic.add_axes Graphic.add_event_handler Graphic.clear_event_handlers + Graphic.format_pick_info + Graphic.map_model_to_world + Graphic.map_world_to_model Graphic.remove_event_handler Graphic.rotate diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 457ba27ee..e6d02c54b 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -34,7 +34,9 @@ Properties ImageGraphic.offset ImageGraphic.right_click_menu ImageGraphic.rotation + ImageGraphic.scale ImageGraphic.supported_events + ImageGraphic.tooltip_format ImageGraphic.visible ImageGraphic.vmax ImageGraphic.vmin @@ -52,6 +54,9 @@ Methods ImageGraphic.add_polygon_selector ImageGraphic.add_rectangle_selector ImageGraphic.clear_event_handlers + ImageGraphic.format_pick_info + ImageGraphic.map_model_to_world + ImageGraphic.map_world_to_model ImageGraphic.remove_event_handler ImageGraphic.reset_vmin_vmax ImageGraphic.rotate diff --git a/docs/source/api/graphics/ImageVolumeGraphic.rst b/docs/source/api/graphics/ImageVolumeGraphic.rst index 8adbc7ac7..8031f12f1 100644 --- a/docs/source/api/graphics/ImageVolumeGraphic.rst +++ b/docs/source/api/graphics/ImageVolumeGraphic.rst @@ -37,11 +37,13 @@ Properties ImageVolumeGraphic.plane ImageVolumeGraphic.right_click_menu ImageVolumeGraphic.rotation + ImageVolumeGraphic.scale ImageVolumeGraphic.shininess ImageVolumeGraphic.step_size ImageVolumeGraphic.substep_size ImageVolumeGraphic.supported_events ImageVolumeGraphic.threshold + ImageVolumeGraphic.tooltip_format ImageVolumeGraphic.visible ImageVolumeGraphic.vmax ImageVolumeGraphic.vmin @@ -55,6 +57,9 @@ Methods ImageVolumeGraphic.add_axes ImageVolumeGraphic.add_event_handler ImageVolumeGraphic.clear_event_handlers + ImageVolumeGraphic.format_pick_info + ImageVolumeGraphic.map_model_to_world + ImageVolumeGraphic.map_world_to_model ImageVolumeGraphic.remove_event_handler ImageVolumeGraphic.reset_vmin_vmax ImageVolumeGraphic.rotate diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index ffbb52f2b..5d0603ab7 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -38,8 +38,10 @@ Properties LineCollection.right_click_menu LineCollection.rotation LineCollection.rotations + LineCollection.scale LineCollection.supported_events LineCollection.thickness + LineCollection.tooltip_format LineCollection.visible LineCollection.visibles LineCollection.world_object @@ -57,6 +59,9 @@ Methods LineCollection.add_polygon_selector LineCollection.add_rectangle_selector LineCollection.clear_event_handlers + LineCollection.format_pick_info + LineCollection.map_model_to_world + LineCollection.map_world_to_model LineCollection.remove_event_handler LineCollection.remove_graphic LineCollection.rotate diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index ddcb00c41..428e8ef56 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -33,9 +33,11 @@ Properties LineGraphic.offset LineGraphic.right_click_menu LineGraphic.rotation + LineGraphic.scale LineGraphic.size_space LineGraphic.supported_events LineGraphic.thickness + LineGraphic.tooltip_format LineGraphic.visible LineGraphic.world_object @@ -51,6 +53,9 @@ Methods LineGraphic.add_polygon_selector LineGraphic.add_rectangle_selector LineGraphic.clear_event_handlers + LineGraphic.format_pick_info + LineGraphic.map_model_to_world + LineGraphic.map_world_to_model LineGraphic.remove_event_handler LineGraphic.rotate diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 4373454be..e7ac21343 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -38,8 +38,10 @@ Properties LineStack.right_click_menu LineStack.rotation LineStack.rotations + LineStack.scale LineStack.supported_events LineStack.thickness + LineStack.tooltip_format LineStack.visible LineStack.visibles LineStack.world_object @@ -57,6 +59,9 @@ Methods LineStack.add_polygon_selector LineStack.add_rectangle_selector LineStack.clear_event_handlers + LineStack.format_pick_info + LineStack.map_model_to_world + LineStack.map_world_to_model LineStack.remove_event_handler LineStack.remove_graphic LineStack.rotate diff --git a/docs/source/api/graphics/MeshGraphic.rst b/docs/source/api/graphics/MeshGraphic.rst index 5e2c5dac5..ec27f1e4e 100644 --- a/docs/source/api/graphics/MeshGraphic.rst +++ b/docs/source/api/graphics/MeshGraphic.rst @@ -38,7 +38,9 @@ Properties MeshGraphic.positions MeshGraphic.right_click_menu MeshGraphic.rotation + MeshGraphic.scale MeshGraphic.supported_events + MeshGraphic.tooltip_format MeshGraphic.visible MeshGraphic.world_object @@ -50,6 +52,9 @@ Methods MeshGraphic.add_axes MeshGraphic.add_event_handler MeshGraphic.clear_event_handlers + MeshGraphic.format_pick_info + MeshGraphic.map_model_to_world + MeshGraphic.map_world_to_model MeshGraphic.remove_event_handler MeshGraphic.rotate diff --git a/docs/source/api/graphics/PolygonGraphic.rst b/docs/source/api/graphics/PolygonGraphic.rst index f9446f425..94c75f999 100644 --- a/docs/source/api/graphics/PolygonGraphic.rst +++ b/docs/source/api/graphics/PolygonGraphic.rst @@ -39,7 +39,9 @@ Properties PolygonGraphic.positions PolygonGraphic.right_click_menu PolygonGraphic.rotation + PolygonGraphic.scale PolygonGraphic.supported_events + PolygonGraphic.tooltip_format PolygonGraphic.visible PolygonGraphic.world_object @@ -51,6 +53,9 @@ Methods PolygonGraphic.add_axes PolygonGraphic.add_event_handler PolygonGraphic.clear_event_handlers + PolygonGraphic.format_pick_info + PolygonGraphic.map_model_to_world + PolygonGraphic.map_world_to_model PolygonGraphic.remove_event_handler PolygonGraphic.rotate diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 7f4336abe..cf8e1224d 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -40,9 +40,11 @@ Properties ScatterGraphic.point_rotations ScatterGraphic.right_click_menu ScatterGraphic.rotation + ScatterGraphic.scale ScatterGraphic.size_space ScatterGraphic.sizes ScatterGraphic.supported_events + ScatterGraphic.tooltip_format ScatterGraphic.visible ScatterGraphic.world_object @@ -54,6 +56,9 @@ Methods ScatterGraphic.add_axes ScatterGraphic.add_event_handler ScatterGraphic.clear_event_handlers + ScatterGraphic.format_pick_info + ScatterGraphic.map_model_to_world + ScatterGraphic.map_world_to_model ScatterGraphic.remove_event_handler ScatterGraphic.rotate diff --git a/docs/source/api/graphics/SurfaceGraphic.rst b/docs/source/api/graphics/SurfaceGraphic.rst index 385ce2432..228dbede1 100644 --- a/docs/source/api/graphics/SurfaceGraphic.rst +++ b/docs/source/api/graphics/SurfaceGraphic.rst @@ -39,7 +39,9 @@ Properties SurfaceGraphic.positions SurfaceGraphic.right_click_menu SurfaceGraphic.rotation + SurfaceGraphic.scale SurfaceGraphic.supported_events + SurfaceGraphic.tooltip_format SurfaceGraphic.visible SurfaceGraphic.world_object @@ -51,6 +53,9 @@ Methods SurfaceGraphic.add_axes SurfaceGraphic.add_event_handler SurfaceGraphic.clear_event_handlers + SurfaceGraphic.format_pick_info + SurfaceGraphic.map_model_to_world + SurfaceGraphic.map_world_to_model SurfaceGraphic.remove_event_handler SurfaceGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 0de52942b..da4909686 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -34,8 +34,10 @@ Properties TextGraphic.outline_thickness TextGraphic.right_click_menu TextGraphic.rotation + TextGraphic.scale TextGraphic.supported_events TextGraphic.text + TextGraphic.tooltip_format TextGraphic.visible TextGraphic.world_object @@ -47,6 +49,9 @@ Methods TextGraphic.add_axes TextGraphic.add_event_handler TextGraphic.clear_event_handlers + TextGraphic.format_pick_info + TextGraphic.map_model_to_world + TextGraphic.map_world_to_model TextGraphic.remove_event_handler TextGraphic.rotate diff --git a/docs/source/api/graphics/VectorsGraphic.rst b/docs/source/api/graphics/VectorsGraphic.rst index 4a629f5db..ec7d891c0 100644 --- a/docs/source/api/graphics/VectorsGraphic.rst +++ b/docs/source/api/graphics/VectorsGraphic.rst @@ -32,7 +32,9 @@ Properties VectorsGraphic.positions VectorsGraphic.right_click_menu VectorsGraphic.rotation + VectorsGraphic.scale VectorsGraphic.supported_events + VectorsGraphic.tooltip_format VectorsGraphic.visible VectorsGraphic.world_object @@ -44,6 +46,9 @@ Methods VectorsGraphic.add_axes VectorsGraphic.add_event_handler VectorsGraphic.clear_event_handlers + VectorsGraphic.format_pick_info + VectorsGraphic.map_model_to_world + VectorsGraphic.map_world_to_model VectorsGraphic.remove_event_handler VectorsGraphic.rotate diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index e306710be..54e91b24f 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -28,8 +28,6 @@ Properties Figure.names Figure.renderer Figure.shape - Figure.show_tooltips - Figure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index 959a98743..46e0c6ed3 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -31,8 +31,6 @@ Properties ImguiFigure.names ImguiFigure.renderer ImguiFigure.shape - ImguiFigure.show_tooltips - ImguiFigure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 93db00a2e..0916859b9 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -40,6 +40,7 @@ Properties Subplot.selectors Subplot.title Subplot.toolbar + Subplot.tooltip Subplot.viewport Methods @@ -67,8 +68,10 @@ Methods Subplot.clear_animations Subplot.delete_graphic Subplot.get_figure + Subplot.get_pick_info Subplot.insert_graphic Subplot.map_screen_to_world + Subplot.map_world_to_screen Subplot.remove_animation Subplot.remove_graphic diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 35b5ae1f4..eb48497cd 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -35,8 +35,10 @@ Properties LinearRegionSelector.parent LinearRegionSelector.right_click_menu LinearRegionSelector.rotation + LinearRegionSelector.scale LinearRegionSelector.selection LinearRegionSelector.supported_events + LinearRegionSelector.tooltip_format LinearRegionSelector.vertex_color LinearRegionSelector.visible LinearRegionSelector.world_object @@ -49,9 +51,12 @@ Methods LinearRegionSelector.add_axes LinearRegionSelector.add_event_handler LinearRegionSelector.clear_event_handlers + LinearRegionSelector.format_pick_info LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices + LinearRegionSelector.map_model_to_world + LinearRegionSelector.map_world_to_model LinearRegionSelector.remove_event_handler LinearRegionSelector.rotate diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 9cbe6fb26..2aa334748 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -35,8 +35,10 @@ Properties LinearSelector.parent LinearSelector.right_click_menu LinearSelector.rotation + LinearSelector.scale LinearSelector.selection LinearSelector.supported_events + LinearSelector.tooltip_format LinearSelector.vertex_color LinearSelector.visible LinearSelector.world_object @@ -49,9 +51,12 @@ Methods LinearSelector.add_axes LinearSelector.add_event_handler LinearSelector.clear_event_handlers + LinearSelector.format_pick_info LinearSelector.get_selected_data LinearSelector.get_selected_index LinearSelector.get_selected_indices + LinearSelector.map_model_to_world + LinearSelector.map_world_to_model LinearSelector.remove_event_handler LinearSelector.rotate diff --git a/docs/source/api/selectors/RectangleSelector.rst b/docs/source/api/selectors/RectangleSelector.rst index dc9727069..51f6801a4 100644 --- a/docs/source/api/selectors/RectangleSelector.rst +++ b/docs/source/api/selectors/RectangleSelector.rst @@ -35,8 +35,10 @@ Properties RectangleSelector.parent RectangleSelector.right_click_menu RectangleSelector.rotation + RectangleSelector.scale RectangleSelector.selection RectangleSelector.supported_events + RectangleSelector.tooltip_format RectangleSelector.vertex_color RectangleSelector.visible RectangleSelector.world_object @@ -49,9 +51,12 @@ Methods RectangleSelector.add_axes RectangleSelector.add_event_handler RectangleSelector.clear_event_handlers + RectangleSelector.format_pick_info RectangleSelector.get_selected_data RectangleSelector.get_selected_index RectangleSelector.get_selected_indices + RectangleSelector.map_model_to_world + RectangleSelector.map_world_to_model RectangleSelector.remove_event_handler RectangleSelector.rotate diff --git a/docs/source/api/tools/Cursor.rst b/docs/source/api/tools/Cursor.rst new file mode 100644 index 000000000..37a706d34 --- /dev/null +++ b/docs/source/api/tools/Cursor.rst @@ -0,0 +1,42 @@ +.. _api.Cursor: + +Cursor +****** + +====== +Cursor +====== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor.alpha + Cursor.color + Cursor.edge_color + Cursor.edge_width + Cursor.enabled + Cursor.marker + Cursor.mode + Cursor.position + Cursor.size + Cursor.size_space + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Cursor_api + + Cursor.add_subplot + Cursor.clear + Cursor.remove_subplot + diff --git a/docs/source/api/tools/HistogramLUTTool.rst b/docs/source/api/tools/HistogramLUTTool.rst index 429f958e2..b3498dd68 100644 --- a/docs/source/api/tools/HistogramLUTTool.rst +++ b/docs/source/api/tools/HistogramLUTTool.rst @@ -32,7 +32,9 @@ Properties HistogramLUTTool.offset HistogramLUTTool.right_click_menu HistogramLUTTool.rotation + HistogramLUTTool.scale HistogramLUTTool.supported_events + HistogramLUTTool.tooltip_format HistogramLUTTool.visible HistogramLUTTool.vmax HistogramLUTTool.vmin @@ -46,6 +48,9 @@ Methods HistogramLUTTool.add_axes HistogramLUTTool.add_event_handler HistogramLUTTool.clear_event_handlers + HistogramLUTTool.format_pick_info + HistogramLUTTool.map_model_to_world + HistogramLUTTool.map_world_to_model HistogramLUTTool.remove_event_handler HistogramLUTTool.rotate HistogramLUTTool.set_data diff --git a/docs/source/api/tools/TextBox.rst b/docs/source/api/tools/TextBox.rst new file mode 100644 index 000000000..b202f4270 --- /dev/null +++ b/docs/source/api/tools/TextBox.rst @@ -0,0 +1,38 @@ +.. _api.TextBox: + +TextBox +******* + +======= +TextBox +======= +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox.background_color + TextBox.font_size + TextBox.outline_color + TextBox.padding + TextBox.position + TextBox.text_color + TextBox.visible + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextBox_api + + TextBox.clear + TextBox.display + diff --git a/docs/source/api/tools/Tooltip.rst b/docs/source/api/tools/Tooltip.rst index 71607bf20..8e017370e 100644 --- a/docs/source/api/tools/Tooltip.rst +++ b/docs/source/api/tools/Tooltip.rst @@ -21,18 +21,20 @@ Properties :toctree: Tooltip_api Tooltip.background_color + Tooltip.continuous_update + Tooltip.enabled Tooltip.font_size Tooltip.outline_color Tooltip.padding + Tooltip.position Tooltip.text_color - Tooltip.world_object + Tooltip.visible Methods ~~~~~~~ .. autosummary:: :toctree: Tooltip_api - Tooltip.register - Tooltip.unregister - Tooltip.unregister_all + Tooltip.clear + Tooltip.display diff --git a/docs/source/api/tools/index.rst b/docs/source/api/tools/index.rst index c2666ed28..2bff8fb50 100644 --- a/docs/source/api/tools/index.rst +++ b/docs/source/api/tools/index.rst @@ -5,4 +5,6 @@ Tools :maxdepth: 1 HistogramLUTTool + TextBox Tooltip + Cursor diff --git a/docs/source/conf.py b/docs/source/conf.py index 8547e9ae7..edc172dad 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,6 +68,7 @@ "../../examples/text", "../../examples/events", "../../examples/selection_tools", + "../../examples/spaces_transforms", "../../examples/machine_learning", "../../examples/guis", "../../examples/ipywidgets", diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst index ba53c3411..42f168bea 100644 --- a/docs/source/user_guide/event_tables.rst +++ b/docs/source/user_guide/event_tables.rst @@ -113,6 +113,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -378,6 +389,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -526,6 +548,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -751,6 +784,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -853,6 +897,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -996,6 +1051,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1124,6 +1190,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1252,6 +1329,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1387,6 +1475,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1541,6 +1640,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1695,6 +1805,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1794,6 +1915,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1895,6 +2027,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ @@ -1996,6 +2139,17 @@ rotation | value | np.ndarray[float, float, float, float] | new rotation quaternion | +----------+----------------------------------------+-------------------------+ +scale +^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------+ +| dict key | type | description | ++==========+========================================+=============+ +| value | np.ndarray[float, float, float, float] | new scale | ++----------+----------------------------------------+-------------+ + alpha ^^^^^ diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index 8bf255507..bd0352aa7 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -648,23 +648,29 @@ There are several spaces to consider when using ``fastplotlib``: World space is the 3D space in which graphical objects live. Objects and the camera can exist anywhere in this space. -2) Data Space +2) Model or Data Space - Data space is simply the world space plus any offset or rotation that has been applied to an object. + Model/Data space is simply the world space plus any offset, scaling and rotation that has been applied to an object. .. note:: - World space does not always correspond directly to data space, you may have to adjust for any offset or rotation of the ``Graphic``. + World space does not always correspond directly to data space, + you may have to adjust for any offset, rotation, and scaling of the ``Graphic``. See below. 3) Screen Space Screen space is the 2D space in which your screen pixels reside. This space is constrained by the screen width and height in pixels. In the rendering process, the camera is responsible for projecting the world space into screen space. -.. note:: - When interacting with ``Graphic`` objects, there is a very helpful function for mapping screen space to world space - (``Figure.map_screen_to_world(pos=(x, y))``). This can be particularly useful when working with click events where click - positions are returned in screen space but ``Graphic`` objects that you may want to interact with exist in world - space. +When interacting with ``Graphic`` objects, there are helpful functions for mapping between these spaces: + - ``Subplot.map_screen_to_world((x, y))`` + - ``Subplot.map_world_to_screen((x, y, z))`` + - ``Graphic.map_model_to_world((x, y, z))`` + - ``Graphic.map_world_to_model((x, y, z))`` + +This can be particularly useful when working with click events where click positions are returned in screen space but + ``Graphic`` objects that you may want to interact with exist in world space. It can also be useful for determining + the screen/canvas pixel position of a datapoint on a graphic by mapping: model -> world -> screen. The entire inverse + transform can also be performed, screen -> world -> model. For more information on the various spaces used by rendering engines please see this `article `_ diff --git a/examples/line_collection/line_collection.py b/examples/line_collection/line_collection.py index 2ddfbe2ed..e3eea7392 100644 --- a/examples/line_collection/line_collection.py +++ b/examples/line_collection/line_collection.py @@ -29,7 +29,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) -figure = fpl.Figure(size=(700, 560), show_tooltips=True) +figure = fpl.Figure(size=(700, 560)) figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5) diff --git a/examples/line_collection/line_stack.py b/examples/line_collection/line_stack.py index 829708cb7..4376c18b4 100644 --- a/examples/line_collection/line_stack.py +++ b/examples/line_collection/line_stack.py @@ -21,7 +21,6 @@ figure = fpl.Figure( size=(700, 560), - show_tooltips=True ) line_stack = figure[0, 0].add_line_stack( @@ -32,25 +31,6 @@ ) -def tooltip_info(ev): - """A custom function to display the index of the graphic within the collection.""" - index = ev.pick_info["vertex_index"] # index of the line datapoint being hovered - - # get index of the hovered line within the line stack - line_index = np.where(line_stack.graphics == ev.graphic)[0].item() - info = f"line index: {line_index}\n" - - # append data value info - info += "\n".join(f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index])) - - # return str to display in tooltip - return info - -# register the line stack with the custom tooltip function -figure.tooltip_manager.register( - line_stack, custom_info=tooltip_info -) - figure.show(maintain_aspect=False) diff --git a/examples/mesh/surface_ripple.py b/examples/mesh/surface_ripple.py index ac556bd1b..1adf676ea 100644 --- a/examples/mesh/surface_ripple.py +++ b/examples/mesh/surface_ripple.py @@ -34,6 +34,9 @@ def create_ripple(shape=(100, 100), phase=0.0, freq=np.pi / 4, ampl=1.0): z, mode="basic", cmap="viridis", clim=(-max_z, max_z) ) +# enable continuous updates for the tooltip +figure[0, 0].tooltip.continuous_update = True + figure[0, 0].camera.show_object(surface.world_object, (-1, 3, -1), up=(0, 0, 1)) figure.show() diff --git a/examples/misc/cursor_transform.py b/examples/misc/cursor_transform.py new file mode 100644 index 000000000..46478d8ce --- /dev/null +++ b/examples/misc/cursor_transform.py @@ -0,0 +1,54 @@ +""" +Cursor transform +================ + +Create a cursor and add them to subplots with a transform function. A common usecase is image registration. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get an image +img1 = iio.imread("imageio:camera.png") + +# create another image, but it is offset +img2 = np.zeros(img1.shape) +img2[50:, 20:] = img1[:-50, :-20] + +figure = fpl.Figure((1, 2), size=(700, 450)) + +# add images +figure[0, 0].add_image(img1) +figure[0, 1].add_image(img2) + +# create cursor +cursor = fpl.Cursor("crosshair") + +# add first subplot to cursor +cursor.add_subplot(figure[0, 0]) + +# a transform function for subplot 2 to indicate that the data is shifted +def transform_func(pos): + return (pos[0] + 20, pos[1] + 50) + +# add second subplot with a transform +cursor.add_subplot(figure[0, 1], transform=transform_func) + +figure.show() + +# you can programmatically set cursor position +cursor.position = (400, 120) + +# you can hide the canvas cursor, this is different and has nothing to do with the fastplotlib Cursor! +figure.canvas.set_cursor("none") + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/cursors.py b/examples/misc/cursors.py new file mode 100644 index 000000000..030c254a4 --- /dev/null +++ b/examples/misc/cursors.py @@ -0,0 +1,48 @@ +""" +Cursor tool +=========== + +Example with multiple subplots and an interactive cursor that marks the same position in each subplot. +Default crosshair mode. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get some data +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:wikkie.png") +scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2) +line_data = np.random.rand(100, 2) * 512 + +# create a figure +figure = fpl.Figure(shape=(2, 2), size=(700, 750)) + +# plot data +figure[0, 0].add_image(img1, cmap="viridis") +figure[0, 1].add_image(img2) +figure[1, 0].add_scatter(scatter_data, sizes=5, colors="r") +figure[1, 1].add_line(line_data, colors="r") + +# creator a cursor in crosshair mode +cursor = fpl.Cursor(color="w") + +# add all subplots to the cursor +for subplot in figure: + cursor.add_subplot(subplot) + +# you can also set the cursor position programmatically +cursor.position = (256, 256) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/cursors_marker.py b/examples/misc/cursors_marker.py new file mode 100644 index 000000000..1b5437fe4 --- /dev/null +++ b/examples/misc/cursors_marker.py @@ -0,0 +1,47 @@ +""" +Cursor tool, marker mode +======================== + +Example with multiple subplots and an interactive cursor that marks the same position in each subplot. Marker mode. +""" + +# test_example = False +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + + +# get some data +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:wikkie.png") +scatter_data = np.random.normal(loc=256, scale=(50), size=(500)).reshape(250, 2) +line_data = np.random.rand(100, 2) * 512 + +# create a figure +figure = fpl.Figure(shape=(2, 2), size=(700, 750)) + +# plot data +figure[0, 0].add_image(img1, cmap="viridis") +figure[0, 1].add_image(img2) +figure[1, 0].add_scatter(scatter_data, sizes=5, colors="r") +figure[1, 1].add_line(line_data, colors="r") + +# creator a cursor in crosshair mode +cursor = fpl.Cursor(mode="marker", color="w", size=15) + +# add all subplots to the cursor +for subplot in figure: + cursor.add_subplot(subplot) + +# you can also set the cursor position programmatically +cursor.position = (256, 256) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/tooltips.py b/examples/misc/tooltips.py deleted file mode 100644 index cad3d807c..000000000 --- a/examples/misc/tooltips.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Tooltips -======== - -Show tooltips on all graphics -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'screenshot' - -import numpy as np -import imageio.v3 as iio -import fastplotlib as fpl - - -# get some data -scatter_data = np.random.rand(1_000, 3) - -xs = np.linspace(0, 2 * np.pi, 100) -ys = np.sin(xs) - -gray = iio.imread("imageio:camera.png") -rgb = iio.imread("imageio:astronaut.png") - -# create a figure -figure = fpl.Figure( - cameras=["3d", "2d", "2d", "2d"], - controller_types=["orbit", "panzoom", "panzoom", "panzoom"], - size=(700, 560), - shape=(2, 2), - show_tooltips=True, # tooltip will display data value info for all graphics -) - -# create graphics -scatter = figure[0, 0].add_scatter(scatter_data, sizes=3, colors="r") -line = figure[0, 1].add_line(np.column_stack([xs, ys])) -image = figure[1, 0].add_image(gray) -image_rgb = figure[1, 1].add_image(rgb) - - -figure.show() - -# to hide tooltips for all graphics in an existing Figure -# figure.show_tooltips = False - -# to show tooltips for all graphics in an existing Figure -# figure.show_tooltips = True - - -# NOTE: fpl.loop.run() should not be used for interactive sessions -# See the "JupyterLab and IPython" section in the user guide -if __name__ == "__main__": - print(__doc__) - fpl.loop.run() diff --git a/examples/misc/tooltips_custom.py b/examples/misc/tooltips_custom.py index d1cc1e297..3a54a945b 100644 --- a/examples/misc/tooltips_custom.py +++ b/examples/misc/tooltips_custom.py @@ -31,20 +31,26 @@ ) -def tooltip_info(ev) -> str: +def tooltip_info(pick_info: dict) -> str: # get index of the scatter point that is being hovered - index = ev.pick_info["vertex_index"] + index = pick_info["vertex_index"] # get the species name target = dataset["target"][index] cluster = agg.labels_[index] - info = f"species: {dataset['target_names'][target]}\ncluster: {cluster}" + + # the default formatting of the pick info + default_info = scatter.format_pick_info(pick_info) + + info = (f"species: {dataset['target_names'][target]}\n" + f"cluster: {cluster}\n\n" + f"{default_info}") # return this string to display it in the tooltip return info -figure.tooltip_manager.register(scatter, custom_info=tooltip_info) +scatter.tooltip_format = tooltip_info figure.show() diff --git a/examples/screenshots/no-imgui-rotation_image.png b/examples/screenshots/no-imgui-rotation_image.png new file mode 100644 index 000000000..3780dc87a --- /dev/null +++ b/examples/screenshots/no-imgui-rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62b9923128bebb489e7da928c5d3fc212cc6228b58dbdaf4bcbaabf0ad12b28c +size 50262 diff --git a/examples/screenshots/no-imgui-rotation_line.png b/examples/screenshots/no-imgui-rotation_line.png new file mode 100644 index 000000000..3eddc6ff2 --- /dev/null +++ b/examples/screenshots/no-imgui-rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c922741a05bc5ab2f6bf165b909bb14d443d93517700ceba522aa05b8aa26df4 +size 42402 diff --git a/examples/screenshots/no-imgui-scaling_image.png b/examples/screenshots/no-imgui-scaling_image.png new file mode 100644 index 000000000..5d3dbeaff --- /dev/null +++ b/examples/screenshots/no-imgui-scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0481db08929abe0622f933b349746f40077fe930d86deed1a1ab08563ea310b +size 45587 diff --git a/examples/screenshots/no-imgui-scaling_line.png b/examples/screenshots/no-imgui-scaling_line.png new file mode 100644 index 000000000..8fd232e31 --- /dev/null +++ b/examples/screenshots/no-imgui-scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71940e060068b1941f81e8aa66dfb9bae19aa60bd3c4ac848f65ecf42708dc85 +size 43106 diff --git a/examples/screenshots/no-imgui-translate_image.png b/examples/screenshots/no-imgui-translate_image.png new file mode 100644 index 000000000..a875ef91a --- /dev/null +++ b/examples/screenshots/no-imgui-translate_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0995cdaf81fc5a25ebdd54545b7be3e4edca6c25896c2aa5ba9d7e4ab0b240e8 +size 44246 diff --git a/examples/screenshots/no-imgui-translate_line.png b/examples/screenshots/no-imgui-translate_line.png new file mode 100644 index 000000000..211c4a5d0 --- /dev/null +++ b/examples/screenshots/no-imgui-translate_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8b3e79aeb1d8d0622e0928932bd98a7ee8a77d370dc7aecc7c1b923608497d7 +size 45889 diff --git a/examples/screenshots/no-imgui-translation_scaling_image.png b/examples/screenshots/no-imgui-translation_scaling_image.png new file mode 100644 index 000000000..a5c7a71d2 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca48b15e42f7e5e2f67152a31e58b2869329d361d21b17718528b9f8f16a4c92 +size 45697 diff --git a/examples/screenshots/no-imgui-translation_scaling_line.png b/examples/screenshots/no-imgui-translation_scaling_line.png new file mode 100644 index 000000000..0c7b625c7 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f2311cbd8a719d9c208d6744df56bba6d592f5e650cedc4c1251b7c5cf2c9b9 +size 42714 diff --git a/examples/screenshots/no-imgui-translation_scaling_rotation_image.png b/examples/screenshots/no-imgui-translation_scaling_rotation_image.png new file mode 100644 index 000000000..418ef1ff4 --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0035495345247d02c113c362699b930d11240e50c8bc14b4178457d029701629 +size 46978 diff --git a/examples/screenshots/no-imgui-translation_scaling_rotation_line.png b/examples/screenshots/no-imgui-translation_scaling_rotation_line.png new file mode 100644 index 000000000..15124c89e --- /dev/null +++ b/examples/screenshots/no-imgui-translation_scaling_rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de3cac77e9f6601abf050b67fdd15f14e3fcfa691cc06284379830e9be57f3d4 +size 45515 diff --git a/examples/screenshots/rotation_image.png b/examples/screenshots/rotation_image.png new file mode 100644 index 000000000..85312949a --- /dev/null +++ b/examples/screenshots/rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6399d67da50abbdf7af4430f2bc4264f893d239eb661d3664ead87563169bee +size 51598 diff --git a/examples/screenshots/rotation_line.png b/examples/screenshots/rotation_line.png new file mode 100644 index 000000000..08b09a417 --- /dev/null +++ b/examples/screenshots/rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f66b0698d2f1fc2481767413377e21fa57bc80c9b34aa3e722a63902fc34a1e +size 44395 diff --git a/examples/screenshots/scaling_image.png b/examples/screenshots/scaling_image.png new file mode 100644 index 000000000..f0b2bdb8b --- /dev/null +++ b/examples/screenshots/scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e820b72d87156d215f895c0668bef80a4a2d7cafeb1435a5df1ac7515d2336ef +size 47270 diff --git a/examples/screenshots/scaling_line.png b/examples/screenshots/scaling_line.png new file mode 100644 index 000000000..48e71b9ab --- /dev/null +++ b/examples/screenshots/scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f611bbbf7c05b754a35065f7f2117fc8062f0024d209ae1fab049f6e7f2d3b8 +size 44380 diff --git a/examples/screenshots/translate_image.png b/examples/screenshots/translate_image.png new file mode 100644 index 000000000..c0e6dd76e --- /dev/null +++ b/examples/screenshots/translate_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c7fb592ea62eed3be0ff6c7650d176513304e455130b64caebcefc7e5fe48e9 +size 45572 diff --git a/examples/screenshots/translate_line.png b/examples/screenshots/translate_line.png new file mode 100644 index 000000000..4c64bbd74 --- /dev/null +++ b/examples/screenshots/translate_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a62c00847ea65187c7025c4eb0ad80767e1609e37d88602424531cbc0c7429a2 +size 46717 diff --git a/examples/screenshots/translation_scaling_image.png b/examples/screenshots/translation_scaling_image.png new file mode 100644 index 000000000..b7d26c937 --- /dev/null +++ b/examples/screenshots/translation_scaling_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:393d26c54bb9a0ac690411df262c3b9c3273274edf4787a18f057a1c3e02389e +size 47386 diff --git a/examples/screenshots/translation_scaling_line.png b/examples/screenshots/translation_scaling_line.png new file mode 100644 index 000000000..e3c6835b6 --- /dev/null +++ b/examples/screenshots/translation_scaling_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0593fa32a6990c2e05aad1b9314b912dc3e196b499938be49fc7074e610581e0 +size 44521 diff --git a/examples/screenshots/translation_scaling_rotation_image.png b/examples/screenshots/translation_scaling_rotation_image.png new file mode 100644 index 000000000..cd384ba15 --- /dev/null +++ b/examples/screenshots/translation_scaling_rotation_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42352d3bedbb42fdac5e45a789520e9f7be75748a32b12ceea7edabd4f17c500 +size 47418 diff --git a/examples/screenshots/translation_scaling_rotation_line.png b/examples/screenshots/translation_scaling_rotation_line.png new file mode 100644 index 000000000..ea92cdd09 --- /dev/null +++ b/examples/screenshots/translation_scaling_rotation_line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25b9c03c40a1b5c91df269f402b953986d996a95660f0c5f4d85c8ef31d479a8 +size 46453 diff --git a/examples/spaces_transforms/README.rst b/examples/spaces_transforms/README.rst new file mode 100644 index 000000000..55747c2a8 --- /dev/null +++ b/examples/spaces_transforms/README.rst @@ -0,0 +1,2 @@ +Spaces and transforms +===================== diff --git a/examples/spaces_transforms/rotation_image.py b/examples/spaces_transforms/rotation_image.py new file mode 100644 index 000000000..ebc6cb3de --- /dev/null +++ b/examples/spaces_transforms/rotation_image.py @@ -0,0 +1,94 @@ +""" +Rotate image +============ + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +image.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/rotation_line.py b/examples/spaces_transforms/rotation_line.py new file mode 100644 index 000000000..bec820eb8 --- /dev/null +++ b/examples/spaces_transforms/rotation_line.py @@ -0,0 +1,89 @@ +""" +Rotate line +=========== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +line.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/scaling_image.py b/examples/spaces_transforms/scaling_image.py new file mode 100644 index 000000000..878a09010 --- /dev/null +++ b/examples/spaces_transforms/scaling_image.py @@ -0,0 +1,94 @@ +""" +Scale image +=========== + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/scaling_line.py b/examples/spaces_transforms/scaling_line.py new file mode 100644 index 000000000..0fcdca55e --- /dev/null +++ b/examples/spaces_transforms/scaling_line.py @@ -0,0 +1,89 @@ +""" +Scale line +=========== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translate_image.py b/examples/spaces_transforms/translate_image.py new file mode 100644 index 000000000..24a90a064 --- /dev/null +++ b/examples/spaces_transforms/translate_image.py @@ -0,0 +1,95 @@ +""" +Translate image +=============== + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translate_line.py b/examples/spaces_transforms/translate_line.py new file mode 100644 index 000000000..d8821b271 --- /dev/null +++ b/examples/spaces_transforms/translate_line.py @@ -0,0 +1,90 @@ +""" +Translate line +============== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_image.py b/examples/spaces_transforms/translation_scaling_image.py new file mode 100644 index 000000000..02e3a2d41 --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_image.py @@ -0,0 +1,99 @@ +""" +Translate and scale image +========================= + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_line.py b/examples/spaces_transforms/translation_scaling_line.py new file mode 100644 index 000000000..6afbfc11c --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_line.py @@ -0,0 +1,94 @@ +""" +Translate and scale line +======================== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_rotation_image.py b/examples/spaces_transforms/translation_scaling_rotation_image.py new file mode 100644 index 000000000..d0060401f --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_rotation_image.py @@ -0,0 +1,102 @@ +""" +Translate scale and rotate image +================================ + +This examples illustrates the various spaces that you may need to map between, +plots an image to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +# an image to demonstrate some data in model/data space +image_data = np.array( + [ + [0, 1, 2], + [3, 4, 5], + [5, 6, 7], + [8, 9, 10], + ] +) +image = figure[0, 0].add_image(image_data, cmap="turbo") + + +# a scatter that will be in the same space as the image +# used to indicates a few points on the image +scatter_data = np.array([[0, 1], [2, 3]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +image.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +image.scale = scaling +scatter.scale = scaling + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +image.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/spaces_transforms/translation_scaling_rotation_line.py b/examples/spaces_transforms/translation_scaling_rotation_line.py new file mode 100644 index 000000000..e4c245a8e --- /dev/null +++ b/examples/spaces_transforms/translation_scaling_rotation_line.py @@ -0,0 +1,99 @@ +""" +Translate scale and rotate line +=============================== + +This examples illustrates the various spaces that you may need to map between, +plots a line to show these mappings. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# a line to demonstrate some data in model/data space +line_data = np.column_stack([xs, ys]) +line = figure[0, 0].add_line(line_data, cmap="jet", thickness=10) + +# a scatter that will be in the same space as the line +# used to indicates a few points on the line +scatter_data = np.array([[np.pi / 4, np.sin(np.pi / 4)], [3 * np.pi / 2 , -1]]) +scatter = figure[0, 0].add_scatter( + scatter_data, + sizes=15, + colors=["blue", "red"], + edge_colors="w", + edge_width=2.0, +) + +# text to indicate the scatter point positions in all spaces +text_0 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) +text_1 = figure[0, 0].add_text( + text="", + anchor="bottom-left", + face_color="w", + outline_color="k", + outline_thickness=0.5, +) + + +# translation and scaling +translation = (2, 3, 0) # x, y, z translation +line.offset = translation +scatter.offset = translation + +scaling = (2, 0.5, 1.0) # scale (x, y, z) +line.scale = scaling +scatter.scale = scaling + +# rotation of pi/4 as a quaternion +rotation_quat = (np.cos(np.pi / 8), np.sin(np.pi / 8), 0, 0) +line.rotation = rotation_quat +scatter.rotation = rotation_quat + + +def update_text(): + # get the position of the scatter points in world space + # graphics can map from model <-> world space + point_0_world = scatter.map_model_to_world(scatter.data[0]) + point_1_world = scatter.map_model_to_world(scatter.data[1]) + + # text is always just set in world space + text_0.offset = point_0_world + text_1.offset = point_1_world + + # use subplot to map to world <-> screen space + point_0_screen = figure[0, 0].map_world_to_screen(point_0_world) + point_1_screen = figure[0, 0].map_world_to_screen(point_1_world) + + # set text to display model, world and screen space position of the 2 points + text_0.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[0])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_0_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_0_screen)}]" + ) + + text_1.text = ( + f"model pos: [{', '.join(str(round(p, 2)) for p in scatter.data[1])}]\n" + f"world pos: [{', '.join(str(round(p, 2)) for p in point_1_world)}]\n" + f"screen pos: [{', '.join(str(round(p)) for p in point_1_screen)}]" + ) + + +figure.add_animations(update_text) + +figure.show() + +fpl.loop.run() diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index aad729c7a..e279809e3 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -30,6 +30,7 @@ "window_layouts/*.py", "events/*.py", "selection_tools/*.py", + "spaces_transforms/*.py", "misc/*.py", "guis/*.py", ] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a4f3e9a67..47673cbc0 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,6 +1,7 @@ +from __future__ import annotations from collections import defaultdict from functools import partial -from typing import Any, Literal, TypeAlias +from typing import Any, Literal, TypeAlias, Callable import weakref import numpy as np @@ -22,6 +23,7 @@ Name, Offset, Rotation, + Scale, Alpha, AlphaMode, Visible, @@ -29,11 +31,16 @@ from ._axes import Axes HexStr: TypeAlias = str +WorldObjectID: TypeAlias = int # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects WORLD_OBJECTS: dict[HexStr, pygfx.WorldObject] = dict() #: {hex id str: WorldObject} +# maps world object to the graphic which owns it, useful when manually picking from the renderer and we +# need to know the graphic associated with the target world object +WORLD_OBJECT_TO_GRAPHIC: dict[WorldObjectID, Graphic] = dict() + PYGFX_EVENTS = [ "key_down", @@ -54,6 +61,11 @@ class Graphic: _features: dict[str, type] = dict() + # It also doesn't make sense to create tooltips for some graphics + # ex: text, that would be very funny. + # They would also get in the way of selector tools + _fpl_support_tooltip: bool = True + def __init_subclass__(cls, **kwargs): # set of all features @@ -62,6 +74,7 @@ def __init_subclass__(cls, **kwargs): "name": Name, "offset": Offset, "rotation": Rotation, + "scale": Scale, "alpha": Alpha, "alpha_mode": AlphaMode, "visible": Visible, @@ -72,8 +85,9 @@ def __init_subclass__(cls, **kwargs): def __init__( self, name: str = None, - offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), - rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), + offset: np.ndarray | tuple[float] = (0.0, 0.0, 0.0), + rotation: np.ndarray | tuple[float] = (0.0, 0.0, 0.0, 1.0), + scale: np.ndarray | tuple[float] = (1.0, 1.0, 1.0), alpha: float = 1.0, alpha_mode: str = "auto", visible: bool = True, @@ -92,6 +106,9 @@ def __init__( rotation: (float, float, float, float), default (0, 0, 0, 1) rotation quaternion + scale: (float, float, float), default (1.0, 1.0, 1.0) + (x, y, z) scale factors + alpha: (float), default 1.0 The global alpha value, i.e. opacity, of the graphic. @@ -155,6 +172,7 @@ def __init__( self._name = Name(name) self._deleted = Deleted(False) self._rotation = Rotation(rotation) + self._scale = Scale(scale) self._offset = Offset(offset) self._alpha = Alpha(alpha) self._alpha_mode = AlphaMode(alpha_mode) @@ -165,6 +183,11 @@ def __init__( self._right_click_menu = None + # store ids of all the WorldObjects that this Graphic manages/uses + self._world_object_ids = list() + + self._tooltip_format: Callable = None + @property def supported_events(self) -> tuple[str]: """events supported by this graphic""" @@ -185,7 +208,7 @@ def offset(self) -> np.ndarray: return self._offset.value @offset.setter - def offset(self, value: np.ndarray | list | tuple): + def offset(self, value: np.ndarray | tuple[float, float, float]): self._offset.set_value(self, value) @property @@ -194,9 +217,18 @@ def rotation(self) -> np.ndarray: return self._rotation.value @rotation.setter - def rotation(self, value: np.ndarray | list | tuple): + def rotation(self, value: np.ndarray | tuple[float, float, float, float]): self._rotation.set_value(self, value) + @property + def scale(self) -> np.ndarray: + """(x, y, z) scaling factor""" + return self._scale.value + + @scale.setter + def scale(self, value: np.ndarray | tuple[float, float, float]): + self._scale.set_value(self, value) + @property def alpha(self) -> float: """The opacity of the graphic""" @@ -251,6 +283,23 @@ def world_object(self) -> pygfx.WorldObject: def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo + # add to world object -> graphic mapping + if isinstance(wo, pygfx.Group): + for child in wo.children: + if isinstance( + child, (pygfx.Image, pygfx.Volume, pygfx.Points, pygfx.Line) + ): + # unique 32 bit integer id for each world object + global_id = child.id + WORLD_OBJECT_TO_GRAPHIC[global_id] = self + # store id to pop from dict when graphic is deleted + self._world_object_ids.append(global_id) + else: + global_id = wo.id + WORLD_OBJECT_TO_GRAPHIC[global_id] = self + # store id to pop from dict when graphic is deleted + self._world_object_ids.append(global_id) + wo.visible = self.visible if "Image" in self.__class__.__name__: # Image and ImageVolume use tiling and share one material @@ -269,6 +318,27 @@ def _set_world_object(self, wo: pygfx.WorldObject): if not all(wo.world.rotation == self.rotation): self.rotation = self.rotation + @property + def tooltip_format(self) -> Callable[[dict], str] | None: + """ + set a custom tooltip format function which takes a ``pick_info`` dict and + returns a str to be displayed in the tooltip + """ + return self._tooltip_format + + @tooltip_format.setter + def tooltip_format(self, func: Callable[[dict], str] | None): + if func is None: + self._tooltip_format = None + return + + if not callable(func): + raise TypeError( + f"`tooltip_format` must be set with a callable that takes a pick_info dict, or it can be set as None" + ) + + self._tooltip_format = func + @property def event_handlers(self) -> list[tuple[str, callable, ...]]: """ @@ -427,6 +497,72 @@ def my_handler(event): feature = getattr(self, f"_{t}") feature.remove_event_handler(wrapper) + def map_model_to_world( + self, position: tuple[float, float, float] | tuple[float, float] | np.ndarray + ) -> np.ndarray: + """ + map position from model (data) space to world space, basically applies the world affine transform + + Parameters + ---------- + position: (float, float, float) or (float, float) + (x, y, z) or (x, y) position. If z is not provided then the graphic's offset z is used. + + Returns + ------- + np.ndarray + (x, y, z) position in world space + + """ + + if len(position) == 2: + # use z of the graphic + position = [*position, self.offset[-1]] + + if len(position) != 3: + raise ValueError( + f"position must be tuple or array indicating (x, y, z) position in *model space*" + ) + + # apply world transform to project from model space to world space + return la.vec_transform(position, self.world_object.world.matrix) + + def map_world_to_model( + self, position: tuple[float, float, float] | tuple[float, float] | np.ndarray + ) -> np.ndarray: + """ + map position from world space to model (data) space, basically applies the inverse world affine transform + + Parameters + ---------- + position: (float, float, float) or (float, float) + (x, y, z) or (x, y) position. If z is not provided then 0 is used. + + Returns + ------- + np.ndarray + (x, y, z) position in world space + + """ + + if len(position) == 2: + # use z of the graphic + position = [*position, self.offset[-1]] + + if len(position) != 3: + raise ValueError( + f"position must be tuple or array indicating (x, y, z) position in *model space*" + ) + + return la.vec_transform(position, self.world_object.world.inverse_matrix) + + def format_pick_info(self, ev: pygfx.PointerEvent) -> str: + """ + Takes a pygfx.PointerEvent and returns formatted pick info. + """ + + raise NotImplementedError("must be implemented in subclass") + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area @@ -444,6 +580,10 @@ def _fpl_prepare_del(self): Optionally implemented in subclasses """ + # remove from world_obj -> graphic map + for global_id in self._world_object_ids: + WORLD_OBJECT_TO_GRAPHIC.pop(global_id) + # remove axes if added to this graphic if self._axes is not None: self._plot_area.scene.remove(self._axes) diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py index 36f83ec7a..5b1fd87f1 100644 --- a/fastplotlib/graphics/_collection_base.py +++ b/fastplotlib/graphics/_collection_base.py @@ -181,6 +181,8 @@ class GraphicCollection(Graphic, CollectionProperties): _child_type: type _indexer: type + # tooltips will come from the child graphics + _fpl_support_tooltip = False def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -308,6 +310,7 @@ def _fpl_prepare_del(self): """ # clear any attached event handlers and animation functions self.world_object._event_handlers.clear() + self.world_object.clear() for g in self: g._fpl_prepare_del() @@ -318,16 +321,6 @@ def __getitem__(self, key) -> CollectionIndexer: return self._indexer(selection=self.graphics[key], features=self._features) - def __del__(self): - # detach children - self.world_object.clear() - - for g in self.graphics: - g._fpl_prepare_del() - del g - - super().__del__() - def __len__(self): return len(self._graphics) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 73520cc84..af7d7badb 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -146,3 +146,11 @@ def __init__( self._size_space = SizeSpace(size_space) super().__init__(*args, **kwargs) + + def format_pick_info(self, pick_info: dict) -> str: + index = pick_info["vertex_index"] + info = "\n".join( + f"{dim}: {val:.4g}" for dim, val in zip("xyz", self.data[index]) + ) + + return info diff --git a/fastplotlib/graphics/_vectors.py b/fastplotlib/graphics/_vectors.py index 6f761bd49..be90db538 100644 --- a/fastplotlib/graphics/_vectors.py +++ b/fastplotlib/graphics/_vectors.py @@ -128,7 +128,7 @@ def __init__( } geometry = create_vector_geometry(color=color, **shape_options) - material = pygfx.MeshBasicMaterial() + material = pygfx.MeshBasicMaterial(pick_write=True) n_vectors = self._positions.value.shape[0] @@ -170,6 +170,16 @@ def directions(self) -> VectorDirections: def directions(self, new_directions): self._directions.set_value(self, new_directions) + def format_pick_info(self, pick_info: dict) -> str: + index = pick_info["instance_index"] + + info = ( + f"position: {self.positions[index]}\n" + f"direction: {self.directions[index]}" + ) + + return info + # mesh code copied and adapted from pygfx def generate_torso( diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index cf99d376d..7f7410cf7 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -71,7 +71,7 @@ LinearRegionSelectionFeature, RectangleSelectionFeature, ) -from ._common import Name, Offset, Rotation, Alpha, AlphaMode, Visible, Deleted +from ._common import Name, Offset, Rotation, Scale, Alpha, AlphaMode, Visible, Deleted __all__ = [ @@ -119,6 +119,7 @@ "Name", "Offset", "Rotation", + "Scale", "Alpha", "AlphaMode", "Visible", diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py index b2b99cc49..6ce167075 100644 --- a/fastplotlib/graphics/features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -130,6 +130,55 @@ def set_value(self, graphic, value: np.ndarray | Sequence[float]): self._call_event_handlers(event) +class Scale(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray[float, float, float, float]", + "description": "new scale", + }, + ] + + def __init__( + self, value: np.ndarray | Sequence[float], property_name: str = "scale" + ): + """Graphic scaling factor""" + + self._validate(value) + # create ones array + self._value = np.ones(3) + + self._value[:] = value + super().__init__(property_name=property_name) + + def _validate(self, value): + if not len(value) in [2, 3]: + raise ValueError( + "scale must be a list, tuple, or array of 2 or 3 float values indicating (x, y) or (x, y, z) scaling" + ) + + @property + def value(self) -> np.ndarray: + return self._value + + @block_reentrance + def set_value(self, graphic, value: np.ndarray | Sequence[float]): + self._validate(value) + + if len(value) == 2: + value = (*value, graphic.world_object.world.scale_z) + + value = np.asarray(value) + + graphic.world_object.world.scale = value + + # set value of existing feature value array + self._value[:] = value + + event = GraphicFeatureEvent(type=self._property_name, info={"value": value}) + self._call_event_handlers(event) + + class Alpha(GraphicFeature): """The alpha value (i.e. opacity) of a graphic.""" diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 1eaf54bb6..ece700385 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -21,6 +21,15 @@ ) +def _format_value(value: float): + """float -> rounded str, or str with scientific notation""" + abs_val = abs(value) + if abs_val < 0.01 or abs_val > 9_999: + return f"{value:.2e}" + else: + return f"{value:.4f}" + + class _ImageTile(pygfx.Image): """ Similar to pygfx.Image, only difference is that it modifies the pick_info @@ -477,3 +486,16 @@ def add_polygon_selector( self._plot_area.add_graphic(selector, center=False) return selector + + def format_pick_info(self, pick_info: dict) -> str: + col, row = pick_info["index"] + if self.data.value.ndim == 2: + val = self.data[row, col] + info = f"{val:.4g}" + else: + info = "\n".join( + f"{channel}: {val:.4g}" + for channel, val in zip("rgba", self.data[row, col]) + ) + + return info diff --git a/fastplotlib/graphics/image_volume.py b/fastplotlib/graphics/image_volume.py index db616b30d..db8f29eaa 100644 --- a/fastplotlib/graphics/image_volume.py +++ b/fastplotlib/graphics/image_volume.py @@ -419,3 +419,18 @@ def reset_vmin_vmax(self): vmin, vmax = quick_min_max(self.data.value) self.vmin = vmin self.vmax = vmax + + def format_pick_info(self, pick_info: dict) -> str: + return "image volume tooltips supported in next version" + + col, row, z = pick_info["index"] + if self.data.value.ndim == 3: + val = self.data[z, row, col] + info = f"{val:.4g}" + else: + info = "\n".join( + f"{channel}: {val:.4g}" + for channel, val in zip("rgba", self.data[z, row, col]) + ) + + return info diff --git a/fastplotlib/graphics/mesh.py b/fastplotlib/graphics/mesh.py index 2e5a11851..0e1ac42a3 100644 --- a/fastplotlib/graphics/mesh.py +++ b/fastplotlib/graphics/mesh.py @@ -291,6 +291,27 @@ def plane(self, value: tuple[float, float, float, float]): self._plane.set_value(self, value) + def format_pick_info(self, pick_info: dict) -> str: + # Get what face was clicked + face_index = pick_info["face_index"] + coords = pick_info["face_coord"] + # Select which of the three vertices was closest + # Note that you can also select all vertices for this face, + # or use the coords to select the closest edge. + sub_index = np.argmax(coords) + # Look up the vertex index + try: + vertex_index = int(self.indices[face_index, sub_index]) + except IndexError: + # if vertex buffer sizes change then the pointer event can have outdated pick info? + return "error, buffer size changed" + + info = "\n".join( + f"{dim}: {val:.4g}" for dim, val in zip("xyz", self.positions[vertex_index]) + ) + + return info + class SurfaceGraphic(MeshGraphic): _features = { diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index e4dbc890b..28c6534a7 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -40,6 +40,8 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): + _fpl_support_tooltip = False + @property def axis(self) -> str: return self._axis diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 9f1aeb8af..37e559576 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -21,6 +21,8 @@ class TextGraphic(Graphic): "outline_thickness": TextOutlineThickness, } + _fpl_support_tooltip = False + def __init__( self, text: str, diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 8fd5dc666..79b5be3a8 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -21,7 +21,6 @@ from ._subplot import Subplot from ._engine import GridLayout, WindowLayout, ScreenSpaceCamera from .. import ImageGraphic -from ..tools import Tooltip class Figure: @@ -52,7 +51,6 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, - show_tooltips: bool = False, ): """ Create a Figure containing Subplots. @@ -124,10 +122,30 @@ def __init__( names: list or array of str, optional subplot names - show_tooltips: bool, default False - show tooltips on graphics - """ + # create canvas and renderer + if canvas_kwargs is not None: + if size not in canvas_kwargs.keys(): + canvas_kwargs["size"] = size + else: + canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} + + canvas, renderer = make_canvas_and_renderer( + canvas, renderer, canvas_kwargs=canvas_kwargs + ) + + canvas.add_event_handler(self._fpl_reset_layout, "resize") + + self._canvas = canvas + self._renderer = renderer + + # underlay render pass + self._underlay_camera = ScreenSpaceCamera() + self._underlay_scene = pygfx.Scene() + + # overlay render pass + self._overlay_camera = ScreenSpaceCamera() + self._fpl_overlay_scene = pygfx.Scene() if rects is not None: if not all(isinstance(v, (np.ndarray, tuple, list)) for v in rects): @@ -202,18 +220,6 @@ def __init__( else: subplot_names = None - if canvas_kwargs is not None: - if size not in canvas_kwargs.keys(): - canvas_kwargs["size"] = size - else: - canvas_kwargs = {"size": size, "max_fps": 60.0, "vsync": True} - - canvas, renderer = make_canvas_and_renderer( - canvas, renderer, canvas_kwargs=canvas_kwargs - ) - - canvas.add_event_handler(self._fpl_reset_layout, "resize") - if isinstance(cameras, str): # create the array representing the views for each subplot in the grid cameras = np.array([cameras] * n_subplots) @@ -392,9 +398,6 @@ def __init__( for cam in cams[1:]: _controller.add_camera(cam) - self._canvas = canvas - self._renderer = renderer - if layout_mode == "grid": n_rows, n_cols = shape grid_index_iterator = list(product(range(n_rows), range(n_cols))) @@ -449,23 +452,10 @@ def __init__( canvas_rect=self.get_pygfx_render_area(), ) - # underlay render pass - self._underlay_camera = ScreenSpaceCamera() - self._underlay_scene = pygfx.Scene() - + # add subplot frames to underlay for subplot in self._subplots.ravel(): self._underlay_scene.add(subplot.frame._world_object) - # overlay render pass - self._overlay_camera = ScreenSpaceCamera() - self._overlay_scene = pygfx.Scene() - - # tooltip in overlay render pass - self._tooltip_manager = Tooltip() - self._overlay_scene.add(self._tooltip_manager.world_object) - - self._show_tooltips = show_tooltips - self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -533,34 +523,11 @@ def names(self) -> np.ndarray[str]: names.flags.writeable = False return names - @property - def tooltip_manager(self) -> Tooltip: - """manage tooltips""" - return self._tooltip_manager - - @property - def show_tooltips(self) -> bool: - """show/hide tooltips for all graphics""" - return self._show_tooltips - @property def animations(self) -> dict[str, list[callable]]: """Returns a dictionary of 'pre' and 'post' animation functions.""" return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} - @show_tooltips.setter - def show_tooltips(self, val: bool): - self._show_tooltips = val - - if val: - # register all graphics - for subplot in self: - for graphic in subplot.graphics: - self._tooltip_manager.register(graphic) - - elif not val: - self._tooltip_manager.unregister_all() - def _render(self, draw=True): # draw the underlay planes self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) @@ -578,7 +545,7 @@ def _render(self, draw=True): # overlay render pass if hasattr(self.renderer, "clear"): self.renderer.clear(depth=True) - self.renderer.render(self._overlay_scene, self._overlay_camera, flush=False) + self.renderer.render(self._fpl_overlay_scene, self._overlay_camera, flush=False) self.renderer.flush() diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 046c622ea..d54be4086 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -44,7 +44,6 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, - show_tooltips: bool = False, ): self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} @@ -61,7 +60,6 @@ def __init__( canvas_kwargs=canvas_kwargs, size=size, names=names, - show_tooltips=show_tooltips, ) self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 01721780c..f83dcfbcb 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -9,11 +9,12 @@ from rendercanvas import BaseRenderCanvas from ._utils import create_controller -from ..graphics._base import Graphic +from ..graphics._base import Graphic, WORLD_OBJECT_TO_GRAPHIC from ..graphics import ImageGraphic from ..graphics.selectors._base_selector import BaseSelector from ._graphic_methods_mixin import GraphicMethodsMixin from ..legends import Legend +from ..tools import Tooltip try: @@ -88,6 +89,8 @@ def __init__( self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() + self._animate_funcs_persist: list[callable] = list() + # list of all graphics managed by this PlotArea self._graphics: list[Graphic] = list() @@ -123,6 +126,10 @@ def __init__( self.scene.add(self._ambient_light) self.scene.add(self._camera.add(self._directional_light)) + self._tooltip = Tooltip() + self.get_figure()._fpl_overlay_scene.add(self._tooltip._fpl_world_object) + self.renderer.add_event_handler(self._fpl_set_tooltip, "pointer_move") + def get_figure(self, obj=None): """Get Figure instance that contains this plot area""" if obj is None: @@ -297,17 +304,27 @@ def animations(self) -> dict[str, list[callable]]: """Returns a dictionary of 'pre' and 'post' animation functions.""" return {"pre": self._animate_funcs_pre, "post": self._animate_funcs_post} + @property + def tooltip(self) -> Tooltip: + """The tooltip in this PlotArea""" + return self._tooltip + def map_screen_to_world( self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False ) -> np.ndarray | None: """ - Map screen position to world position + Map screen (canvas) position to world position Parameters ---------- pos: (float, float) | pygfx.PointerEvent ``(x, y)`` screen coordinates, or ``pygfx.PointerEvent`` + Returns + ------- + (float, float, float) + (x, y, z) position in world space, z is always 0 + """ if isinstance(pos, pygfx.PointerEvent): pos = pos.x, pos.y @@ -333,6 +350,117 @@ def map_screen_to_world( # default z is zero for now return np.array([*pos_world[:2], 0]) + def map_world_to_screen( + self, pos: tuple[float, float, float] | np.ndarray + ) -> tuple[float, float]: + """ + Map world position to screen (canvas) position + + Parameters + ---------- + pos: (x, y, z) + world space position + + Returns + ------- + (float, float) + (x, y) position in screen (canvas) space + + """ + + if not len(pos) == 3: + raise ValueError(f"must pass 3d (x, y, z) position, you passed: {pos}") + + # apply camera transform and get NDC position + ndc = vec_transform(np.asarray(pos), self.camera.camera_matrix) + + # get viewport rect + x_offset, y_offset, w, h = self.viewport.rect + + # ndc to screen position + x_screen = x_offset + (ndc[0] + 1) * 0.5 * w + y_screen = y_offset + (1 - ndc[1]) * 0.5 * h + + return x_screen, y_screen + + def get_pick_info(self, pos): + """ + Get pick info at this screen position + + Parameters + ---------- + pos: (x, y) + screen space position + + Returns + ------- + dict | None + pick info if a graphic is at this position, else None + + """ + + info = self.renderer.get_pick_info(pos) + + if info["world_object"] is not None: + # if this world object is owned by a graphic + if info["world_object"].id in WORLD_OBJECT_TO_GRAPHIC.keys(): + info["graphic"] = WORLD_OBJECT_TO_GRAPHIC[info["world_object"].id] + return info + + def _fpl_set_tooltip(self, ev: pygfx.PointerEvent): + # set tooltip using pointer position + if not self._tooltip.enabled: + return + + # is pointer in this plot area + if not self.viewport.is_inside(ev.x, ev.y): + return + + # is there a world object under the pointer + if ev.target is not None: + # is it owned by a graphic + if ev.target.id in WORLD_OBJECT_TO_GRAPHIC.keys(): + graphic = WORLD_OBJECT_TO_GRAPHIC[ev.target.id] + if not graphic._fpl_support_tooltip: + return + + pick_info = ev.pick_info + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format(pick_info) + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + self._tooltip.display((ev.x, ev.y), info) + return + + # not over a graphic that supports tooltips + self._tooltip.clear() + + def _fpl_update_tooltip_render(self): + # update tooltip on every render + # TODO: improve performance + if (not self._tooltip.visible) or (not self._tooltip.enabled): + return + + pick_info = self.get_pick_info(self._tooltip.position) + + # None if no graphic is at this position + if pick_info is not None: + graphic = pick_info["graphic"] + if graphic._fpl_support_tooltip: + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format(pick_info) + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + self._tooltip.display(self._tooltip.position, info) + return + + # tooltip cleared if none of the above condiitionals reached the tooltip display call + self._tooltip.clear() + def _render(self): self._call_animate_functions(self._animate_funcs_pre) @@ -344,6 +472,9 @@ def _render(self): self._call_animate_functions(self._animate_funcs_post) + if self._tooltip.continuous_update: + self._fpl_update_tooltip_render() + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: @@ -565,10 +696,6 @@ def _add_or_insert_graphic( obj_list = self._graphics self._fpl_graphics_scene.add(graphic.world_object) - # add to tooltip registry - if self.get_figure().show_tooltips: - self.get_figure().tooltip_manager.register(graphic) - else: raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") diff --git a/fastplotlib/tools/__init__.py b/fastplotlib/tools/__init__.py index df129a369..761183f76 100644 --- a/fastplotlib/tools/__init__.py +++ b/fastplotlib/tools/__init__.py @@ -1,7 +1,10 @@ from ._histogram_lut import HistogramLUTTool -from ._tooltip import Tooltip +from ._textbox import TextBox, Tooltip +from ._cursor import Cursor __all__ = [ "HistogramLUTTool", + "TextBox", "Tooltip", + "Cursor", ] diff --git a/fastplotlib/tools/_cursor.py b/fastplotlib/tools/_cursor.py new file mode 100644 index 000000000..6b7946cd0 --- /dev/null +++ b/fastplotlib/tools/_cursor.py @@ -0,0 +1,420 @@ +from functools import partial +from typing import Literal, Sequence, Callable + +import numpy as np +import pygfx + +from ..layouts import Subplot +from ..utils import RenderQueue + + +class Cursor: + def __init__( + self, + mode: Literal["crosshair", "marker"] = "crosshair", + size: float = 1.0, # in screen space + color: str | Sequence[float] | pygfx.Color | np.ndarray = "w", + marker: str = "+", + edge_color: str | Sequence[float] | pygfx.Color | np.ndarray = "k", + edge_width: float = 0.5, + alpha: float = 0.7, + size_space: Literal["screen", "world"] = "screen", + ): + """ + A cursor that indicates the same position in world-space across subplots. + + Parameters + ---------- + mode: "crosshair" | "marker" + cursor mode + + size: float, default 1.0 + * if ``mode`` == 'crosshair', this is the crosshair line thickness + * if ``mode`` == 'marker', it's the size of the marker + + You probably want to use ``size > 5`` if ``mode`` is 'marker' and ``size_space`` is ``screen`` + + color: str | Sequence[float] | pygfx.Color | np.ndarray, default "r" + color of the marker + + marker: str, default "+" + marker shape, used if mode == 'marker' + + edge_color: str | Sequence[float] | pygfx.Color | np.ndarray, default "k" + marker edge color, used if ``mode`` == 'marker' + + edge_width: float, default 0.5 + marker edge widget, used if ``mode`` == 'marker' + + alpha: float, default 0.7 + alpha (transparency) of the cursor + + size_space: "screen" | "world", default "screen" + size space of the cursor, if "screen" the ``size`` is exact screen pixels. + if "world" the ``size`` is in world-space + + """ + + self._cursors: dict[Subplot, pygfx.Points | pygfx.Group[pygfx.Line]] = dict() + self._transforms: dict[Subplot, Callable | None] = dict() + + self._mode = None + self.mode = mode + self.size = size + self.color = color + self.marker = marker + self.edge_color = edge_color + self.edge_width = edge_width + self.alpha = alpha + self.size_space = size_space + + self._enabled = True + + self._position: list[float, float] = [0.0, 0.0] + + @property + def mode(self) -> Literal["crosshair", "marker"]: + """cursor mode, one of 'crosshair' or 'marker'""" + return self._mode + + @mode.setter + def mode(self, mode: Literal["crosshair", "marker"]): + if not (mode == "crosshair" or mode == "marker"): + raise ValueError( + f"mode must be one of: 'crosshair' | 'marker', you passed: {mode}" + ) + + if mode == self.mode: + return + + # mode has changed, clear and create new world objects + subplots = list(self._cursors.keys()) + + self.clear() + + for subplot in subplots: + self.add_subplot(subplot) + + self._mode = mode + + @property + def size(self) -> float: + """size of marker or crosshair line thickness""" + return self._size + + @size.setter + def size(self, new_size: float): + for c in self._cursors.values(): + if self.mode == "marker": + c.material.size = new_size + elif self.mode == "crosshair": + h, v = c.children + h.material.thickness = new_size + v.material.thickness = new_size + + self._size = new_size + + @property + def size_space(self) -> Literal["screen", "world"]: + """interpret cursor size in screen or world space""" + return self._size_space + + @size_space.setter + def size_space(self, space: Literal["screen", "world"]): + if space not in ["screen", "world", "model"]: + raise ValueError( + f"valid `size_space` is one of: 'screen' | 'world'. You passed: {space}" + ) + + for c in self._cursors.values(): + if self.mode == "marker": + c.material.size_space = space + + elif self.mode == "crosshair": + h, v = c.children + h.material.thickness_space = space + v.material.thickness_space = space + + self._size_space = space + + @property + def color(self) -> pygfx.Color: + """cursor color""" + return self._color + + @color.setter + def color(self, new_color): + new_color = pygfx.Color(new_color) + + for c in self._cursors.values(): + c.material.color = new_color + + self._color = new_color + + @property + def marker(self) -> str: + """cursor marker shape, if `mode` is 'marker'""" + return self._marker + + @marker.setter + def marker(self, new_marker: str): + if self.mode == "marker": + for c in self._cursors.values(): + c.material.marker = new_marker + + self._marker = new_marker + + @property + def edge_color(self) -> pygfx.Color: + """cursor marker edge color, if `mode` is 'marker'""" + return self._edge_color + + @edge_color.setter + def edge_color(self, new_color: str | Sequence | np.ndarray | pygfx.Color): + new_color = pygfx.Color(new_color) + + if self.mode == "marker": + for c in self._cursors.values(): + c.material.edge_color = new_color + + self._edge_color = new_color + + @property + def edge_width(self) -> float: + """cursor marker edge width, if `mode` is 'marker'""" + return self._edge_width + + @edge_width.setter + def edge_width(self, new_width: float): + if self.mode == "marker": + for c in self._cursors.values(): + c.material.edge_width = new_width + + self._edge_width = new_width + + @property + def alpha(self) -> float: + """cursor alpha value""" + return self._alpha + + @alpha.setter + def alpha(self, value: float): + for c in self._cursors.values(): + c.material.opacity = value + + self._alpha = value + + @property + def enabled(self) -> bool: + """enable/disable the cursor, if False the cursor will not respond to mouse pointer events""" + return self._enabled + + @enabled.setter + def enabled(self, pause: bool): + self._enabled = bool(pause) + + @property + def position(self) -> tuple[float, float]: + """(x, y) position in world space""" + return tuple(self._position) + + @position.setter + def position(self, pos: tuple[float, float]): + for subplot, cursor in self._cursors.items(): + if self._transforms[subplot] is not None: + pos_transformed = self._transforms[subplot](pos) + else: + pos_transformed = pos + + if self.mode == "marker": + cursor.geometry.positions.data[0, :-1] = pos_transformed + cursor.geometry.positions.update_full() + + elif self.mode == "crosshair": + line_h, line_v = cursor.children + + # set x vals for horizontal line + line_h.geometry.positions.data[0, 0] = pos_transformed[0] - 1 + line_h.geometry.positions.data[1, 0] = pos[0] + 1 + + # set y value + line_h.geometry.positions.data[:, 1] = pos_transformed[1] + + line_h.geometry.positions.update_full() + + # set y vals for vertical line + line_v.geometry.positions.data[0, 1] = pos_transformed[1] - 1 + line_v.geometry.positions.data[1, 1] = pos_transformed[1] + 1 + + # set x value + line_v.geometry.positions.data[:, 0] = pos_transformed[0] + + line_v.geometry.positions.update_full() + + # set tooltip using pick info if a graphic is at this position + # for now we just set z = 1 + screen_pos = subplot.map_world_to_screen((*pos_transformed, 1)) + pick_info = subplot.get_pick_info(screen_pos) + + self._position[:] = pos_transformed + + if pick_info is not None: + graphic = pick_info["graphic"] + if ( + graphic._fpl_support_tooltip + ): # some graphics don't support tooltips, ex: Text + if graphic.tooltip_format is not None: + # custom formatter + info = graphic.tooltip_format + else: + # default formatter for this graphic + info = graphic.format_pick_info(pick_info) + + subplot.tooltip.display(screen_pos, info) + continue + + # tooltip cleared if none of the above condiitionals reached the tooltip display call + subplot.tooltip.clear() + + def add_subplot(self, subplot: Subplot, transform: Callable | None = None): + """ + Add a subplot to this cursor, with an optional position transform function + + Parameters + ---------- + subplot: Subplot + subplot to add + + transform: Callable[[tuple[float, float]], tuple[float, float]] | None + a transform function that takes the cursor's position and returns a transformed + position at which the cursor will visually appear. + + """ + if subplot in self._cursors.keys(): + raise KeyError(f"The given subplot has already been added to this cursor") + + if (not callable(transform)) and (transform is not None): + raise TypeError( + f"`transform` must be a callable or `None`, you passed: {transform}" + ) + + if self.mode == "marker": + cursor = self._create_marker() + + elif self.mode == "crosshair": + cursor = self._create_crosshair() + + subplot.scene.add(cursor) + subplot.renderer.add_event_handler( + partial(self._pointer_moved, subplot), "pointer_move" + ) + + self._cursors[subplot] = cursor + self._transforms[subplot] = transform + + # let cursor manage tooltips + subplot.renderer.remove_event_handler(subplot._fpl_set_tooltip, "pointer_move") + + def remove_subplot(self, subplot: Subplot): + """remove a subplot""" + if subplot not in self._cursors.keys(): + raise KeyError("cursor not in given supblot") + + subplot.scene.remove(self._cursors.pop(subplot)) + + # give back tooltip control to the subplot + subplot.renderer.add_event_handler(subplot._fpl_set_tooltip, "pointer_move") + + def clear(self): + """remove all subplots""" + for subplot in self._cursors.keys(): + self.remove_subplot(subplot) + + def _create_marker(self) -> pygfx.Points: + # creates a Point object, used for "marker" mode + point = pygfx.Points( + pygfx.Geometry(positions=np.array([[*self.position, 0]], dtype=np.float32)), + pygfx.PointsMarkerMaterial( + marker=self.marker, + size=self.size, + size_space=self.size_space, + color=self.color, + edge_color=self.edge_color, + edge_width=self.edge_width, + opacity=self.alpha, + alpha_mode="blend", + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + return point + + def _create_crosshair(self) -> pygfx.Group: + # Creates two infinite lines, used for "crosshair" mode + x, y = self.position + line_h_data = np.array( + [ + [x - 1, y, 0], + [x + 1, y, 0], + ], + dtype=np.float32, + ) + + line_v_data = np.array( + [ + [x, y - 1, 0], + [x, y + 1, 0], + ], + dtype=np.float32, + ) + + line_h = pygfx.Line( + geometry=pygfx.Geometry(positions=line_h_data), + material=pygfx.LineInfiniteSegmentMaterial( + thickness=self.size, + thickness_space=self.size_space, + color=self.color, + opacity=self.alpha, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + line_v = pygfx.Line( + geometry=pygfx.Geometry(positions=line_v_data), + material=pygfx.LineInfiniteSegmentMaterial( + thickness=self.size, + thickness_space=self.size_space, + color=self.color, + opacity=self.alpha, + alpha_mode="blend", + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=False, + ), + ) + + lines = pygfx.Group() + lines.add(line_h, line_v) + + return lines + + def _pointer_moved(self, subplot, ev: pygfx.PointerEvent): + if not self.enabled: + return + + pos = subplot.map_screen_to_world(ev) + + if pos is None: + return + + self.position = pos[:-1] diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index 7507a7ff2..d651137da 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -27,6 +27,8 @@ def _get_image_graphic_events(image_graphic: ImageGraphic) -> list[str]: # TODO: This is a widget, we can think about a BaseWidget class later if necessary class HistogramLUTTool(Graphic): + _fpl_support_tooltip = False + def __init__( self, data: np.ndarray, diff --git a/fastplotlib/tools/_tooltip.py b/fastplotlib/tools/_textbox.py similarity index 55% rename from fastplotlib/tools/_tooltip.py rename to fastplotlib/tools/_textbox.py index f6c9cf531..46a468ae7 100644 --- a/fastplotlib/tools/_tooltip.py +++ b/fastplotlib/tools/_textbox.py @@ -1,11 +1,7 @@ -from functools import partial - import numpy as np import pygfx from ..utils.enums import RenderQueue -from ..graphics import LineGraphic, ImageGraphic, ScatterGraphic, Graphic -from ..graphics.features import GraphicFeatureEvent class MeshMasks: @@ -51,21 +47,48 @@ class MeshMasks: masks = MeshMasks -class Tooltip: - def __init__(self): +class TextBox: + def __init__( + self, + font_size: int = 12, + text_color: str | pygfx.Color | tuple = "w", + background_color: str | pygfx.Color | tuple = (0.1, 0.1, 0.3, 0.95), + outline_color: str | pygfx.Color | tuple = (0.8, 0.8, 1.0, 1.0), + padding: tuple[float, float] = (5, 5), + ): + """ + Create a Textbox + + Parameters + ---------- + font_size: int, default 12 + text font size + + text_color: str | pygfx.Color | tuple, default "w" + text color, interpretable by pygfx.Color + + background_color: str | pygfx.Color | tuple, default (0.1, 0.1, 0.3, 0.95), + background color, interpretable by pygfx.Color + + outline_color: str | pygfx.Color | tuple, default (0.8, 0.8, 1.0, 1.0) + outline color, interpretable by pygfx.Color + + padding: (float, float), default (5, 5) + the amount of pixels in (x, y) by which to extend the rectangle behind the text + + """ + # text object self._text = pygfx.Text( text="", - font_size=12, - screen_space=False, + font_size=font_size, + screen_space=False, # these are added to the overlay render pass so it will actually be in screen space! anchor="bottom-left", material=pygfx.TextMaterial( alpha_mode="blend", aa=True, render_queue=RenderQueue.overlay, - color="w", - outline_color="w", - outline_thickness=0.0, + color=text_color, depth_write=False, depth_test=False, pick_write=False, @@ -77,7 +100,7 @@ def __init__(self): material = pygfx.MeshBasicMaterial( alpha_mode="blend", render_queue=RenderQueue.overlay, - color=(0.1, 0.1, 0.3, 0.95), + color=background_color, depth_write=False, depth_test=False, ) @@ -101,7 +124,7 @@ def __init__(self): alpha_mode="blend", render_queue=RenderQueue.overlay, thickness=1.0, - color=(0.8, 0.8, 1.0, 1.0), + color=outline_color, depth_write=False, depth_test=False, ), @@ -109,18 +132,21 @@ def __init__(self): # Plane gets rendered before text and line self._plane.render_order = -1 - self._world_object = pygfx.Group() - self._world_object.add(self._plane, self._text, self._line) + self._fpl_world_object = pygfx.Group() + self._fpl_world_object.add(self._plane, self._text, self._line) # padded to bbox so the background box behind the text extends a bit further # making the text easier to read - self._padding = np.array([[5, 5, 0], [-5, -5, 0]], dtype=np.float32) + self._padding = np.zeros(shape=(2, 3), dtype=np.float32) + self.padding = padding - self._registered_graphics = dict() + # position of the tooltip in screen space + self._position = np.array([0.0, 0.0]) @property - def world_object(self) -> pygfx.Group: - return self._world_object + def position(self) -> np.ndarray: + """position of the tooltip in screen space""" + return self._position @property def font_size(self): @@ -172,9 +198,37 @@ def padding(self, padding_xy: tuple[float, float]): self._padding[0, :2] = padding_xy self._padding[1, :2] = -np.asarray(padding_xy) - def _set_position(self, pos: tuple[float, float]): + @property + def visible(self) -> bool: + """get or set the visibility""" + return self._fpl_world_object.visible + + @visible.setter + def visible(self, visible: bool): + self._fpl_world_object.visible = visible + + def display(self, position: tuple[float, float], info: str): + """ + display at the given position in screen space + + Parameters + ---------- + position: (x, y) + position in screen space + + info: str + tooltip text to display + + """ + # set the text and top left position of the tooltip + self.visible = True + self._text.set_text(info) + self._draw_tooltip(position) + self._position[:] = position + + def _draw_tooltip(self, pos: tuple[float, float]): """ - Set the position of the tooltip + Sets the positions of the world objects so it's draw at the given position Parameters ---------- @@ -182,6 +236,9 @@ def _set_position(self, pos: tuple[float, float]): position in screen space """ + if np.array_equal(self.position, pos): + return + # need to flip due to inverted y x, y = pos[0], pos[1] @@ -207,110 +264,36 @@ def _set_position(self, pos: tuple[float, float]): self._line.geometry.positions.data[:, :2] = pts self._line.geometry.positions.update_range() - def _event_handler(self, custom_tooltip: callable, ev: pygfx.PointerEvent): - """Handles the tooltip appear event, determines the text to be set in the tooltip""" - if custom_tooltip is not None: - info = custom_tooltip(ev) - - elif isinstance(ev.graphic, ImageGraphic): - col, row = ev.pick_info["index"] - if ev.graphic.data.value.ndim == 2: - info = str(ev.graphic.data[row, col]) - else: - info = "\n".join( - f"{channel}: {val}" - for channel, val in zip("rgba", ev.graphic.data[row, col]) - ) - - elif isinstance(ev.graphic, (LineGraphic, ScatterGraphic)): - index = ev.pick_info["vertex_index"] - info = "\n".join( - f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index]) - ) - else: - raise TypeError("Unsupported graphic") - - # make the tooltip object visible - self.world_object.visible = True - - # set the text and top left position of the tooltip - self._text.set_text(info) - self._set_position((ev.x, ev.y)) - - def _clear(self, ev): + def clear(self, *args): + """clear the text box and make it invisible""" self._text.set_text("") - self.world_object.visible = False - - def register( - self, - graphic: Graphic, - appear_event: str = "pointer_move", - disappear_event: str = "pointer_leave", - custom_info: callable = None, - ): - """ - Register a Graphic to display tooltips. - - **Note:** if the passed graphic is already registered then it first unregistered - and then re-registered using the given arguments. - - Parameters - ---------- - graphic: Graphic - Graphic to register - - appear_event: str, default "pointer_move" - the pointer that triggers the tooltip to appear. Usually one of "pointer_move" | "click" | "double_click" - - disappear_event: str, default "pointer_leave" - the event that triggers the tooltip to disappear, does not have to be a pointer event. + self._fpl_world_object.visible = False - custom_info: callable, default None - a custom function that takes the pointer event defined as the `appear_event` and returns the text - to display in the tooltip - """ - if graphic in list(self._registered_graphics.keys()): - # unregister first and then re-register - self.unregister(graphic) - - pfunc = partial(self._event_handler, custom_info) - graphic.add_event_handler(pfunc, appear_event) - graphic.add_event_handler(self._clear, disappear_event) - - self._registered_graphics[graphic] = (pfunc, appear_event, disappear_event) - - # automatically unregister when graphic is deleted - graphic.add_event_handler(self.unregister, "deleted") - - def unregister(self, graphic: Graphic): - """ - Unregister a Graphic to no longer display tooltips for this graphic. - - **Note:** if the passed graphic is not registered then it is just ignored without raising any exception. - - Parameters - ---------- - graphic: Graphic - Graphic to unregister - - """ +class Tooltip(TextBox): + def __init__(self): + super().__init__() + self._enabled: bool = True + self._continuous_update = False + self.visible = False - if isinstance(graphic, GraphicFeatureEvent): - # this happens when the deleted event is triggered - graphic = graphic.graphic + @property + def enabled(self) -> bool: + """enable or disable the tooltip""" + return self._enabled - if graphic not in self._registered_graphics: - return + @enabled.setter + def enabled(self, value: bool): + self._enabled = bool(value) - # get pfunc and event names - pfunc, appear_event, disappear_event = self._registered_graphics.pop(graphic) + if not self.enabled: + self.visible = False - # remove handlers from graphic - graphic.remove_event_handler(pfunc, appear_event) - graphic.remove_event_handler(self._clear, disappear_event) + @property + def continuous_update(self) -> bool: + """update the tooltip on every render""" + return self._continuous_update - def unregister_all(self): - """unregister all graphics""" - for graphic in self._registered_graphics.keys(): - self.unregister(graphic) + @continuous_update.setter + def continuous_update(self, value: bool): + self._continuous_update = bool(value) From baf7b169e6cf01b186a2d188338fd888d4ef9266 Mon Sep 17 00:00:00 2001 From: Amol Pasarkar Date: Mon, 2 Feb 2026 19:27:49 -0500 Subject: [PATCH 36/52] Fixes bug where tooltip info is incorrectly retrieved when tooltip info is not none (#986) --- fastplotlib/tools/_cursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/tools/_cursor.py b/fastplotlib/tools/_cursor.py index 6b7946cd0..21b16feef 100644 --- a/fastplotlib/tools/_cursor.py +++ b/fastplotlib/tools/_cursor.py @@ -265,7 +265,7 @@ def position(self, pos: tuple[float, float]): ): # some graphics don't support tooltips, ex: Text if graphic.tooltip_format is not None: # custom formatter - info = graphic.tooltip_format + info = graphic.tooltip_format(pick_info) else: # default formatter for this graphic info = graphic.format_pick_info(pick_info) From 317ba0cec893cde2787134087f1a3965e696c383 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 3 Feb 2026 09:51:56 -0500 Subject: [PATCH 37/52] fix type annot (#987) * fix type annot * fix in other places --- fastplotlib/graphics/image.py | 2 +- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ece700385..44bffcedc 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -466,7 +466,7 @@ def add_polygon_selector( Parameters ---------- - selection: List of positions, optional + selection: list[tuple[float, float]], optional Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). """ diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f2d862067..a4f42704f 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -302,7 +302,7 @@ def add_polygon_selector( Parameters ---------- - selection: List of positions, optional + selection: list[tuple[float, float]], optional Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). """ diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 275cc1e47..d08231f7d 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -488,7 +488,7 @@ def add_polygon_selector( Parameters ---------- - selection: List of positions, optional + selection: list[tuple[float, float]], optional Initial points for the polygon. If not given or None, you'll start drawing the selection (clicking adds points to the polygon). """ bbox = self.world_object.get_world_bounding_box() From 5721c9b547b6c39694725df4c863eda4f5c8a704 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 4 Feb 2026 10:03:22 -0500 Subject: [PATCH 38/52] rename test that was never being run before (#988) --- tests/{events.py => test_events.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{events.py => test_events.py} (100%) diff --git a/tests/events.py b/tests/test_events.py similarity index 100% rename from tests/events.py rename to tests/test_events.py From 0d754f5e4073300460eaa390c4decfa04264bbb4 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 6 Feb 2026 11:08:03 -0500 Subject: [PATCH 39/52] fix typos (#991) * fix typos * add rendercanvas to intersphinx_mapping --- docs/source/conf.py | 1 + docs/source/developer_notes/graphics.rst | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index edc172dad..52e203e9f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -134,5 +134,6 @@ "numpy": ("https://numpy.org/doc/stable", None), "pygfx": ("https://docs.pygfx.org/stable", None), "wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None), + "rendercanvas": ("https://rendercanvas.readthedocs.io/stable/", None), # "fastplotlib": ("https://www.fastplotlib.org/", None), } diff --git a/docs/source/developer_notes/graphics.rst b/docs/source/developer_notes/graphics.rst index 71d99854a..c774f1883 100644 --- a/docs/source/developer_notes/graphics.rst +++ b/docs/source/developer_notes/graphics.rst @@ -56,15 +56,14 @@ For example let's look at ``LineGraphic`` in ``fastplotlib/graphics/line.py``. E ``"data", "colors", "cmap", "thickness"`` in addition to properties common to all graphics, such as ``"name", "offset", "rotation", and "visible"`` Now look at the constructor for the ``LineGraphic`` base class ``PositionsGraphic``, it first creates an instance of ``VertexPositions``. -This is a class that manages vertex positions buffer. For the user, it defines the line data, and provides additional useful functionality. -It defines the line, and provides additional useful functionality. +This is a class that manages vertex positions buffer. For the user, it defines the line vertex positions, and provides additional useful functionality. For example, every time that the ``data`` is changed, the new data will be marked for upload to the GPU before the next draw. In addition, event handlers will be called if any event handlers are registered. -``VertexColors`` behaves similarly, but it can perform additional parsing that can create the colors buffer from different +``VertexColors`` behaves similarly, but it can perform additional parsing that can create or set the colors buffer from different forms of user input. For example if a user runs: ``line_graphic.colors = "blue"``, then ``VertexColors.__setitem__()`` will -create a buffer that corresponds to what ``pygfx.Color`` thinks is "blue". Users can also take advantage of fancy indexing, -ex: ``line_graphics.colors[bool_array] = "red"`` 😊 +set the buffer that corresponds to what ``pygfx.Color`` thinks is "blue", i.e the RGBA array `[0, 0, 1, 1]. Users can also take advantage of fancy indexing, +ex: ``line_graphics.colors[bool_array] = "red"`` 😊 to set the color of specific vertices. ``LineGraphic`` also has a ``VertexCmap``, this manages the line ``VertexColors`` instance to parse colormaps, for example: ``line_graphic.cmap = "jet"`` or even ``line_graphic.cmap[50:] = "viridis"``. @@ -73,4 +72,4 @@ ex: ``line_graphics.colors[bool_array] = "red"`` 😊 callbacks to indicate that the graphic has been deleted (for example, removing references to a graphic from a legend). Other graphics have properties that are relevant to them, for example ``ImageGraphic`` has ``cmap``, ``vmin``, ``vmax``, -properties unique to images. \ No newline at end of file +properties unique to images. From 7c2c7c8e7979495db6eb4fccbe13937f4d9942ca Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 17 Feb 2026 14:46:52 -0500 Subject: [PATCH 40/52] make ndc pos tuple an array before adding (#992) --- fastplotlib/layouts/_plot_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index f83dcfbcb..5d38ce37d 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -341,7 +341,7 @@ def map_screen_to_world( ) # convert screen position to NDC - pos_ndc = (pos_rel[0] / vs[0] * 2 - 1, -(pos_rel[1] / vs[1] * 2 - 1), 0) + pos_ndc = np.asarray([pos_rel[0] / vs[0] * 2 - 1, -(pos_rel[1] / vs[1] * 2 - 1), 0]) # get world position pos_ndc += vec_transform(self.camera.world.position, self.camera.camera_matrix) From 0d4c5b9e98f7a7d271b236b49f5e56a9b21727b7 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 17 Feb 2026 14:50:31 -0500 Subject: [PATCH 41/52] Small updates (#989) * update readm * update year * Update conf.py --- LICENSE | 2 +- README.md | 5 +++-- docs/source/conf.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 33e2266c5..540c35e42 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Kushal Kolar, Caitlin Lewis + Copyright 2022-2026 Kushal Kolar, Caitlin Lewis Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 41c1ba72e..da5ed64f8 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ For more detailed information, such as use on cloud computing infrastructure, se We welcome contributions! See the contributing guide: https://github.com/fastplotlib/fastplotlib/blob/main/CONTRIBUTING.md -You can also take a look at our [**Roadmap for 2025**](https://github.com/fastplotlib/fastplotlib/issues/55) and [**Issues**](https://github.com/fastplotlib/fastplotlib/issues) for ideas on how to contribute! +You can also take a look at our [**Roadmap for 2026**](https://github.com/fastplotlib/fastplotlib/issues/55) and [**Issues**](https://github.com/fastplotlib/fastplotlib/issues) for ideas on how to contribute! # Developers :brain: @@ -152,7 +152,8 @@ A special thanks to all of the `pygfx` developers and the amazing work they have Fastplotlib is free and open source. We would like to thank the following institutions for helping to support fastplotlib over the past few years. - UNC Chapel Hill, Giovannucci Lab & Hantman Lab -- Flatiron Institute CCN, Chklovskii Lab +- NYU & Flatiron Institute CCN, Williams lab & Chklovskii Lab - Duke University, Pearson Lab +- Columbia University, Paninski lab We are always open to new sponsors that can help further develop and improve the library. diff --git a/docs/source/conf.py b/docs/source/conf.py index 52e203e9f..ead9f05c4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "fastplotlib" -copyright = "2025, Kushal Kolar, Caitlin Lewis" +copyright = "2022-2026, Kushal Kolar, Caitlin Lewis" author = "Kushal Kolar, Caitlin Lewis" release = fastplotlib.__version__ From 08744986ac81ffe2c67c118d85d2b2900cd67ba3 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 18 Feb 2026 07:59:47 -0500 Subject: [PATCH 42/52] setting initial scale was missing from `Graphic._set_world_object` (#993) --- fastplotlib/graphics/_base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 47673cbc0..5279cf306 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -318,6 +318,10 @@ def _set_world_object(self, wo: pygfx.WorldObject): if not all(wo.world.rotation == self.rotation): self.rotation = self.rotation + # set scale if it's not (1, 1, 1) + if not all(wo.world.scale == self.scale): + self.scale = self.scale + @property def tooltip_format(self) -> Callable[[dict], str] | None: """ From 9a8a1b87c6925d6f3afcb871793abe5875e24f34 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 23 Feb 2026 13:05:45 -0500 Subject: [PATCH 43/52] Adding top gui to fpl (#999) * top gui * fix * proper logic * allow for y and x offset scaling * also fix for setting by pixel * add screenshot * small fixes --- examples/guis/imgui_top.py | 61 +++++++++++++++++++++++++++ examples/screenshots/imgui_top.png | Bin 0 -> 18432 bytes fastplotlib/layouts/_imgui_figure.py | 10 ++++- fastplotlib/layouts/_rect.py | 30 ++++++++----- fastplotlib/ui/_base.py | 14 +++++- 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 examples/guis/imgui_top.py create mode 100644 examples/screenshots/imgui_top.png diff --git a/examples/guis/imgui_top.py b/examples/guis/imgui_top.py new file mode 100644 index 000000000..e1f865fe0 --- /dev/null +++ b/examples/guis/imgui_top.py @@ -0,0 +1,61 @@ +""" +ImGUI Header GUI +================ + +Basic examples demonstrating how to use create a header gui +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure! +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + +# make some initial data +np.random.seed(0) + +xs = np.linspace(0, np.pi * 10, 100) +ys = np.sin(xs) + np.random.normal(scale=0.0, size=100) +data = np.column_stack([xs, ys]) + + +# make a figure +figure = fpl.Figure(size=(700, 560)) + +# make some scatter points at every 10th point +figure[0, 0].add_scatter(data[::10], colors="cyan", sizes=15, name="sine-scatter", uniform_color=True) + +# place a line above the scatter +figure[0, 0].add_line(data, thickness=3, colors="r", name="sine-wave", uniform_color=True) + + +class ImguiExample(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title, window_flags=imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_resize) + + def update(self): + imgui.text("This is a top window") + + +# make GUI instance +gui = ImguiExample( + figure, # the figure this GUI instance should live inside + size=30, # width or height of the GUI window within the figure + location="top", # the edge to place this window at + title=" ", # window title +) + +# add it to the figure +figure.add_gui(gui) + +figure.show() + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() \ No newline at end of file diff --git a/examples/screenshots/imgui_top.png b/examples/screenshots/imgui_top.png new file mode 100644 index 0000000000000000000000000000000000000000..f83f2f49930a5a5fb9484be0dce735540527f55a GIT binary patch literal 18432 zcmeIabzGEd+b%p97?gm5k~#>Al+uj~2udkRr*wCxij)Y7C`e074doC+iF9{&OAg(! z&sop=eb2jp`-}bUKfe9XJ8La}u3_fB@9VnG^Em4`2z)LtMRb|!G6I1hdMYiUh(Mev zga7VdJOl46WpETB5K{F|B_6$UiCGzSb$KQUV?VarYKCyutV$AA$Jsz61XH^$HaN@rG>vy`MX_yq~%H zZwhliKH?$y3u6<0n~8_}DRy>tbZ&nW%F2ZOZ~2qZpVU!XJFE<$c=PEe7q_PO@pjceuioC?#>PhYtBRJRQnt9)W-H%|7cUkT7PgN* zHr;nKZ9?NAIvlv?wrVhrOP4QSmX8-bUaKYvYRG+E-PGKS7ArqoEiEZ2p=8$_$TyUk zZ;Q30e{ti9#K}fPWsHb>;m@BY?QtS!@d1pyY{4ZX; za+vu2HaYor_Kk_T+X8&s)zvkONiN^>b?ceM{lF)(1_(q{R5LLVToE49 z=K_hmf;&Azt+iIk^bQQCdo)(8$LX{>aCB=K6>_^e(7=Ir>n&bN)TgqmFD1KGKrSHlb|grGY$s$z=0>)Zxxb0ezNXb2vO* z(U@DN<+4@9{@nQSsx!U!(fa#KMQIn&HhSqA4RE-pSjJcPeBbl{hHmGDmj?T&yOx45W=sfv2j09sGZHyndzmz>}W3IHZTxg z*kjA#!jt2}{-akS)yXL-!Pg&5!7(v~nT{0O;`=0^;zK@uY-w-r*NzvkpZnuW0G3-o zuM1{-?bh8U3t6{q2pB|^@)x}IqT=V zKe!K^7rGywGdLiip|LTKUibGKyho27-MDdM$C^&SZYIus6&|i-cicNZI$FrE=_`X& z2sghX*cvG*>4)>&tGKhd*hWZ1BDyyL7Z)(?O3uyAZE9*#kG{Nkg9(XD%+KEdYvLRC zu&?wwripqWQDebd1h<5L={>hzWGZJx$FZa5{2Ad| zaT1X&cmT9n?=$RU*vZEXPey7BU{BwWVJ{J#9`sk57$Fe99n0?EpfLjZe+`h2&ms`d zAOC}9XCBc<;1aIF?mJFi6_j%-A01E@~ zWT!ieIAGvLjO73fi;8+}{q4L!LU%M1rU3W9BNxT(vNjPA5CEr#(kgXo4rkk*_sh@E z7xg^&c!Mhyj*O9!QKQgef2&)8<=#Cjuu;6T_!dLIOsoBH&}Bt)2O)Tq8=;NDV8EKE z*6Zo9Wgia%S)N zxHmj|zn1eT0OKLxMSC<~yBV5^kr9B~s__dWz~0hm>8{A`NLX}qX8j(X4gKS@6RoTi zG5{V@AFq>MzyAHp7dzO-M?UzqwY4ko;k@_NIU7M&XciYa$$mr5% z#VdxGyJj>yJ9~Vv4R_X%mu~}?l$eP!M?JVs2mpX2qoytetZiv&SzB8JJh@6q*?)vP zJ7TyfgfStZ!-*=NY>gs{P&mfGhF%pNHsrYXsV8hFpxEKo!b?TP#Cp{;HsrV!K6~~I z1kPU)&(SmVsji*pmA@9}>}D=8_#e(fI~&cmX> zoYb-FNJTX@Q|eR^e!NMvcK~?+7~`9 z=2Zsgz{E5MD7d}QoyKQ10>&%`!Nh*@kFP&5Efu%PbFgLH`(SF@E5p0rRJ0#Z1uKYb z_rL?9yNSRbaVQV|1s@PG@5?gJDK7*JxOC~#;Naj=e~$H7SxJ>{KyWZxl@$!8+uZ@L zm;z^&;P;TuiMsDtXliO=$I7`lIYVd!Hck$)VjB$%5Q1n#J>ZlE0EvaLWnr#xQeA18FZR_vk{5%}rE?golHTB@|kc5Um_hp(qxX}aieYikSK5pen*3yXPos+@Wy0R$nw z>xAyRtE1)aMaS7!V7ag@Q6la;Ym?Q0FpzxYgZuRK^cb~985$dtQ1dQlWTFmshOI4w z$q{AOV5N3!>c95DRLRN6@QJ9nT{mVRA9(bTy6z>gtY zQ}Xce(9@gB@jhPqBzka{m6i4G-G&4W-*7e!tAX6tuoR6VDumzeV( zQ3T<3e{ymXpaeYA(UT34GijMnWrj98o;Rn3NJQN6N;SI=oY33bJ6lTHI30z15nsHh z>v-1VZGv1hFItqOpInVK9Do|E=tBcU5Xi&%x9||ZnU9W+KEgeI{CK#;)z%GnAiBXh z6U;YuB~-1z$Pea;OI1j1q7 z^1_sIrqa?$xYZgS!iHUbA0p)cScLqi82kTTvHaJE|8xEJ=@Jo`0l{px{&pro zYR-T}N*L4xka8Kd0CKc8H2m#J?{10Yx$n zUQNKypFeAZC_m8(?f8(0;=)KnD8tf$i=?D)H368()hFcyHd}2%*sl>0l^abgfIve+ zkp17~;1KiJ%MGC6xA_5)56YGu`Wu`E4OwahkfUZRn@dYe>+1dp5I*>u1jQu2Pn7{@ zYb4hl9v-DM(ldzPGTbE{pTB%TBH<@H@-{X$kZZcSmIT0UR8&-kH{LU`v*TP35~&?) zrrWnoIH0mr$x@v}ZNTc)Vf8()l$2)Y=Z(3kBx}aU$J^U;GBTRH-ehHEef@e>Lq;5u zY6$~K`r*RrGUCe0;a;<65fhBUH@OV}bFSb20l6wJlv7{1VzW9TaEPb)@};FQ{g*Fa z!dcajNC-z-6&@w>jPNDjC=-+W*9i-`t^y3u@|g8Nl0r^HBWyb*Q7~8rB?w0J`qis5 zYtUBUmn0`5VhVdI0Y#eROf`vE*?Ju{u41Irc7dprqgk>6Z}yZvL?Arm;iCi;tZSQ_ z02EhAGkXOg$p%=2*boP4U=T+|N2wru?ge}<-g77N_*9*hV)YHAusU)mdq{>mxJmzM z1pJp<{b#rO-?$q!Kb`T2ppc(FOCsEnDo0HU8DpA4yw~AM5hN{JDDbH9DZGRPa2N7B z@`S|1?Ck8k*9jq@Ks|x$?~JsZeJs8?*XrZrQ@KAEjSFJ{5n#g5eQ_kxeDPxPA0zX< z@ZJw{&)>g)LkJC~;#u5U=!QJP_To8lKf*{Jb7d2gpS@;8kWdg3UJaF^_v9837}UN0 zT{abp*5uUGMLxKfi|_aE*OT&|O8G;1kA*8h9ogE{B@3j`ZK>@vHvG-s9fuuL=%NJrgVH4_`EG#VW zv%ap5933&z!4i98HMPjTuHWacfQvlpwl>XmZWq~|nVVzR!u&$vfzy2m!oA@8BN|}S z+RggvRDLA%bl0vqLdFkw*RU$7g^ZVrs{r-Z=iNIRZ79^z*4}j7w;0G>pRSJx52qsZ z12BU;DqA)0F*Xs(IRUyWRLUq#)BD#2ot90|>fK#k5YdgiA3-FuAtAX)1nvy&qRcTR zK)NND9jFHXqg?#2P)7E@>P8>kC?}N2Bm{$0B_(QZD&ESM#eNa4Jr{bDX>_(A-bxnJ ztXElRMu@DJEL>C#^Vb!hZ@x&Rl2O@UEr|(y736TmRKJKlQqm~k75wADmM>#aI@Zg} zJe0h2kL<2pth8?MQ)7y!l!HjS4|4k{bwPxr3(Q^BR+vGiQQ1%eRy*|5D+@R6!Wwvx zD=U48bH)A`&4jn?E%w+9a%JW1M>@MnIR3K<=| z6e2lmdnBSmQc>daQmhqP9-|m~HPoPdslRh% zh%95jYe9J_nPqF`sftRmwM|!~gp0 zlHwZ}kouCy&_;6hkMY?dCQ+7rsRLYTtyl9DK zqjZRA^hPKHQb#AoN{?>=DS^6niSp8wOV>)|lH;YnG}whYJ5ZC*-VVAQNO<*9`JQ}= zbe?on0|`)=V5Y6!}HJ(WKlp%m!+`7efb&E(259DwlDGmdmQ(Ohiz9cy3mXo^9X4#NwLIB zXM?9s)e}rAV6(9PK~EzlTdCwnUBV`Rn#$m90UAb?4>Y7|Y@7uNPx-VByn?H19*3qN zk4Kcty$%9d%eS8AyfP?w+`J!*DWW8xOqQdsyYxr%Z2Z_nqCY_(iZ% zAsXFtr(Zdzej%`#OX~LYhiDIT-d$RX$kma@(cf*6%n?=+oDy?7bC;r`*jm0JFG$oC zqgI)Izb@)8*76#s%B!9^OKt0qM%&I8elgc@m@oc0m6~8AZCT^D`Xp4!j1_aC%u{@x z;3825jgz}d#Tou{t{n1H`s}pbEW(;g9Gs_Jv2Q7@Ys~$6Qjnh1hn9_Kw(7vP?BYh% z+j@4|(;vhcA|oVFm&gqkUqy0CrpkYNe{My@|FfH~3TbbY7{9_K6x+|pI7U|l%)?%o zUs=x&-lOSXo}lHmWD1pR+Z$r4^(vaLVR=Y97hqguH;CO~lI<9?SIw4H3IAn5qi!zj zZYd`pzP-dKrt={v<)`$AAVv`_<(xP!2NOR6r?qZRUCV7+Xov~W+!Z=5E}VNZ^0G*R zr)OkEVBI6v>B{ulah$WG^y7C;R_!I!YW@wgp%=XM>jhVLHZ~2b{c0M+v@nH&+*jy! zY6|p&C}mzJsAdzNQqYQ8P;oa%rjgM^(Fi+Qo=-@4wBzp3uZ7Q(S{L3>ZRg|k+qh`h zUYZakRH&*sy?EtQqyyi`O#%vOH%zp%I~CmL=>=~*JWgknfdM>>T&2zAT4oN$?Ni*} zsicejQ9>%RsYB%AdWo3|6+Is{=?V&`kdLb+hw$R*N=1UJt5WK@soJCOXcS6o$6ckA zvCW&AZS2dQ)^=}AxzI!`Y$wC*ltsgT^V>J#;Z=#SMxs&?>cfWF2PLl$w$w?!QqqfR z@_S;?uJ(L14Hyh_gVMon#MTx*S#?cdkhZ&AVv^RY$BB1P*}38-S8j-DE9ac-zfl-o zu^dD~7Wmp0aX?mFd2X z{!}e&A>&X@WH<4!)zUe!(ZOY1k1w9H=#wU0?j|*VZr@YqMk@|x)6R`qPJ@6WbRfLfPyr2W}EjHtjKWFt5Ek zk1l$k8RL+eoSf1ezFj&?vHMkDY$58PaW0Z8<=3x@n$O>h@3P6sGDQk>6V1A)LXKbM zg$ZV1lY9DEYC)Y`-ZZ`C`()Q+Gv2H()WW`ZeEpz%{xpmUMlkx*b-|mP znONJgjD&=^Zg>6qMVCJtU)VZfo4dNqD46|8WD>nt)a~qAne^&w))q4Z!|s$8+^uJO zae5MH-ls`c(qh!u+$~_oc)P~}S#kTpZ657mqegUQE5*LrPc)C-;%wSA@D_WJ%i ziiha7U$wvYNp}>_YI;MMo%pk7vF1yOl7Tza)s9Oe&qI!r8Tec-LoElT5loR*p(j*O zP@oVeEZ}VA$*0BQi|^a*?oY{jeRDCB{YJbWob*9TdbQWfzn`gBF)|Sw2FC|rBU813 zOkoxS6DM6>gJiz`^x7EiysWI9r)ery-vyOE1W`*94#g`fj;KZGTm8II@=OLia#3qr zbG7SyPFsyG8qUXxC1YpArdy8FHJ?@UZhE?0O5l>7iOJ0e-&?H+vWQEnHmxMrMd-+> zzJ!IrBUT=5G+&@|nSf@0YfDRvu5)doPh;2}^Mu!SB2pNl*fF6plNWlfB{U*tOFK2V zVZ<-zob0Nkwvk*nG%;H4`_29RZAScI-n(Kti*a(O0+YYbhg0Ui7Pyw2!Q#FAwRVtmK z<`5~T)J3z4I{f`o^CMfjwwBW|qqJUGqR_&0;$c&vj;(iU$(CwKh2>}Mm)6^6^+zgP zd|$(x%3K9|j3}uVWeDBg`^DRfpPuxstsOAM!rCZh9=?e-CuduVZVlNVbK)F3i7%B` z7#JAmw~XR8?ffLxN2WwTV3_9>t9P`d6G+KEz+RYbe}eJMoEmsFeFb;2VmiA7C97m= zcQ|#DOF|^M@!-sqv(<5v6&QMcX!!m>zb!_fK(F@lAs&-ll*8&s31rYyQ&TXSfL3Xq z>NDtc1_jAHd)5{!#0SlJnTpQUtvJD+#XUrYZft-0^9G6aX{>Pp1-dl9Wt zNI*nHgZ`^HRvUy*)kdUISX|0mn%@6VuOo-;wry)vwM+zWu1;fne_FZckxOH{&e!n1 z-!W{gJ+n+n5~0=W>*_NZ1&+D7&r_G|#T=(T-}2XSOMfe1&beTRzxZY3xynoz_A$E# zgXY@?39Wl3oWe}pSz4t#HG$+sqfXHQAE|htg}4MoZ!Z1sO9_)z7tGZ{9WD_?&^7f( zP;WJH@9#fbiNVCgBRSe@>DjZ*B(H`1WnJ?8*k^biIt1m!6v1S^K`= zv~mto+Va2Xep&K6yf&+i~uW0s^aBaLB4T}MJY$Q$?scyA3Mu$Nf;bt<_ux- ze_B`CRTHh|wDz5h+aP;(@AXei3^}_WXwPD75?(3sjQG7$UyKuaY)M02EyKa6UHn{z zJRl@ZUc#X6qvW`^HI-P!Q>}(?E~ZjUL6TeiZ2j+DWKH%ePY)}h`Dq;I`mbU8%YmOM+DW14%)cZY#Nzm2B8_h}jn5&0d3isb1(n5Y29lI6_0l}kG% zw{YfF7$>l0Vp^O`Ubj9Stxizr`Q7R!U-9vBBcsaD9q510^mGZ^PrQjMLh8i05zXWC zY1_SMKK>={*HC%F$jtmDj(vYW-_<_sFOSvsVye?El|*Al&p8U%ReTaU4;Z;X>$qL) z^k{E=`T{XcamlKA4QeJ_rrxsS-FXyM%0X|qX0EE%6*n{mr2aii$|7 z5b0+hST%ntY<`rZg-_*g+N%i3ogL*Ea|n~QplV%lI*#BDB`B%;eBEsNLBgZbB~Bh5 zM;IVc^IAxwcCYMQ7IHqAjn|f|+1};07$zg4`t`R@V^lL@n)h$7in+CZM^S9S6F#yK zjl?{C%9YWjIf=h6fi9XcE#KGeDd!j&#ddWnY!bGx*tQIs-oJKvZ6gzP`m+aac+aFi zn|5!R5@#)Rop1N*`W8YR@#PC=8?WUk=eBTq^_*!pjN*3CN}mWyXs`*6+gMj(h08=p zvMX0QVAhU*p>)G4Ipx5Gyc%BD6QT!Rei)ozm20SGm4_E7QxV)lvWH>CrNQpV+{FLgV+mp=U+2Z=@F*-(r6zQ=sLZ(`?@$ ztV`W9(H-XFLvn{n{!_4{ad5$mf-S6GOlO_)p^q6;2M7H8DISL_{3>H2ldjZ)MGV|7xltum*EEp+JFiJJLh*^eMsjfk zYLbZFZH+FS=g*q6Qb-d*QW>u6)z@eC1!ywiY7))0X*$;*SvUAlI|Uz@x+k5-(QuT4 zcCKqy3X_l7SJLmITqJ4-WH!s$Ki%89_#VGqdGldk3HACyL?Ma$0Pjz2)L4=`e3<;o zS~COtOT}e|h~vPFXQ5}?yxKme(cK;HaZ~OzqmB6X?aO!7#r%fZ+~LgzG#W)vBFi-F ztBD#&>etk8bx_v2NHq4_iy_mpCN}@HV4TmzaLb9`5G9GKP&q<-a#%*MH;ihFr|M9Z z#Goy1yH{DY*EuP7Qk2knsp{#Qqq5}(#RQgjgk3DX_yWs9Hn;iI`bZd1)6r^(Em|Jny~o3j^oGV?xbzCJs-H5C4u zqj^_e%4Cg>nB`4jnzzJEUGqK}8$164ZnFi@tXlsPLlg`ET zs3>#ul%XNFqz2j0UZj*+4KXd9Lb|_gu$G4P)yTKtKB{Num;}HStPc&+>4eGYW=N(Xa~pa zY~@ht(0>a2AvU|Ap@R}wtd`ryciM9wM%cR=t;Ex^vm=q$oWkUztr#TVNel(m^ztrH zd|2)=zAgNz)8yla4~Z#N!y`BIAKpIhx{o?$A!W`#G)6smI30CZR;L=ejdbNn{aEdg zDX>7*|7Tgw%rZ+c-a*vvt7c}@e4$ya17lWp#G<#YxQZG!Ia6TNV&%1)v-XlJzsO{( znPpJsl1{pGMS~mt7-Fzny{#zVDmV9N3$j*}o^n~_ZayaDx4M1TY&!bh;l&jR1zXD( zah}>D2mPKOUo9o;Tb7flo2+W@D}m|0-kKC~OEudP+VCkOGxm#QZk?m-3<&5CL$v0)sFc3^$4cdZYt*xZ`#-2bl^fY41f|~R74R} zt9zeD$Z1)@`=I-i*vU_4Ett(wKrE~0kOfpziDVd+ zMe|t$5oZ}#SGZmZuq;Dph5EMF|GPm#xi3tQ(f_phWy6F-*5RI_qM{W)e>RwKAMEc3 z;wpLI;N*GiZveS3OS>H7j2(j^aQf0Ta9xRkhVkYlcLFbd>kd4WOngUzOvAp8F4J-M|gu127&OQ%w8&>@Ls=u7YU3O6Fa;7D&6~$vOrmV zDks-#`W~iZWH3Z;0hW~nv=9v3?+6P6u_-Y*SwaFvaWKAPU|?7gBcr6`g8|#bgzh09 zaB+T70?`UMvNkHXIUw7ycr|bju$8AL9UUFefdKl94mdRNSyfgFpQ<0_05LG$H$xAR zdFWX(+*nwspUFXAC4Vz)?$o+LdG#=SKpV2F%ysk4n>T;{JpS*;^oc;V(Ww~GV!%VZ+3gnN_5ju?j*l8d!3J!;U-nn1 zG?xAVZae{&6wq3Mbreh^(4~7Hh_Xc}>w?-4_00ccf^EH#3bahH|-*lBV# z6k|p}!uwh-?`IY(QzlN`AvXO~4ai>7nMyS}smltyIkxCptJe z>tF;iCaz)Cz^-ozbh2g{q z7oMH*!gTGegluG+=Bd`(Es&u7gWQ1%c-t5f%hyDTf z5g<`iNJs-qrS%7R&%l)j5QAk6{xS^3t}LB0hdK>880g+ zu&V$13k_9Z7QzeI<}sq481lO6a`xRV2~EurV2^o2=Nl+euujPQy0>|FF9ClNG8ZAh zF<`v{53CTN3Si`~*4O6a1qE+tCF3a8zfOU)xB_w`36ZA*f%2G-;6)?0J0~19omcDN7MAMxgHQjyx|#u+WLcSrPLW4_dwq- zfK{`&!z&(DK4=o6P;xaCqBqd{IVz93^zy#9&cU}?=ps^A$K0{Ut=3)zu%P3^U4`%W zCqa0zQWs2ZDRo;r3zm*!VmCJr7uzOvngGA_T@js~DKOx4+*i;LHl93rGQ>{YHFpA+ zhX4R|!PfBji4zcIAq(~ys}&psvOk^cOeh>Cuwl$dj-hW~5PyhM6$tFQZ~c9prz*NX zL+@KlLOl@& zydKc%jJsn+%7Yx4e5$Toy9TiwZ8zHlF&_qUfI6-qkbv{yzD}s)ee4L3ZfKZdZ-nub zsRkw+2u5sI(EWmw?v8dPY($Wp;6f%afh7&#Yak{76;$q0bI&-ug|j}7eLH$rMH}E6 z_XC68tTzLUa!62E4Q-}mWHbllL`j3OGFSNCC*h5Nv2TLo)YfHrudDq)h!xLo`&gsL zx^$)RTIosICgh;$9rrvLbWrhmp33N92gS9&dMt6xcJp_)p?;y68kLH8lpeA zRBL;CQLqaz#Vl8?!MB0@oVlY(&tVU61$%CGrvxb8klLO-a|Rv&s;^9<&tYMJEeoER z(5#x@T^>v-Uj=~##5f>e0sR!r9zZTKDhlKb>N#bVEoSH^Qc|Gvz!tQNy3a)#14kL$ zdl--bFmk#cBx8|W#FDSP1TYvzRCRTAf#)b;F`5uM#CPuGTH5O@ouokQ(wMJs z5-9K-x4EMbHhj$kaDJmvLQ(1WjPzMYr6XtLGphO`cCe|*oOX*X|%Fc`j_BOEO zu3!J@9E{q4m=BR=6O0aORZ}o+xCHP+!F#8GxH~lkW6Bc4W{PrihkW3tIRUXB;06-U zmd3_QuxEsXzf@Vl>4wB_z`m6^hB$)=3j~Tm0Rg${+3MB50gmkL?VACHewo^Xg@aCE z1d>vfJ(jwqoXQih(fd@vP`<{4_;P0z-LG8=unmb0q=9M#CfW<2A;|zyCisz?w=g4?*dnK1VDdi3Gyl&A(3y)C235uB<47T3v?n-FM#F;VjFNAi3tccV@oQ% zPrZgm;<-(`KoMEC;r7sy>)>E12MF|sVG5#=Le5r-ieC!*drqjXUIqTH(u)@f&XMfv z5tT=?pr{+H^saQeHS>iL>Up)qW7GAiTCfs$?N)T;0!D%4@*e1{tj8-902IODK-oBn z#paCif-}HGWFp)C2v(<&Z}8N?VGE2-2*ifKnuhhMAHIZ5haqY&2(ECOa+zajQ$|6- z8#=8N?3y@9B2GPmjgn;o#gdlR%67Rm6b^;3KAhyxNg=9Mud$(_qx7zGu5}3N=g$tK zBGk2o2_RU*`iGb=#m62R6%6X-natlUmxW{>rvU|MS_kz}uNyx2Uk_;QU_>i%o1dSb zmzP&eY@Fp&&Iv2YC&S$PMw?8pIHwC4rt0JY6B)#^xp)MNoF_?6d3O z(eo{lNm|s~pctd1prE*V)d36+!ZAqDHV@cUUcP(@^SC|Gsg1eL$3k8rG>}37%7>#oNbsk-2lR6&xqxO1x84DzE8rktC5|H9>{tI1_q!thEgkFVrDk|^ZpXx#wt`(P(PWOWk5Jl^YA!= zBeUd6w>CGs+m^M9Z4xjT z3}iV_j6ltR!!jtdV!5Bl^+O=;hU$bAWQ=yrYyfIFrU>BR7JCyd=)d6Xpt>f+7l-EG zRBe#wYbOZ!XY1jnKM_3%BP~{NhpF`wX1~o5~Ga%E;aobr|Q&K`hY=nKG z6LQJ|Xj942ev$;N7Ypby%B!^!L1_t~)jyODHJerOq#tA)fC7W;72B-;c}SwH!+Efa zr3V3Bc=#NHh^`s985kfA860;Kods5}P*C&%M1gKAyOcZlQ_Jz*-X742U`?i!M?-TJ zed|GEy#hMVpFfBE=of{0wr5I4#v=3r!7T87Ci^t3VeNgGXIr8mVzTH?g-S3!`^5@W zcF+I{1bhk%WM*Lj@C~cg>nxRx0R2Td$jG5K1Xx}QT45Ga zLHY^i1lmV4ER2ilUS4B;U(X=kutWEo5SZtkrG&;1n4oV3sw?2Z z$0Wd{BCsrbTNHZIckfDL@8P7uzsCOWg-MFzqvYAEF4LDQMXXmW(!A}p#%~Nv7C!M# z+wVDe;$64^EuA+qon6q7`Rbw}MkL<@5e%gN>}+h=;AfDnmOxHFb`1~lBMoTEm6h2q zT~vSU9C*AQ9FFFcoT<_nl0skX9AjnYY1i;hv~`p?cQDFVtgkBlW+m1v$gy{)rFy=Z zde>J4$;`YEcqc9(D5$xyQLV(z(9UiH*C&UvWe$*-gN@B`I|o-GL8XaQR96=S5g-VM zSB^oo1?fwmw3pWjVCC{6Cse#YsHpINne%%7Au2r0{`u3xdUN5SclBBFgA4da8;n9B zGhW`9WKu_2nvR{5(gUHg8_r65ADx}~riM{9mPMmGO)NR*E?gL{ym9?Hzgf>uhKhHZb3kF_x|Roy&ZsPoh~R^N&pm>PP0 z=TxM;uM#rK;ad*|Mm#oKp?$eO6~v*b{!mv}7uE^MtCqU@0i@^V)^uWCN1$Fafo1`S z^dMb>k|ie>rUUFM`Y6-2-Q6W;tz~cLIPy?Y5xt|>_1ldEM$IwSAv*iJ+|t9ZgQV+C zgUm-QA!W)#wSz<;8l3O4xVX=G6Cy$d;q~{`kd^JH1{^^)56SHCH!z5Y)0)~Q_f?dm zTfepbdFM~O%qvx+dw;}DUxk#JosEQ;@Z!ZaYl-vpA`gCQSr~|%{od4CFm}|(OvyLk zG=Nl$DoN{m9mQv@-Ol&RYILagJfuQ%kaRkFibFXF?FSM%VHZY2kndc~NSNH)&nHi7 z_ulBODsVOAXf5U`Q(OD|R*W7?@$IXK5O!GpL2DgJ=?;9XIs4%xUSFH*A9OXbkn{8( z^$Rlc@=h+s;5&u=ka*iV;vF9fX%D}~LMZ^jiIN(pX%9}NqH6p4DrA4Do%V{zZhKE< zJ00CsXarui?=9G@81=|E*=$S%&n+aqChO+;l0j6dGUVlWRUP1Pwb-A66yl<_b)mpE*-j&cF52yF4w0J9v_2X^0%LNnS^9iaIRr+^E{)^1&7|Y zX93ciMz_TAouV1Y$VZQc%=Hvb47c03f)^Tp2Nr3D$BW&d>3u=yHDo39!_}M*o12sS z2G{k4Rsy}p+M{gm@`coBNQrmr#ZEejM4tSIxn9@{YRJK$J5N+fL#o_}WkNhk1`Y?EUrher9BFQZ2=>LbyV)-xLriiR-WdCxQO)2K2nG z%eR$%bO6)HdFH~K7tNaKDJ@1k@}(uT_xkJl>FB*?owWpa!(|Rf3wJ`?sKw>wakcy6 zfw`L-CX)cHHs=od+_`fB(lCK)Qh^4zMDTLj>x1t)-iM!Bk#?uCl@)(#CK5H+y$3CI z=F|cKHG6wk^tH9efPgl$ct#=_5>`kW;preN+<@X%A{i91FlL0`opGBTf!IpKRJONp zw70Ub-(&ASL4P15*x-79VGnIVYUIwvm$PS}O^%t}7{OG2ix+b(5n8?z=kn)-P9_?f zI$*8_hK41WH<0D@mT6awpU!WJ!kwTWiEG&%P@Vx@pr2dRl*Z_K-8C=wv7tfv(>Y89 zS8las@QM6p`Zh6Ec)RB%{Dv137$-uoY%~yrK_L8Yf}Q2`av>%aoRht*PRcXi9Wu{Z z(1K~1To5@OHQ0m)-ao(bCLJ3EE{>p9|DV&h&77W9I`^e#tMx1wd<*eZQeFb}Soht3 E0|`;K3;+NC literal 0 HcmV?d00001 diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index d54be4086..33cc6d925 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -195,6 +195,7 @@ def add_gui(self, gui: EdgeWindow): self._fpl_reset_layout() + def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ Get rect for the portion of the canvas that the pygfx renderer draws to, @@ -208,6 +209,8 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: """ width, height = self.canvas.get_logical_size() + x = 0 + y = 0 for edge in ["right"]: if self.guis[edge]: @@ -217,7 +220,12 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: if self.guis[edge]: height -= self._guis[edge].size - return 0, 0, max(1, width), max(1, height) + for edge in ["top"]: + if self.guis[edge]: + y += self._guis[edge].size + height -= self._guis[edge].size + + return x, y, max(1, width), max(1, height) def register_popup(self, popup: Popup.__class__): """ diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index aa84ee8a2..a67f32133 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -39,8 +39,8 @@ def _set(self, rect): def _set_from_fract(self, rect): """set rect from fractional representation""" - _, _, cw, ch = self._canvas_rect - mult = np.array([cw, ch, cw, ch]) + rect = np.asarray(rect, dtype=float).copy() + x_offset, y_offset, cw, ch = self._canvas_rect # check that widths, heights are valid: if rect[0] + rect[2] > 1: @@ -54,25 +54,35 @@ def _set_from_fract(self, rect): # assign values to the arrays, don't just change the reference self._rect_frac[:] = rect - self._rect_screen_space[:] = self._rect_frac * mult + x_px = x_offset + rect[0] * cw + y_px = y_offset + rect[1] * ch + w_px = rect[2] * cw + h_px = rect[3] * ch + self._rect_screen_space[:] = np.array([x_px, y_px, w_px, h_px]) def _set_from_screen_space(self, rect): """set rect from screen space representation""" - _, _, cw, ch = self._canvas_rect + x_offset, y_offset, cw, ch = self._canvas_rect mult = np.array([cw, ch, cw, ch]) # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid - if rect[0] + rect[2] > cw: + + # account for potential x and y offset + rect_offset = rect.copy() + rect_offset[0] -= x_offset + rect_offset[1] -= y_offset + + if rect_offset[0] + rect_offset[2] > cw: raise ValueError( - f"invalid rect: {rect}\n x + width > canvas width: {rect[0]} + {rect[2]} > {cw}" + f"invalid rect: {rect}\n x + width > canvas width: {rect_offset[0]} + {rect_offset[2]} > {cw}" ) - if rect[1] + rect[3] > ch: + if rect_offset[1] + rect_offset[3] > ch: raise ValueError( - f"invalid rect: {rect}\n y + height > canvas height: {rect[1]} + {rect[3]} >{ch}" + f"invalid rect: {rect}\n y + height > canvas height: {rect_offset[1]} + {rect_offset[3]} >{ch}" ) - self._rect_frac[:] = rect / mult - self._rect_screen_space[:] = rect + self._rect_frac[:] = rect_offset / mult + self._rect_screen_space[:] = rect_offset @property def x(self) -> np.float64: diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index 3e763e08c..355edc46d 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -7,7 +7,7 @@ from ..layouts._figure import Figure -GUI_EDGES = ["right", "bottom"] +GUI_EDGES = ["right", "bottom", "top"] class BaseGUI: @@ -41,7 +41,7 @@ def __init__( self, figure: Figure, size: int, - location: Literal["bottom", "right"], + location: Literal["bottom", "right", "top"], title: str, window_flags: enum.IntFlag = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize, @@ -180,6 +180,16 @@ def get_rect(self) -> tuple[int, int, int, int]: if self._figure.guis["bottom"] is not None: height -= self._figure.guis["bottom"].size + if self._figure.guis["top"] is not None: + # decrease the height + height -= self._figure.guis["top"].size + # increase the y start + y_pos += self._figure.guis["top"].size + + case "top": + x_pos, y_pos = (0, 0) + width, height = (width_canvas, self.size) + return x_pos, y_pos, width, height def draw_window(self): From 88b4e0f95d977ae6ba1c0523c84362b91ce5618a Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 23 Feb 2026 16:13:39 -0500 Subject: [PATCH 44/52] fix bug (#1001) --- fastplotlib/layouts/_rect.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fastplotlib/layouts/_rect.py b/fastplotlib/layouts/_rect.py index a67f32133..7ecd6ad8b 100644 --- a/fastplotlib/layouts/_rect.py +++ b/fastplotlib/layouts/_rect.py @@ -27,7 +27,6 @@ def _set(self, rect): raise ValueError( f"Invalid rect value < 0: {rect}\n All values must be non-negative." ) - if (rect[2:] <= 1).all(): # fractional bbox self._set_from_fract(rect) @@ -66,7 +65,6 @@ def _set_from_screen_space(self, rect): mult = np.array([cw, ch, cw, ch]) # for screen coords allow (x, y) = 1 or 0, but w, h must be > 1 # check that widths, heights are valid - # account for potential x and y offset rect_offset = rect.copy() rect_offset[0] -= x_offset @@ -82,7 +80,7 @@ def _set_from_screen_space(self, rect): ) self._rect_frac[:] = rect_offset / mult - self._rect_screen_space[:] = rect_offset + self._rect_screen_space[:] = rect @property def x(self) -> np.float64: From 6ec3c9caf18339b4bc315ce51aabff0df9f21e49 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sat, 28 Feb 2026 22:12:07 -0500 Subject: [PATCH 45/52] add AI statements (#1007) * add AI statements * add more contrib details --- CODE_OF_CONDUCT.md | 7 +++++++ CONTRIBUTING.md | 51 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e1b7919f2..545171687 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -87,6 +87,13 @@ Standards for behavior in the fastplotlib community are detailed in the Code of Conduct above. Participants in our community should uphold these standards in all their interactions and help others to do so as well (see next section). +# AI statement + +The fastplotlib project welcomes contributions by everyone. While we recognize that LLMs may be useful, at our core, we are a small team of developers who enjoy discussing code written by other humans. +As such, our preference is that contributions are written without the use of AI. + +Please see our [Contributing Guide](https://github.com/fastplotlib/fastplotlib/blob/main/CONTRIBUTING.md) for more specific details on AI usage in fastplotlib. + # Reporting guidelines diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be9e175e6..a10f9fb9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,27 +2,70 @@ `fastplotlib` is a next-generation plotting library built on top of the `pygfx` rendering engine that leverages modern GPU hardware and new graphics APIs to build large-scale scientific visualizations. We welcome and encourage contributions -from everyone! :smile: +from everyone! :smile: -This guide explains how to contribute: if you have questions about the process, please +The rest of this guide explains how to contribute; if you have questions about the process, please reach out on [GitHub Discussions](https://github.com/fastplotlib/fastplotlib/discussions). > **_NOTE:_** If you are already familiar with contributing to open-source software packages, -> please check out the [quick guide](#contributing-quick-guide)! +> please check out the [Quick Guide](#contributing-quick-guide)! ## General Guidelines Developers are encouraged to contribute to various areas of development. This could include the addition of new features (e.g. graphics or selector tools), bug fixes, or the addition of new examples to the [examples gallery](https://www.fastplotlib.org/ver/dev/_gallery/index.html). -Enhancements to documentation and the overall readability of the code are also greatly appreciated. +Enhancements to documentation and the overall readability of the code are also greatly appreciated. :) Feel free to work on any section of the code that you believe you can improve. More importantly, remember to thoroughly test all your classes and functions, and to provide clear, detailed comments within your code. This not only aids others in using the library, but also facilitates future maintenance and further development. +If your PR will introduce **significant** changes, or new features that are not in our Roadmap, please open an +Issue describing your proposed changes so we can assess whether the contribution would be accepted or not, and also so we can provide guidance +on how the proposed implementation can be tailored to conform with the rest of the codebase. + For more detailed information about `fastplotlib` modules, including design choices and implementation details, visit the [`For Develeopers`](https://www.fastplotlib.org/ver/dev/developer_notes/index.html) section of the package documentation. +## AI Policy + +*This policy was adapted from `pygfx`, `scikit-learn`, and `SciPy`* + +While we recognize that LLMs may be useful, at our core, we are a small team of developers who enjoy discussing code written by other humans. +As such, our preference is that contributions are written without the use of AI. + +### Responsibility + +You are responsible for all the code that you contribute, including AI +generated code. You must understand and be able to explain the submitted code as +well as its relation to existing code. It is not acceptable to submit a +PR for code that you cannot understand and explain yourself. + +### Disclosure + +You must disclose whether AI has been used to produce any code of your +pull-request. If so, you must document which tool(s) have been used, how they +were used, and specify what code or text is AI generated. + +### Copyright + +Contributors must own the copyright of any code submitted to `fastplotlib`. Code +generated by AI may infringe on copyright and it is your responsibility to not +infringe. We reserve the right to reject any pull requests where the copyright +is in question. + +### Communication + +When interacting with developers (in discussions, issues, pull-requests, +etc.) do not use AI to speak for you, except for translation or grammar editing. +Human-to-human communication is essential for an open source community to +thrive. + +### AI Agents + +The use of an AI agent that writes code and then submits a pull request +autonomously is not permitted. + ## Contributing to the code ### Contribution workflow cycle From b632d9056162137ff6a214786d21810a9e904c9d Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 4 Mar 2026 20:07:56 -0500 Subject: [PATCH 46/52] remove screenshot accidentally uploaded to repo (#1008) * remove file from repo * upload to lfs * update with new imgui release --- examples/guis/imgui_basic.py | 7 ++++--- .../image_volume/image_volume_render_modes.py | 4 +++- examples/screenshots/imgui_top.png | Bin 18432 -> 130 bytes 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/guis/imgui_basic.py b/examples/guis/imgui_basic.py index 26b5603c0..74d3c3629 100644 --- a/examples/guis/imgui_basic.py +++ b/examples/guis/imgui_basic.py @@ -52,10 +52,10 @@ def update(self): # the UI will be used to modify the line self._line = figure[0, 0]["sine-wave"] - # get the current line RGB values - rgb_color = self._line.colors[:-1] + # get the current line RGBA values + rgba_color = self._line.colors # make color picker - changed_color, rgb = imgui.color_picker3("color", col=rgb_color) + changed_color, rgba = imgui.color_picker3("color", col=imgui.ImVec4(tuple(rgba_color))) # get current line color alpha value alpha = self._line.colors[-1] @@ -65,6 +65,7 @@ def update(self): # if RGB or alpha changed if changed_color | changed_alpha: # set new color along with alpha + rgb = (rgba[0], rgba[1], rgba[2]) self._line.colors = [*rgb, new_alpha] # example of a slider, you can also use input_float diff --git a/examples/image_volume/image_volume_render_modes.py b/examples/image_volume/image_volume_render_modes.py index d29e3b166..36705d17d 100644 --- a/examples/image_volume/image_volume_render_modes.py +++ b/examples/image_volume/image_volume_render_modes.py @@ -60,7 +60,9 @@ def update(self): _, self.graphic.substep_size = imgui.slider_float( "substep_size", v=self.graphic.substep_size, v_max=10.0, v_min=0.1, ) - _, self.graphic.emissive = imgui.color_picker3("emissive color", col=self.graphic.emissive.rgb) + + col = imgui.ImVec4((*self.graphic.emissive.rgb, 1)) + _, self.graphic.emissive = imgui.color_picker3("emissive color", col=col) if self.graphic.mode == "slice": imgui.text("Select plane defined by:\nax + by + cz + d = 0") diff --git a/examples/screenshots/imgui_top.png b/examples/screenshots/imgui_top.png index f83f2f49930a5a5fb9484be0dce735540527f55a..495446b34153fbd77cea34e1baaab0e6cbc1e1b9 100644 GIT binary patch literal 130 zcmWN@K@x)?3_#Jnr{Dq=0s_(7pd^Kvwm1#C=;_P+#rrpXrM8bMy`Qpa{jB}*Vws2K z+UKLawVZV58>UvXl9H?s8!{8wUf9TgI4gdfE literal 18432 zcmeIabzGEd+b%p97?gm5k~#>Al+uj~2udkRr*wCxij)Y7C`e074doC+iF9{&OAg(! z&sop=eb2jp`-}bUKfe9XJ8La}u3_fB@9VnG^Em4`2z)LtMRb|!G6I1hdMYiUh(Mev zga7VdJOl46WpETB5K{F|B_6$UiCGzSb$KQUV?VarYKCyutV$AA$Jsz61XH^$HaN@rG>vy`MX_yq~%H zZwhliKH?$y3u6<0n~8_}DRy>tbZ&nW%F2ZOZ~2qZpVU!XJFE<$c=PEe7q_PO@pjceuioC?#>PhYtBRJRQnt9)W-H%|7cUkT7PgN* zHr;nKZ9?NAIvlv?wrVhrOP4QSmX8-bUaKYvYRG+E-PGKS7ArqoEiEZ2p=8$_$TyUk zZ;Q30e{ti9#K}fPWsHb>;m@BY?QtS!@d1pyY{4ZX; za+vu2HaYor_Kk_T+X8&s)zvkONiN^>b?ceM{lF)(1_(q{R5LLVToE49 z=K_hmf;&Azt+iIk^bQQCdo)(8$LX{>aCB=K6>_^e(7=Ir>n&bN)TgqmFD1KGKrSHlb|grGY$s$z=0>)Zxxb0ezNXb2vO* z(U@DN<+4@9{@nQSsx!U!(fa#KMQIn&HhSqA4RE-pSjJcPeBbl{hHmGDmj?T&yOx45W=sfv2j09sGZHyndzmz>}W3IHZTxg z*kjA#!jt2}{-akS)yXL-!Pg&5!7(v~nT{0O;`=0^;zK@uY-w-r*NzvkpZnuW0G3-o zuM1{-?bh8U3t6{q2pB|^@)x}IqT=V zKe!K^7rGywGdLiip|LTKUibGKyho27-MDdM$C^&SZYIus6&|i-cicNZI$FrE=_`X& z2sghX*cvG*>4)>&tGKhd*hWZ1BDyyL7Z)(?O3uyAZE9*#kG{Nkg9(XD%+KEdYvLRC zu&?wwripqWQDebd1h<5L={>hzWGZJx$FZa5{2Ad| zaT1X&cmT9n?=$RU*vZEXPey7BU{BwWVJ{J#9`sk57$Fe99n0?EpfLjZe+`h2&ms`d zAOC}9XCBc<;1aIF?mJFi6_j%-A01E@~ zWT!ieIAGvLjO73fi;8+}{q4L!LU%M1rU3W9BNxT(vNjPA5CEr#(kgXo4rkk*_sh@E z7xg^&c!Mhyj*O9!QKQgef2&)8<=#Cjuu;6T_!dLIOsoBH&}Bt)2O)Tq8=;NDV8EKE z*6Zo9Wgia%S)N zxHmj|zn1eT0OKLxMSC<~yBV5^kr9B~s__dWz~0hm>8{A`NLX}qX8j(X4gKS@6RoTi zG5{V@AFq>MzyAHp7dzO-M?UzqwY4ko;k@_NIU7M&XciYa$$mr5% z#VdxGyJj>yJ9~Vv4R_X%mu~}?l$eP!M?JVs2mpX2qoytetZiv&SzB8JJh@6q*?)vP zJ7TyfgfStZ!-*=NY>gs{P&mfGhF%pNHsrYXsV8hFpxEKo!b?TP#Cp{;HsrV!K6~~I z1kPU)&(SmVsji*pmA@9}>}D=8_#e(fI~&cmX> zoYb-FNJTX@Q|eR^e!NMvcK~?+7~`9 z=2Zsgz{E5MD7d}QoyKQ10>&%`!Nh*@kFP&5Efu%PbFgLH`(SF@E5p0rRJ0#Z1uKYb z_rL?9yNSRbaVQV|1s@PG@5?gJDK7*JxOC~#;Naj=e~$H7SxJ>{KyWZxl@$!8+uZ@L zm;z^&;P;TuiMsDtXliO=$I7`lIYVd!Hck$)VjB$%5Q1n#J>ZlE0EvaLWnr#xQeA18FZR_vk{5%}rE?golHTB@|kc5Um_hp(qxX}aieYikSK5pen*3yXPos+@Wy0R$nw z>xAyRtE1)aMaS7!V7ag@Q6la;Ym?Q0FpzxYgZuRK^cb~985$dtQ1dQlWTFmshOI4w z$q{AOV5N3!>c95DRLRN6@QJ9nT{mVRA9(bTy6z>gtY zQ}Xce(9@gB@jhPqBzka{m6i4G-G&4W-*7e!tAX6tuoR6VDumzeV( zQ3T<3e{ymXpaeYA(UT34GijMnWrj98o;Rn3NJQN6N;SI=oY33bJ6lTHI30z15nsHh z>v-1VZGv1hFItqOpInVK9Do|E=tBcU5Xi&%x9||ZnU9W+KEgeI{CK#;)z%GnAiBXh z6U;YuB~-1z$Pea;OI1j1q7 z^1_sIrqa?$xYZgS!iHUbA0p)cScLqi82kTTvHaJE|8xEJ=@Jo`0l{px{&pro zYR-T}N*L4xka8Kd0CKc8H2m#J?{10Yx$n zUQNKypFeAZC_m8(?f8(0;=)KnD8tf$i=?D)H368()hFcyHd}2%*sl>0l^abgfIve+ zkp17~;1KiJ%MGC6xA_5)56YGu`Wu`E4OwahkfUZRn@dYe>+1dp5I*>u1jQu2Pn7{@ zYb4hl9v-DM(ldzPGTbE{pTB%TBH<@H@-{X$kZZcSmIT0UR8&-kH{LU`v*TP35~&?) zrrWnoIH0mr$x@v}ZNTc)Vf8()l$2)Y=Z(3kBx}aU$J^U;GBTRH-ehHEef@e>Lq;5u zY6$~K`r*RrGUCe0;a;<65fhBUH@OV}bFSb20l6wJlv7{1VzW9TaEPb)@};FQ{g*Fa z!dcajNC-z-6&@w>jPNDjC=-+W*9i-`t^y3u@|g8Nl0r^HBWyb*Q7~8rB?w0J`qis5 zYtUBUmn0`5VhVdI0Y#eROf`vE*?Ju{u41Irc7dprqgk>6Z}yZvL?Arm;iCi;tZSQ_ z02EhAGkXOg$p%=2*boP4U=T+|N2wru?ge}<-g77N_*9*hV)YHAusU)mdq{>mxJmzM z1pJp<{b#rO-?$q!Kb`T2ppc(FOCsEnDo0HU8DpA4yw~AM5hN{JDDbH9DZGRPa2N7B z@`S|1?Ck8k*9jq@Ks|x$?~JsZeJs8?*XrZrQ@KAEjSFJ{5n#g5eQ_kxeDPxPA0zX< z@ZJw{&)>g)LkJC~;#u5U=!QJP_To8lKf*{Jb7d2gpS@;8kWdg3UJaF^_v9837}UN0 zT{abp*5uUGMLxKfi|_aE*OT&|O8G;1kA*8h9ogE{B@3j`ZK>@vHvG-s9fuuL=%NJrgVH4_`EG#VW zv%ap5933&z!4i98HMPjTuHWacfQvlpwl>XmZWq~|nVVzR!u&$vfzy2m!oA@8BN|}S z+RggvRDLA%bl0vqLdFkw*RU$7g^ZVrs{r-Z=iNIRZ79^z*4}j7w;0G>pRSJx52qsZ z12BU;DqA)0F*Xs(IRUyWRLUq#)BD#2ot90|>fK#k5YdgiA3-FuAtAX)1nvy&qRcTR zK)NND9jFHXqg?#2P)7E@>P8>kC?}N2Bm{$0B_(QZD&ESM#eNa4Jr{bDX>_(A-bxnJ ztXElRMu@DJEL>C#^Vb!hZ@x&Rl2O@UEr|(y736TmRKJKlQqm~k75wADmM>#aI@Zg} zJe0h2kL<2pth8?MQ)7y!l!HjS4|4k{bwPxr3(Q^BR+vGiQQ1%eRy*|5D+@R6!Wwvx zD=U48bH)A`&4jn?E%w+9a%JW1M>@MnIR3K<=| z6e2lmdnBSmQc>daQmhqP9-|m~HPoPdslRh% zh%95jYe9J_nPqF`sftRmwM|!~gp0 zlHwZ}kouCy&_;6hkMY?dCQ+7rsRLYTtyl9DK zqjZRA^hPKHQb#AoN{?>=DS^6niSp8wOV>)|lH;YnG}whYJ5ZC*-VVAQNO<*9`JQ}= zbe?on0|`)=V5Y6!}HJ(WKlp%m!+`7efb&E(259DwlDGmdmQ(Ohiz9cy3mXo^9X4#NwLIB zXM?9s)e}rAV6(9PK~EzlTdCwnUBV`Rn#$m90UAb?4>Y7|Y@7uNPx-VByn?H19*3qN zk4Kcty$%9d%eS8AyfP?w+`J!*DWW8xOqQdsyYxr%Z2Z_nqCY_(iZ% zAsXFtr(Zdzej%`#OX~LYhiDIT-d$RX$kma@(cf*6%n?=+oDy?7bC;r`*jm0JFG$oC zqgI)Izb@)8*76#s%B!9^OKt0qM%&I8elgc@m@oc0m6~8AZCT^D`Xp4!j1_aC%u{@x z;3825jgz}d#Tou{t{n1H`s}pbEW(;g9Gs_Jv2Q7@Ys~$6Qjnh1hn9_Kw(7vP?BYh% z+j@4|(;vhcA|oVFm&gqkUqy0CrpkYNe{My@|FfH~3TbbY7{9_K6x+|pI7U|l%)?%o zUs=x&-lOSXo}lHmWD1pR+Z$r4^(vaLVR=Y97hqguH;CO~lI<9?SIw4H3IAn5qi!zj zZYd`pzP-dKrt={v<)`$AAVv`_<(xP!2NOR6r?qZRUCV7+Xov~W+!Z=5E}VNZ^0G*R zr)OkEVBI6v>B{ulah$WG^y7C;R_!I!YW@wgp%=XM>jhVLHZ~2b{c0M+v@nH&+*jy! zY6|p&C}mzJsAdzNQqYQ8P;oa%rjgM^(Fi+Qo=-@4wBzp3uZ7Q(S{L3>ZRg|k+qh`h zUYZakRH&*sy?EtQqyyi`O#%vOH%zp%I~CmL=>=~*JWgknfdM>>T&2zAT4oN$?Ni*} zsicejQ9>%RsYB%AdWo3|6+Is{=?V&`kdLb+hw$R*N=1UJt5WK@soJCOXcS6o$6ckA zvCW&AZS2dQ)^=}AxzI!`Y$wC*ltsgT^V>J#;Z=#SMxs&?>cfWF2PLl$w$w?!QqqfR z@_S;?uJ(L14Hyh_gVMon#MTx*S#?cdkhZ&AVv^RY$BB1P*}38-S8j-DE9ac-zfl-o zu^dD~7Wmp0aX?mFd2X z{!}e&A>&X@WH<4!)zUe!(ZOY1k1w9H=#wU0?j|*VZr@YqMk@|x)6R`qPJ@6WbRfLfPyr2W}EjHtjKWFt5Ek zk1l$k8RL+eoSf1ezFj&?vHMkDY$58PaW0Z8<=3x@n$O>h@3P6sGDQk>6V1A)LXKbM zg$ZV1lY9DEYC)Y`-ZZ`C`()Q+Gv2H()WW`ZeEpz%{xpmUMlkx*b-|mP znONJgjD&=^Zg>6qMVCJtU)VZfo4dNqD46|8WD>nt)a~qAne^&w))q4Z!|s$8+^uJO zae5MH-ls`c(qh!u+$~_oc)P~}S#kTpZ657mqegUQE5*LrPc)C-;%wSA@D_WJ%i ziiha7U$wvYNp}>_YI;MMo%pk7vF1yOl7Tza)s9Oe&qI!r8Tec-LoElT5loR*p(j*O zP@oVeEZ}VA$*0BQi|^a*?oY{jeRDCB{YJbWob*9TdbQWfzn`gBF)|Sw2FC|rBU813 zOkoxS6DM6>gJiz`^x7EiysWI9r)ery-vyOE1W`*94#g`fj;KZGTm8II@=OLia#3qr zbG7SyPFsyG8qUXxC1YpArdy8FHJ?@UZhE?0O5l>7iOJ0e-&?H+vWQEnHmxMrMd-+> zzJ!IrBUT=5G+&@|nSf@0YfDRvu5)doPh;2}^Mu!SB2pNl*fF6plNWlfB{U*tOFK2V zVZ<-zob0Nkwvk*nG%;H4`_29RZAScI-n(Kti*a(O0+YYbhg0Ui7Pyw2!Q#FAwRVtmK z<`5~T)J3z4I{f`o^CMfjwwBW|qqJUGqR_&0;$c&vj;(iU$(CwKh2>}Mm)6^6^+zgP zd|$(x%3K9|j3}uVWeDBg`^DRfpPuxstsOAM!rCZh9=?e-CuduVZVlNVbK)F3i7%B` z7#JAmw~XR8?ffLxN2WwTV3_9>t9P`d6G+KEz+RYbe}eJMoEmsFeFb;2VmiA7C97m= zcQ|#DOF|^M@!-sqv(<5v6&QMcX!!m>zb!_fK(F@lAs&-ll*8&s31rYyQ&TXSfL3Xq z>NDtc1_jAHd)5{!#0SlJnTpQUtvJD+#XUrYZft-0^9G6aX{>Pp1-dl9Wt zNI*nHgZ`^HRvUy*)kdUISX|0mn%@6VuOo-;wry)vwM+zWu1;fne_FZckxOH{&e!n1 z-!W{gJ+n+n5~0=W>*_NZ1&+D7&r_G|#T=(T-}2XSOMfe1&beTRzxZY3xynoz_A$E# zgXY@?39Wl3oWe}pSz4t#HG$+sqfXHQAE|htg}4MoZ!Z1sO9_)z7tGZ{9WD_?&^7f( zP;WJH@9#fbiNVCgBRSe@>DjZ*B(H`1WnJ?8*k^biIt1m!6v1S^K`= zv~mto+Va2Xep&K6yf&+i~uW0s^aBaLB4T}MJY$Q$?scyA3Mu$Nf;bt<_ux- ze_B`CRTHh|wDz5h+aP;(@AXei3^}_WXwPD75?(3sjQG7$UyKuaY)M02EyKa6UHn{z zJRl@ZUc#X6qvW`^HI-P!Q>}(?E~ZjUL6TeiZ2j+DWKH%ePY)}h`Dq;I`mbU8%YmOM+DW14%)cZY#Nzm2B8_h}jn5&0d3isb1(n5Y29lI6_0l}kG% zw{YfF7$>l0Vp^O`Ubj9Stxizr`Q7R!U-9vBBcsaD9q510^mGZ^PrQjMLh8i05zXWC zY1_SMKK>={*HC%F$jtmDj(vYW-_<_sFOSvsVye?El|*Al&p8U%ReTaU4;Z;X>$qL) z^k{E=`T{XcamlKA4QeJ_rrxsS-FXyM%0X|qX0EE%6*n{mr2aii$|7 z5b0+hST%ntY<`rZg-_*g+N%i3ogL*Ea|n~QplV%lI*#BDB`B%;eBEsNLBgZbB~Bh5 zM;IVc^IAxwcCYMQ7IHqAjn|f|+1};07$zg4`t`R@V^lL@n)h$7in+CZM^S9S6F#yK zjl?{C%9YWjIf=h6fi9XcE#KGeDd!j&#ddWnY!bGx*tQIs-oJKvZ6gzP`m+aac+aFi zn|5!R5@#)Rop1N*`W8YR@#PC=8?WUk=eBTq^_*!pjN*3CN}mWyXs`*6+gMj(h08=p zvMX0QVAhU*p>)G4Ipx5Gyc%BD6QT!Rei)ozm20SGm4_E7QxV)lvWH>CrNQpV+{FLgV+mp=U+2Z=@F*-(r6zQ=sLZ(`?@$ ztV`W9(H-XFLvn{n{!_4{ad5$mf-S6GOlO_)p^q6;2M7H8DISL_{3>H2ldjZ)MGV|7xltum*EEp+JFiJJLh*^eMsjfk zYLbZFZH+FS=g*q6Qb-d*QW>u6)z@eC1!ywiY7))0X*$;*SvUAlI|Uz@x+k5-(QuT4 zcCKqy3X_l7SJLmITqJ4-WH!s$Ki%89_#VGqdGldk3HACyL?Ma$0Pjz2)L4=`e3<;o zS~COtOT}e|h~vPFXQ5}?yxKme(cK;HaZ~OzqmB6X?aO!7#r%fZ+~LgzG#W)vBFi-F ztBD#&>etk8bx_v2NHq4_iy_mpCN}@HV4TmzaLb9`5G9GKP&q<-a#%*MH;ihFr|M9Z z#Goy1yH{DY*EuP7Qk2knsp{#Qqq5}(#RQgjgk3DX_yWs9Hn;iI`bZd1)6r^(Em|Jny~o3j^oGV?xbzCJs-H5C4u zqj^_e%4Cg>nB`4jnzzJEUGqK}8$164ZnFi@tXlsPLlg`ET zs3>#ul%XNFqz2j0UZj*+4KXd9Lb|_gu$G4P)yTKtKB{Num;}HStPc&+>4eGYW=N(Xa~pa zY~@ht(0>a2AvU|Ap@R}wtd`ryciM9wM%cR=t;Ex^vm=q$oWkUztr#TVNel(m^ztrH zd|2)=zAgNz)8yla4~Z#N!y`BIAKpIhx{o?$A!W`#G)6smI30CZR;L=ejdbNn{aEdg zDX>7*|7Tgw%rZ+c-a*vvt7c}@e4$ya17lWp#G<#YxQZG!Ia6TNV&%1)v-XlJzsO{( znPpJsl1{pGMS~mt7-Fzny{#zVDmV9N3$j*}o^n~_ZayaDx4M1TY&!bh;l&jR1zXD( zah}>D2mPKOUo9o;Tb7flo2+W@D}m|0-kKC~OEudP+VCkOGxm#QZk?m-3<&5CL$v0)sFc3^$4cdZYt*xZ`#-2bl^fY41f|~R74R} zt9zeD$Z1)@`=I-i*vU_4Ett(wKrE~0kOfpziDVd+ zMe|t$5oZ}#SGZmZuq;Dph5EMF|GPm#xi3tQ(f_phWy6F-*5RI_qM{W)e>RwKAMEc3 z;wpLI;N*GiZveS3OS>H7j2(j^aQf0Ta9xRkhVkYlcLFbd>kd4WOngUzOvAp8F4J-M|gu127&OQ%w8&>@Ls=u7YU3O6Fa;7D&6~$vOrmV zDks-#`W~iZWH3Z;0hW~nv=9v3?+6P6u_-Y*SwaFvaWKAPU|?7gBcr6`g8|#bgzh09 zaB+T70?`UMvNkHXIUw7ycr|bju$8AL9UUFefdKl94mdRNSyfgFpQ<0_05LG$H$xAR zdFWX(+*nwspUFXAC4Vz)?$o+LdG#=SKpV2F%ysk4n>T;{JpS*;^oc;V(Ww~GV!%VZ+3gnN_5ju?j*l8d!3J!;U-nn1 zG?xAVZae{&6wq3Mbreh^(4~7Hh_Xc}>w?-4_00ccf^EH#3bahH|-*lBV# z6k|p}!uwh-?`IY(QzlN`AvXO~4ai>7nMyS}smltyIkxCptJe z>tF;iCaz)Cz^-ozbh2g{q z7oMH*!gTGegluG+=Bd`(Es&u7gWQ1%c-t5f%hyDTf z5g<`iNJs-qrS%7R&%l)j5QAk6{xS^3t}LB0hdK>880g+ zu&V$13k_9Z7QzeI<}sq481lO6a`xRV2~EurV2^o2=Nl+euujPQy0>|FF9ClNG8ZAh zF<`v{53CTN3Si`~*4O6a1qE+tCF3a8zfOU)xB_w`36ZA*f%2G-;6)?0J0~19omcDN7MAMxgHQjyx|#u+WLcSrPLW4_dwq- zfK{`&!z&(DK4=o6P;xaCqBqd{IVz93^zy#9&cU}?=ps^A$K0{Ut=3)zu%P3^U4`%W zCqa0zQWs2ZDRo;r3zm*!VmCJr7uzOvngGA_T@js~DKOx4+*i;LHl93rGQ>{YHFpA+ zhX4R|!PfBji4zcIAq(~ys}&psvOk^cOeh>Cuwl$dj-hW~5PyhM6$tFQZ~c9prz*NX zL+@KlLOl@& zydKc%jJsn+%7Yx4e5$Toy9TiwZ8zHlF&_qUfI6-qkbv{yzD}s)ee4L3ZfKZdZ-nub zsRkw+2u5sI(EWmw?v8dPY($Wp;6f%afh7&#Yak{76;$q0bI&-ug|j}7eLH$rMH}E6 z_XC68tTzLUa!62E4Q-}mWHbllL`j3OGFSNCC*h5Nv2TLo)YfHrudDq)h!xLo`&gsL zx^$)RTIosICgh;$9rrvLbWrhmp33N92gS9&dMt6xcJp_)p?;y68kLH8lpeA zRBL;CQLqaz#Vl8?!MB0@oVlY(&tVU61$%CGrvxb8klLO-a|Rv&s;^9<&tYMJEeoER z(5#x@T^>v-Uj=~##5f>e0sR!r9zZTKDhlKb>N#bVEoSH^Qc|Gvz!tQNy3a)#14kL$ zdl--bFmk#cBx8|W#FDSP1TYvzRCRTAf#)b;F`5uM#CPuGTH5O@ouokQ(wMJs z5-9K-x4EMbHhj$kaDJmvLQ(1WjPzMYr6XtLGphO`cCe|*oOX*X|%Fc`j_BOEO zu3!J@9E{q4m=BR=6O0aORZ}o+xCHP+!F#8GxH~lkW6Bc4W{PrihkW3tIRUXB;06-U zmd3_QuxEsXzf@Vl>4wB_z`m6^hB$)=3j~Tm0Rg${+3MB50gmkL?VACHewo^Xg@aCE z1d>vfJ(jwqoXQih(fd@vP`<{4_;P0z-LG8=unmb0q=9M#CfW<2A;|zyCisz?w=g4?*dnK1VDdi3Gyl&A(3y)C235uB<47T3v?n-FM#F;VjFNAi3tccV@oQ% zPrZgm;<-(`KoMEC;r7sy>)>E12MF|sVG5#=Le5r-ieC!*drqjXUIqTH(u)@f&XMfv z5tT=?pr{+H^saQeHS>iL>Up)qW7GAiTCfs$?N)T;0!D%4@*e1{tj8-902IODK-oBn z#paCif-}HGWFp)C2v(<&Z}8N?VGE2-2*ifKnuhhMAHIZ5haqY&2(ECOa+zajQ$|6- z8#=8N?3y@9B2GPmjgn;o#gdlR%67Rm6b^;3KAhyxNg=9Mud$(_qx7zGu5}3N=g$tK zBGk2o2_RU*`iGb=#m62R6%6X-natlUmxW{>rvU|MS_kz}uNyx2Uk_;QU_>i%o1dSb zmzP&eY@Fp&&Iv2YC&S$PMw?8pIHwC4rt0JY6B)#^xp)MNoF_?6d3O z(eo{lNm|s~pctd1prE*V)d36+!ZAqDHV@cUUcP(@^SC|Gsg1eL$3k8rG>}37%7>#oNbsk-2lR6&xqxO1x84DzE8rktC5|H9>{tI1_q!thEgkFVrDk|^ZpXx#wt`(P(PWOWk5Jl^YA!= zBeUd6w>CGs+m^M9Z4xjT z3}iV_j6ltR!!jtdV!5Bl^+O=;hU$bAWQ=yrYyfIFrU>BR7JCyd=)d6Xpt>f+7l-EG zRBe#wYbOZ!XY1jnKM_3%BP~{NhpF`wX1~o5~Ga%E;aobr|Q&K`hY=nKG z6LQJ|Xj942ev$;N7Ypby%B!^!L1_t~)jyODHJerOq#tA)fC7W;72B-;c}SwH!+Efa zr3V3Bc=#NHh^`s985kfA860;Kods5}P*C&%M1gKAyOcZlQ_Jz*-X742U`?i!M?-TJ zed|GEy#hMVpFfBE=of{0wr5I4#v=3r!7T87Ci^t3VeNgGXIr8mVzTH?g-S3!`^5@W zcF+I{1bhk%WM*Lj@C~cg>nxRx0R2Td$jG5K1Xx}QT45Ga zLHY^i1lmV4ER2ilUS4B;U(X=kutWEo5SZtkrG&;1n4oV3sw?2Z z$0Wd{BCsrbTNHZIckfDL@8P7uzsCOWg-MFzqvYAEF4LDQMXXmW(!A}p#%~Nv7C!M# z+wVDe;$64^EuA+qon6q7`Rbw}MkL<@5e%gN>}+h=;AfDnmOxHFb`1~lBMoTEm6h2q zT~vSU9C*AQ9FFFcoT<_nl0skX9AjnYY1i;hv~`p?cQDFVtgkBlW+m1v$gy{)rFy=Z zde>J4$;`YEcqc9(D5$xyQLV(z(9UiH*C&UvWe$*-gN@B`I|o-GL8XaQR96=S5g-VM zSB^oo1?fwmw3pWjVCC{6Cse#YsHpINne%%7Au2r0{`u3xdUN5SclBBFgA4da8;n9B zGhW`9WKu_2nvR{5(gUHg8_r65ADx}~riM{9mPMmGO)NR*E?gL{ym9?Hzgf>uhKhHZb3kF_x|Roy&ZsPoh~R^N&pm>PP0 z=TxM;uM#rK;ad*|Mm#oKp?$eO6~v*b{!mv}7uE^MtCqU@0i@^V)^uWCN1$Fafo1`S z^dMb>k|ie>rUUFM`Y6-2-Q6W;tz~cLIPy?Y5xt|>_1ldEM$IwSAv*iJ+|t9ZgQV+C zgUm-QA!W)#wSz<;8l3O4xVX=G6Cy$d;q~{`kd^JH1{^^)56SHCH!z5Y)0)~Q_f?dm zTfepbdFM~O%qvx+dw;}DUxk#JosEQ;@Z!ZaYl-vpA`gCQSr~|%{od4CFm}|(OvyLk zG=Nl$DoN{m9mQv@-Ol&RYILagJfuQ%kaRkFibFXF?FSM%VHZY2kndc~NSNH)&nHi7 z_ulBODsVOAXf5U`Q(OD|R*W7?@$IXK5O!GpL2DgJ=?;9XIs4%xUSFH*A9OXbkn{8( z^$Rlc@=h+s;5&u=ka*iV;vF9fX%D}~LMZ^jiIN(pX%9}NqH6p4DrA4Do%V{zZhKE< zJ00CsXarui?=9G@81=|E*=$S%&n+aqChO+;l0j6dGUVlWRUP1Pwb-A66yl<_b)mpE*-j&cF52yF4w0J9v_2X^0%LNnS^9iaIRr+^E{)^1&7|Y zX93ciMz_TAouV1Y$VZQc%=Hvb47c03f)^Tp2Nr3D$BW&d>3u=yHDo39!_}M*o12sS z2G{k4Rsy}p+M{gm@`coBNQrmr#ZEejM4tSIxn9@{YRJK$J5N+fL#o_}WkNhk1`Y?EUrher9BFQZ2=>LbyV)-xLriiR-WdCxQO)2K2nG z%eR$%bO6)HdFH~K7tNaKDJ@1k@}(uT_xkJl>FB*?owWpa!(|Rf3wJ`?sKw>wakcy6 zfw`L-CX)cHHs=od+_`fB(lCK)Qh^4zMDTLj>x1t)-iM!Bk#?uCl@)(#CK5H+y$3CI z=F|cKHG6wk^tH9efPgl$ct#=_5>`kW;preN+<@X%A{i91FlL0`opGBTf!IpKRJONp zw70Ub-(&ASL4P15*x-79VGnIVYUIwvm$PS}O^%t}7{OG2ix+b(5n8?z=kn)-P9_?f zI$*8_hK41WH<0D@mT6awpU!WJ!kwTWiEG&%P@Vx@pr2dRl*Z_K-8C=wv7tfv(>Y89 zS8las@QM6p`Zh6Ec)RB%{Dv137$-uoY%~yrK_L8Yf}Q2`av>%aoRht*PRcXi9Wu{Z z(1K~1To5@OHQ0m)-ao(bCLJ3EE{>p9|DV&h&77W9I`^e#tMx1wd<*eZQeFb}Soht3 E0|`;K3;+NC From 57fd4fb89c6684009c948085a33e531511eb0a3b Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 8 Mar 2026 23:24:17 -0400 Subject: [PATCH 47/52] Update GOVERNANCE.md (#1009) * Update GOVERNANCE.md * remove screenshot accidentally uploaded to repo (#1008) * remove file from repo * upload to lfs * update with new imgui release --- GOVERNANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 9baaaa321..337d524c9 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -122,7 +122,7 @@ Governance decisions, meeting minutes, and voting outcomes are publicly document ## Changes to this governance document -**Effective until February 5, 2026** +**Effective until February 5, 2027** Moving forward, `fastplotlib` will maintain the governance model as outlined above. The core maintainers (Kushal Kolar & Caitlin Lewis) will revisit in one year to propose any necessary changes to the governance structure. From 4a13244994d2e2ba3ccda11c42f32327354fc7ad Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 17 Mar 2026 16:55:48 -0400 Subject: [PATCH 48/52] `Figure` can accept `rects` or `extents` as an `dict` with keys indicating subplot names (#1014) * extents or rects can also be an OrderedDict * black * even better, just regular dict --- fastplotlib/layouts/_figure.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 79b5be3a8..28b7c4a49 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -1,14 +1,13 @@ -import os +from inspect import getfullargspec from itertools import product, chain +import os from pathlib import Path - -import numpy as np from typing import Literal, Iterable -from inspect import getfullargspec from warnings import warn -import pygfx +import numpy as np +import pygfx from rendercanvas import BaseRenderCanvas from ._utils import ( @@ -27,8 +26,8 @@ class Figure: def __init__( self, shape: tuple[int, int] = (1, 1), - rects: list[tuple | np.ndarray] = None, - extents: list[tuple | np.ndarray] = None, + rects: list[tuple | np.ndarray] | dict[str, tuple | np.ndarray] = None, + extents: list[tuple | np.ndarray] | dict[str, tuple | np.ndarray] = None, cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -60,15 +59,17 @@ def __init__( shape: tuple[int, int], default (1, 1) shape [n_rows, n_cols] that defines a grid of subplots - rects: list of tuples or arrays - list of rects (x, y, width, height) that define the subplots. + rects: list of tuples or arrays, or a dict mapping subplot name -> rect + list or dict of rects (x, y, width, height) that define the subplots. + If it is a dict, the keys are used as the subplot names. rects can be defined in absolute pixels or as a fraction of the canvas. If width & height <= 1 the rect is assumed to be fractional. Conversely, if width & height > 1 the rect is assumed to be in absolute pixels. width & height must be > 0. Negative values are not allowed. - extents: list of tuples or arrays - list of extents (xmin, xmax, ymin, ymax) that define the subplots. + extents: list of tuples or arrays, or a dict mapping subplot name -> extent + list or dict of extents (xmin, xmax, ymin, ymax) that define the subplots. + If it is a dict, the keys are used as the subplot names. extents can be defined in absolute pixels or as a fraction of the canvas. If xmax & ymax <= 1 the extent is assumed to be fractional. Conversely, if xmax & ymax > 1 the extent is assumed to be in absolute pixels. @@ -120,7 +121,7 @@ def __init__( starting size of canvas in absolute pixels, default (500, 300) names: list or array of str, optional - subplot names + subplot names, ignored if extents or rects are provided as a dict """ # create canvas and renderer @@ -148,6 +149,10 @@ def __init__( self._fpl_overlay_scene = pygfx.Scene() if rects is not None: + if isinstance(rects, dict): + # the actual rects are the dict values, subplot names are the keys + names, rects = zip(*rects.items()) + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in rects): raise TypeError( f"rects must a list of arrays, tuples, or lists of rects (x, y, w, h), you have passed: {rects}" @@ -157,6 +162,10 @@ def __init__( extents = [None] * n_subplots elif extents is not None: + if isinstance(extents, dict): + # the actual extents are the dict values, subplot names are the keys + names, extents = zip(*extents.items()) + if not all(isinstance(v, (np.ndarray, tuple, list)) for v in extents): raise TypeError( f"extents must a list of arrays, tuples, or lists of extents (xmin, xmax, ymin, ymax), " From b9cc2ec883ce553d549d146f7cdc90411125e662 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 10 Apr 2026 08:21:46 -0400 Subject: [PATCH 49/52] Create partial_camera_linking.py (#1020) --- .../controllers/partial_camera_linking.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/controllers/partial_camera_linking.py diff --git a/examples/controllers/partial_camera_linking.py b/examples/controllers/partial_camera_linking.py new file mode 100644 index 000000000..5cebe66ce --- /dev/null +++ b/examples/controllers/partial_camera_linking.py @@ -0,0 +1,55 @@ +""" +Partial camera linking +====================== + +You can customize the camera axes that a controller acts on. In this example with two subplots you can pan and zoom +in x-y in each individual subplot, but only the x-axis panning is linked between the two subplots. The y-axis pan +and zoom in independent on each subplot. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +ys_big = np.random.rand(100) * 10 + +# create cameras, fov=0 means Orthographic projection +camera1 = pygfx.PerspectiveCamera(fov=0) +camera2 = pygfx.PerspectiveCamera(fov=0) + +# create controllers, first add the "main" camera for the subplot +controller1 = pygfx.PanZoomController(camera1) +controller2 = pygfx.PanZoomController(camera2) + +# add the other camera to each controller, but only include the 'x' state, i.e. 'y' for height is not included +# this must be done only after adding the "main" cameras to the controller as done above +controller1.add_camera(camera2, include_state={"x", "width"}) +controller2.add_camera(camera1, include_state={"x", "width"}) + +# create figure using these cameras and controllers +figure = fpl.Figure( + shape=(2, 1), + cameras=[camera1, camera2], + controllers=[controller1, controller2], + size=(700, 560) +) + +figure[0, 0].add_line(np.column_stack([xs, ys_big])) +figure[1, 0].add_line(np.column_stack([xs, ys])) + +for subplot in figure: + subplot.camera.zoom = 1.0 + +figure.show(maintain_aspect=False, autoscale=True) + +# NOTE: fpl.loop.run() should not be used for interactive sessions +# See the "JupyterLab and IPython" section in the user guide +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() From 76db8ffb094d57375336b9c2754ee7cac49cf215 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 16 Jun 2026 08:08:06 -0700 Subject: [PATCH 50/52] require python >= 3.11 in pyproject.toml (#1062) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 73dfd7ee3..42a0e9e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ keywords = [ "vulkan", "gpu", ] -requires-python = ">= 3.10" +requires-python = ">= 3.11" dependencies = [ "numpy>=1.23.0", "pygfx==0.15.3", From d0b97862cd178d0c22fee05beed5d966d43308ed Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 5 Aug 2026 09:06:22 -0400 Subject: [PATCH 51/52] Edit contributing.md, add PR template (#1059) * Update CONTRIBUTING.md * Create pull_request_template.md * Update CONTRIBUTING.md Co-authored-by: Caitlin Lewis * Apply suggestion from @clewis7 --------- Co-authored-by: Caitlin Lewis --- .github/pull_request_template.md | 4 ++++ CONTRIBUTING.md | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b37aafdbc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,4 @@ +Type of PR: +- [ ] minor/trivial bugfix +- [ ] other minor/trivial change (ex: small changes to docs, typos, etc.) +- [ ] new features: Reference the issue that describes the new feature and outlines the implementation, this helps facilitate rapid review and approval of your PR. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a10f9fb9a..58b880d13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,15 @@ `fastplotlib` is a next-generation plotting library built on top of the `pygfx` rendering engine that leverages modern GPU hardware and new graphics APIs to build large-scale scientific visualizations. We welcome and encourage contributions -from everyone! :smile: +from everyone! :smile: The rest of this guide explains how to contribute; if you have questions about the process, please reach out on [GitHub Discussions](https://github.com/fastplotlib/fastplotlib/discussions). +If your contribution/PR isn't minor (ex: more than a bugfix, small change in the docs, etc.) **please open an issue first** that briefly +describes what you want to contribute and how you plan to implement it. This makes it much easier for us to review your PR, and +also facilitates more rapid approval of your PR. + > **_NOTE:_** If you are already familiar with contributing to open-source software packages, > please check out the [Quick Guide](#contributing-quick-guide)! @@ -72,9 +76,10 @@ autonomously is not permitted. In order to contribute, you will need to do the following: -1) Create your own branch -2) Make sure that tests pass -3) Open a Pull Request +1) If your contribution isn't trivially small (ex: bugfix, small docs change, etc.) please open an issue that briefly describes your contribution and how you plan to implement it. +2) Create your own branch +3) Make sure that tests pass +4) Open a Pull Request The `fastplotlib` package follows the [Git feature branch](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) workflow. In essence, `main` is the primary branch to which no one is allowed to push directly. All development happens in separate feature branches that are then merged into `main` once we have determined they are ready. When enough changes have accumulated, a new release is From 01175653cf8c31494763767e8ed58b558bc2af72 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Fri, 11 Sep 2026 00:03:41 -0400 Subject: [PATCH 52/52] Use namespaced "matplotlib:coolwarm" to silence cmap ambiguity warning (#1079) --- fastplotlib/utils/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index a839ed9d0..bc282287b 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -59,7 +59,7 @@ "RdYlBu", "RdYlGn", "Spectral", - "coolwarm", + "matplotlib:coolwarm", "bwr", "seismic", "berlin",