From dd5bc2ecb9b547a0adca370756a151f0fd1bed15 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:13:18 -0400 Subject: [PATCH 01/96] restarted tests, now work from the command line --- .gitignore | 3 + examples/image/cmap.py | 33 ++++++++ examples/image/rgb.py | 30 +++++++ examples/image/rgbvminvmax.py | 33 ++++++++ examples/image/simple.py | 32 ++++++++ examples/image/vminvmax.py | 34 ++++++++ examples/tests/__init__.py | 0 examples/tests/test_examples.py | 79 +++++++++++++++++++ examples/tests/testutils.py | 60 ++++++++++++++ .../garbage_collection.ipynb | 0 {examples => notebooks}/gridplot.ipynb | 0 {examples => notebooks}/gridplot_simple.ipynb | 0 {examples => notebooks}/gridplot_simple.py | 0 {examples => notebooks}/histogram.ipynb | 0 {examples => notebooks}/image_widget.ipynb | 0 .../line_collection_event.ipynb | 0 .../linear_region_selector.ipynb | 0 {examples => notebooks}/linear_selector.ipynb | 0 {examples => notebooks}/lineplot.ipynb | 0 {examples => notebooks}/scatter.ipynb | 0 {examples => notebooks}/simple.ipynb | 0 .../single_contour_event.ipynb | 0 {examples => notebooks}/text.ipynb | 0 23 files changed, 304 insertions(+) create mode 100644 examples/image/cmap.py create mode 100644 examples/image/rgb.py create mode 100644 examples/image/rgbvminvmax.py create mode 100644 examples/image/simple.py create mode 100644 examples/image/vminvmax.py create mode 100644 examples/tests/__init__.py create mode 100644 examples/tests/test_examples.py create mode 100644 examples/tests/testutils.py rename {examples => notebooks}/garbage_collection.ipynb (100%) rename {examples => notebooks}/gridplot.ipynb (100%) rename {examples => notebooks}/gridplot_simple.ipynb (100%) rename {examples => notebooks}/gridplot_simple.py (100%) rename {examples => notebooks}/histogram.ipynb (100%) rename {examples => notebooks}/image_widget.ipynb (100%) rename {examples => notebooks}/line_collection_event.ipynb (100%) rename {examples => notebooks}/linear_region_selector.ipynb (100%) rename {examples => notebooks}/linear_selector.ipynb (100%) rename {examples => notebooks}/lineplot.ipynb (100%) rename {examples => notebooks}/scatter.ipynb (100%) rename {examples => notebooks}/simple.ipynb (100%) rename {examples => notebooks}/single_contour_event.ipynb (100%) rename {examples => notebooks}/text.ipynb (100%) diff --git a/.gitignore b/.gitignore index f87eb1c51..98b21a799 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ dmypy.json # Pycharm .idea/ +# Binary files for testing +/examples/data +/examples/screenshots diff --git a/examples/image/cmap.py b/examples/image/cmap.py new file mode 100644 index 000000000..684d90f5e --- /dev/null +++ b/examples/image/cmap.py @@ -0,0 +1,33 @@ +""" +Simple Plot +============ +Example showing simple plot creation and subsequent cmap change with 512 x 512 pre-saved random image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +image_graphic.cmap = "viridis" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/rgb.py b/examples/image/rgb.py new file mode 100644 index 000000000..a8281df49 --- /dev/null +++ b/examples/image/rgb.py @@ -0,0 +1,30 @@ +""" +Simple Plot +============ +Example showing the simple plot creation with 512 x 512 2D RGB image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +import imageio.v3 as iio + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +im = iio.imread("imageio:astronaut.png") + +# plot the image data +image_graphic = plot.add_image(data=im, name="iio astronaut") + +plot.show() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) \ No newline at end of file diff --git a/examples/image/rgbvminvmax.py b/examples/image/rgbvminvmax.py new file mode 100644 index 000000000..8852b48a8 --- /dev/null +++ b/examples/image/rgbvminvmax.py @@ -0,0 +1,33 @@ +""" +Simple Plot +============ +Example showing the simple plot followed by changing the vmin/vmax with 512 x 512 2D RGB image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +import imageio.v3 as iio + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +im = iio.imread("imageio:astronaut.png") + +# plot the image data +image_graphic = plot.add_image(data=im, name="iio astronaut") + +plot.show() + +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/simple.py b/examples/image/simple.py new file mode 100644 index 000000000..2dacc5ecc --- /dev/null +++ b/examples/image/simple.py @@ -0,0 +1,32 @@ +""" +Simple Plot +============ +Example showing the simple plot creation with 512 x 512 pre-saved random image. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/vminvmax.py b/examples/image/vminvmax.py new file mode 100644 index 000000000..645c06b5c --- /dev/null +++ b/examples/image/vminvmax.py @@ -0,0 +1,34 @@ +""" +Simple Plot +============ +Example showing the simple plot creation followed by changing the vmin/vmax with 512 x 512 pre-saved random image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/tests/__init__.py b/examples/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py new file mode 100644 index 000000000..a0f5e2bf8 --- /dev/null +++ b/examples/tests/test_examples.py @@ -0,0 +1,79 @@ +""" +Test that examples run without error. +""" +import importlib +import runpy +import pytest +import os +import numpy as np + +from .testutils import ( + ROOT, + examples_dir, + screenshots_dir, + find_examples, + wgpu_backend, + is_lavapipe, +) + +# run all tests unless they opt-out +examples_to_run = find_examples(negative_query="# run_example = false") + +# only test output of examples that opt-in +examples_to_test = find_examples(query="# test_example = true") + + +@pytest.mark.parametrize("module", examples_to_run, ids=lambda x: x.stem) +def test_examples_run(module, force_offscreen): + """Run every example marked to see if they run without error.""" + + runpy.run_path(module, run_name="__main__") + + +@pytest.fixture +def force_offscreen(): + """Force the offscreen canvas to be selected by the auto gui module.""" + os.environ["WGPU_FORCE_OFFSCREEN"] = "true" + try: + yield + finally: + del os.environ["WGPU_FORCE_OFFSCREEN"] + + +def test_that_we_are_on_lavapipe(): + print(wgpu_backend) + if os.getenv("PYGFX_EXPECT_LAVAPIPE"): + assert is_lavapipe + + +@pytest.mark.parametrize("module", examples_to_test, ids=lambda x: x.stem) +def test_example_screenshots(module, force_offscreen, regenerate_screenshots=False): + """Make sure that every example marked outputs the expected.""" + # (relative) module name from project root + module_name = module.relative_to(ROOT/"examples").with_suffix("").as_posix().replace("/", ".") + + # import the example module + example = importlib.import_module(module_name) + + # render a frame + img = np.asarray(example.renderer.target.draw()) + + # check if _something_ was rendered + assert img is not None and img.size > 0 + + screenshot_path = screenshots_dir / f"{module.stem}.npy" + + if regenerate_screenshots: + np.save(screenshot_path, img) + + assert ( + screenshot_path.exists() + ), "found # test_example = true but no reference screenshot available" + stored_img = np.load(screenshot_path) + is_similar = np.allclose(img, stored_img, atol=1) + assert is_similar + + +if __name__ == "__main__": + test_examples_run() + test_example_screenshots() diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py new file mode 100644 index 000000000..b25209d71 --- /dev/null +++ b/examples/tests/testutils.py @@ -0,0 +1,60 @@ +""" +Test suite utilities. +""" + +from pathlib import Path +import subprocess +import sys +from itertools import chain + + +ROOT = Path(__file__).parents[2] # repo root +examples_dir = ROOT / "examples" +screenshots_dir = examples_dir / "screenshots" + +# examples live in themed sub-folders +# example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] +example_globs = ["image/*.py"] + + +def get_wgpu_backend(): + """ + Query the configured wgpu backend driver. + """ + code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.request_adapter_info(); print(info['adapter_type'], info['backend_type'])" + result = subprocess.run( + [ + sys.executable, + "-c", + code, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=ROOT, + ) + out = result.stdout.strip() + err = result.stderr.strip() + return err if "traceback" in err.lower() else out + + +wgpu_backend = get_wgpu_backend() +is_lavapipe = wgpu_backend.lower() == "cpu vulkan" + + +def find_examples(query=None, negative_query=None, return_stems=False): + """Finds all modules to be tested.""" + result = [] + for example_path in chain(*(examples_dir.glob(x) for x in example_globs)): + example_code = example_path.read_text(encoding="UTF-8") + query_match = query is None or query in example_code + negative_query_match = ( + negative_query is None or negative_query not in example_code + ) + if query_match and negative_query_match: + result.append(example_path) + result = list(sorted(result)) + if return_stems: + result = [r.stem for r in result] + return result + diff --git a/examples/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb similarity index 100% rename from examples/garbage_collection.ipynb rename to notebooks/garbage_collection.ipynb diff --git a/examples/gridplot.ipynb b/notebooks/gridplot.ipynb similarity index 100% rename from examples/gridplot.ipynb rename to notebooks/gridplot.ipynb diff --git a/examples/gridplot_simple.ipynb b/notebooks/gridplot_simple.ipynb similarity index 100% rename from examples/gridplot_simple.ipynb rename to notebooks/gridplot_simple.ipynb diff --git a/examples/gridplot_simple.py b/notebooks/gridplot_simple.py similarity index 100% rename from examples/gridplot_simple.py rename to notebooks/gridplot_simple.py diff --git a/examples/histogram.ipynb b/notebooks/histogram.ipynb similarity index 100% rename from examples/histogram.ipynb rename to notebooks/histogram.ipynb diff --git a/examples/image_widget.ipynb b/notebooks/image_widget.ipynb similarity index 100% rename from examples/image_widget.ipynb rename to notebooks/image_widget.ipynb diff --git a/examples/line_collection_event.ipynb b/notebooks/line_collection_event.ipynb similarity index 100% rename from examples/line_collection_event.ipynb rename to notebooks/line_collection_event.ipynb diff --git a/examples/linear_region_selector.ipynb b/notebooks/linear_region_selector.ipynb similarity index 100% rename from examples/linear_region_selector.ipynb rename to notebooks/linear_region_selector.ipynb diff --git a/examples/linear_selector.ipynb b/notebooks/linear_selector.ipynb similarity index 100% rename from examples/linear_selector.ipynb rename to notebooks/linear_selector.ipynb diff --git a/examples/lineplot.ipynb b/notebooks/lineplot.ipynb similarity index 100% rename from examples/lineplot.ipynb rename to notebooks/lineplot.ipynb diff --git a/examples/scatter.ipynb b/notebooks/scatter.ipynb similarity index 100% rename from examples/scatter.ipynb rename to notebooks/scatter.ipynb diff --git a/examples/simple.ipynb b/notebooks/simple.ipynb similarity index 100% rename from examples/simple.ipynb rename to notebooks/simple.ipynb diff --git a/examples/single_contour_event.ipynb b/notebooks/single_contour_event.ipynb similarity index 100% rename from examples/single_contour_event.ipynb rename to notebooks/single_contour_event.ipynb diff --git a/examples/text.ipynb b/notebooks/text.ipynb similarity index 100% rename from examples/text.ipynb rename to notebooks/text.ipynb From 97f6f9e319fbd3265ec62aaeab2d69ad06e4d610 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:38:09 -0400 Subject: [PATCH 02/96] more tests --- examples/line/colorslice.py | 62 ++++++++++++++++++++++++++++++++ examples/line/dataslice.py | 54 ++++++++++++++++++++++++++++ examples/line/line.py | 50 ++++++++++++++++++++++++++ examples/line/present_scaling.py | 55 ++++++++++++++++++++++++++++ examples/scatter/scatter.py | 33 +++++++++++++++++ examples/tests/test_examples.py | 4 +-- examples/tests/testutils.py | 2 +- 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 examples/line/colorslice.py create mode 100644 examples/line/dataslice.py create mode 100644 examples/line/line.py create mode 100644 examples/line/present_scaling.py create mode 100644 examples/scatter/scatter.py diff --git a/examples/line/colorslice.py b/examples/line/colorslice.py new file mode 100644 index 000000000..d3fc918cf --- /dev/null +++ b/examples/line/colorslice.py @@ -0,0 +1,62 @@ +""" +Line Plot +============ +Example showing color slicing with cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +# indexing of colors +cosine_graphic.colors[:15] = "magenta" +cosine_graphic.colors[90:] = "red" +cosine_graphic.colors[60] = "w" + +# indexing to assign colormaps to entire lines or segments +sinc_graphic.cmap[10:50] = "gray" +sine_graphic.cmap = "seismic" + +# more complex indexing, set the blue value directly from an array +cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65) + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/dataslice.py b/examples/line/dataslice.py new file mode 100644 index 000000000..f46307cc7 --- /dev/null +++ b/examples/line/dataslice.py @@ -0,0 +1,54 @@ +""" +Line Plot +============ +Example showing data slicing with cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.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]]) + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/line.py b/examples/line/line.py new file mode 100644 index 000000000..b44d7bd4c --- /dev/null +++ b/examples/line/line.py @@ -0,0 +1,50 @@ +""" +Line Plot +============ +Example showing cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/present_scaling.py b/examples/line/present_scaling.py new file mode 100644 index 000000000..e5b063bc8 --- /dev/null +++ b/examples/line/present_scaling.py @@ -0,0 +1,55 @@ +""" +Line Plot +============ +Example showing present and scaling feature for lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +sinc_graphic.present = False + +plot.center_scene() + +plot.auto_scale() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) + diff --git a/examples/scatter/scatter.py b/examples/scatter/scatter.py new file mode 100644 index 000000000..5a9706f04 --- /dev/null +++ b/examples/scatter/scatter.py @@ -0,0 +1,33 @@ +""" +Scatter Plot +============ +Example showing scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index a0f5e2bf8..ed9eb3a53 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -75,5 +75,5 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal if __name__ == "__main__": - test_examples_run() - test_example_screenshots() + test_examples_run("simple") + test_example_screenshots("simple", regenerate_screenshots=True) diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index b25209d71..48fd2cea4 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -14,7 +14,7 @@ # examples live in themed sub-folders # example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] -example_globs = ["image/*.py"] +example_globs = ["image/*.py", "scatter/*.py", "line/*.py"] def get_wgpu_backend(): From 3344414dd5da849cd5f8dc9e7a951d4e0a434d6a Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:46:06 -0400 Subject: [PATCH 03/96] more tests, still working on iw --- examples/gridplot/gridplot.py | 35 +++++++++++++++++++++++++++++ examples/imagewidget/imagewidget.py | 0 examples/tests/testutils.py | 3 +-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 examples/gridplot/gridplot.py create mode 100644 examples/imagewidget/imagewidget.py diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py new file mode 100644 index 000000000..59566f4b2 --- /dev/null +++ b/examples/gridplot/gridplot.py @@ -0,0 +1,35 @@ +""" +GridPlot Simple +============ +Example showing simple 2x3 GridPlot with pre-saved 512x512 random images. +""" + +# test_example = true + +from fastplotlib import GridPlot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = GridPlot(shape=(2,3), canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +for subplot in plot: + subplot.add_image(data=data) + +plot.show() + +for subplot in plot: + subplot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/imagewidget/imagewidget.py b/examples/imagewidget/imagewidget.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 48fd2cea4..1733481e2 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -13,8 +13,7 @@ screenshots_dir = examples_dir / "screenshots" # examples live in themed sub-folders -# example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] -example_globs = ["image/*.py", "scatter/*.py", "line/*.py"] +example_globs = ["image/*.py", "scatter/*.py", "line/*.py", "gridplot/*.py"] def get_wgpu_backend(): From 4fc8d578fe64d1aab3835eb2bab0a0a029206379 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 20 Apr 2023 09:25:21 -0400 Subject: [PATCH 04/96] ipywidget can only be tested w/ nbmake --- examples/imagewidget/imagewidget.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/imagewidget/imagewidget.py diff --git a/examples/imagewidget/imagewidget.py b/examples/imagewidget/imagewidget.py deleted file mode 100644 index e69de29bb..000000000 From 2c9a998b34da5c2b8d362c48c67170fff10aa28c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 20 Apr 2023 10:05:08 -0400 Subject: [PATCH 05/96] updated tests --- .gitignore | 5 ++-- examples/gridplot/gridplot.py | 7 ++--- examples/image/cmap.py | 7 ++--- examples/scatter/scatter_cmap.py | 35 +++++++++++++++++++++++ examples/scatter/scatter_colorslice.py | 36 ++++++++++++++++++++++++ examples/scatter/scatter_dataslice.py | 39 ++++++++++++++++++++++++++ examples/scatter/scatter_present.py | 35 +++++++++++++++++++++++ 7 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 examples/scatter/scatter_cmap.py create mode 100644 examples/scatter/scatter_colorslice.py create mode 100644 examples/scatter/scatter_dataslice.py create mode 100644 examples/scatter/scatter_present.py diff --git a/.gitignore b/.gitignore index 98b21a799..603fab511 100644 --- a/.gitignore +++ b/.gitignore @@ -132,5 +132,6 @@ dmypy.json .idea/ # Binary files for testing -/examples/data -/examples/screenshots +examples/data +examples/screenshots + diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py index 59566f4b2..1c92a3440 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/gridplot/gridplot.py @@ -8,7 +8,7 @@ from fastplotlib import GridPlot import numpy as np -from pathlib import Path +import imageio.v3 as iio from wgpu.gui.offscreen import WgpuCanvas from pygfx import WgpuRenderer @@ -18,11 +18,10 @@ plot = GridPlot(shape=(2,3), canvas=canvas, renderer=renderer) -data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") -data = np.load(data_path) +im = iio.imread("imageio:clock.png") for subplot in plot: - subplot.add_image(data=data) + subplot.add_image(data=im) plot.show() diff --git a/examples/image/cmap.py b/examples/image/cmap.py index 684d90f5e..9fef516d5 100644 --- a/examples/image/cmap.py +++ b/examples/image/cmap.py @@ -7,7 +7,7 @@ from fastplotlib import Plot import numpy as np -from pathlib import Path +import imageio.v3 as iio from wgpu.gui.offscreen import WgpuCanvas from pygfx import WgpuRenderer @@ -17,11 +17,10 @@ plot = Plot(canvas=canvas, renderer=renderer) -data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") -data = np.load(data_path) +im = iio.imread("imageio:clock.png") # plot the image data -image_graphic = plot.add_image(data=data, name="random-image") +image_graphic = plot.add_image(data=im, name="random-image") plot.show() diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py new file mode 100644 index 000000000..af9f8ad68 --- /dev/null +++ b/examples/scatter/scatter_cmap.py @@ -0,0 +1,35 @@ +""" +Scatter Plot +============ +Example showing cmap change for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.cmap = "viridis" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/scatter/scatter_colorslice.py b/examples/scatter/scatter_colorslice.py new file mode 100644 index 000000000..749f0fd37 --- /dev/null +++ b/examples/scatter/scatter_colorslice.py @@ -0,0 +1,36 @@ +""" +Scatter Plot +============ +Example showing color slice for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.colors[0:5000] = "red" +scatter_graphic.colors[10000:20000] = "white" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/scatter/scatter_dataslice.py b/examples/scatter/scatter_dataslice.py new file mode 100644 index 000000000..6831d3eca --- /dev/null +++ b/examples/scatter/scatter_dataslice.py @@ -0,0 +1,39 @@ +""" +Scatter Plot +============ +Example showing data slice for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.data[0] = np.array([[35, -20, -5]]) +scatter_graphic.data[1] = np.array([[30, -20, -5]]) +scatter_graphic.data[2] = np.array([[40, -20, -5]]) +scatter_graphic.data[3] = np.array([[25, -20, -5]]) + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) + \ No newline at end of file diff --git a/examples/scatter/scatter_present.py b/examples/scatter/scatter_present.py new file mode 100644 index 000000000..0dbae0077 --- /dev/null +++ b/examples/scatter/scatter_present.py @@ -0,0 +1,35 @@ +""" +Scatter Plot +============ +Example showing present feature for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.present = False + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) From 0ff0d42b7c53d7320dec7a45e7689f4f6575bc41 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 20 Apr 2023 23:10:09 -0400 Subject: [PATCH 06/96] support numpy fancy indexing for colors and data features (#177) --- fastplotlib/graphics/features/_base.py | 58 +++++++++++++++++++++++- fastplotlib/graphics/features/_colors.py | 26 +++++++---- fastplotlib/graphics/features/_data.py | 12 +++-- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 1119bed6b..ed08a9008 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -170,6 +170,12 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: if isinstance(key, int): return key + if isinstance(key, np.ndarray): + return cleanup_array_slice(key, upper_bound) + + # if isinstance(key, np.integer): + # return int(key) + if isinstance(key, tuple): # if tuple of slice we only need the first obj # since the first obj is the datapoint indices @@ -197,13 +203,54 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: stop = upper_bound elif stop > upper_bound: - raise IndexError("Index out of bounds") + raise IndexError(f"Index: `{stop}` out of bounds for feature array of size: `{upper_bound}`") step = key.step if step is None: step = 1 return slice(start, stop, step) + # return slice(int(start), int(stop), int(step)) + + +def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: + """ + Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. + + Parameters + ---------- + key: np.ndarray + integer or boolean array + + upper_bound + + Returns + ------- + np.ndarray + integer indexing array + + """ + + if key.ndim > 1: + raise TypeError( + f"Can only use 1D boolean or integer arrays for fancy indexing" + ) + + # if boolean array convert to integer array of indices + if key.dtype == bool: + key = np.nonzero(key)[0] + + # make sure indices within bounds of feature buffer range + if key[-1] > upper_bound: + raise IndexError(f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`") + + # make sure indices are integers + if np.issubdtype(key.dtype, np.integer): + return key + + raise TypeError( + f"Can only use 1D boolean or integer arrays for fancy indexing" + ) class GraphicFeatureIndexable(GraphicFeature): @@ -236,7 +283,8 @@ def _upper_bound(self) -> int: def _update_range_indices(self, key): """Currently used by colors and positions data""" - key = cleanup_slice(key, self._upper_bound) + if not isinstance(key, np.ndarray): + key = cleanup_slice(key, self._upper_bound) if isinstance(key, int): self.buffer.update_range(key, size=1) @@ -254,6 +302,12 @@ def _update_range_indices(self, key): ixs = range(key.start, key.stop, step) for ix in ixs: self.buffer.update_range(ix, size=1) + + # TODO: See how efficient this is with large indexing + elif isinstance(key, np.ndarray): + for ix in key: + self.buffer.update_range(int(ix), size=1) + else: raise TypeError("must pass int or slice to update range") diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index 7813df61f..5ff82ca72 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -1,6 +1,6 @@ import numpy as np -from ._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice, FeatureEvent +from ._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice, FeatureEvent, cleanup_array_slice from ...utils import make_colors, get_cmap_texture, make_pygfx_colors from pygfx import Color @@ -102,9 +102,8 @@ def __setitem__(self, key, value): indices = range(_key.start, _key.stop, _key.step) # or single numerical index - elif isinstance(key, int): - if key > self._upper_bound: - raise IndexError("Index out of bounds") + elif isinstance(key, (int, np.integer)): + key = cleanup_slice(key, self._upper_bound) indices = [key] elif isinstance(key, tuple): @@ -128,6 +127,10 @@ def __setitem__(self, key, value): self._feature_changed(key, value) return + elif isinstance(key, np.ndarray): + key = cleanup_array_slice(key, self._upper_bound) + indices = key + else: raise TypeError("Graphic features only support integer and numerical fancy indexing") @@ -181,6 +184,8 @@ def _feature_changed(self, key, new_data): indices = [key] elif isinstance(key, slice): indices = range(key.start, key.stop, key.step) + elif isinstance(key, np.ndarray): + indices = key else: raise TypeError("feature changed key must be slice or int") @@ -205,11 +210,16 @@ def __init__(self, parent, colors): def __setitem__(self, key, value): key = cleanup_slice(key, self._upper_bound) - if not isinstance(key, slice): - raise TypeError("Cannot set cmap on single indices, must pass a slice object or " - "set it on the entire data.") + if not isinstance(key, (slice, np.ndarray)): + raise TypeError("Cannot set cmap on single indices, must pass a slice object, " + "numpy.ndarray or set it on the entire data.") + + if isinstance(key, slice): + n_colors = len(range(key.start, key.stop, key.step)) - n_colors = len(range(key.start, key.stop, key.step)) + else: + # numpy array + n_colors = key.size colors = make_colors(n_colors, cmap=value).astype(self._data.dtype) super(CmapFeature, self).__setitem__(key, colors) diff --git a/fastplotlib/graphics/features/_data.py b/fastplotlib/graphics/features/_data.py index 5063b4200..6c7dbfa75 100644 --- a/fastplotlib/graphics/features/_data.py +++ b/fastplotlib/graphics/features/_data.py @@ -3,7 +3,7 @@ import numpy as np from pygfx import Buffer, Texture -from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent, to_gpu_supported_dtype +from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent, to_gpu_supported_dtype, cleanup_array_slice class PointsDataFeature(GraphicFeatureIndexable): @@ -48,8 +48,12 @@ def _fix_data(self, data, parent): return data def __setitem__(self, key, value): + if isinstance(key, np.ndarray): + # make sure 1D array of int or boolean + key = cleanup_array_slice(key, self._upper_bound) + # put data into right shape if they're only indexing datapoints - if isinstance(key, (slice, int)): + if isinstance(key, (slice, int, np.ndarray, np.integer)): value = self._fix_data(value, self._parent) # otherwise assume that they have the right shape # numpy will throw errors if it can't broadcast @@ -66,10 +70,12 @@ def _update_range(self, key): def _feature_changed(self, key, new_data): if key is not None: key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): + if isinstance(key, (int, np.integer)): indices = [key] elif isinstance(key, slice): indices = range(key.start, key.stop, key.step) + elif isinstance(key, np.ndarray): + indices = key elif key is None: indices = None From 5c34539435fe0471b0f1c3607d4ff6e509033bdf Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 9 May 2023 11:58:52 -0400 Subject: [PATCH 07/96] type annotation fix for older python versions --- fastplotlib/graphics/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index ae0715b64..c1c3fbdfc 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -301,7 +301,7 @@ class PreviouslyModifiedData: indices: Any -COLLECTION_GRAPHICS: dict[str, Graphic] = dict() +COLLECTION_GRAPHICS: Dict[str, Graphic] = dict() class GraphicCollection(Graphic): From 4823fec68b1645f23e7d81c368eb89a3795fc517 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 10:22:10 -0400 Subject: [PATCH 08/96] adding fancy indexing implemented in #177 --- examples/line/colorslice.py | 7 +++++++ examples/line/dataslice.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/examples/line/colorslice.py b/examples/line/colorslice.py index d3fc918cf..1a4595196 100644 --- a/examples/line/colorslice.py +++ b/examples/line/colorslice.py @@ -54,6 +54,13 @@ # more complex indexing, set the blue value directly from an array cosine_graphic.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" +# +# key2 = np.array([True, False, True, False, True, True, True, True]) +# cosine_graphic.colors[key2] = "Green" + plot.center_scene() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/line/dataslice.py b/examples/line/dataslice.py index f46307cc7..b8a30cb01 100644 --- a/examples/line/dataslice.py +++ b/examples/line/dataslice.py @@ -46,6 +46,10 @@ cosine_graphic.data[90:, 1] = 7 cosine_graphic.data[0] = np.array([[-10, 0, 0]]) +# additional fancy indexing using numpy +# key2 = np.array([True, False, True, False, True, True, True, True]) +# sinc_graphic.data[key2] = np.array([[5, 1, 2]]) + plot.center_scene() img = np.asarray(plot.renderer.target.draw()) From b2350f3af01c3483bfc95746bd165e271e08f04f Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 17:37:58 -0400 Subject: [PATCH 09/96] requested changes --- .gitignore | 1 - examples/data/iris.npy | Bin 0 -> 4928 bytes examples/gridplot/gridplot.py | 15 ++++++++++----- examples/image/cmap.py | 4 ++-- examples/image/simple.py | 9 ++++----- examples/image/vminvmax.py | 8 ++++---- examples/scatter/scatter.py | 7 +++++-- examples/scatter/scatter_cmap.py | 7 +++++-- examples/scatter/scatter_colorslice.py | 12 ++++++++---- examples/scatter/scatter_dataslice.py | 17 +++++++++++------ examples/scatter/scatter_present.py | 10 ++++++++-- examples/tests/test_examples.py | 4 ++++ 12 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 examples/data/iris.npy diff --git a/.gitignore b/.gitignore index 603fab511..e46391f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,5 @@ dmypy.json .idea/ # Binary files for testing -examples/data examples/screenshots diff --git a/examples/data/iris.npy b/examples/data/iris.npy new file mode 100644 index 0000000000000000000000000000000000000000..a8c7e5ab0a48138e424a1ed78323e4a7f6158259 GIT binary patch literal 4928 zcmbW5y>4Sw6oqY%^XG&lc5KJ_nc&VKr2qjELP&N+2cn^%LJ^aQ0x_cm(IB3J3f?4- z5DBWO(joBzi86`?C5o72t#5DLbCm*ab9L@Md+)W^{yE3~^^31Q|MKf*sopL}=l;_%J-o%cU_Z-4yZ zyZhr`#{cu_96db!S>xeZnQ47J{r$K5=gjX7%NJ|%KmWLYdNb``nrZzi^kuK-?~Atc zI^^=(xBM~xgV_fU&zj#i)<2(OU-IR^r}fJz_K|Cy{)M}1y9=RT#;>?a{HMk-aQtu8 z;*;ME+h6v+e_@`GI8`{s2u_(3k7$?tOL!$103m;8c{`dYF5i-;e6y*pvQ@$md*eltJX zcNhBFNBr=g`9rRF%KV<2AN0Wo|7RgbU;eZI#BcVOcu4=!+lc-u-x#H`ALs+%d287w66Es6Y8{S#Le^AUoT}HSc%Yug<4= z>oYH2ncpqzb!->D=riWSlh`M}7tY_d>x=Uv?aP1P@y~hRn(bUif5=|L_?Q#SG3Gn< zoqa03k?l9Fj~slXz{fnH4)h)t^6Ufc-!Agg4gBZ}2j^q*ulm}I__aUr$$p7H^{e*V z!Jqnwa}fK?7v|xu^D{DD@}Ko1d|T$NZJb$u@}K(fCq2&Dt>91JyHTIg-?n|~JNeW8 zoxrE{M(_*&ReqUgH?E(|ul6N>(r13RjU($z`^i7`%^bOm{u8bz=7)3m!uM`! z75J2|Imbmm&|d@Ns@IS35g+qA`xAcjrav-%;h?`;#$V=#^N0DG^#MP6!(Ux4|I`on z$*ZV8>0>|VqwsCIUebP*KhB4i`RmMNkN)O%1o-m%oKa^x=QObzbtv z`38RK{Ia+oGM^J)H9y2Pa(wiA)|dJTKJLu#uJ_{?J{!iH{mDF0+_$dJ>`(HcdTpBT zy|7O|OTSUf3*ztD?~VK8X)zzR%=4}J!GFdJKD~FnpZibpqr7e#$5Ql{zJrhRm;M}h z-<|R8M10a?UNr2N{vbZ`4ZoYVM}C=4%nST8pYh+aU*f0#@ee7y8xwa)06eLHyXy`<3jD><51G zd{X_^&p+wWA1PPd%t!K9_m6(HKbYT7i~NI!-%)Qz{_qDstkqZPU*DV2zg2nWPv7CA zZ9nAe+Wl4bFZfzvUvcsOL7s=;Q@-l;Nj|Fll>C 0 + # if screenshots dir does not exist, will create and generate screenshots + if not os.path.exists(screenshots_dir): + os.mkdir(screenshots_dir) + screenshot_path = screenshots_dir / f"{module.stem}.npy" if regenerate_screenshots: From eef74ef9e10d13d94084fd4b60ed2da56d985c65 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 17:48:51 -0400 Subject: [PATCH 10/96] adding diff storage for CI pipeline usage --- examples/tests/test_examples.py | 42 ++++++++++++++++++++++++++++++++- examples/tests/testutils.py | 1 + 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 58316c241..e79d98ef0 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -6,6 +6,7 @@ import pytest import os import numpy as np +import imageio.v3 as iio from .testutils import ( ROOT, @@ -14,6 +15,7 @@ find_examples, wgpu_backend, is_lavapipe, + diffs_dir ) # run all tests unless they opt-out @@ -75,7 +77,45 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal ), "found # test_example = true but no reference screenshot available" stored_img = np.load(screenshot_path) is_similar = np.allclose(img, stored_img, atol=1) - assert is_similar + update_diffs(module.stem, is_similar, img, stored_img) + assert is_similar, ( + f"rendered image for example {module.stem} changed, see " + f"the {diffs_dir.relative_to(ROOT).as_posix()} folder" + " for visual diffs (you can download this folder from" + " CI build artifacts as well)" + ) + + +def update_diffs(module, is_similar, img, stored_img): + diffs_dir.mkdir(exist_ok=True) + + diffs_rgba = None + + def get_diffs_rgba(slicer): + # lazily get and cache the diff computation + nonlocal diffs_rgba + if diffs_rgba is None: + # cast to float32 to avoid overflow + # compute absolute per-pixel difference + diffs_rgba = np.abs(stored_img.astype("f4") - img) + # magnify small values, making it easier to spot small errors + diffs_rgba = ((diffs_rgba / 255) ** 0.25) * 255 + # cast back to uint8 + diffs_rgba = diffs_rgba.astype("u1") + return diffs_rgba[..., slicer] + + # split into an rgb and an alpha diff + diffs = { + diffs_dir / f"{module}-rgb.png": slice(0, 3), + diffs_dir / f"{module}-alpha.png": 3, + } + + for path, slicer in diffs.items(): + if not is_similar: + diff = get_diffs_rgba(slicer) + iio.imwrite(path, diff) + elif path.exists(): + path.unlink() if __name__ == "__main__": diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 1733481e2..8e248e1e4 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).parents[2] # repo root examples_dir = ROOT / "examples" screenshots_dir = examples_dir / "screenshots" +diffs_dir = examples_dir / "diffs" # examples live in themed sub-folders example_globs = ["image/*.py", "scatter/*.py", "line/*.py", "gridplot/*.py"] From 9f41382de75de6ecd04a7db8a80c1389b0c59f32 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 13 May 2023 02:55:09 -0400 Subject: [PATCH 11/96] use readthedocs.yaml for doc build (#189) --- .readthedocs.yaml | 22 ++++++++++++++++++++++ requirements_rtd.txt | 6 ------ setup.py | 9 +++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 requirements_rtd.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..1de8fcd91 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + apt_packages: + - libegl1-mesa + - libgl1-mesa-dri + - libxcb-xfixes0-dev + - mesa-vulkan-drivers + - libglfw3 + +sphinx: + configuration: docs/source/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/requirements_rtd.txt b/requirements_rtd.txt deleted file mode 100644 index 14d59e156..000000000 --- a/requirements_rtd.txt +++ /dev/null @@ -1,6 +0,0 @@ -numpy -jupyterlab -jupyter_rfb -pygfx>=0.1.10 -pydata-sphinx-theme<0.10.0 -glfw diff --git a/setup.py b/setup.py index f671a59d6..47b9b4877 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,14 @@ ] +extras_require = { + "docs": [ + "sphinx", + "pydata-sphinx-theme<0.10.0", + "glfw" + ], +} + with open(Path(__file__).parent.joinpath("README.md")) as f: readme = f.read() @@ -37,6 +45,7 @@ author_email='', python_requires='>=3.8', install_requires=install_requires, + extras_require=extras_require, include_package_data=True, description='A fast plotting library built using the pygfx render engine' ) From e8c8ac5660e1b0e3d1603a5767eb1fb828714f9a Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 14:32:35 -0400 Subject: [PATCH 12/96] tests changes, ready to start CI pipeline --- .github/workflows/ci.yml | 0 examples/tests/test_examples.py | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index e79d98ef0..85818ed05 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -49,7 +49,7 @@ def test_that_we_are_on_lavapipe(): @pytest.mark.parametrize("module", examples_to_test, ids=lambda x: x.stem) -def test_example_screenshots(module, force_offscreen, regenerate_screenshots=False): +def test_example_screenshots(module, force_offscreen): """Make sure that every example marked outputs the expected.""" # (relative) module name from project root module_name = module.relative_to(ROOT/"examples").with_suffix("").as_posix().replace("/", ".") @@ -63,14 +63,14 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal # check if _something_ was rendered assert img is not None and img.size > 0 - # if screenshots dir does not exist, will create and generate screenshots + # if screenshots dir does not exist, will create if not os.path.exists(screenshots_dir): os.mkdir(screenshots_dir) screenshot_path = screenshots_dir / f"{module.stem}.npy" - if regenerate_screenshots: - np.save(screenshot_path, img) + # if regenerate_screenshots == "True": + # np.save(screenshot_path, img) assert ( screenshot_path.exists() @@ -120,4 +120,4 @@ def get_diffs_rgba(slicer): if __name__ == "__main__": test_examples_run("simple") - test_example_screenshots("simple", regenerate_screenshots=True) + test_example_screenshots("simple") From 78cb8c7e8449d289a863164e4638ac4bde58b0f5 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 15:28:19 -0400 Subject: [PATCH 13/96] allow for screenshots to be regenerated via os.environ key --- examples/tests/test_examples.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 85818ed05..5aa78a164 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -69,8 +69,9 @@ def test_example_screenshots(module, force_offscreen): screenshot_path = screenshots_dir / f"{module.stem}.npy" - # if regenerate_screenshots == "True": - # np.save(screenshot_path, img) + if "REGENERATE_SCREENSHOTS" in os.environ.keys(): + if os.environ["REGENERATE_SCREENSHOTS"] == "1": + np.save(screenshot_path, img) assert ( screenshot_path.exists() From 43f39bab0f6e0dca30aeb0b3c26f766d40963e46 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 16:52:59 -0400 Subject: [PATCH 14/96] CI build --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29bb..7708aa2df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: + - master + - fpl-tests + pull_request: + branches: + - master + + jobs: + + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up Python '3.10' + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install package and dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + + test-examples-build: + name: Test examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: + REGENERATE_SCREENSHOTS=1 pytest -v examples + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: screenshot-diffs + path: examples/screenshots/diffs \ No newline at end of file From 00fd18424eb9fdc2c8af6b6376823b05f59ad508 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 17:28:02 -0400 Subject: [PATCH 15/96] linux-build passes, still working on examples passing --- .github/workflows/ci.yml | 98 ++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7708aa2df..ca427dccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,53 +9,55 @@ on: branches: - master - jobs: +jobs: - build: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - steps: - - uses: actions/checkout@v3 - - name: Set up Python '3.10' - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - name: Install package and dev dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -e . + linux-build: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up Python '3.10' + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install package and dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . - test-examples-build: - name: Test examples - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - name: Install llvmpipe and lavapipe for offscreen canvas - run: | - sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - - name: Install dev dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - - name: Show wgpu backend - run: - python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" - - name: Test examples - env: - PYGFX_EXPECT_LAVAPIPE: true - run: - REGENERATE_SCREENSHOTS=1 pytest -v examples - - uses: actions/upload-artifact@v3 - if: ${{ failure() }} - with: - name: screenshot-diffs - path: examples/screenshots/diffs \ No newline at end of file +# test-examples-build: +# name: Test examples +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# steps: +# - uses: actions/checkout@v3 +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: '3.10' +# - name: Install llvmpipe and lavapipe for offscreen canvas +# run: | +# sudo apt-get update -y -qq +# sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt +# pip install -e . +# - name: Show wgpu backend +# run: +# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" +# - name: Test examples +# env: +# PYGFX_EXPECT_LAVAPIPE: true +# run: +# pip install pytest +# REGENERATE_SCREENSHOTS=1 pytest -v examples +# - uses: actions/upload-artifact@v3 +# if: ${{ failure() }} +# with: +# name: screenshot-diffs +# path: examples/screenshots/diffs \ No newline at end of file From 243e0b689d93fe5c861327bc08bb33684cbcd1b9 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 19:19:42 -0400 Subject: [PATCH 16/96] progress on CI pipeline, still need to figure out test build --- .github/workflows/ci.yml | 6 ++++-- requirements.txt | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca427dccc..65b671973 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - fpl-tests pull_request: branches: - master @@ -47,13 +46,16 @@ jobs: # python -m pip install --upgrade pip # pip install -r requirements.txt # pip install -e . +# git clone https://github.com/pygfx/pygfx.git +# cd pygfx +# git reset --hard 88ed3f31ed14840eb84259c3b1bd1cac740ecf25 # - name: Show wgpu backend # run: # python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" # - name: Test examples # env: # PYGFX_EXPECT_LAVAPIPE: true -# run: +# run: | # pip install pytest # REGENERATE_SCREENSHOTS=1 pytest -v examples # - uses: actions/upload-artifact@v3 diff --git a/requirements.txt b/requirements.txt index 79375c66a..d0bc814f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy jupyterlab jupyter_rfb pygfx>=0.1.10 +imageio \ No newline at end of file From 4f74c3091399d81be2d764c539651ed4730708d9 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 20:16:12 -0400 Subject: [PATCH 17/96] still failing test build, what I have for now --- .github/workflows/ci.yml | 72 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65b671973..95e3ea516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,40 +26,38 @@ jobs: pip install -r requirements.txt pip install -e . -# test-examples-build: -# name: Test examples -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# steps: -# - uses: actions/checkout@v3 -# - name: Set up Python -# uses: actions/setup-python@v3 -# with: -# python-version: '3.10' -# - name: Install llvmpipe and lavapipe for offscreen canvas -# run: | -# sudo apt-get update -y -qq -# sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers -# - name: Install dev dependencies -# run: | -# python -m pip install --upgrade pip -# pip install -r requirements.txt -# pip install -e . -# git clone https://github.com/pygfx/pygfx.git -# cd pygfx -# git reset --hard 88ed3f31ed14840eb84259c3b1bd1cac740ecf25 -# - name: Show wgpu backend -# run: -# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" -# - name: Test examples -# env: -# PYGFX_EXPECT_LAVAPIPE: true -# run: | -# pip install pytest -# REGENERATE_SCREENSHOTS=1 pytest -v examples -# - uses: actions/upload-artifact@v3 -# if: ${{ failure() }} -# with: -# name: screenshot-diffs -# path: examples/screenshots/diffs \ No newline at end of file + test-examples-build: + name: Test examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: | + pip install pytest + pip install git+https://github.com/pygfx/pygfx.git@e683ae4542ae96ae8dce59a17f74b50bf996a4fa + REGENERATE_SCREENSHOTS=1 pytest -v examples + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: screenshot-diffs + path: examples/screenshots/diffs \ No newline at end of file From 9236ff12c513fd4ce1e79e62c63d36add9d465fc Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 17 May 2023 16:33:41 -0400 Subject: [PATCH 18/96] kwargs for Plot and GridPlot to set canvas star size (#194) --- fastplotlib/layouts/_gridplot.py | 8 ++++++++ fastplotlib/plot.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 3f694b007..ab79a3804 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -30,6 +30,7 @@ def __init__( controllers: Union[np.ndarray, str] = None, canvas: WgpuCanvas = None, renderer: pygfx.Renderer = None, + size: Tuple[int, int] = (500, 300), **kwargs ): """ @@ -62,6 +63,9 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance + size: (int, int) + starting size of canvas, default (500, 300) + """ self.shape = shape @@ -160,6 +164,8 @@ def __init__( self._current_iter = None + self._starting_size = size + def __getitem__(self, index: Union[Tuple[int, int], str]): if isinstance(index, str): for subplot in self._subplots.ravel(): @@ -266,6 +272,8 @@ def show(self): for subplot in self: subplot.auto_scale(maintain_aspect=True, zoom=0.95) + self.canvas.set_logical_size(*self._starting_size) + return self.canvas def _get_iterator(self): diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 97e19effd..96b1ac8dc 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -11,6 +11,7 @@ def __init__( renderer: pygfx.Renderer = None, camera: str = '2d', controller: Union[pygfx.PanZoomController, pygfx.OrbitController] = None, + size: Tuple[int, int] = (500, 300), **kwargs ): """ @@ -31,6 +32,9 @@ def __init__( Usually ``None``, you can pass an existing controller from another ``Plot`` or ``Subplot`` within a ``GridPlot`` to synchronize them. + size: (int, int) + starting size of canvas, default (500, 300) + kwargs passed to Subplot, for example ``name`` @@ -83,6 +87,8 @@ def __init__( **kwargs ) + self._starting_size = size + def render(self): super(Plot, self).render() @@ -103,4 +109,6 @@ def show(self, autoscale: bool = True): if autoscale: self.auto_scale(maintain_aspect=True, zoom=0.95) + self.canvas.set_logical_size(*self._starting_size) + return self.canvas From f04745218988bf747eb0f4f6504228eb3cbfea23 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 00:59:50 -0400 Subject: [PATCH 19/96] Update ci.yml use pygfx before pylinalg refactor --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95e3ea516..ebca8790d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,10 +54,10 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest - pip install git+https://github.com/pygfx/pygfx.git@e683ae4542ae96ae8dce59a17f74b50bf996a4fa + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} with: name: screenshot-diffs - path: examples/screenshots/diffs \ No newline at end of file + path: examples/screenshots/diffs From 0318498e6bba58c4cf28884e99b5e4e55398a34f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:17:02 -0400 Subject: [PATCH 20/96] Update ci.yml sed to remove pygfx from setup.py --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebca8790d..15837db94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,9 @@ jobs: - name: Install package and dev dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 pip install -e . test-examples-build: @@ -44,7 +46,9 @@ jobs: - name: Install dev dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 pip install -e . - name: Show wgpu backend run: @@ -54,7 +58,6 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest - pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} From 59d33abea1d1338ad96510189d77fbe1d8696d16 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:19:57 -0400 Subject: [PATCH 21/96] Update ci.yml imageio --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15837db94..195d199cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest + pip install pytest imageio REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} From b244c5363b8c260b8889aae85925b1c51bfcc200 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:22:42 -0400 Subject: [PATCH 22/96] Delete requirements_rtd.txt --- requirements_rtd.txt | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 requirements_rtd.txt diff --git a/requirements_rtd.txt b/requirements_rtd.txt deleted file mode 100644 index 14d59e156..000000000 --- a/requirements_rtd.txt +++ /dev/null @@ -1,6 +0,0 @@ -numpy -jupyterlab -jupyter_rfb -pygfx>=0.1.10 -pydata-sphinx-theme<0.10.0 -glfw From d2b4005810656c93cca0462ee67d4723802f8f3c Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:29:28 -0400 Subject: [PATCH 23/96] Update ci.yml testing upload screens --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 195d199cc..bbc47b7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: pip install pytest imageio REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 - if: ${{ failure() }} + #if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs From 5862c6ff9993baae6225f0fd446eb7cbd1509569 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 21 May 2023 08:52:47 -0400 Subject: [PATCH 24/96] create BaseSelector, heatmap also works with LinearSelector and LinearRegionSelector (#187) * BaseSelector handles most of the event handling, simplifies a lot of implementation. * same functionality between Linear and LinearRegion, shift + arrow keys to move * sync not tested for LinearRegion * HeatmapGraphic has helpers to add Linear and LinearRegion * indices getters and get_selected_data() are now based on Graphic type, so line and heatmap are handled differently * started RectangleRegionSelector, will finish in another PR --- docs/source/api/graphics.rst | 9 +- docs/source/api/selectors.rst | 15 + docs/source/index.rst | 1 + fastplotlib/graphics/__init__.py | 1 - fastplotlib/graphics/features/_data.py | 6 +- fastplotlib/graphics/heatmap.py | 139 ------- fastplotlib/graphics/image.py | 149 ++++++- .../graphics/selectors/_base_selector.py | 293 +++++++++++++ fastplotlib/graphics/selectors/_linear.py | 257 +++--------- .../graphics/selectors/_linear_region.py | 391 +++++++----------- .../graphics/selectors/_mesh_positions.py | 31 ++ .../graphics/selectors/_rectangle_region.py | 304 ++++++++++++++ fastplotlib/graphics/selectors/_sync.py | 2 +- 13 files changed, 996 insertions(+), 602 deletions(-) create mode 100644 docs/source/api/selectors.rst delete mode 100644 fastplotlib/graphics/heatmap.py create mode 100644 fastplotlib/graphics/selectors/_base_selector.py create mode 100644 fastplotlib/graphics/selectors/_mesh_positions.py create mode 100644 fastplotlib/graphics/selectors/_rectangle_region.py diff --git a/docs/source/api/graphics.rst b/docs/source/api/graphics.rst index c3e264a50..d38045dae 100644 --- a/docs/source/api/graphics.rst +++ b/docs/source/api/graphics.rst @@ -30,18 +30,11 @@ Line Stack .. autoclass:: fastplotlib.graphics.line_collection.LineStack :members: :inherited-members: - -Line Slider -########### - -.. autoclass:: fastplotlib.graphics.line_slider.LineSlider - :members: - :inherited-members: Heatmap ####### -.. autoclass:: fastplotlib.graphics.heatmap.HeatmapGraphic +.. autoclass:: fastplotlib.graphics.image.HeatmapGraphic :members: :inherited-members: diff --git a/docs/source/api/selectors.rst b/docs/source/api/selectors.rst new file mode 100644 index 000000000..c43f936bd --- /dev/null +++ b/docs/source/api/selectors.rst @@ -0,0 +1,15 @@ +.. _api_selectors: + +Selectors +********* + +Linear +###### + +.. autoclass:: fastplotlib.graphics.selectors.LinearSelector + :members: + :inherited-members: + +.. autoclass:: fastplotlib.graphics.selectors.LinearRegionSelector + :members: + :inherited-members: diff --git a/docs/source/index.rst b/docs/source/index.rst index b7f823f07..8ccc160b5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ Welcome to fastplotlib's documentation! Subplot Gridplot Graphics + Selectors Widgets Summary diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 66ad5820f..5a4786ca2 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -2,7 +2,6 @@ from .line import LineGraphic from .scatter import ScatterGraphic from .image import ImageGraphic, HeatmapGraphic -# from .heatmap import HeatmapGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack diff --git a/fastplotlib/graphics/features/_data.py b/fastplotlib/graphics/features/_data.py index 6c7dbfa75..0228e2a15 100644 --- a/fastplotlib/graphics/features/_data.py +++ b/fastplotlib/graphics/features/_data.py @@ -161,15 +161,11 @@ def buffer(self) -> List[Texture]: """list of Texture buffer for the image data""" return [img.geometry.grid for img in self._parent.world_object.children] - def update_gpu(self): - """Update the GPU with the buffer""" - self._update_range(None) - def __getitem__(self, item): return self._data[item] def __call__(self, *args, **kwargs): - return self.buffer.data + return self._data def __setitem__(self, key, value): # make sure supported type, not float64 etc. diff --git a/fastplotlib/graphics/heatmap.py b/fastplotlib/graphics/heatmap.py deleted file mode 100644 index 42ad67c73..000000000 --- a/fastplotlib/graphics/heatmap.py +++ /dev/null @@ -1,139 +0,0 @@ -import numpy as np -import pygfx -from typing import * -from .image import ImageGraphic - -from ..utils import quick_min_max, get_cmap_texture - - -default_selection_options = { - "mode": "single", - "orientation": "row", - "callbacks": None, -} - - -class SelectionOptions: - def __init__( - self, - event: str = "double_click", # click or double_click - event_button: Union[int, str] = 1, - mode: str = "single", - axis: str = "row", - color: Tuple[int, int, int, int] = None, - callbacks: List[callable] = None, - ): - self.event = event - self.event_button = event_button - self.mode = mode - self.axis = axis - - if color is not None: - self.color = color - - else: - self.color = (1, 1, 1, 0.4) - - if callbacks is None: - self.callbacks = list() - else: - self.callbacks = callbacks - - -class HeatmapGraphic(ImageGraphic): - def __init__( - self, - data: np.ndarray, - vmin: int = None, - vmax: int = None, - cmap: str = 'plasma', - selection_options: dict = None, - *args, - **kwargs - ): - """ - Create a Heatmap Graphic - - Parameters - ---------- - data: array-like, must be 2-dimensional - | array-like, usually numpy.ndarray, must support ``memoryview()`` - | Tensorflow Tensors also work **probably**, but not thoroughly tested - vmin: int, optional - minimum value for color scaling, calculated from data if not provided - vmax: int, optional - maximum value for color scaling, calculated from data if not provided - cmap: str, optional - colormap to use to display the image data, default is ``"plasma"`` - selection_options - args: - additional arguments passed to Graphic - kwargs: - additional keyword arguments passed to Graphic - - """ - super().__init__(data, vmin, vmax, cmap, *args, **kwargs) - - self.selection_options = SelectionOptions() - self.selection_options.callbacks = list() - - if selection_options is not None: - for k in selection_options.keys(): - setattr(self.selection_options, k, selection_options[k]) - - self.world_object.add_event_handler( - self.handle_selection_event, self.selection_options.event - ) - - self._highlights = list() - - def handle_selection_event(self, event): - if not event.button == self.selection_options.event_button: - return - - if self.selection_options.mode == "single": - for h in self._highlights: - self.remove_highlight(h) - - rval = self.add_highlight(event) - - for f in self.selection_options.callbacks: - f(rval) - - def remove_highlight(self, h): - self._highlights.remove(h) - self.world_object.remove(h) - - def add_highlight(self, event): - index = event.pick_info["index"] - - if self.selection_options.axis == "row": - index = index[1] - w = self.data.shape[1] - h = 1 - - pos = ((self.data.shape[1] / 2) - 0.5, index, 1) - rval = self.data[index, :] # returned to selection.callbacks functions - - elif self.selection_options.axis == "column": - index = index[0] - w = 1 - h = self.data.shape[0] - - pos = (index, (self.data.shape[0] / 2) - 0.5, 1) - rval = self.data[:, index] - - geometry = pygfx.plane_geometry( - width=w, - height=h - ) - - material = pygfx.MeshBasicMaterial(color=self.selection_options.color) - - self.selection_graphic = pygfx.Mesh(geometry, material) - self.selection_graphic.position.set(*pos) - - self.world_object.add(self.selection_graphic) - self._highlights.append(self.selection_graphic) - - return rval \ No newline at end of file diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 835061328..98f2fb3ee 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,17 +1,162 @@ from typing import * from math import ceil from itertools import product +import weakref import numpy as np import pygfx from ._base import Graphic, Interaction, PreviouslyModifiedData +from .selectors import LinearSelector, LinearRegionSelector from .features import ImageCmapFeature, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature from .features._base import to_gpu_supported_dtype from ..utils import quick_min_max -class ImageGraphic(Graphic, Interaction): +class _ImageHeatmapSelectorsMixin: + def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: + """ + Adds a linear selector. + + Parameters + ---------- + selection: int + initial position of the selector + + padding: float + pad the length of the selector + + kwargs + passed to :class:`.LinearSelector` + + Returns + ------- + LinearSelector + + """ + + bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + + if selection is None: + selection = limits[0] + + if selection < limits[0] or selection > limits[1]: + raise ValueError(f"the passed selection: {selection} is beyond the limits: {limits}") + + selector = LinearSelector( + selection=selection, + limits=limits, + end_points=end_points, + parent=weakref.proxy(self), + **kwargs + ) + + self._plot_area.add_graphic(selector, center=False) + selector.position.z = self.position.z + 1 + + return weakref.proxy(selector) + + def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> LinearRegionSelector: + """ + Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, + remove, or delete them from a plot area just like any other ``Graphic``. + + Parameters + ---------- + padding: float, default 100.0 + Extends the linear selector along the y-axis to make it easier to interact with. + + kwargs + passed to ``LinearRegionSelector`` + + Returns + ------- + LinearRegionSelector + linear selection graphic + + """ + + bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + + # create selector + selector = LinearRegionSelector( + bounds=bounds_init, + limits=limits, + size=size, + origin=origin, + parent=weakref.proxy(self), + **kwargs + ) + + self._plot_area.add_graphic(selector, center=False) + # so that it is above this graphic + selector.position.set_z(self.position.z + 3) + selector.fill.material.color = (*selector.fill.material.color[:-1], 0.2) + + # 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 weakref.proxy(selector) + + # TODO: this method is a bit of a mess, can refactor later + def _get_linear_selector_init_args(self, padding: float, **kwargs): + # computes initial bounds, limits, size and origin of linear selectors + data = self.data() + + if "axis" in kwargs.keys(): + axis = kwargs["axis"] + else: + axis = "x" + + if axis == "x": + offset = self.position.x + # x limits, number of columns + limits = (offset, data.shape[1]) + + # size is number of rows + padding + # used by LinearRegionSelector but not LinearSelector + size = data.shape[0] + padding + + # initial position of the selector + # center row + position_y = data.shape[0] / 2 + + # need y offset too for this + origin = (limits[0] - offset, position_y + self.position.y) + + # endpoints of the data range + # used by linear selector but not linear region + # padding, n_rows + padding + end_points = (0 - padding, data.shape[0] + padding) + else: + offset = self.position.y + # y limits + limits = (offset, data.shape[0]) + + # width + padding + # used by LinearRegionSelector but not LinearSelector + size = data.shape[1] + padding + + # initial position of the selector + position_x = data.shape[1] / 2 + + # need x offset too for this + origin = (position_x + self.position.x, limits[0] - offset) + + # endpoints of the data range + # used by linear selector but not linear region + end_points = (0 - padding, data.shape[1] + padding) + + # initial bounds are 20% of the limits range + # used by LinearRegionSelector but not LinearSelector + bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) + + return bounds_init, limits, size, origin, axis, end_points + + def _add_plot_area_hook(self, plot_area): + self._plot_area = plot_area + + +class ImageGraphic(Graphic, Interaction, _ImageHeatmapSelectorsMixin): feature_events = ( "data", "cmap", @@ -178,7 +323,7 @@ def col_chunk_index(self, index: int): self._col_chunk_index = index -class HeatmapGraphic(Graphic, Interaction): +class HeatmapGraphic(Graphic, Interaction, _ImageHeatmapSelectorsMixin): feature_events = ( "data", "cmap", diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py new file mode 100644 index 000000000..84c72283d --- /dev/null +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -0,0 +1,293 @@ +from typing import * +from dataclasses import dataclass +from functools import partial + +from pygfx.linalg import Vector3 +from pygfx import WorldObject, Line, Mesh, Points + + +@dataclass +class MoveInfo: + """ + stores move info for a WorldObject + """ + + # last position for an edge, fill, or vertex in world coordinates + # can be None, such as key events + last_position: Vector3 | None + + # WorldObject or "key" event + source: WorldObject | str + + +# key bindings used to move the selector +key_bind_direction = { + "ArrowRight": Vector3(1, 0, 0), + "ArrowLeft": Vector3(-1, 0, 0), + "ArrowUp": Vector3(0, 1, 0), + "ArrowDown": Vector3(0, -1, 0), +} + + +# Selector base class +class BaseSelector: + def __init__( + self, + edges: Tuple[Line, ...] = None, + fill: Tuple[Mesh, ...] = None, + vertices: Tuple[Points, ...] = None, + hover_responsive: Tuple[WorldObject, ...] = None, + arrow_keys_modifier: str = None, + axis: str = None + ): + if edges is None: + edges = tuple() + + if fill is None: + fill = tuple() + + if vertices is None: + vertices = tuple() + + self._edges: Tuple[Line, ...] = edges + self._fill: Tuple[Mesh, ...] = fill + self._vertices: Tuple[Points, ...] = vertices + + self._world_objects: Tuple[WorldObject, ...] = self._edges + self._fill + self._vertices + + self._hover_responsive: Tuple[WorldObject, ...] = hover_responsive + + if hover_responsive is not None: + self._original_colors = dict() + for wo in self._hover_responsive: + self._original_colors[wo] = wo.material.color + + self.axis = axis + + # current delta in world coordinates + self.delta: Vector3 = None + + self.arrow_keys_modifier = arrow_keys_modifier + # if not False, moves the slider on every render cycle + self._key_move_value = False + self.step: float = 1.0 #: step size for moving selector using the arrow keys + + self._move_info: MoveInfo = None + + def get_selected_index(self): + raise NotImplementedError + + def get_selected_indices(self): + raise NotImplementedError + + def get_selected_data(self): + raise NotImplementedError + + def _get_source(self, graphic): + if self.parent is None and graphic is None: + raise AttributeError( + "No Graphic to apply selector. " + "You must either set a ``parent`` Graphic on the selector, or pass a graphic." + ) + + # use passed graphic if provided, else use parent + if graphic is not None: + source = graphic + else: + source = self.parent + + return source + + def _add_plot_area_hook(self, plot_area): + self._plot_area = plot_area + + # when the pointer is pressed on a fill, edge or vertex + for wo in self._world_objects: + pfunc_down = partial(self._move_start, wo) + wo.add_event_handler(pfunc_down, "pointer_down") + + # when the pointer moves + self._plot_area.renderer.add_event_handler(self._move, "pointer_move") + + # when the pointer is released + self._plot_area.renderer.add_event_handler(self._move_end, "pointer_up") + + # move directly to location of center mouse button click + self._plot_area.renderer.add_event_handler(self._move_to_pointer, "click") + + # mouse hover color events + for wo in self._hover_responsive: + wo.add_event_handler(self._pointer_enter, "pointer_enter") + wo.add_event_handler(self._pointer_leave, "pointer_leave") + + # arrow key bindings + self._plot_area.renderer.add_event_handler(self._key_down, "key_down") + self._plot_area.renderer.add_event_handler(self._key_up, "key_up") + self._plot_area.add_animations(self._key_hold) + + def _move_start(self, event_source: WorldObject, ev): + """ + Called on "pointer_down" events + + Parameters + ---------- + event_source: WorldObject + event source, for example selection fill area ``Mesh`` an edge ``Line`` or vertex ``Points`` + + ev: Event + pygfx ``Event`` + + """ + last_position = self._plot_area.map_screen_to_world((ev.x, ev.y)) + + self._move_info = MoveInfo( + last_position=last_position, + source=event_source + ) + + def _move(self, ev): + """ + Called on pointer move events + + Parameters + ---------- + ev + + Returns + ------- + + """ + if self._move_info is None: + return + + # disable controller during moves + self._plot_area.controller.enabled = False + + # get pointer current world position + pointer_pos_screen = (ev.x, ev.y) + world_pos = self._plot_area.map_screen_to_world(pointer_pos_screen) + + # outside this viewport + if world_pos is None: + return + + # compute the delta + self.delta = world_pos.clone().sub(self._move_info.last_position) + self._pygfx_event = ev + + self._move_graphic(self.delta, ev) + + # update last position + self._move_info.last_position = world_pos + + self._plot_area.controller.enabled = True + + def _move_graphic(self, delta, ev): + raise NotImplementedError("Must be implemented in subclass") + + def _move_end(self, ev): + self._move_info = None + self._plot_area.controller.enabled = True + + def _move_to_pointer(self, ev): + """ + Calculates delta just using current world object position and calls self._move_graphic(). + """ + current_position = self.world_object.position.clone() + + # middle mouse button clicks + if ev.button != 3: + return + + click_pos = (ev.x, ev.y) + world_pos = self._plot_area.map_screen_to_world(click_pos) + + # outside this viewport + if world_pos is None: + return + + self.delta = world_pos.clone().sub(current_position) + self._pygfx_event = ev + + # use fill by default as the source + if len(self._fill) > 0: + self._move_info = MoveInfo(last_position=current_position, source=self._fill[0]) + # else use an edge + else: + self._move_info = MoveInfo(last_position=current_position, source=self._edges[0]) + + self._move_graphic(self.delta, ev) + self._move_info = None + + def _pointer_enter(self, ev): + if self._hover_responsive is None: + return + + wo = ev.pick_info["world_object"] + if wo not in self._hover_responsive: + return + + wo.material.color = "magenta" + + def _pointer_leave(self, ev): + if self._hover_responsive is None: + return + + # reset colors + for wo in self._hover_responsive: + wo.material.color = self._original_colors[wo] + + def _key_hold(self): + if self._key_move_value: + # direction vector * step + delta = key_bind_direction[self._key_move_value].clone().multiply_scalar(self.step) + + # set event source + # use fill by default as the source + if len(self._fill) > 0: + self._move_info = MoveInfo(last_position=None, source=self._fill[0]) + # else use an edge + else: + self._move_info = MoveInfo(last_position=None, source=self._edges[0]) + + # move the graphic + self._move_graphic(delta=delta, ev=None) + + self._move_info = None + + def _key_down(self, ev): + # key bind modifier must be set and must be used for the event + # for example. if "Shift" is set as a modifier, then "Shift" must be used as a modifier during this event + if self.arrow_keys_modifier is not None and self.arrow_keys_modifier not in ev.modifiers: + return + + # ignore if non-arrow key is pressed + if ev.key not in key_bind_direction.keys(): + return + + # print(ev.key) + + self._key_move_value = ev.key + + def _key_up(self, ev): + # if arrow key is released, stop moving + if ev.key in key_bind_direction.keys(): + self._key_move_value = False + + self._move_info = None + + def __del__(self): + # clear wo event handlers + for wo in self._world_objects: + wo._event_handlers.clear() + + # remove renderer event handlers + self._plot_area.renderer.remove_event_handler(self._move, "pointer_move") + self._plot_area.renderer.remove_event_handler(self._move_end, "pointer_up") + self._plot_area.renderer.remove_event_handler(self._move_to_pointer, "click") + + self._plot_area.renderer.remove_event_handler(self._key_down, "key_down") + self._plot_area.renderer.remove_event_handler(self._key_up, "key_up") + + # remove animation func + self._plot_area.remove_animation(self._key_hold) \ No newline at end of file diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index ffdcab662..b02233135 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -14,15 +14,7 @@ from .._base import Graphic, GraphicFeature, GraphicCollection from ..features._base import FeatureEvent - - -# key bindings used to move the slider -key_bind_direction = { - "ArrowRight": 1, - "ArrowLeft": -1, - "ArrowUp": 1, - "ArrowDown": -1, -} +from ._base_selector import BaseSelector class LinearSelectionFeature(GraphicFeature): @@ -42,12 +34,16 @@ class LinearSelectionFeature(GraphicFeature): ================== ================================================================ """ - def __init__(self, parent, axis: str, value: float): + def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): super(LinearSelectionFeature, self).__init__(parent, data=value) self.axis = axis + self.limits = limits def _set(self, value: float): + if not (self.limits[0] <= value <= self.limits[1]): + return + if self.axis == "x": self._parent.position.x = value else: @@ -85,8 +81,8 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): self._call_event_handlers(event_data) -class LinearSelector(Graphic): - feature_events = ("selection") +class LinearSelector(Graphic, BaseSelector): + feature_events = ("selection",) # TODO: make `selection` arg in graphics data space not world space def __init__( @@ -111,7 +107,7 @@ def __init__( initial x or y selected position for the slider, in world space limits: (int, int) - (min, max) limits along the x or y axis for the selector + (min, max) limits along the x or y axis for the selector, in world space axis: str, default "x" "x" | "y", the axis which the slider can move along @@ -147,8 +143,10 @@ def __init__( called when the LinearSelector selection changes. See feaure class for event pick_info table """ + if len(limits) != 2: + raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)") - self.limits = tuple(map(round, limits)) + limits = tuple(map(round, limits)) selection = round(selection) if axis == "x": @@ -168,27 +166,26 @@ def __init__( line_data = line_data.astype(np.float32) - self.axis = axis - - super(LinearSelector, self).__init__(name=name) + # super(LinearSelector, self).__init__(name=name) + # init Graphic + Graphic.__init__(self, name=name) if thickness < 1.1: material = pygfx.LineThinMaterial else: material = pygfx.LineMaterial - colors_inner = np.repeat([pygfx.Color(color)], 2, axis=0).astype(np.float32) - self.colors_outer = np.repeat([pygfx.Color([0.3, 0.3, 0.3, 1.0])], 2, axis=0).astype(np.float32) + 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, colors=colors_inner), - material=material(thickness=thickness, vertex_colors=True) + geometry=pygfx.Geometry(positions=line_data), + material=material(thickness=thickness, color=color) ) self.line_outer = pygfx.Line( - geometry=pygfx.Geometry(positions=line_data, colors=self.colors_outer.copy()), - material=material(thickness=thickness + 6, vertex_colors=True) + geometry=pygfx.Geometry(positions=line_data), + material=material(thickness=thickness + 6, color=self.colors_outer) ) line_inner.position.z = self.line_outer.position.z + 1 @@ -206,7 +203,7 @@ def __init__( else: self.position.y = selection - self.selection = LinearSelectionFeature(self, axis=axis, value=selection) + self.selection = LinearSelectionFeature(self, axis=axis, value=selection, limits=limits) self.ipywidget_slider = ipywidget_slider @@ -219,13 +216,17 @@ def __init__( self.parent = parent - # if not False, moves the slider on every render cycle - self._key_move_value = False - self.step: float = 1.0 #: step size for moving selector using the arrow keys - self.key_bind_modifier = arrow_keys_modifier - self._block_ipywidget_call = False + # init base selector + BaseSelector.__init__( + self, + edges=(line_inner, self.line_outer), + hover_responsive=(line_inner, self.line_outer), + arrow_keys_modifier=arrow_keys_modifier, + axis=axis, + ) + def _setup_ipywidget_slider(self, widget): # setup ipywidget slider with callbacks to this LinearSelector widget.value = int(self.selection()) @@ -277,8 +278,8 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): cls = getattr(ipywidgets, kind) slider = cls( - min=self.limits[0], - max=self.limits[1], + min=self.selection.limits[0], + max=self.selection.limits[1], value=int(self.selection()), step=1, **kwargs @@ -290,7 +291,9 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]: """ - Data index the slider is currently at w.r.t. the Graphic data. + Data index the slider is currently at w.r.t. the Graphic data. With LineGraphic data, the geometry x or y + position is not always the data position, for example if plotting data using np.linspace. Use this to get + the data index of the slider. Parameters ---------- @@ -303,191 +306,55 @@ def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]: data index the slider is currently at, list of ``int`` if a Collection """ - graphic = self._get_source(graphic) + source = self._get_source(graphic) - if isinstance(graphic, GraphicCollection): + if isinstance(source, GraphicCollection): ixs = list() - for g in graphic.graphics: + for g in source.graphics: ixs.append(self._get_selected_index(g)) return ixs - return self._get_selected_index(graphic) + return self._get_selected_index(source) def _get_selected_index(self, graphic): # the array to search for the closest value along that axis if self.axis == "x": - to_search = graphic.data()[:, 0] + geo_positions = graphic.data()[:, 0] offset = getattr(graphic.position, self.axis) else: - to_search = graphic.data()[:, 1] + geo_positions = graphic.data()[:, 1] offset = getattr(graphic.position, self.axis) - find_value = self.selection() - offset - - # get closest data index to the world space position of the slider - idx = np.searchsorted(to_search, find_value, side="left") - - if idx > 0 and (idx == len(to_search) or math.fabs(find_value - to_search[idx - 1]) < math.fabs(find_value - to_search[idx])): - return int(idx - 1) - else: - return int(idx) - - def _get_source(self, graphic): - if self.parent is None and graphic is None: - raise AttributeError( - "No Graphic to apply selector. " - "You must either set a ``parent`` Graphic on the selector, or pass a graphic." - ) - - # use passed graphic if provided, else use parent - if graphic is not None: - source = graphic - else: - source = self.parent - - return source - - def _key_move(self): - if self._key_move_value: - # step * direction - # TODO: step size in world space intead of screen space - direction = key_bind_direction[self._key_move_value] - delta = Vector3(self.step, self.step, 0).multiply_scalar(direction) - # we provide both x and y, depending on the axis this selector is on the other value is ignored anyways - self._move_graphic(delta=delta) - - def _key_move_start(self, ev): - if self.key_bind_modifier is not None and self.key_bind_modifier not in ev.modifiers: - return - - if self.axis == "x" and ev.key in ["ArrowRight", "ArrowLeft"]: - self._key_move_value = ev.key - - elif self.axis == "y" and ev.key in ["ArrowUp", "ArrowDown"]: - self._key_move_value = ev.key - - def _key_move_end(self, ev): - if self.axis == "x" and ev.key in ["ArrowRight", "ArrowLeft"]: - self._key_move_value = False - - elif self.axis == "y" and ev.key in ["ArrowUp", "ArrowDown"]: - self._key_move_value = False - - def _add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - - # move events - self.world_object.add_event_handler(self._move_start, "pointer_down") - self._plot_area.renderer.add_event_handler(self._move, "pointer_move") - self._plot_area.renderer.add_event_handler(self._move_end, "pointer_up") - - # move directly to location of center mouse button click - self._plot_area.renderer.add_event_handler(self._move_to_pointer, "click") - - # mouse hover color events - self.world_object.add_event_handler(self._pointer_enter, "pointer_enter") - self.world_object.add_event_handler(self._pointer_leave, "pointer_leave") - - # arrow key bindings - self._plot_area.renderer.add_event_handler(self._key_move_start, "key_down") - self._plot_area.renderer.add_event_handler(self._key_move_end, "key_up") - - self._plot_area.add_animations(self._key_move) - - def _move_to_pointer(self, ev): - # middle mouse button clicks - if ev.button != 3: - return - - click_pos = (ev.x, ev.y) - world_pos = self._plot_area.map_screen_to_world(click_pos) - - # outside this viewport - if world_pos is None: - return + if "Line" in graphic.__class__.__name__: + # we want to find the index of the geometry position that is closest to the slider's geometry position + find_value = self.selection() - offset - if self.axis == "x": - self.selection = world_pos.x - else: - self.selection = world_pos.y + # get closest data index to the world space position of the slider + idx = np.searchsorted(geo_positions, find_value, side="left") - def _move_start(self, ev): - self._move_info = {"last_pos": (ev.x, ev.y)} - - def _move(self, ev): - if self._move_info is None: - return + if idx > 0 and (idx == len(geo_positions) or math.fabs(find_value - geo_positions[idx - 1]) < math.fabs(find_value - geo_positions[idx])): + return int(idx - 1) + else: + return int(idx) - self._plot_area.controller.enabled = False + if "Heatmap" in graphic.__class__.__name__ or "Image" in graphic.__class__.__name__: + # indices map directly to grid geometry for image data buffer + index = self.selection() - offset + return int(index) - last = self._move_info["last_pos"] - - # new - last - # pointer move events are in viewport or canvas space - delta = Vector3(ev.x - last[0], ev.y - last[1]) - - self._pygfx_event = ev - - self._move_graphic(delta) - - self._move_info = {"last_pos": (ev.x, ev.y)} - self._plot_area.controller.enabled = True - - def _move_graphic(self, delta: Vector3): + def _move_graphic(self, delta: Vector3, ev): """ - Moves the graphic, updates SelectionFeature + Moves the graphic Parameters ---------- - delta_ndc: Vector3 - the delta by which to move this Graphic, in screen coordinates + delta: Vector3 + delta in world space """ - self.delta = delta.clone() - - viewport_size = self._plot_area.viewport.logical_size - - # convert delta to NDC coordinates using viewport size - # also since these are just deltas we don't have to calculate positions relative to the viewport - delta_ndc = delta.clone().multiply( - Vector3( - 2 / viewport_size[0], - -2 / viewport_size[1], - 0 - ) - ) - - camera = self._plot_area.camera - # current world position - vec = self.position.clone() - - # compute and add delta in projected NDC space and then unproject back to world space - vec.project(camera).add(delta_ndc).unproject(camera) - - new_value = getattr(vec, self.axis) - - if new_value < self.limits[0] or new_value > self.limits[1]: - return - - self.selection = new_value - self.delta = None - - def _move_end(self, ev): - self._move_info = None - self._plot_area.controller.enabled = True - - def _pointer_enter(self, ev): - self.line_outer.geometry.colors.data[:] = np.repeat([pygfx.Color("magenta")], 2, axis=0) - self.line_outer.geometry.colors.update_range() - - def _pointer_leave(self, ev): - if self._move_info is not None: - return - - self._reset_color() - - def _reset_color(self): - self.line_outer.geometry.colors.data[:] = self.colors_outer - self.line_outer.geometry.colors.update_range() + if self.axis == "x": + self.selection = self.selection() + delta.x + else: + self.selection = self.selection() + delta.y diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 8f68a754a..8cd0313a1 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -1,39 +1,14 @@ from typing import * import numpy as np -from functools import partial import pygfx from pygfx.linalg import Vector3 -from .._base import Graphic, Interaction, GraphicCollection +from .._base import Graphic, GraphicCollection from ..features._base import GraphicFeature, FeatureEvent +from ._base_selector import BaseSelector - -# positions for indexing the BoxGeometry to set the "width" and "size" of the box -# hacky, but I don't think we can morph meshes in pygfx yet: https://github.com/pygfx/pygfx/issues/346 -x_right = np.array([ - True, True, True, True, False, False, False, False, False, - True, False, True, True, False, True, False, False, True, - False, True, True, False, True, False -]) - -x_left = np.array([ - False, False, False, False, True, True, True, True, True, - False, True, False, False, True, False, True, True, False, - True, False, False, True, False, True -]) - -y_top = np.array([ - False, True, False, True, False, True, False, True, True, - True, True, True, False, False, False, False, False, False, - True, True, False, False, True, True -]) - -y_bottom = np.array([ - True, False, True, False, True, False, True, False, False, - False, False, False, True, True, True, True, True, True, - False, False, True, True, False, False -]) +from ._mesh_positions import x_right, x_left, y_top, y_bottom class LinearBoundsFeature(GraphicFeature): @@ -52,17 +27,20 @@ class LinearBoundsFeature(GraphicFeature): +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ """ - def __init__(self, parent, bounds: Tuple[int, int], axis: str): + def __init__(self, parent, bounds: Tuple[int, int], axis: str, limits: Tuple[int, int]): super(LinearBoundsFeature, self).__init__(parent, data=bounds) self._axis = axis + self.limits = limits + + self._set(bounds) @property def axis(self) -> str: """one of "x" | "y" """ return self._axis - def _set(self, value): + def _set(self, value: Tuple[float, float]): # sets new bounds if not isinstance(value, tuple): raise TypeError( @@ -70,6 +48,17 @@ def _set(self, value): "where `min_bound` and `max_bound` are numeric values." ) + # make sure bounds not exceeded + for v in value: + if not (self.limits[0] <= v <= self.limits[1]): + return + + # make sure `selector width >= 2`, left edge must not move past right edge! + # or bottom edge must not move past top edge! + # has to be at least 2 otherwise can't join datapoints for lines + if not (value[1] - value[0]) >= 2: + return + if self.axis == "x": # change left x position of the fill mesh self._parent.fill.geometry.positions.data[x_left, 0] = value[0] @@ -132,7 +121,7 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): self._call_event_handlers(event_data) -class LinearRegionSelector(Graphic, Interaction): +class LinearRegionSelector(Graphic, BaseSelector): feature_events = ( "bounds" ) @@ -148,13 +137,17 @@ def __init__( resizable: bool = True, fill_color=(0, 0, 0.35), edge_color=(0.8, 0.8, 0), + arrow_keys_modifier: str = "Shift", name: str = None ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. - bounds[0], limits[0], and position[0] must be identical + bounds[0], limits[0], and position[0] must be identical. + + Holding the right mouse button while dragging an edge will force the entire region selector to move. This is + a when using transparent fill areas due to ``pygfx`` picking limitations. Parameters ---------- @@ -174,7 +167,7 @@ def __init__( "x" | "y", axis for the selector parent: Graphic, default ``None`` - associated this selector with a parent Graphic + associate this selector with a parent Graphic resizable: bool if ``True``, the edges can be dragged to resize the width of the linear selection @@ -211,7 +204,7 @@ def __init__( # f"{limits[0]} != {origin[1]} != {bounds[0]}" # ) - super(LinearRegionSelector, self).__init__(name=name) + Graphic.__init__(self, name=name) self.parent = parent @@ -235,23 +228,12 @@ def __init__( # the fill of the selection self.fill = mesh - self.fill.position.set(*origin, -2) self.world_object.add(self.fill) - # will be used to store the mouse pointer x y movements - # so deltas can be calculated for interacting with the selection - self._move_info = None - - # mouse events can come from either the fill mesh world object, or one of the lines on the edge of the selector - self._event_source: str = None - - self.limits = limits self._resizable = resizable - self._edge_color = np.repeat([pygfx.Color(edge_color)], 2, axis=0) - if axis == "x": # position data for the left edge line left_line_data = np.array( @@ -260,8 +242,8 @@ def __init__( ).astype(np.float32) left_line = pygfx.Line( - pygfx.Geometry(positions=left_line_data, colors=self._edge_color.copy()), - pygfx.LineMaterial(thickness=3, vertex_colors=True) + pygfx.Geometry(positions=left_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) ) # position data for the right edge line @@ -271,8 +253,8 @@ def __init__( ).astype(np.float32) right_line = pygfx.Line( - pygfx.Geometry(positions=right_line_data, colors=self._edge_color.copy()), - pygfx.LineMaterial(thickness=3, vertex_colors=True) + pygfx.Geometry(positions=right_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (left_line, right_line) @@ -286,8 +268,8 @@ def __init__( ).astype(np.float32) bottom_line = pygfx.Line( - pygfx.Geometry(positions=bottom_line_data, colors=self._edge_color.copy()), - pygfx.LineMaterial(thickness=3, vertex_colors=True) + pygfx.Geometry(positions=bottom_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) ) # position data for the right edge line @@ -297,28 +279,32 @@ def __init__( ).astype(np.float32) top_line = pygfx.Line( - pygfx.Geometry(positions=top_line_data, colors=self._edge_color.copy()), - pygfx.LineMaterial(thickness=3, vertex_colors=True) + pygfx.Geometry(positions=top_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (bottom_line, top_line) + else: + raise ValueError("axis argument must be one of 'x' or 'y'") + # add the edge lines for edge in self.edges: edge.position.set_z(-1) self.world_object.add(edge) - # highlight the edges when mouse is hovered - for edge_line in self.edges: - edge_line.add_event_handler( - partial(self._pointer_enter_edge, edge_line), - "pointer_enter" - ) - edge_line.add_event_handler(self._pointer_leave_edge, "pointer_leave") - # set the initial bounds of the selector - self._bounds = LinearBoundsFeature(self, bounds, axis=axis) - self._bounds: LinearBoundsFeature = bounds + self._bounds = LinearBoundsFeature(self, bounds, axis=axis, limits=limits) + # self._bounds: LinearBoundsFeature = bounds + + BaseSelector.__init__( + self, + edges=self.edges, + fill=(self.fill,), + hover_responsive=self.edges, + arrow_keys_modifier=arrow_keys_modifier, + axis=axis, + ) @property def bounds(self) -> LinearBoundsFeature: @@ -355,27 +341,35 @@ def get_selected_data(self, graphic: Graphic = None) -> Union[np.ndarray, List[n source = self._get_source(graphic) ixs = self.get_selected_indices(source) - if isinstance(source, GraphicCollection): - # this will return a list of views of the arrays, therefore no copy operations occur - # it's fine and fast even as a list of views because there is no re-allocating of memory - # this is fast even for slicing a 10,000 x 5,000 LineStack - data_selections: List[np.ndarray] = list() - - for i, g in enumerate(source.graphics): - if ixs[i].size == 0: - data_selections.append(None) - else: - s = slice(ixs[i][0], ixs[i][-1]) - data_selections.append(g.data.buffer.data[s]) - - return source[:].data[s] - # just for one graphic - else: - if ixs.size == 0: - return None - + if "Line" in source.__class__.__name__: + if isinstance(source, GraphicCollection): + # this will return a list of views of the arrays, therefore no copy operations occur + # it's fine and fast even as a list of views because there is no re-allocating of memory + # this is fast even for slicing a 10,000 x 5,000 LineStack + data_selections: List[np.ndarray] = list() + + for i, g in enumerate(source.graphics): + if ixs[i].size == 0: + data_selections.append(None) + else: + s = slice(ixs[i][0], ixs[i][-1]) + data_selections.append(g.data.buffer.data[s]) + + return source[:].data[s] + # just for one Line graphic + else: + if ixs.size == 0: + return None + + s = slice(ixs[0], ixs[-1]) + return source.data.buffer.data[s] + + if "Heatmap" in source.__class__.__name__ or "Image" in source.__class__.__name__: s = slice(ixs[0], ixs[-1]) - return source.data.buffer.data[s] + if self.axis == "x": + return source.data()[:, s] + elif self.axis == "y": + return source.data()[s] def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, List[np.ndarray]]: """ @@ -384,7 +378,7 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis as the Line Geometry positions x-vals or y-vals. For example, if if you used a np.linspace(0, 100, 1000) for xvals in your line, then you will have 1,000 x-positions. If the selection ``bounds`` are set to ``(0, 10)``, the returned - indices would be ``(0, 100``. + indices would be ``(0, 100)``. Parameters ---------- @@ -394,7 +388,7 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis Returns ------- Union[np.ndarray, List[np.ndarray]] - data indices of the selection + data indices of the selection, list of np.ndarray if graphic is LineCollection """ source = self._get_source(graphic) @@ -402,6 +396,7 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis # if the graphic position is not at (0, 0) then the bounds must be offset offset = getattr(source.position, self.bounds.axis) offset_bounds = tuple(v - offset for v in self.bounds()) + # need them to be int to use as indices offset_bounds = tuple(map(int, offset_bounds)) @@ -409,185 +404,79 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis dim = 0 else: dim = 1 - # now we need to map from graphic space to data space - # we can have more than 1 datapoint between two integer locations in the world space - if isinstance(source, GraphicCollection): - ixs = list() - for g in source.graphics: - # map for each graphic in the collection - g_ixs = np.where( - (g.data()[:, dim] >= offset_bounds[0]) & (g.data()[:, dim] <= offset_bounds[1]) - )[0] - ixs.append(g_ixs) - else: - # map this only this graphic - ixs = np.where( - (source.data()[:, dim] >= offset_bounds[0]) & (source.data()[:, dim] <= offset_bounds[1]) - )[0] - - return ixs - - def _get_source(self, graphic): - if self.parent is None and graphic is None: - raise AttributeError( - "No Graphic to apply selector. " - "You must either set a ``parent`` Graphic on the selector, or pass a graphic." - ) - - # use passed graphic if provided, else use parent - if graphic is not None: - source = graphic - else: - source = self.parent - - return source - - def _add_plot_area_hook(self, plot_area): - # called when this selector is added to a plot area - self._plot_area = plot_area - - # need partials so that the source of the event is passed to the `_move_start` handler - self._move_start_fill = partial(self._move_start, "fill") - self._move_start_edge_0 = partial(self._move_start, "edge-0") - self._move_start_edge_1 = partial(self._move_start, "edge-1") - - self.fill.add_event_handler(self._move_start_fill, "pointer_down") - - if self._resizable: - self.edges[0].add_event_handler(self._move_start_edge_0, "pointer_down") - self.edges[1].add_event_handler(self._move_start_edge_1, "pointer_down") - - self._plot_area.renderer.add_event_handler(self._move, "pointer_move") - self._plot_area.renderer.add_event_handler(self._move_end, "pointer_up") - - def _move_start(self, event_source: str, ev): - """ - Parameters - ---------- - event_source: str - "fill" | "edge-left" | "edge-right" - - """ - # self._plot_area.controller.enabled = False - # last pointer position - self._move_info = {"last_pos": (ev.x, ev.y)} - self._event_source = event_source - - def _move(self, ev): - if self._move_info is None: - return - # disable the controller, otherwise the panzoom or other controllers will move the camera and will not - # allow the selector to process the mouse events - self._plot_area.controller.enabled = False - - last = self._move_info["last_pos"] - - # new - last - # pointer move events are in viewport or canvas space - delta = Vector3(ev.x - last[0], ev.y - last[1]) + if "Line" in source.__class__.__name__: + # now we need to map from graphic space to data space + # we can have more than 1 datapoint between two integer locations in the world space + if isinstance(source, GraphicCollection): + ixs = list() + for g in source.graphics: + # map for each graphic in the collection + g_ixs = np.where( + (g.data()[:, dim] >= offset_bounds[0]) & (g.data()[:, dim] <= offset_bounds[1]) + )[0] + ixs.append(g_ixs) + else: + # map this only this graphic + ixs = np.where( + (source.data()[:, dim] >= offset_bounds[0]) & (source.data()[:, dim] <= offset_bounds[1]) + )[0] - self._move_info = {"last_pos": (ev.x, ev.y)} + return ixs - viewport_size = self._plot_area.viewport.logical_size - - # convert delta to NDC coordinates using viewport size - # also since these are just deltas we don't have to calculate positions relative to the viewport - delta_ndc = delta.multiply( - Vector3( - 2 / viewport_size[0], - -2 / viewport_size[1], - 0 - ) - ) - - camera = self._plot_area.camera + if "Heatmap" in source.__class__.__name__ or "Image" in source.__class__.__name__: + # indices map directly to grid geometry for image data buffer + ixs = np.arange(*self.bounds(), dtype=int) + return ixs + def _move_graphic(self, delta, ev): # edge-0 bound current world position if self.bounds.axis == "x": - # left bound position - vec0 = Vector3(self.bounds()[0]) - else: - # bottom bound position - vec0 = Vector3(0, self.bounds()[0]) - # compute and add delta in projected NDC space and then unproject back to world space - vec0.project(camera).add(delta_ndc).unproject(camera) + # new left bound position + bound_pos_0 = Vector3(self.bounds()[0]).add(delta) - # edge-1 bound current world position - if self.bounds.axis == "x": - vec1 = Vector3(self.bounds()[1]) + # new right bound position + bound_pos_1 = Vector3(self.bounds()[1]).add(delta) + else: + # new bottom bound position + bound_pos_0 = Vector3(0, self.bounds()[0]).add(delta) + + # new top bound position + bound_pos_1 = Vector3(0, self.bounds()[1]).add(delta) + + # workaround because transparent objects are not pickable in pygfx + if ev is not None: + if 2 in ev.buttons: + force_fill_move = True + else: + force_fill_move = False else: - vec1 = Vector3(0, self.bounds()[1]) - # compute and add delta in projected NDC space and then unproject back to world space - vec1.project(camera).add(delta_ndc).unproject(camera) + force_fill_move = False + + # move entire selector if source was fill + if self._move_info.source == self.fill or force_fill_move: + bound0 = getattr(bound_pos_0, self.bounds.axis) + bound1 = getattr(bound_pos_1, self.bounds.axis) + # set the new bounds + self.bounds = (bound0, bound1) + return + + # if selector is not resizable do nothing + if not self._resizable: + return - if self._event_source == "edge-0": - # change only the left bound or bottom bound - bound0 = getattr(vec0, self.bounds.axis) # gets either vec.x or vec.y + # if resizable, move edges + if self._move_info.source == self.edges[0]: + # change only left or bottom bound + bound0 = getattr(bound_pos_0, self.bounds.axis) bound1 = self.bounds()[1] - elif self._event_source == "edge-1": - # change only the right bound or top bound + elif self._move_info.source == self.edges[1]: + # change only right or top bound bound0 = self.bounds()[0] - bound1 = getattr(vec1, self.bounds.axis) # gets either vec.x or vec.y - - elif self._event_source == "fill": - # move the entire selector - bound0 = getattr(vec0, self.bounds.axis) - bound1 = getattr(vec1, self.bounds.axis) - - # if the limits are met do nothing - if bound0 < self.limits[0] or bound1 > self.limits[1]: - return - - # make sure `selector width >= 2`, left edge must not move past right edge! - # or bottom edge must not move past top edge! - # has to be at least 2 otherwise can't join datapoints for lines - if not (bound1 - bound0) >= 2: + bound1 = getattr(bound_pos_1, self.bounds.axis) + else: return # set the new bounds self.bounds = (bound0, bound1) - - # re-enable the controller - self._plot_area.controller.enabled = True - - def _move_end(self, ev): - self._move_info = None - # sometimes weird stuff happens so we want to make sure the controller is reset - self._plot_area.controller.enabled = True - - self._reset_edge_color() - - def _pointer_enter_edge(self, edge: pygfx.Line, ev): - edge.material.thickness = 6 - edge.geometry.colors.data[:] = np.repeat([pygfx.Color("magenta")], 2, axis=0) - edge.geometry.colors.update_range() - - def _pointer_leave_edge(self, ev): - if self._move_info is not None and self._event_source.startswith("edge"): - return - - self._reset_edge_color() - - def _reset_edge_color(self): - for edge in self.edges: - edge.material.thickness = 3 - edge.geometry.colors.data[:] = self._edge_color - edge.geometry.colors.update_range() - - def _set_feature(self, feature: str, new_data: Any, indices: Any): - pass - - def _reset_feature(self, feature: str): - pass - - def __del__(self): - self.fill.remove_event_handler(self._move_start_fill, "pointer_down") - - if self._resizable: - self.edges[0].remove_event_handler(self._move_start_edge_0, "pointer_down") - self.edges[1].remove_event_handler(self._move_start_edge_1, "pointer_down") - - self._plot_area.renderer.remove_event_handler(self._move, "pointer_move") - self._plot_area.renderer.remove_event_handler(self._move_end, "pointer_up") diff --git a/fastplotlib/graphics/selectors/_mesh_positions.py b/fastplotlib/graphics/selectors/_mesh_positions.py new file mode 100644 index 000000000..9542aee58 --- /dev/null +++ b/fastplotlib/graphics/selectors/_mesh_positions.py @@ -0,0 +1,31 @@ +import numpy as np + + +""" +positions for indexing the BoxGeometry to set the "width" and "size" of the box +hacky, but I don't think we can morph meshes in pygfx yet: https://github.com/pygfx/pygfx/issues/346 +""" + +x_right = np.array([ + True, True, True, True, False, False, False, False, False, + True, False, True, True, False, True, False, False, True, + False, True, True, False, True, False +]) + +x_left = np.array([ + False, False, False, False, True, True, True, True, True, + False, True, False, False, True, False, True, True, False, + True, False, False, True, False, True +]) + +y_top = np.array([ + False, True, False, True, False, True, False, True, True, + True, True, True, False, False, False, False, False, False, + True, True, False, False, True, True +]) + +y_bottom = np.array([ + True, False, True, False, True, False, True, False, False, + False, False, False, True, True, True, True, True, True, + False, False, True, True, False, False +]) \ No newline at end of file diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py new file mode 100644 index 000000000..5188161b9 --- /dev/null +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -0,0 +1,304 @@ +from typing import * +import numpy as np + +import pygfx +from pygfx.linalg import Vector3 + +from .._base import Graphic, GraphicCollection +from ..features._base import GraphicFeature, FeatureEvent +from ._base_selector import BaseSelector + +from ._mesh_positions import x_right, x_left, y_top, y_bottom + + +class RectangleBoundsFeature(GraphicFeature): + """ + Feature for a linearly bounding region + + Pick Info + --------- + + +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ + | key | type | description | + +====================+===============================+======================================================================================+ + | "selected_indices" | ``numpy.ndarray`` or ``None`` | selected graphic data indices | + | "selected_data" | ``numpy.ndarray`` or ``None`` | selected graphic data | + | "new_data" | ``(float, float)`` | current bounds in world coordinates, NOT necessarily the same as "selected_indices". | + +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ + + """ + def __init__(self, parent, bounds: Tuple[int, int], axis: str, limits: Tuple[int, int]): + super(RectangleBoundsFeature, self).__init__(parent, data=bounds) + + self._axis = axis + self.limits = limits + + self._set(bounds) + + @property + def axis(self) -> str: + """one of "x" | "y" """ + return self._axis + + def _set(self, value: Tuple[float, float, float, float]): + """ + + Parameters + ---------- + value: Tuple[float] + new values: (xmin, xmax, ymin, ymax) + + Returns + ------- + + """ + xmin, xmax, ymin, ymax = value + + # TODO: make sure new values do not exceed limits + + # change fill mesh + # change left x position of the fill mesh + self._parent.fill.geometry.positions.data[x_left, 0] = xmin + + # change right x position of the fill mesh + self._parent.fill.geometry.positions.data[x_right, 0] = xmax + + # change bottom y position of the fill mesh + self._parent.fill.geometry.positions.data[y_bottom, 1] = ymin + + # change top position of the fill mesh + self._parent.fill.geometry.positions.data[y_top, 1] = ymax + + # change the edge lines + + # [x0, y0, z0] + # [x1, y1, z0] + + # left line + z = self._parent.edges[0].geometry.positions.data[:, -1][0] + self._parent.edges[0].geometry.position.data[:] = np.array( + [ + [xmin, ymin, z], + [xmin, ymax, z] + ] + ) + + # right line + self._parent.edges[1].geometry.position.data[:] = np.array( + [ + [xmax, ymin, z], + [xmax, ymax, z] + ] + ) + + # bottom line + self._parent.edges[2].geometry.position.data[:] = np.array( + [ + [xmin, ymin, z], + [xmax, ymin, z] + ] + ) + + # top line + self._parent.edges[3].geometry.position.data[:] = np.array( + [ + [xmin, ymax, z], + [xmax, ymax, z] + ] + ) + + self._data = value#(value[0], value[1]) + + # send changes to GPU + self._parent.fill.geometry.positions.update_range() + + for edge in self._parent.edges: + edge.geometry.positions.update_range() + + # calls any events + self._feature_changed(key=None, new_data=value) + + def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + return + + if len(self._event_handlers) < 1: + return + + if self._parent.parent is not None: + selected_ixs = self._parent.get_selected_indices() + selected_data = self._parent.get_selected_data() + else: + selected_ixs = None + selected_data = None + + pick_info = { + "index": None, + "collection-index": self._collection_index, + "world_object": self._parent.world_object, + "new_data": new_data, + "selected_indices": selected_ixs, + "selected_data": selected_data + } + + event_data = FeatureEvent(type="bounds", pick_info=pick_info) + + self._call_event_handlers(event_data) + + +class RectangleRegionSelector(Graphic, BaseSelector): + feature_events = ( + "bounds" + ) + + def __init__( + self, + bounds: Tuple[int, int, int, int], + limits: Tuple[int, int], + origin: Tuple[int, int], + axis: str = "x", + parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35), + edge_color=(0.8, 0.8, 0), + arrow_keys_modifier: str = "Shift", + name: str = None + ): + """ + Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. + Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. + + bounds[0], limits[0], and position[0] must be identical + + Parameters + ---------- + bounds: (int, int, int, int) + the initial bounds of the rectangle, ``(x_min, x_max, y_min, y_max)`` + + limits: (int, int, int, int) + limits of the selector, ``(x_min, x_max, y_min, y_max)`` + + origin: (int, int) + initial position of the selector + + axis: str, default ``None`` + Restrict the selector to the "x" or "y" axis. + If the selector is restricted to an axis you cannot change the bounds along the other axis. For example, + if you set ``axis="x"``, then the ``y_min``, ``y_max`` values of the bounds will stay constant. + + parent: Graphic, default ``None`` + associate this selector with a parent Graphic + + resizable: bool + if ``True``, the edges can be dragged to resize the selection + + fill_color: str, array, or tuple + fill color for the selector, passed to pygfx.Color + + edge_color: str, array, or tuple + edge color for the selector, passed to pygfx.Color + + name: str + name for this selector graphic + """ + + # lots of very close to zero values etc. so round them + bounds = tuple(map(round, bounds)) + limits = tuple(map(round, limits)) + origin = tuple(map(round, origin)) + + Graphic.__init__(self, name=name) + + self.parent = parent + + # world object for this will be a group + # basic mesh for the fill area of the selector + # line for each edge of the selector + group = pygfx.Group() + self._set_world_object(group) + + xmin, xmax, ymin, ymax = bounds + + width = xmax - xmin + height = ymax - ymin + + self.fill = pygfx.Mesh( + pygfx.box_geometry(width, height, 1), + pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) + ) + + self.fill.position.set(*origin, -2) + self.world_object.add(self.fill) + + # position data for the left edge line + left_line_data = np.array( + [[origin[0], (-height / 2) + origin[1], 0.5], + [origin[0], (height / 2) + origin[1], 0.5]] + ).astype(np.float32) + + left_line = pygfx.Line( + pygfx.Geometry(positions=left_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) + ) + + # position data for the right edge line + right_line_data = np.array( + [[bounds[1], (-height / 2) + origin[1], 0.5], + [bounds[1], (height / 2) + origin[1], 0.5]] + ).astype(np.float32) + + right_line = pygfx.Line( + pygfx.Geometry(positions=right_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) + ) + + # position data for the left edge line + bottom_line_data = \ + np.array( + [[(-width / 2) + origin[0], origin[1], 0.5], + [(width / 2) + origin[0], origin[1], 0.5]] + ).astype(np.float32) + + bottom_line = pygfx.Line( + pygfx.Geometry(positions=bottom_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) + ) + + # position data for the right edge line + top_line_data = np.array( + [[(-width / 2) + origin[0], bounds[1], 0.5], + [(width / 2) + origin[0], bounds[1], 0.5]] + ).astype(np.float32) + + top_line = pygfx.Line( + pygfx.Geometry(positions=top_line_data), + pygfx.LineMaterial(thickness=3, color=edge_color) + ) + + self.edges: Tuple[pygfx.Line, ...] = ( + left_line, right_line, bottom_line, top_line + ) # left line, right line, bottom line, top line + + # add the edge lines + for edge in self.edges: + edge.position.set_z(-1) + self.world_object.add(edge) + + self._bounds = RectangleBoundsFeature(self, bounds, axis=axis, limits=limits) + + BaseSelector.__init__( + self, + edges=self.edges, + fill=(self.fill,), + hover_responsive=self.edges, + arrow_keys_modifier=arrow_keys_modifier, + axis=axis, + ) + + @property + def bounds(self) -> RectangleBoundsFeature: + """ + The current bounds of the selection in world space. These bounds will NOT necessarily correspond to the + indices of the data that are under the selection. Use ``get_selected_indices()` which maps from + world space to data indices. + """ + return self._bounds \ No newline at end of file diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index 15cb01726..72d25b542 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -16,7 +16,7 @@ def __init__(self, *selectors: LinearSelector, key_bind: str = "Shift"): selectors to synchronize key_bind: str, default ``"Shift"`` - one of ``"Control"``, ``"Shift"`` and ``"Alt"`` + one of ``"Control"``, ``"Shift"`` and ``"Alt"`` or ``None`` """ self._selectors = list() self.key_bind = key_bind From c50beb6d0889ff839eda07e90a43917463e0a753 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 21 May 2023 08:56:56 -0400 Subject: [PATCH 25/96] add features section to docs for Graphics --- docs/source/api/graphic_features.rst | 15 ++++++++ docs/source/conf.py | 8 ++-- docs/source/index.rst | 1 + fastplotlib/graphics/features/_base.py | 52 ++++++++++++++++---------- fastplotlib/graphics/image.py | 15 +++++++- 5 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 docs/source/api/graphic_features.rst diff --git a/docs/source/api/graphic_features.rst b/docs/source/api/graphic_features.rst new file mode 100644 index 000000000..449eaf297 --- /dev/null +++ b/docs/source/api/graphic_features.rst @@ -0,0 +1,15 @@ +Features +******** + +.. autoclass:: fastplotlib.graphics.features.ImageDataFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +.. autoclass:: fastplotlib.graphics.features.ImageCmapFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + diff --git a/docs/source/conf.py b/docs/source/conf.py index 0aa0265ac..7be450060 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,12 +17,11 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = ["sphinx.ext.napoleon", "sphinx.ext.autodoc"] -autodoc_typehints = "description" templates_path = ['_templates'] exclude_patterns = [] - +napoleon_custom_sections = ['Features'] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output @@ -32,9 +31,12 @@ html_static_path = ['_static'] -autodoc_member_order = 'bysource' +autodoc_member_order = 'groupwise' autoclass_content = "both" +autodoc_typehints = "description" +autodoc_typehints_description_target = "documented_params" + def _setup_navbar_side_toctree(app: Any): def add_class_toctree_function(app: Any, pagename: Any, templatename: Any, context: Any, doctree: Any): diff --git a/docs/source/index.rst b/docs/source/index.rst index 8ccc160b5..9a0723af7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ Welcome to fastplotlib's documentation! Subplot Gridplot Graphics + Graphic Features Selectors Widgets diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index ed08a9008..57cd15a1d 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -3,6 +3,7 @@ from warnings import warn from typing import * import weakref +from dataclasses import dataclass import numpy as np from pygfx import Buffer, Texture @@ -40,13 +41,20 @@ def to_gpu_supported_dtype(array): class FeatureEvent: """ - type: , example: "colors" + Dataclass that holds feature event information. Has ``type`` and ``pick_info`` attributes. + + Attributes + ---------- + type: str, example "colors" + pick_info: dict in the form: - { - "index": indices where feature data was changed, ``range`` object or List[int], - "world_object": world object the feature belongs to, - "new_values": the new values - } + ============== ======================================================================= + index indices where feature data was changed, ``range`` object or List[int] + ============== ======================================================================= + world_object world object the feature belongs to + new_data the new data for this feature + ============== ======================================================================= + """ def __init__(self, type: str, pick_info: dict): self.type = type @@ -60,18 +68,15 @@ def __repr__(self): class GraphicFeature(ABC): def __init__(self, parent, data: Any, collection_index: int = None): - """ - - Parameters - ---------- - parent - - data: Any - - collection_index: int - if part of a collection, index of this graphic within the collection - - """ + # not shown as a docstring so it doesn't show up in the docs + # Parameters + # ---------- + # parent + # + # data: Any + # + # collection_index: int + # if part of a collection, index of this graphic within the collection self._parent = weakref.proxy(parent) self._data = to_gpu_supported_dtype(data) @@ -119,6 +124,15 @@ def add_event_handler(self, handler: callable): self._event_handlers.append(handler) def remove_event_handler(self, handler: callable): + """ + Remove a registered event handler + + Parameters + ---------- + handler: callable + event handler to remove + + """ if handler not in self._event_handlers: raise KeyError(f"event handler {handler} not registered.") @@ -254,7 +268,7 @@ def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: class GraphicFeatureIndexable(GraphicFeature): - """And indexable Graphic Feature, colors, data, sizes etc.""" + """An indexable Graphic Feature, colors, data, sizes etc.""" def _set(self, value): value = self._parse_set_value(value) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 98f2fb3ee..5df96b2dd 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -8,7 +8,7 @@ from ._base import Graphic, Interaction, PreviouslyModifiedData from .selectors import LinearSelector, LinearRegionSelector -from .features import ImageCmapFeature, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature +from .features import ImageCmapFeature, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature, PresentFeature from .features._base import to_gpu_supported_dtype from ..utils import quick_min_max @@ -160,6 +160,7 @@ class ImageGraphic(Graphic, Interaction, _ImageHeatmapSelectorsMixin): feature_events = ( "data", "cmap", + "present" ) def __init__( @@ -199,6 +200,18 @@ def __init__( kwargs: additional keyword arguments passed to Graphic + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the data buffer displayed in the ImageGraphic + + **cmap**: :class:`.ImageCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + Examples -------- .. code-block:: python From 75b2fd803a0c45f73109fcb15277698c85b9a555 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 21 May 2023 09:16:30 -0400 Subject: [PATCH 26/96] fix sync bug, arrow key movements are toggleable (#198) --- fastplotlib/graphics/selectors/_base_selector.py | 9 ++++++++- fastplotlib/graphics/selectors/_linear.py | 3 ++- fastplotlib/graphics/selectors/_linear_region.py | 4 ++++ fastplotlib/graphics/selectors/_sync.py | 11 ++++++++--- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 84c72283d..682e1855b 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -71,6 +71,7 @@ def __init__( # if not False, moves the slider on every render cycle self._key_move_value = False self.step: float = 1.0 #: step size for moving selector using the arrow keys + self.arrow_key_events_enabled = False self._move_info: MoveInfo = None @@ -106,6 +107,9 @@ def _add_plot_area_hook(self, plot_area): pfunc_down = partial(self._move_start, wo) wo.add_event_handler(pfunc_down, "pointer_down") + # double-click to enable arrow-key moveable mode + wo.add_event_handler(self._toggle_arrow_key_moveable, "double_click") + # when the pointer moves self._plot_area.renderer.add_event_handler(self._move, "pointer_move") @@ -237,8 +241,11 @@ def _pointer_leave(self, ev): for wo in self._hover_responsive: wo.material.color = self._original_colors[wo] + def _toggle_arrow_key_moveable(self, ev): + self.arrow_key_events_enabled = not self.arrow_key_events_enabled + def _key_hold(self): - if self._key_move_value: + if self._key_move_value and self.arrow_key_events_enabled: # direction vector * step delta = key_bind_direction[self._key_move_value].clone().multiply_scalar(self.step) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index b02233135..ec18061d6 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -120,7 +120,8 @@ def __init__( arrow_keys_modifier: str modifier key that must be pressed to initiate movement using arrow keys, must be one of: - "Control", "Shift", "Alt" or ``None`` + "Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the + arrow key movements, or set the attribute ``arrow_key_events_enabled = True`` ipywidget_slider: IntSlider, optional ipywidget slider to associate with this graphic diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 8cd0313a1..d41c47de8 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -178,6 +178,10 @@ def __init__( edge_color: str, array, or tuple edge color for the selector, passed to pygfx.Color + arrow_keys_modifier: str + modifier key that must be pressed to initiate movement using arrow keys, must be one of: + "Control", "Shift", "Alt" or ``None`` + name: str name for this selector graphic """ diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index 72d25b542..d697b9d07 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -26,6 +26,8 @@ def __init__(self, *selectors: LinearSelector, key_bind: str = "Shift"): self.block_event = False + self.enabled: bool = True + @property def selectors(self): """Selectors managed by the Synchronizer""" @@ -46,6 +48,9 @@ def _handle_event(self, ev): # because infinite recursion return + if not self.enabled: + return + self.block_event = True source = ev.pick_info["graphic"] @@ -63,18 +68,18 @@ def _handle_event(self, ev): return if delta is not None: - self._move_selectors(source, delta) + self._move_selectors(source, delta, ev) self.block_event = False - def _move_selectors(self, source, delta): + def _move_selectors(self, source, delta, ev): for s in self.selectors: # must use == and not is to compare Graphics because they are weakref proxies! if s == source: # if it's the source, since it has already movied continue - s._move_graphic(delta) + s._move_graphic(delta, ev) def __del__(self): for s in self.selectors: From edc1701b5c72ccecf7c144dcf16d380389887c66 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 21 May 2023 09:42:25 -0400 Subject: [PATCH 27/96] finishes #107, #119, #192 (#200) --- fastplotlib/graphics/_base.py | 4 ++++ fastplotlib/layouts/_gridplot.py | 5 ++++- fastplotlib/plot.py | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index c1c3fbdfc..9eee05555 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -223,6 +223,10 @@ def link( self.registered_callbacks[event_type].append(callback_data) if bidirectional: + if event_type in PYGFX_EVENTS: + warn("cannot use bidirectional link for pygfx events") + return + target.link( event_type=event_type, target=self, diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index ab79a3804..f1db58044 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -166,7 +166,7 @@ def __init__( self._starting_size = size - def __getitem__(self, index: Union[Tuple[int, int], str]): + def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): if subplot.name == index: @@ -276,6 +276,9 @@ def show(self): return self.canvas + def close(self): + self.canvas.close() + def _get_iterator(self): return product(range(self.shape[0]), range(self.shape[1])) diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 96b1ac8dc..2f5b61261 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -112,3 +112,6 @@ def show(self, autoscale: bool = True): self.canvas.set_logical_size(*self._starting_size) return self.canvas + + def close(self): + self.canvas.close() From 123b3ae8290a63f355f98c3cd69caed72afbb2ca Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 22 May 2023 00:29:17 -0400 Subject: [PATCH 28/96] more selector stuff (#199) * garbage collection of selectors, not tested yet * selecors gc progress, not there yet but not broken either * move events for transparent fill areas * rectangle region selector basic functionality works * more rectangle region stuff --- fastplotlib/graphics/image.py | 28 +++-- fastplotlib/graphics/selectors/__init__.py | 1 + .../graphics/selectors/_base_selector.py | 38 +++++- fastplotlib/graphics/selectors/_linear.py | 4 +- .../graphics/selectors/_linear_region.py | 18 +-- .../graphics/selectors/_rectangle_region.py | 119 +++++++++++++----- fastplotlib/graphics/selectors/_sync.py | 6 +- fastplotlib/layouts/_base.py | 84 +++++++------ 8 files changed, 198 insertions(+), 100 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 5df96b2dd..9ba9a4143 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -14,16 +14,16 @@ class _ImageHeatmapSelectorsMixin: - def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: + def add_linear_selector(self, selection: int = None, padding: float = None, **kwargs) -> LinearSelector: """ Adds a linear selector. Parameters ---------- - selection: int + selection: int, optional initial position of the selector - padding: float + padding: float, optional pad the length of the selector kwargs @@ -35,6 +35,12 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar """ + # default padding is 15% the height or width of the image + if "axis" in kwargs.keys(): + axis = kwargs["axis"] + else: + axis = "x" + bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) if selection is None: @@ -56,17 +62,17 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar return weakref.proxy(selector) - def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> LinearRegionSelector: + def add_linear_region_selector(self, padding: float = None, **kwargs) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. Parameters ---------- - padding: float, default 100.0 + padding: float, optional Extends the linear selector along the y-axis to make it easier to interact with. - kwargs + kwargs, optional passed to ``LinearRegionSelector`` Returns @@ -85,13 +91,13 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear size=size, origin=origin, parent=weakref.proxy(self), + fill_color=(0, 0, 0.35, 0.2), **kwargs ) self._plot_area.add_graphic(selector, center=False) # so that it is above this graphic selector.position.set_z(self.position.z + 3) - selector.fill.material.color = (*selector.fill.material.color[:-1], 0.2) # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end @@ -107,6 +113,14 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): else: axis = "x" + if padding is None: + if axis == "x": + # based on number of rows + padding = int(data.shape[0] * 0.15) + elif axis == "y": + # based on number of columns + padding = int(data.shape[1] * 0.15) + if axis == "x": offset = self.position.x # x limits, number of columns diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index c67e15e40..8ebcaf053 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -1,3 +1,4 @@ from ._linear import LinearSelector from ._linear_region import LinearRegionSelector +from ._rectangle_region import RectangleRegionSelector from ._sync import Synchronizer diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 682e1855b..165b322da 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -110,6 +110,11 @@ def _add_plot_area_hook(self, plot_area): # double-click to enable arrow-key moveable mode wo.add_event_handler(self._toggle_arrow_key_moveable, "double_click") + for fill in self._fill: + if fill.material.color_is_transparent: + pfunc_fill = partial(self._check_fill_pointer_event, fill) + self._plot_area.renderer.add_event_handler(pfunc_fill, "pointer_down") + # when the pointer moves self._plot_area.renderer.add_event_handler(self._move, "pointer_move") @@ -129,6 +134,27 @@ def _add_plot_area_hook(self, plot_area): self._plot_area.renderer.add_event_handler(self._key_up, "key_up") self._plot_area.add_animations(self._key_hold) + def _check_fill_pointer_event(self, event_source: WorldObject, ev): + world_pos = self._plot_area.map_screen_to_world((ev.x, ev.y)) + # outside viewport, ignore + # this shouldn't be possible since the event handler is registered to the fill mesh world object + # but I like sanity checks anyways + if world_pos is None: + return + + bbox = event_source.get_world_bounding_box() + + xmin, ymin, zmin = bbox[0] + xmax, ymax, zmax = bbox[1] + + if not (xmin <= world_pos.x <= xmax): + return + + if not (ymin <= world_pos.y <= ymax): + return + + self._move_start(event_source, ev) + def _move_start(self, event_source: WorldObject, ev): """ Called on "pointer_down" events @@ -136,7 +162,9 @@ def _move_start(self, event_source: WorldObject, ev): Parameters ---------- event_source: WorldObject - event source, for example selection fill area ``Mesh`` an edge ``Line`` or vertex ``Points`` + event source, for example selection fill area ``Mesh`` an edge ``Line`` or vertex ``Points``. + This helps keep the event source within the MoveInfo so that during "pointer_move" events (which are + from the renderer) we know the original source of the "move action". ev: Event pygfx ``Event`` @@ -179,14 +207,14 @@ def _move(self, ev): self.delta = world_pos.clone().sub(self._move_info.last_position) self._pygfx_event = ev - self._move_graphic(self.delta, ev) + self._move_graphic(self.delta) # update last position self._move_info.last_position = world_pos self._plot_area.controller.enabled = True - def _move_graphic(self, delta, ev): + def _move_graphic(self, delta): raise NotImplementedError("Must be implemented in subclass") def _move_end(self, ev): @@ -220,7 +248,7 @@ def _move_to_pointer(self, ev): else: self._move_info = MoveInfo(last_position=current_position, source=self._edges[0]) - self._move_graphic(self.delta, ev) + self._move_graphic(self.delta) self._move_info = None def _pointer_enter(self, ev): @@ -258,7 +286,7 @@ def _key_hold(self): self._move_info = MoveInfo(last_position=None, source=self._edges[0]) # move the graphic - self._move_graphic(delta=delta, ev=None) + self._move_graphic(delta=delta) self._move_info = None diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index ec18061d6..cb3b7ec27 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -163,7 +163,7 @@ def __init__( line_data = np.column_stack([xs, ys, zs]) else: - raise ValueError("`axis` must be one of 'v' or 'h'") + raise ValueError("`axis` must be one of 'x' or 'y'") line_data = line_data.astype(np.float32) @@ -344,7 +344,7 @@ def _get_selected_index(self, graphic): index = self.selection() - offset return int(index) - def _move_graphic(self, delta: Vector3, ev): + def _move_graphic(self, delta: Vector3): """ Moves the graphic diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index d41c47de8..142f40677 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -229,6 +229,8 @@ def __init__( pygfx.box_geometry(size, 1, 1), pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) ) + else: + raise ValueError("`axis` must be one of 'x' or 'y'") # the fill of the selection self.fill = mesh @@ -314,7 +316,7 @@ def __init__( def bounds(self) -> LinearBoundsFeature: """ The current bounds of the selection in world space. These bounds will NOT necessarily correspond to the - indices of the data that are under the selection. Use ``get_selected_indices()` which maps from + indices of the data that are under the selection. Use ``get_selected_indices()`` which maps from world space to data indices. """ return self._bounds @@ -433,8 +435,7 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis ixs = np.arange(*self.bounds(), dtype=int) return ixs - def _move_graphic(self, delta, ev): - # edge-0 bound current world position + def _move_graphic(self, delta): if self.bounds.axis == "x": # new left bound position bound_pos_0 = Vector3(self.bounds()[0]).add(delta) @@ -448,17 +449,8 @@ def _move_graphic(self, delta, ev): # new top bound position bound_pos_1 = Vector3(0, self.bounds()[1]).add(delta) - # workaround because transparent objects are not pickable in pygfx - if ev is not None: - if 2 in ev.buttons: - force_fill_move = True - else: - force_fill_move = False - else: - force_fill_move = False - # move entire selector if source was fill - if self._move_info.source == self.fill or force_fill_move: + if self._move_info.source == self.fill: bound0 = getattr(bound_pos_0, self.bounds.axis) bound1 = getattr(bound_pos_1, self.bounds.axis) # set the new bounds diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index 5188161b9..6332f9bcc 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -71,12 +71,14 @@ def _set(self, value: Tuple[float, float, float, float]): # change the edge lines + # each edge line is defined by two end points which are stored in the + # geometry.positions # [x0, y0, z0] # [x1, y1, z0] # left line z = self._parent.edges[0].geometry.positions.data[:, -1][0] - self._parent.edges[0].geometry.position.data[:] = np.array( + self._parent.edges[0].geometry.positions.data[:] = np.array( [ [xmin, ymin, z], [xmin, ymax, z] @@ -84,7 +86,7 @@ def _set(self, value: Tuple[float, float, float, float]): ) # right line - self._parent.edges[1].geometry.position.data[:] = np.array( + self._parent.edges[1].geometry.positions.data[:] = np.array( [ [xmax, ymin, z], [xmax, ymax, z] @@ -92,7 +94,7 @@ def _set(self, value: Tuple[float, float, float, float]): ) # bottom line - self._parent.edges[2].geometry.position.data[:] = np.array( + self._parent.edges[2].geometry.positions.data[:] = np.array( [ [xmin, ymin, z], [xmax, ymin, z] @@ -100,7 +102,7 @@ def _set(self, value: Tuple[float, float, float, float]): ) # top line - self._parent.edges[3].geometry.position.data[:] = np.array( + self._parent.edges[3].geometry.positions.data[:] = np.array( [ [xmin, ymax, z], [xmax, ymax, z] @@ -118,31 +120,34 @@ def _set(self, value: Tuple[float, float, float, float]): # calls any events self._feature_changed(key=None, new_data=value) + # TODO: feature_changed def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): return - - if len(self._event_handlers) < 1: - return - - if self._parent.parent is not None: - selected_ixs = self._parent.get_selected_indices() - selected_data = self._parent.get_selected_data() - else: - selected_ixs = None - selected_data = None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - "selected_indices": selected_ixs, - "selected_data": selected_data - } - - event_data = FeatureEvent(type="bounds", pick_info=pick_info) - - self._call_event_handlers(event_data) + # if len(self._event_handlers) < 1: + # return + # + # if self._parent.parent is not None: + # selected_ixs = self._parent.get_selected_indices() + # selected_data = self._parent.get_selected_data() + # else: + # selected_ixs = None + # selected_data = None + # + # pick_info = { + # "index": None, + # "collection-index": self._collection_index, + # "world_object": self._parent.world_object, + # "new_data": new_data, + # "selected_indices": selected_ixs, + # "selected_data": selected_data + # "graphic", + # "delta", + # "pygfx_event" + # } + # + # event_data = FeatureEvent(type="bounds", pick_info=pick_info) + # + # self._call_event_handlers(event_data) class RectangleRegionSelector(Graphic, BaseSelector): @@ -280,9 +285,10 @@ def __init__( # add the edge lines for edge in self.edges: - edge.position.set_z(-1) + edge.position.set(*origin, -1) self.world_object.add(edge) + self._resizable = resizable self._bounds = RectangleBoundsFeature(self, bounds, axis=axis, limits=limits) BaseSelector.__init__( @@ -297,8 +303,57 @@ def __init__( @property def bounds(self) -> RectangleBoundsFeature: """ - The current bounds of the selection in world space. These bounds will NOT necessarily correspond to the - indices of the data that are under the selection. Use ``get_selected_indices()` which maps from - world space to data indices. + (xmin, xmax, ymin, ymax) The current bounds of the selection in world space. + + These bounds will NOT necessarily correspond to the indices of the data that are under the selection. + Use ``get_selected_indices()` which maps from world space to data indices. """ - return self._bounds \ No newline at end of file + return self._bounds + + def _move_graphic(self, delta): + # new left bound position + xmin_new = Vector3(self.bounds()[0]).add(delta).x + + # new right bound position + xmax_new = Vector3(self.bounds()[1]).add(delta).x + + # new bottom bound position + ymin_new = Vector3(0, self.bounds()[2]).add(delta).y + + # new top bound position + ymax_new = Vector3(0, self.bounds()[3]).add(delta).y + + # move entire selector if source was fill + if self._move_info.source == self.fill: + # set the new bounds + self.bounds = (xmin_new, xmax_new, ymin_new, ymax_new) + return + + # if selector is not resizable do nothing + if not self._resizable: + return + + # if resizable, move edges + + xmin, xmax, ymin, ymax = self.bounds() + + # change only left bound + if self._move_info.source == self.edges[0]: + xmin = xmin_new + + # change only right bound + elif self._move_info.source == self.edges[1]: + xmax = xmax_new + + # change only bottom bound + elif self._move_info.source == self.edges[2]: + ymin = ymin_new + + # change only top bound + elif self._move_info.source == self.edges[3]: + ymax = ymax_new + else: + return + + # set the new bounds + self.bounds = (xmin, xmax, ymin, ymax) diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index d697b9d07..385f2cea1 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -68,18 +68,18 @@ def _handle_event(self, ev): return if delta is not None: - self._move_selectors(source, delta, ev) + self._move_selectors(source, delta) self.block_event = False - def _move_selectors(self, source, delta, ev): + def _move_selectors(self, source, delta): for s in self.selectors: # must use == and not is to compare Graphics because they are weakref proxies! if s == source: # if it's the source, since it has already movied continue - s._move_graphic(delta, ev) + s._move_graphic(delta) def __del__(self): for s in self.selectors: diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index 3408b3a82..1571f51e9 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -9,13 +9,14 @@ from wgpu.gui.auto import WgpuCanvas from ..graphics._base import Graphic, GraphicCollection -from ..graphics.selectors import LinearSelector +from ..graphics.selectors._base_selector import BaseSelector # dict to store Graphic instances # this is the only place where the real references to Graphics are stored in a Python session # {hex id str: Graphic} GRAPHICS: Dict[str, Graphic] = dict() +SELECTORS: Dict[str, BaseSelector] = dict() class PlotArea: @@ -81,8 +82,9 @@ def __init__( # the real Graphic instances are stored in the ``GRAPHICS`` dict self._graphics: List[str] = list() - # hacky workaround for now to exclude from bbox calculations - self._selectors = list() + # selectors are in their own list so they can be excluded from scene bbox calculations + # managed similar to GRAPHICS for garbage collection etc. + self._selectors: List[str] = list() self.name = name @@ -133,7 +135,7 @@ def controller(self) -> Union[PanZoomController, OrbitController]: return self._controller @property - def graphics(self) -> Tuple[Graphic]: + def graphics(self) -> Tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" proxies = list() for loc in self._graphics: @@ -142,6 +144,16 @@ def graphics(self) -> Tuple[Graphic]: return tuple(proxies) + @property + def selectors(self) -> Tuple[BaseSelector, ...]: + """Selectors in the plot area. Always returns a proxy to the Graphic instances.""" + proxies = list() + for loc in self._selectors: + p = weakref.proxy(SELECTORS[loc]) + proxies.append(p) + + return tuple(proxies) + def get_rect(self) -> Tuple[float, float, float, float]: """allows setting the region occupied by the viewport w.r.t. the parent""" raise NotImplementedError("Must be implemented in subclass") @@ -212,9 +224,11 @@ def add_graphic(self, graphic: Graphic, center: bool = True): if graphic.name is not None: # skip for those that have no name self._check_graphic_name_exists(graphic.name) - # TODO: need to refactor LinearSelector entirely - if isinstance(graphic, LinearSelector): - self._selectors.append(graphic) # don't manage garbage collection of LineSliders for now + if isinstance(graphic, BaseSelector): + # store in SELECTORS dict + loc = graphic.loc + SELECTORS[loc] = graphic + self._selectors.append(loc) # don't manage garbage collection of LineSliders for now else: # store in GRAPHICS dict loc = graphic.loc @@ -292,10 +306,10 @@ def auto_scale(self, maintain_aspect: bool = False, zoom: float = 0.8): zoom value for the camera after auto-scaling, if zoom = 1.0 then the graphics in the scene will fill the entire canvas. """ - # hacky workaround for now until I figure out how to put it in its own scene - # TODO: remove all selectors from a scene to calculate scene bbox - for slider in self._selectors: - self.scene.remove(slider.world_object) + # hacky workaround for now until we decided if we want to put selectors in their own scene + # remove all selectors from a scene to calculate scene bbox + for selector in self.selectors: + self.scene.remove(selector.world_object) self.center_scene() if not isinstance(maintain_aspect, bool): @@ -307,8 +321,8 @@ def auto_scale(self, maintain_aspect: bool = False, zoom: float = 0.8): else: width, height, depth = (1, 1, 1) - for slider in self._selectors: - self.scene.add(slider.world_object) + for selector in self.selectors: + self.scene.add(selector.world_object) self.camera.width = width self.camera.height = height @@ -340,38 +354,32 @@ def delete_graphic(self, graphic: Graphic): The graphic to delete """ - - # graphic_loc = hex(id(graphic.__repr__.__self__)) - + # TODO: proper gc of selectors, RAM is freed for regular graphics but not selectors + # TODO: references to selectors must be lingering somewhere # get location - graphic_loc = graphic.loc - - if graphic_loc not in self._graphics: - raise KeyError(f"Graphic with following address not found in plot area: {graphic_loc}") + loc = graphic.loc + + # check which dict it's in + if loc in self._graphics: + glist = self._graphics + kind = "graphic" + elif loc in self._selectors: + kind = "selector" + glist = self._selectors + else: + raise KeyError(f"Graphic with following address not found in plot area: {loc}") # remove from scene if necessary if graphic.world_object in self.scene.children: self.scene.remove(graphic.world_object) # remove from list of addresses - self._graphics.remove(graphic_loc) - - # for GraphicCollection objects - # if isinstance(graphic, GraphicCollection): - # # clear Group - # graphic.world_object.clear() - # graphic.clear() - # delete all child world objects in the collection - # for g in graphic.graphics: - # subloc = hex(id(g)) - # del WORLD_OBJECTS[subloc] - - # get mem location of graphic - # loc = hex(id(graphic)) - # delete world object - #del WORLD_OBJECTS[graphic_loc] - - del GRAPHICS[graphic_loc] + glist.remove(loc) + + if kind == "graphic": + del GRAPHICS[loc] + elif kind == "selector": + del SELECTORS[loc] def clear(self): """ From 162910a85a127d3df7ec72ade8775dcde7bbd124 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 22 May 2023 12:54:26 -0400 Subject: [PATCH 29/96] basic recording for Plot (#178) * basic recording for Plot * opencv conditional import, reduce fps slightly in recorded vid to compensat, looks fine now * forgot to add the actual recorder * record Plot or GridPlot, uses PyAV, works well enough * comment, organization * cleaup --- fastplotlib/layouts/_gridplot.py | 6 +- fastplotlib/layouts/_record_mixin.py | 238 +++++++++++++++++++++++++++ fastplotlib/plot.py | 5 +- 3 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 fastplotlib/layouts/_record_mixin.py diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index f1db58044..b625622ef 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -7,6 +7,7 @@ from wgpu.gui.auto import WgpuCanvas from ._defaults import create_controller from ._subplot import Subplot +from ._record_mixin import RecordMixin def to_array(a) -> np.ndarray: @@ -22,7 +23,7 @@ def to_array(a) -> np.ndarray: valid_cameras = ["2d", "2d-big", "3d", "3d-big"] -class GridPlot: +class GridPlot(RecordMixin): def __init__( self, shape: Tuple[int, int], @@ -91,7 +92,6 @@ def __init__( self._controllers = np.empty(shape=cameras.shape, dtype=object) - if cameras.shape != self.shape: raise ValueError @@ -165,6 +165,8 @@ def __init__( self._current_iter = None self._starting_size = size + + super(RecordMixin, self).__init__() def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: if isinstance(index, str): diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py new file mode 100644 index 000000000..6af722624 --- /dev/null +++ b/fastplotlib/layouts/_record_mixin.py @@ -0,0 +1,238 @@ +from typing import * +from pathlib import Path +from multiprocessing import Queue, Process +from time import time + +try: + import av +except ImportError: + HAS_AV = False +else: + HAS_AV = True + + +class VideoWriterAV(Process): + """Video writer, uses PyAV in an external process to write frames to disk""" + def __init__( + self, + path: Union[Path, str], + queue: Queue, + fps: int, + width: int, + height: int, + codec: str, + pixel_format: str, + options: dict = None + ): + super().__init__() + self.queue = queue + + self.container = av.open(path, mode="w") + + self.stream = self.container.add_stream(codec, rate=fps, options=options) + + # in case libx264, trim last rows and/or column + # because libx264 doesn't like non-even number width or height + if width % 2 != 0: + width -= 1 + if height % 2 != 0: + height -= 1 + + self.stream.width = width + self.stream.height = height + + self.stream.pix_fmt = pixel_format + + def run(self): + while True: + if self.queue.empty(): # no frame to write + continue + + frame = self.queue.get() + + # recording has ended + if frame is None: + self.container.close() + break + + frame = av.VideoFrame.from_ndarray( + frame[:self.stream.height, :self.stream.width], # trim if necessary because of x264 + format="rgb24" + ) + + for packet in self.stream.encode(frame): + self.container.mux(packet) + + # I don't exactly know what this does, copied from pyav example + for packet in self.stream.encode(): + self.container.mux(packet) + + # close file + self.container.close() + + # close process, release resources + self.close() + + +# adds recording functionality to GridPlot and Plot +class RecordMixin: + def __init__(self): + self._video_writer: VideoWriterAV = None + self._video_writer_queue = Queue() + self._record_fps = 25 + self._record_timer = 0 + self._record_start_time = 0 + + def _record(self): + """ + Sends frame to VideoWriter through video writer queue + """ + # current time + t = time() + + # put frame in queue only if enough time as passed according to the desired framerate + # otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering + if t - self._record_timer < (1 / self._record_fps): + return + + # reset timer + self._record_timer = t + + if self._video_writer is not None: + ss = self.canvas.snapshot() + # exclude alpha channel + self._video_writer_queue.put(ss.data[..., :-1]) + + def record_start( + self, + path: Union[str, Path], + fps: int = 25, + codec: str = "mpeg4", + pixel_format: str = "yuv420p", + options: dict = None + ): + """ + Start a recording, experimental. Call ``record_end()`` to end a recording. + Note: playback duration does not exactly match recording duration. + + Requires PyAV: https://github.com/PyAV-Org/PyAV + + **Do not resize canvas during a recording, the width and height must remain constant!** + + Parameters + ---------- + path: str or Path + path to save the recording + + fps: int, default ``25`` + framerate, do not use > 25 within jupyter + + codec: str, default "mpeg4" + codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html . + In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a + better option if you have it installed. + + pixel_format: str, default "yuv420p" + pixel format + + options: dict, optional + Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between + 1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where + the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more + info on codec options + + Examples + -------- + + With ``"mpeg4"`` + + .. code-block:: python + + # create a plot or gridplot etc + + # start recording video + plot.record_start("./video.mp4", options={"q:v": "20"} + + # do stuff like interacting with the plot, change things, etc. + + # end recording + plot.record_end() + + With ``"libx264"`` + + .. code-block:: python + + # create a plot or gridplot etc + + # start recording video + plot.record_start("./vid_x264.mp4", codec="libx264", options={"crf": "25"}) + + # do stuff like interacting with the plot, change things, etc. + + # end recording + plot.record_end() + + """ + + if not HAS_AV: + raise ModuleNotFoundError( + "Recording to video file requires `av`:\n" + "https://github.com/PyAV-Org/PyAV" + ) + + if Path(path).exists(): + raise FileExistsError(f"File already exists at given path: {path}") + + # queue for sending frames to VideoWriterAV process + self._video_writer_queue = Queue() + + # snapshot to get canvas width height + ss = self.canvas.snapshot() + + # writer process + self._video_writer = VideoWriterAV( + path=str(path), + queue=self._video_writer_queue, + fps=int(fps), + width=ss.width, + height=ss.height, + codec=codec, + pixel_format=pixel_format, + options=options + ) + + # start writer process + self._video_writer.start() + + # 1.3 seems to work well to reduce that difference between playback time and recording time + # will properly investigate later + self._record_fps = fps * 1.3 + self._record_start_time = time() + + # record timer used to maintain desired framerate + self._record_timer = time() + + self.add_animations(self._record) + + def record_stop(self) -> float: + """ + End a current recording. Returns the real duration of the recording + + Returns + ------- + float + recording duration + """ + + # tell video writer that recording has finished + self._video_writer_queue.put(None) + + # wait for writer to finish + self._video_writer.join(timeout=5) + + self._video_writer = None + + # so self._record() is no longer called on every render cycle + self.remove_animation(self._record) + + return time() - self._record_start_time diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 2f5b61261..0e286d588 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -1,10 +1,12 @@ from typing import * import pygfx from wgpu.gui.auto import WgpuCanvas + from .layouts._subplot import Subplot +from .layouts._record_mixin import RecordMixin -class Plot(Subplot): +class Plot(Subplot, RecordMixin): def __init__( self, canvas: WgpuCanvas = None, @@ -86,6 +88,7 @@ def __init__( controller=controller, **kwargs ) + super(RecordMixin, self).__init__() self._starting_size = size From 3decd61aca264213ead66601a1197feda52050e1 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 22 May 2023 13:12:50 -0400 Subject: [PATCH 30/96] changing linux build to have python versions matrix --- .github/workflows/ci.yml | 42 +++- examples/gridplot/__init__.py | 0 examples/image/__init__.py | 0 examples/image/{cmap.py => image_cmap.py} | 0 examples/image/{rgb.py => image_rgb.py} | 0 .../{rgbvminvmax.py => image_rgbvminvmax.py} | 0 examples/image/{simple.py => image_simple.py} | 0 .../image/{vminvmax.py => image_vminvmax.py} | 0 examples/line/__init__.py | 0 .../{colorslice.py => line_colorslice.py} | 0 .../line/{dataslice.py => line_dataslice.py} | 0 ...ent_scaling.py => line_present_scaling.py} | 0 examples/scatter/__init__.py | 0 notebooks/line_collection_event.ipynb | 212 ----------------- notebooks/single_contour_event.ipynb | 225 ------------------ 15 files changed, 38 insertions(+), 441 deletions(-) create mode 100644 examples/gridplot/__init__.py create mode 100644 examples/image/__init__.py rename examples/image/{cmap.py => image_cmap.py} (100%) rename examples/image/{rgb.py => image_rgb.py} (100%) rename examples/image/{rgbvminvmax.py => image_rgbvminvmax.py} (100%) rename examples/image/{simple.py => image_simple.py} (100%) rename examples/image/{vminvmax.py => image_vminvmax.py} (100%) create mode 100644 examples/line/__init__.py rename examples/line/{colorslice.py => line_colorslice.py} (100%) rename examples/line/{dataslice.py => line_dataslice.py} (100%) rename examples/line/{present_scaling.py => line_present_scaling.py} (100%) create mode 100644 examples/scatter/__init__.py delete mode 100644 notebooks/line_collection_event.ipynb delete mode 100644 notebooks/single_contour_event.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc47b7ec..945635dad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,27 @@ on: jobs: - linux-build: + test-linux-builds: + name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: - max-parallel: 5 + fail-fast: false + matrix: + include: + - name: Test py38 + pyversion: '3.8' + - name: Test py39 + pyversion: '3.9' + - name: Test py310 + pyversion: '3.10' + - name: Test py311 + pyversion: '3.11' steps: - uses: actions/checkout@v3 - - name: Set up Python '3.10' + - name: Set up Python ${{ matrix.pyversion }} uses: actions/setup-python@v3 with: - python-version: '3.10' + python-version: ${{ matrix.pyversion }} - name: Install package and dev dependencies run: | python -m pip install --upgrade pip @@ -64,3 +75,26 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs + +# test-notebooks-build: +# name: Test notebooks +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# steps: +# - uses: actions/checkout@v3 +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: '3.10' +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip +# # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving +# sed -i "/pygfx/d" ./setup.py +# pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 +# pip install -e . +# - name: Test notebooks +# run: | +# pip install pytest nbmake +# pytest --nbmake notebooks/simple.ipynb diff --git a/examples/gridplot/__init__.py b/examples/gridplot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/image/__init__.py b/examples/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/image/cmap.py b/examples/image/image_cmap.py similarity index 100% rename from examples/image/cmap.py rename to examples/image/image_cmap.py diff --git a/examples/image/rgb.py b/examples/image/image_rgb.py similarity index 100% rename from examples/image/rgb.py rename to examples/image/image_rgb.py diff --git a/examples/image/rgbvminvmax.py b/examples/image/image_rgbvminvmax.py similarity index 100% rename from examples/image/rgbvminvmax.py rename to examples/image/image_rgbvminvmax.py diff --git a/examples/image/simple.py b/examples/image/image_simple.py similarity index 100% rename from examples/image/simple.py rename to examples/image/image_simple.py diff --git a/examples/image/vminvmax.py b/examples/image/image_vminvmax.py similarity index 100% rename from examples/image/vminvmax.py rename to examples/image/image_vminvmax.py diff --git a/examples/line/__init__.py b/examples/line/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/line/colorslice.py b/examples/line/line_colorslice.py similarity index 100% rename from examples/line/colorslice.py rename to examples/line/line_colorslice.py diff --git a/examples/line/dataslice.py b/examples/line/line_dataslice.py similarity index 100% rename from examples/line/dataslice.py rename to examples/line/line_dataslice.py diff --git a/examples/line/present_scaling.py b/examples/line/line_present_scaling.py similarity index 100% rename from examples/line/present_scaling.py rename to examples/line/line_present_scaling.py diff --git a/examples/scatter/__init__.py b/examples/scatter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notebooks/line_collection_event.ipynb b/notebooks/line_collection_event.ipynb deleted file mode 100644 index c88971042..000000000 --- a/notebooks/line_collection_event.ipynb +++ /dev/null @@ -1,212 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "f02f7349-ac1a-49b7-b304-d5b701723e0f", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d9f54448-7718-4212-ac6d-163a2d3be146", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib.graphics import LineGraphic, LineCollection, ImageGraphic\n", - "from fastplotlib.plot import Plot\n", - "import pickle" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b683c6d1-d926-43e3-a221-4ec6450e3677", - "metadata": {}, - "outputs": [], - "source": [ - "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "26ced005-c52f-4696-903d-a6974ae6cefc", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2bc75fa52d484cd1b03006a6530f10b3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", - "\n" - ] - } - ], - "source": [ - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "1aea0a61-db63-41e1-b330-5a922da4bac5", - "metadata": {}, - "outputs": [], - "source": [ - "line_collection = LineCollection(contours, cmap=\"Oranges\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "310fd5b3-49f2-4dbb-831e-cb9f369d58c8", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(line_collection)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "661a5991-0de3-44d7-a626-2ae72704dcec", - "metadata": {}, - "outputs": [], - "source": [ - "image = ImageGraphic(data=np.ones((180, 180)))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "816f6382-f4ea-4cd4-b9f3-2dc5c232b0a5", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(image)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8187fd25-18e1-451f-b2fe-8cd2e7785c8b", - "metadata": {}, - "outputs": [], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5ddd3c4-84e2-44a3-a14f-74871aa0bb8f", - "metadata": {}, - "outputs": [], - "source": [ - "black = np.array([0.0, 0.0, 0.0, 1.0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ac64f1c-21e0-4c21-b968-3953e7858848", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import *" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d0d0c971-aac9-4c77-b88c-de9d79c7d74e", - "metadata": {}, - "outputs": [], - "source": [ - "def callback_function(source: Any, target: Any, event, new_data: Any):\n", - " # calculate coms of line collection\n", - " indices = np.array(event.pick_info[\"index\"])\n", - " \n", - " coms = list()\n", - "\n", - " for contour in target.items:\n", - " coors = contour.data.feature_data[~np.isnan(contour.data.feature_data).any(axis=1)]\n", - " com = coors.mean(axis=0)\n", - " coms.append(com)\n", - "\n", - " # euclidean distance to find closest index of com \n", - " indices = np.append(indices, [0])\n", - " \n", - " ix = np.linalg.norm((coms - indices), axis=1).argsort()[0]\n", - " \n", - " ix = int(ix)\n", - " \n", - " print(ix)\n", - " \n", - " target._set_feature(feature=\"colors\", new_data=new_data, indices=ix)\n", - " \n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a46d148-3007-4c7a-bf2b-91057eba855d", - "metadata": {}, - "outputs": [], - "source": [ - "image.link(event_type=\"click\", target=line_collection, feature=\"colors\", new_data=black, callback_function=callback_function)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8040456e-24a5-423b-8822-99a20e7ea470", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/single_contour_event.ipynb b/notebooks/single_contour_event.ipynb deleted file mode 100644 index a4b7f1f91..000000000 --- a/notebooks/single_contour_event.ipynb +++ /dev/null @@ -1,225 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "3a992f41-b157-4b6f-9630-ef370389f318", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "589eaea4-e749-46ff-ac3d-e22aa4f75641", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib.graphics import LineGraphic\n", - "from fastplotlib.plot import Plot\n", - "import pickle" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "650279ac-e7df-4c6f-aac1-078ae4287028", - "metadata": {}, - "outputs": [], - "source": [ - "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "49dc6123-39d8-4f60-b14b-9cfd9a008940", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour = LineGraphic(data=contours[0], size=10.0, cmap=\"jet\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "776e916f-16c9-4114-b1ff-7ea209aa7b04", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b9c85f639ca34c6c882396fc4753818b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", - "\n" - ] - } - ], - "source": [ - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "203768f0-6bb4-4ba9-b099-395f2bdd2a8c", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(single_contour)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4e46d687-d81a-4b6f-bece-c9edf3606d4f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "730d202ce0424559b30cd4d7d5f3b77b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "dbcda1d6-3f21-4a5e-b60f-75bf9103fbe6", - "metadata": {}, - "outputs": [], - "source": [ - "white = np.ones(shape=single_contour.colors.feature_data.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6db453cf-5856-4a7b-879f-83032cb9e9ac", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour.link(event_type=\"click\", target=single_contour, feature=\"colors\", new_data=white, indices_mapper=None)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "712c3f43-3339-4d1b-9d64-fe4f4d6bd672", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'click': [CallbackData(target=fastplotlib.LineGraphic @ 0x7f516cc02880, feature='colors', new_data=array([[1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.]]), indices_mapper=None)]}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "single_contour.registered_callbacks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d353d7b-a0d0-4629-a8c0-87b767d99bd2", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From f872155eb687b18e3cc9b3b720eb9e241a9f974c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Tue, 23 May 2023 12:53:47 -0400 Subject: [PATCH 31/96] working on toolbar (#195) * working on toolbar * toolbar updates * basic toolbar for plot implemented * base logic in notebook for gridplot toolbar * gp toolbar working when subplots have names & mix names/no names * initial changes to simple plot toolbar * updates to plot toolbar, still need to refactor into separate file * fixed gridplot toolbar, still need to move to separate class * add flip button * adding click event handler for gp toolbar dropdown options * keep new clicked plot up-to-date with toggle button values * requested changes --- examples/buttons.ipynb | 278 +++++++++++++++++++++++++++++++ fastplotlib/layouts/_gridplot.py | 111 +++++++++++- fastplotlib/layouts/_toolbar.py | 1 + fastplotlib/plot.py | 74 +++++++- 4 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 examples/buttons.ipynb create mode 100644 fastplotlib/layouts/_toolbar.py diff --git a/examples/buttons.ipynb b/examples/buttons.ipynb new file mode 100644 index 000000000..60419a229 --- /dev/null +++ b/examples/buttons.ipynb @@ -0,0 +1,278 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "6725ce7d-eea7-44f7-bedc-813e8ce5bf4f", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import Plot\n", + "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "33bf59c4-14e5-43a8-8a16-69b6859864c5", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95be7ab3326347359f783946ec8d9339", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:33: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot = Plot()\n", + "xs = np.linspace(-10, 10, 100)\n", + "# sine wave\n", + "ys = np.sin(xs)\n", + "sine = np.dstack([xs, ys])[0]\n", + "plot.add_line(sine)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "68ea8011-d6fd-448f-9bf6-34073164d271", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cecbc6a1fec54d03876ac5a8609e5200", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0bbb459c-cb49-448e-b0b8-c541e55da313", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import GridPlot\n", + "from ipywidgets import Dropdown" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "91a31531-818b-46a2-9587-5d9ef5b59b93", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "05c6d9d2f62846e8ab6135a3d218964c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gp = GridPlot(\n", + " shape=(1,2),\n", + " names=[['plot1', 'plot2']])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e96bbda7-3693-42f2-bd52-f668f39134f6", + "metadata": {}, + "outputs": [], + "source": [ + "img = np.random.rand(512,512)\n", + "for subplot in gp:\n", + " subplot.add_image(data=img)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "03b877ba-cf9c-47d9-a0e5-b3e694274a28", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9b792858ff24411db756810bb8eea00f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "afc0cd52-fb24-4561-9876-50fbdf784502", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 1)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp[0,1].position" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f902391ceb614f2a812725371184ce81", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gp2 = GridPlot(shape=(1,2))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", + "metadata": {}, + "outputs": [], + "source": [ + "for subplot in gp2:\n", + " subplot.add_image(data=img)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65a2a30036f44e22a479c6edd215e25a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp2.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd3e7b3c-f4d2-44c7-931c-d172c7bdad36", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index b625622ef..c121e42f2 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,3 +1,4 @@ +import itertools from itertools import product import numpy as np from typing import * @@ -7,6 +8,8 @@ from wgpu.gui.auto import WgpuCanvas from ._defaults import create_controller from ._subplot import Subplot +from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown +from wgpu.gui.jupyter import JupyterWgpuCanvas from ._record_mixin import RecordMixin @@ -69,6 +72,7 @@ def __init__( """ self.shape = shape + self.toolbar = None if isinstance(cameras, str): if cameras not in valid_cameras: @@ -259,7 +263,7 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) - def show(self): + def show(self, toolbar: bool = True): """ begins the rendering event loop and returns the canvas @@ -273,10 +277,20 @@ def show(self): for subplot in self: subplot.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - return self.canvas + # check if in jupyter notebook or not + if not isinstance(self.canvas, JupyterWgpuCanvas): + return self.canvas + + if toolbar and self.toolbar is None: + self.toolbar = GridPlotToolBar(self).widget + return VBox([self.canvas, self.toolbar]) + elif toolbar and self.toolbar is not None: + return VBox([self.canvas, self.toolbar]) + else: + return self.canvas def close(self): self.canvas.close() @@ -294,3 +308,94 @@ def __next__(self) -> Subplot: def __repr__(self): return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + + +class GridPlotToolBar: + def __init__(self, + plot: GridPlot): + """ + Basic toolbar for a GridPlot instance. + + Parameters + ---------- + plot: + """ + self.plot = plot + + self.autoscale_button = Button(value=False, disabled=False, icon='expand-arrows-alt', + layout=Layout(width='auto'), tooltip='auto-scale scene') + self.center_scene_button = Button(value=False, disabled=False, icon='align-center', + layout=Layout(width='auto'), tooltip='auto-center scene') + self.panzoom_controller_button = ToggleButton(value=True, disabled=False, icon='hand-pointer', + layout=Layout(width='auto'), tooltip='panzoom controller') + self.maintain_aspect_button = ToggleButton(value=True, disabled=False, description="1:1", + layout=Layout(width='auto'), tooltip='maintain aspect') + self.maintain_aspect_button.style.font_weight = "bold" + self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + layout=Layout(width='auto'), tooltip='flip') + + positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) + values = list() + for pos in positions: + if self.plot[pos].name is not None: + values.append(self.plot[pos].name) + else: + values.append(str(pos)) + self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:') + + self.widget = HBox([self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button, + self.dropdown]) + + self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.autoscale_button.on_click(self.auto_scale) + self.center_scene_button.on_click(self.center_scene) + self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.flip_camera_button.on_click(self.flip_camera) + + self.plot.renderer.add_event_handler(self.update_current_subplot, "click") + + @property + def current_subplot(self) -> Subplot: + # parses dropdown value as plot name or position + current = self.dropdown.value + if current[0] == "(": + return self.plot[eval(current)] + else: + return self.plot[current] + + def auto_scale(self, obj): + current = self.current_subplot + current.auto_scale(maintain_aspect=current.camera.maintain_aspect) + + def center_scene(self, obj): + current = self.current_subplot + current.center_scene() + + def panzoom_control(self, obj): + current = self.current_subplot + current.controller.enabled = self.panzoom_controller_button.value + + def maintain_aspect(self, obj): + current = self.current_subplot + current.camera.maintain_aspect = self.maintain_aspect_button.value + + def flip_camera(self, obj): + current = self.current_subplot + current.camera.scale.y = -1 * current.camera.scale.y + + def update_current_subplot(self, ev): + for subplot in self.plot: + pos = subplot.map_screen_to_world((ev.x, ev.y)) + if pos is not None: + # update self.dropdown + if subplot.name is None: + self.dropdown.value = str(subplot.position) + else: + self.dropdown.value = subplot.name + self.panzoom_controller_button.value = subplot.controller.enabled + self.maintain_aspect_button.value = subplot.camera.maintain_aspect + diff --git a/fastplotlib/layouts/_toolbar.py b/fastplotlib/layouts/_toolbar.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/fastplotlib/layouts/_toolbar.py @@ -0,0 +1 @@ + diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 0e286d588..105dbfb96 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -3,6 +3,8 @@ from wgpu.gui.auto import WgpuCanvas from .layouts._subplot import Subplot +from ipywidgets import HBox, Layout, Button, ToggleButton, VBox +from wgpu.gui.jupyter import JupyterWgpuCanvas from .layouts._record_mixin import RecordMixin @@ -92,13 +94,15 @@ def __init__( self._starting_size = size + self.toolbar = None + def render(self): super(Plot, self).render() self.renderer.flush() self.canvas.request_draw() - def show(self, autoscale: bool = True): + def show(self, autoscale: bool = True, toolbar: bool = True): """ begins the rendering event loop and returns the canvas @@ -111,10 +115,74 @@ def show(self, autoscale: bool = True): self.canvas.request_draw(self.render) if autoscale: self.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - return self.canvas + # check if in jupyter notebook or not + if not isinstance(self.canvas, JupyterWgpuCanvas): + return self.canvas + + if toolbar and self.toolbar is None: + self.toolbar = ToolBar(self).widget + return VBox([self.canvas, self.toolbar]) + elif toolbar and self.toolbar is not None: + return VBox([self.canvas, self.toolbar]) + else: + return self.canvas def close(self): self.canvas.close() + + +class ToolBar: + def __init__(self, + plot: Plot): + """ + Basic toolbar for a Plot instance. + + Parameters + ---------- + plot: encapsulated plot instance that will be manipulated using the toolbar buttons + """ + self.plot = plot + + self.autoscale_button = Button(value=False, disabled=False, icon='expand-arrows-alt', + layout=Layout(width='auto'), tooltip='auto-scale scene') + self.center_scene_button = Button(value=False, disabled=False, icon='align-center', + layout=Layout(width='auto'), tooltip='auto-center scene') + self.panzoom_controller_button = ToggleButton(value=True, disabled=False, icon='hand-pointer', + layout=Layout(width='auto'), tooltip='panzoom controller') + self.maintain_aspect_button = ToggleButton(value=True, disabled=False, description="1:1", + layout=Layout(width='auto'), + tooltip='maintain aspect') + self.maintain_aspect_button.style.font_weight = "bold" + self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + layout=Layout(width='auto'), tooltip='flip') + + self.widget = HBox([self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button]) + + self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.autoscale_button.on_click(self.auto_scale) + self.center_scene_button.on_click(self.center_scene) + self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.flip_camera_button.on_click(self.flip_camera) + + def auto_scale(self, obj): + self.plot.auto_scale(maintain_aspect=self.plot.camera.maintain_aspect) + + def center_scene(self, obj): + self.plot.center_scene() + + def panzoom_control(self, obj): + self.plot.controller.enabled = self.panzoom_controller_button.value + + def maintain_aspect(self, obj): + self.plot.camera.maintain_aspect = self.maintain_aspect_button.value + + def flip_camera(self, obj): + self.plot.camera.scale.y = -1 * self.plot.camera.scale.y + \ No newline at end of file From 12e55e024398397423513663eee56f26f796a654 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 23 May 2023 15:21:34 -0400 Subject: [PATCH 32/96] catchup with pygfx linalg refactor (#203) * WIP, mapping to points and selectors not updated * everything updated for linalg refactor except rectangle selector * update toolbar w.r.t. linalg refactor * update examples w.r.t. linalg refactor * update readme --- README.md | 3 + examples/linear_region_selector.ipynb | 16 +--- examples/linear_selector.ipynb | 12 +++ examples/lineplot.ipynb | 67 ++------------- examples/scatter.ipynb | 81 +++++-------------- fastplotlib/graphics/_base.py | 36 +++++++-- fastplotlib/graphics/image.py | 4 +- fastplotlib/graphics/line.py | 14 ++-- fastplotlib/graphics/line_collection.py | 12 ++- fastplotlib/graphics/scatter.py | 2 +- .../graphics/selectors/_base_selector.py | 45 +++++------ fastplotlib/graphics/selectors/_linear.py | 23 +++--- .../graphics/selectors/_linear_region.py | 37 +++++---- .../graphics/selectors/_rectangle_region.py | 1 - fastplotlib/graphics/text.py | 4 +- fastplotlib/layouts/_base.py | 19 +++-- fastplotlib/layouts/_gridplot.py | 2 +- fastplotlib/layouts/_subplot.py | 6 +- fastplotlib/plot.py | 3 +- 19 files changed, 159 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 3999a15ea..a6e567a25 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ Questions, ideas? Post an issue or [chat on gitter](https://gitter.im/fastplotli **See the examples directory. Start out with `simple.ipynb`.** +**IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi` (i.e. pip install pygfx), you will need to use the examples from this commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples .** +The current examples will work if you installed `fastplotlib` and `pygfx` directly from github. + ### Neuroscience usecase demonstrating some of fastplotlib's capabilities https://user-images.githubusercontent.com/9403332/210304485-e554b648-50b4-4243-b292-a9ed30514a2d.mp4 diff --git a/examples/linear_region_selector.ipynb b/examples/linear_region_selector.ipynb index 8a3ae6cd0..8aeb37e27 100644 --- a/examples/linear_region_selector.ipynb +++ b/examples/linear_region_selector.ipynb @@ -41,8 +41,8 @@ "sine_graphic_y = gp[0, 1].add_line(np.column_stack([sine_y, xs]))\n", "\n", "# offset the position of the graphic to demonstrate `get_selected_data()` later\n", - "sine_graphic_y.position.set_x(50)\n", - "sine_graphic_y.position.set_y(50)\n", + "sine_graphic_y.position_x = 50\n", + "sine_graphic_y.position_y = 50\n", "\n", "# add linear selectors\n", "ls_x = sine_graphic_x.add_linear_region_selector() # default axis is \"x\"\n", @@ -256,18 +256,6 @@ "plot.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "3fa61ffd-43d5-42d0-b3e1-5541f58185cd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot[0, 0].auto_scale()" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/examples/linear_selector.ipynb b/examples/linear_selector.ipynb index a00225a5f..a4d6b97ea 100644 --- a/examples/linear_selector.ipynb +++ b/examples/linear_selector.ipynb @@ -55,6 +55,18 @@ "VBox([plot.show(), ipywidget_slider])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a632c8ee-2d4c-44fc-9391-7b2880223fdb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "selector.step = 0.1" + ] + }, { "cell_type": "markdown", "id": "2c49cdc2-0555-410c-ae2e-da36c3bf3bf0", diff --git a/examples/lineplot.ipynb b/examples/lineplot.ipynb index 36b921cf3..52ce66547 100644 --- a/examples/lineplot.ipynb +++ b/examples/lineplot.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "9c974494-712e-4981-bae2-a3ee176a6b20", "metadata": {}, "outputs": [], @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c3d8f967-f60f-4f0b-b6ba-21b1251b4856", "metadata": {}, "outputs": [], @@ -41,60 +41,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "78cffe56-1147-4255-82c1-53cec6bc986a", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7993e0a4358f4678a7343b78b3b0b24c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushalk/repos/fastplotlib/fastplotlib/layouts/_base.py:214: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "898a109f489741a5b4624be77bd27db0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 2 columns\n", "shape = (2, 2)\n", @@ -124,13 +74,6 @@ " subplot.set_axes_visibility(True)\n", " subplot.set_grid_visibility(True)\n", " \n", - " # invert the camera for some subplots to get\n", - " # different perspectives on the same data\n", - " if i == 1:\n", - " subplot.camera.scale.x = -1\n", - " if i == 2:\n", - " subplot.camera.scale.y = -1\n", - " \n", " marker = subplot.add_scatter(data=spiral[0], sizes=10, name=\"marker\")\n", " \n", "marker_index = 0\n", @@ -178,7 +121,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/scatter.ipynb b/examples/scatter.ipynb index e32b7a5b6..2253a3387 100644 --- a/examples/scatter.ipynb +++ b/examples/scatter.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "9b3041ad-d94e-4b2a-af4d-63bcd19bf6c2", "metadata": { "tags": [] @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "51f1d76a-f815-460f-a884-097fe3ea81ac", "metadata": {}, "outputs": [], @@ -54,60 +54,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "922990b6-24e9-4fa0-977b-6577f9752d84", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0d49371132174eb4a9501964b4584d67", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushalk/repos/fastplotlib/fastplotlib/layouts/_base.py:214: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a94246496c054599bc44a0a77ea7d58e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 2 columns\n", "shape = (2, 2)\n", @@ -142,16 +92,13 @@ " subplot.set_axes_visibility(True)\n", " subplot.set_grid_visibility(True)\n", "\n", - "# different perspectives on the synced views\n", - "grid_plot[1, 0].camera.scale.x = -1\n", - "grid_plot[1, 1].camera.scale.y = -1\n", "\n", "grid_plot.show()" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "7b912961-f72e-46ef-889f-c03234831059", "metadata": {}, "outputs": [], @@ -161,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c6085806-c001-4632-ab79-420b4692693a", "metadata": {}, "outputs": [], @@ -171,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "6f416825-df31-4e5d-b66b-07f23b48e7db", "metadata": {}, "outputs": [], @@ -181,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "c0fd611e-73e5-49e6-a25c-9d5b64afa5f4", "metadata": {}, "outputs": [], @@ -191,7 +138,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "cd390542-3a44-4973-8172-89e5583433bc", "metadata": {}, "outputs": [], @@ -206,6 +153,14 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc1e8581-0f6b-49d1-af7a-98920bef2eb0", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -224,7 +179,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 9eee05555..6ec76f625 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -7,8 +7,6 @@ from .features._base import cleanup_slice from pygfx import WorldObject, Group -from pygfx.linalg import Vector3 - from .features import GraphicFeature, PresentFeature, GraphicFeatureIndexable from abc import ABC, abstractmethod @@ -83,10 +81,38 @@ def _set_world_object(self, wo: WorldObject): WORLD_OBJECTS[hex(id(self))] = wo @property - def position(self) -> Vector3: + def position(self) -> np.ndarray: """The position of the graphic. You can access or change using position.x, position.y, etc.""" - return self.world_object.position + return self.world_object.world.position + + @property + def position_x(self) -> float: + return self.world_object.world.x + + @property + def position_y(self) -> float: + return self.world_object.world.y + + @property + def position_z(self) -> float: + return self.world_object.world.z + + @position.setter + def position(self, val): + self.world_object.world.position = val + + @position_x.setter + def position_x(self, val): + self.world_object.world.x = val + + @position_y.setter + def position_y(self, val): + self.world_object.world.y = val + + @position_z.setter + def position_z(self, val): + self.world_object.world.z = val @property def visible(self) -> bool: @@ -99,7 +125,7 @@ def visible(self, v: bool): self.world_object.visible = v @property - def children(self) -> WorldObject: + def children(self) -> List[WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 9ba9a4143..8773569c0 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -58,7 +58,7 @@ def add_linear_selector(self, selection: int = None, padding: float = None, **kw ) self._plot_area.add_graphic(selector, center=False) - selector.position.z = self.position.z + 1 + selector.position_z = self.position_z + 1 return weakref.proxy(selector) @@ -97,7 +97,7 @@ def add_linear_region_selector(self, padding: float = None, **kwargs) -> LinearR self._plot_area.add_graphic(selector, center=False) # so that it is above this graphic - selector.position.set_z(self.position.z + 3) + selector.position_z = self.position_z + 3 # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 9a1fb1cb6..dbfdfc40e 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -97,7 +97,7 @@ def __init__( self._set_world_object(world_object) if z_position is not None: - self.world_object.position.z = z_position + self.position_z = z_position def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: """ @@ -137,7 +137,7 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar ) self._plot_area.add_graphic(selector, center=False) - selector.position.z = self.position.z + 1 + selector.position_z = self.position_z + 1 return weakref.proxy(selector) @@ -175,7 +175,7 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear self._plot_area.add_graphic(selector, center=False) # so that it is below this graphic - selector.position.set_z(self.position.z - 1) + selector.position_z = self.position_z - 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 @@ -192,7 +192,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): axis = "x" if axis == "x": - offset = self.position.x + offset = self.position_x # x limits limits = (data[0, 0] + offset, data[-1, 0] + offset) @@ -203,13 +203,13 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_y = (data[:, 1].min() + data[:, 1].max()) / 2 # need y offset too for this - origin = (limits[0] - offset, position_y + self.position.y) + origin = (limits[0] - offset, position_y + self.position_y) # endpoints of the data range # used by linear selector but not linear region end_points = (self.data()[:, 1].min() - padding, self.data()[:, 1].max() + padding) else: - offset = self.position.y + offset = self.position_y # y limits limits = (data[0, 1] + offset, data[-1, 1] + offset) @@ -220,7 +220,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_x = (data[:, 0].min() + data[:, 0].max()) / 2 # need x offset too for this - origin = (position_x + self.position.x, limits[0] - offset) + origin = (position_x + self.position_x, limits[0] - offset) end_points = (self.data()[:, 0].min() - padding, self.data()[:, 0].max() + padding) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 3b5deac65..be223677d 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -265,7 +265,7 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar ) self._plot_area.add_graphic(selector, center=False) - selector.position.z = self.position.z + 1 + selector.position_z = self.position_z + 1 return weakref.proxy(selector) @@ -302,7 +302,7 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear ) self._plot_area.add_graphic(selector, center=False) - selector.position.set_z(self.position.z - 1) + selector.position_z = self.position_z - 1 return weakref.proxy(selector) @@ -346,7 +346,7 @@ def _get_linear_selector_init_args(self, padding, **kwargs): # a better way to get the max y value? # graphics y-position + data y-max + padding - end_points[1] = self.graphics[-1].position.y + self.graphics[-1].data()[:, 1].max() + padding + end_points[1] = self.graphics[-1].position_y + self.graphics[-1].data()[:, 1].max() + padding else: # just the biggest one if not stacked @@ -521,7 +521,11 @@ def __init__( axis_zero = 0 for i, line in enumerate(self.graphics): - getattr(line.position, f"set_{separation_axis}")(axis_zero) + if separation_axis == "x": + line.position_x = axis_zero + elif separation_axis == "y": + line.position_y = axis_zero + axis_zero = axis_zero + line.data()[:, axes[separation_axis]].max() + separation self.separation = separation diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index b53985de0..cfad8e7ce 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -79,4 +79,4 @@ def __init__( self._set_world_object(world_object) - self.world_object.position.z = z_position + self.position_z = z_position diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 165b322da..64934b6fa 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -2,7 +2,8 @@ from dataclasses import dataclass from functools import partial -from pygfx.linalg import Vector3 +import numpy as np + from pygfx import WorldObject, Line, Mesh, Points @@ -14,18 +15,18 @@ class MoveInfo: # last position for an edge, fill, or vertex in world coordinates # can be None, such as key events - last_position: Vector3 | None + last_position: Union[np.ndarray, None] # WorldObject or "key" event - source: WorldObject | str + source: Union[WorldObject, str] # key bindings used to move the selector key_bind_direction = { - "ArrowRight": Vector3(1, 0, 0), - "ArrowLeft": Vector3(-1, 0, 0), - "ArrowUp": Vector3(0, 1, 0), - "ArrowDown": Vector3(0, -1, 0), + "ArrowRight": np.array([1, 0, 0]), + "ArrowLeft": np.array([-1, 0, 0]), + "ArrowUp": np.array([0, 1, 0]), + "ArrowDown": np.array([0, -1, 0]), } @@ -65,7 +66,7 @@ def __init__( self.axis = axis # current delta in world coordinates - self.delta: Vector3 = None + self.delta: np.ndarray = None self.arrow_keys_modifier = arrow_keys_modifier # if not False, moves the slider on every render cycle @@ -135,7 +136,7 @@ def _add_plot_area_hook(self, plot_area): self._plot_area.add_animations(self._key_hold) def _check_fill_pointer_event(self, event_source: WorldObject, ev): - world_pos = self._plot_area.map_screen_to_world((ev.x, ev.y)) + world_pos = self._plot_area.map_screen_to_world(ev) # outside viewport, ignore # this shouldn't be possible since the event handler is registered to the fill mesh world object # but I like sanity checks anyways @@ -147,10 +148,10 @@ def _check_fill_pointer_event(self, event_source: WorldObject, ev): xmin, ymin, zmin = bbox[0] xmax, ymax, zmax = bbox[1] - if not (xmin <= world_pos.x <= xmax): + if not (xmin <= world_pos[0] <= xmax): return - if not (ymin <= world_pos.y <= ymax): + if not (ymin <= world_pos[1] <= ymax): return self._move_start(event_source, ev) @@ -170,7 +171,7 @@ def _move_start(self, event_source: WorldObject, ev): pygfx ``Event`` """ - last_position = self._plot_area.map_screen_to_world((ev.x, ev.y)) + last_position = self._plot_area.map_screen_to_world(ev) self._move_info = MoveInfo( last_position=last_position, @@ -196,15 +197,14 @@ def _move(self, ev): self._plot_area.controller.enabled = False # get pointer current world position - pointer_pos_screen = (ev.x, ev.y) - world_pos = self._plot_area.map_screen_to_world(pointer_pos_screen) + world_pos = self._plot_area.map_screen_to_world(ev) # outside this viewport if world_pos is None: return # compute the delta - self.delta = world_pos.clone().sub(self._move_info.last_position) + self.delta = world_pos - self._move_info.last_position self._pygfx_event = ev self._move_graphic(self.delta) @@ -214,7 +214,7 @@ def _move(self, ev): self._plot_area.controller.enabled = True - def _move_graphic(self, delta): + def _move_graphic(self, delta: np.ndarray): raise NotImplementedError("Must be implemented in subclass") def _move_end(self, ev): @@ -225,26 +225,25 @@ def _move_to_pointer(self, ev): """ Calculates delta just using current world object position and calls self._move_graphic(). """ - current_position = self.world_object.position.clone() + current_position: np.ndarray = self.position # middle mouse button clicks if ev.button != 3: return - click_pos = (ev.x, ev.y) - world_pos = self._plot_area.map_screen_to_world(click_pos) + world_pos = self._plot_area.map_screen_to_world(ev) # outside this viewport if world_pos is None: return - self.delta = world_pos.clone().sub(current_position) + self.delta = world_pos - current_position self._pygfx_event = ev - # use fill by default as the source + # use fill by default as the source, such as in region selectors if len(self._fill) > 0: self._move_info = MoveInfo(last_position=current_position, source=self._fill[0]) - # else use an edge + # else use an edge, such as for linear selector else: self._move_info = MoveInfo(last_position=current_position, source=self._edges[0]) @@ -275,7 +274,7 @@ def _toggle_arrow_key_moveable(self, ev): def _key_hold(self): if self._key_move_value and self.arrow_key_events_enabled: # direction vector * step - delta = key_bind_direction[self._key_move_value].clone().multiply_scalar(self.step) + delta = key_bind_direction[self._key_move_value] * self.step # set event source # use fill by default as the source diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index cb3b7ec27..55b64b62f 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -4,7 +4,6 @@ import numpy as np import pygfx -from pygfx.linalg import Vector3 try: import ipywidgets @@ -45,9 +44,9 @@ def _set(self, value: float): return if self.axis == "x": - self._parent.position.x = value + self._parent.position_x = value else: - self._parent.position.y = value + self._parent.position_y = value self._data = value self._feature_changed(key=None, new_data=value) @@ -189,7 +188,7 @@ def __init__( material=material(thickness=thickness + 6, color=self.colors_outer) ) - line_inner.position.z = self.line_outer.position.z + 1 + line_inner.world.z = self.line_outer.world.z + 1 world_object = pygfx.Group() @@ -200,9 +199,9 @@ def __init__( # set x or y position if axis == "x": - self.position.x = selection + self.position_x = selection else: - self.position.y = selection + self.position_y = selection self.selection = LinearSelectionFeature(self, axis=axis, value=selection, limits=limits) @@ -322,10 +321,10 @@ def _get_selected_index(self, graphic): # the array to search for the closest value along that axis if self.axis == "x": geo_positions = graphic.data()[:, 0] - offset = getattr(graphic.position, self.axis) + offset = getattr(graphic, f"position_{self.axis}") else: geo_positions = graphic.data()[:, 1] - offset = getattr(graphic.position, self.axis) + offset = getattr(graphic, f"position_{self.axis}") if "Line" in graphic.__class__.__name__: # we want to find the index of the geometry position that is closest to the slider's geometry position @@ -344,18 +343,18 @@ def _get_selected_index(self, graphic): index = self.selection() - offset return int(index) - def _move_graphic(self, delta: Vector3): + def _move_graphic(self, delta: np.ndarray): """ Moves the graphic Parameters ---------- - delta: Vector3 + delta: np.ndarray delta in world space """ if self.axis == "x": - self.selection = self.selection() + delta.x + self.selection = self.selection() + delta[0] else: - self.selection = self.selection() + delta.y + self.selection = self.selection() + delta[1] diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 142f40677..30f223fad 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -2,7 +2,6 @@ import numpy as np import pygfx -from pygfx.linalg import Vector3 from .._base import Graphic, GraphicCollection from ..features._base import GraphicFeature, FeatureEvent @@ -234,7 +233,7 @@ def __init__( # the fill of the selection self.fill = mesh - self.fill.position.set(*origin, -2) + self.fill.world.position = (*origin, -2) self.world_object.add(self.fill) @@ -296,7 +295,7 @@ def __init__( # add the edge lines for edge in self.edges: - edge.position.set_z(-1) + edge.world.z = -1 self.world_object.add(edge) # set the initial bounds of the selector @@ -400,7 +399,7 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis source = self._get_source(graphic) # if the graphic position is not at (0, 0) then the bounds must be offset - offset = getattr(source.position, self.bounds.axis) + offset = getattr(source, f"position_{self.bounds.axis}") offset_bounds = tuple(v - offset for v in self.bounds()) # need them to be int to use as indices @@ -435,26 +434,31 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis ixs = np.arange(*self.bounds(), dtype=int) return ixs - def _move_graphic(self, delta): + def _move_graphic(self, delta: np.ndarray): + # add delta to current bounds to get new positions if self.bounds.axis == "x": + # min and max of current bounds, i.e. the edges + xmin, xmax = self.bounds() + # new left bound position - bound_pos_0 = Vector3(self.bounds()[0]).add(delta) + bound0_new = xmin + delta[0] # new right bound position - bound_pos_1 = Vector3(self.bounds()[1]).add(delta) + bound1_new = xmax + delta[0] else: + # min and max of current bounds, i.e. the edges + ymin, ymax = self.bounds() + # new bottom bound position - bound_pos_0 = Vector3(0, self.bounds()[0]).add(delta) + bound0_new = ymin + delta[1] # new top bound position - bound_pos_1 = Vector3(0, self.bounds()[1]).add(delta) + bound1_new = ymax + delta[1] # move entire selector if source was fill if self._move_info.source == self.fill: - bound0 = getattr(bound_pos_0, self.bounds.axis) - bound1 = getattr(bound_pos_1, self.bounds.axis) # set the new bounds - self.bounds = (bound0, bound1) + self.bounds = (bound0_new, bound1_new) return # if selector is not resizable do nothing @@ -464,15 +468,10 @@ def _move_graphic(self, delta): # if resizable, move edges if self._move_info.source == self.edges[0]: # change only left or bottom bound - bound0 = getattr(bound_pos_0, self.bounds.axis) - bound1 = self.bounds()[1] + self.bounds = (bound0_new, self.bounds()[1]) elif self._move_info.source == self.edges[1]: # change only right or top bound - bound0 = self.bounds()[0] - bound1 = getattr(bound_pos_1, self.bounds.axis) + self.bounds = (self.bounds()[0], bound1_new) else: return - - # set the new bounds - self.bounds = (bound0, bound1) diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index 6332f9bcc..7065abe2d 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -2,7 +2,6 @@ import numpy as np import pygfx -from pygfx.linalg import Vector3 from .._base import Graphic, GraphicCollection from ..features._base import GraphicFeature, FeatureEvent diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 8225bb300..42bc3dba8 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -39,13 +39,13 @@ def __init__( super(TextGraphic, self).__init__(name=name) world_object = pygfx.Text( - pygfx.TextGeometry(text=text, font_size=size, screen_space=False), + pygfx.TextGeometry(text=str(text), font_size=size, screen_space=False), pygfx.TextMaterial(color=face_color, outline_color=outline_color, outline_thickness=outline_thickness) ) self._set_world_object(world_object) - self.world_object.position.set(*position) + self.world_object.position = position self.name = None diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index 1571f51e9..f3781a4e7 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -3,9 +3,10 @@ import numpy as np +import pygfx from pygfx import Scene, OrthographicCamera, PerspectiveCamera, PanZoomController, OrbitController, \ Viewport, WgpuRenderer -from pygfx.linalg import Vector3 +from pylinalg import vec_transform, vec_unproject from wgpu.gui.auto import WgpuCanvas from ..graphics._base import Graphic, GraphicCollection @@ -158,16 +159,18 @@ def get_rect(self) -> Tuple[float, float, float, float]: """allows setting the region occupied by the viewport w.r.t. the parent""" raise NotImplementedError("Must be implemented in subclass") - def map_screen_to_world(self, pos: Tuple[float, float]) -> Vector3: + def map_screen_to_world(self, pos: Union[Tuple[float, float], pygfx.PointerEvent]) -> np.ndarray: """ Map screen position to world position Parameters ---------- - pos: (float, float) - (x, y) screen coordinates + pos: (float, float) | pygfx.PointerEvent + ``(x, y)`` screen coordinates, or ``pygfx.PointerEvent`` """ + if isinstance(pos, pygfx.PointerEvent): + pos = pos.x, pos.y if not self.viewport.is_inside(*pos): return None @@ -181,16 +184,18 @@ def map_screen_to_world(self, pos: Tuple[float, float]) -> Vector3: ) # convert screen position to NDC - pos_ndc = Vector3( + pos_ndc = ( pos_rel[0] / vs[0] * 2 - 1, -(pos_rel[1] / vs[1] * 2 - 1), 0 ) # get world position - pos_world = self.camera.position.clone().project(self.camera).add(pos_ndc).unproject(self.camera) + pos_ndc += vec_transform(self.camera.world.position, self.camera.camera_matrix) + pos_world = vec_unproject(pos_ndc[:2], self.camera.camera_matrix) - return pos_world + # default z is zero for now + return np.array([*pos_world[:2], 0]) def set_viewport_rect(self, *args): self.viewport.rect = self.get_rect() diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index c121e42f2..734d004d4 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -385,7 +385,7 @@ def maintain_aspect(self, obj): def flip_camera(self, obj): current = self.current_subplot - current.camera.scale.y = -1 * current.camera.scale.y + current.camera.world.scale_y *= -1 def update_current_subplot(self, ev): for subplot in self.plot: diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 1f3da61d9..0ce84ddcd 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -151,9 +151,9 @@ def center_title(self): if self._title_graphic is None: raise AttributeError("No title graphic is set") - self._title_graphic.world_object.position.set(0, 0, 0) + self._title_graphic.world_object.position = (0, 0, 0) self.docked_viewports["top"].center_graphic(self._title_graphic, zoom=1.5) - self._title_graphic.world_object.position.y = -3.5 + self._title_graphic.world_object.position_y = -3.5 def get_rect(self): """Returns the bounding box that defines the Subplot within the canvas.""" @@ -260,7 +260,7 @@ def remove_animation(self, func): def add_graphic(self, graphic, center: bool = True): """Adds a Graphic to the subplot.""" - graphic.world_object.position.z = len(self._graphics) + graphic.position_z = len(self._graphics) super(Subplot, self).add_graphic(graphic, center) def set_axes_visibility(self, visible: bool): diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 105dbfb96..6f8e1bc44 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -184,5 +184,4 @@ def maintain_aspect(self, obj): self.plot.camera.maintain_aspect = self.maintain_aspect_button.value def flip_camera(self, obj): - self.plot.camera.scale.y = -1 * self.plot.camera.scale.y - \ No newline at end of file + self.plot.camera.world.scale_y *= -1 From a75cbfdf2163a0691ba8bf4e06e662b0890b91d3 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 24 May 2023 00:38:50 -0400 Subject: [PATCH 33/96] image selector and heatmap linalg fixes (#204) --- fastplotlib/graphics/image.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 8773569c0..5adfde338 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -122,7 +122,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): padding = int(data.shape[1] * 0.15) if axis == "x": - offset = self.position.x + offset = self.position_x # x limits, number of columns limits = (offset, data.shape[1]) @@ -135,14 +135,14 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_y = data.shape[0] / 2 # need y offset too for this - origin = (limits[0] - offset, position_y + self.position.y) + origin = (limits[0] - offset, position_y + self.position_y) # endpoints of the data range # used by linear selector but not linear region # padding, n_rows + padding end_points = (0 - padding, data.shape[0] + padding) else: - offset = self.position.y + offset = self.position_y # y limits limits = (offset, data.shape[0]) @@ -154,7 +154,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): position_x = data.shape[1] / 2 # need x offset too for this - origin = (position_x + self.position.x, limits[0] - offset) + origin = (position_x + self.position_x, limits[0] - offset) # endpoints of the data range # used by linear selector but not linear region @@ -463,8 +463,8 @@ def __init__( img.row_chunk_index = chunk[0] img.col_chunk_index = chunk[1] - img.position.set_x(x_pos) - img.position.set_y(y_pos) + img.position_x = x_pos + img.position_y = y_pos self.world_object.add(img) From 6712c41293107f80cdcd32eb080802df1261770d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 24 May 2023 01:25:40 -0400 Subject: [PATCH 34/96] add selector performance example nb --- examples/selector_performance.ipynb | 211 ++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 examples/selector_performance.ipynb diff --git a/examples/selector_performance.ipynb b/examples/selector_performance.ipynb new file mode 100644 index 000000000..ccc9196cf --- /dev/null +++ b/examples/selector_performance.ipynb @@ -0,0 +1,211 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "c32d3bd2-4839-4ed1-ae6b-0a99c768d566", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from typing import *\n", + "from itertools import product\n", + "\n", + "import numpy as np\n", + "import fastplotlib as fpl\n", + "import pygfx" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67602111-9b99-461a-bfe3-b5688bcd3228", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def make_circle(center: Tuple[float, float], radius: float, n_points: int = 50) -> np.ndarray:\n", + " theta = np.linspace(0, 2 * np.pi, n_points)\n", + " xs = radius * np.sin(theta)\n", + " ys = radius * np.cos(theta)\n", + " \n", + " return np.column_stack([xs, ys]) + center" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65989bfb-472e-42e6-8788-9d1db3e37d6d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spatial_dims = (1000, 1000)\n", + "\n", + "circles = list()\n", + "for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)):\n", + " circles.append(make_circle(center, 5, n_points=75))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "794052b3-14d3-4d88-a4b7-f589f0e50765", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "len(circles)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a08a7e80-196f-4dd8-aba6-7a795fac2683", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "temporal = list()\n", + "\n", + "xs = np.arange(0, 10_000)\n", + "for i in range(len(circles)):\n", + " if i % 2 == 0:\n", + " ys = np.sin(xs) * 10\n", + " else:\n", + " ys = np.cos(xs) * 10\n", + " \n", + " temporal.append(ys)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "942956b8-dd24-4d64-920b-e374c7892fb4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "temporal = np.vstack(temporal)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "194e0147-8ffe-4bf5-be20-728527b2f766", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot = fpl.GridPlot((1, 2))\n", + "\n", + "contours = plot[0, 0].add_line(np.vstack(circles), thickness=3)\n", + "heatmap = plot[0, 1].add_heatmap(temporal)\n", + "selector = heatmap.add_linear_region_selector(axis=\"y\")\n", + "\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a7e2bac-61d4-4191-92e4-9b99bb83e037", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "zero_alpha_ixs = list()\n", + "\n", + "start_offset = 0\n", + "for c in circles:\n", + " start_offset += c.shape[0]\n", + " zero_alpha_ixs += [start_offset - 1, start_offset]\n", + " \n", + "zero_alpha_ixs = zero_alpha_ixs[:-1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9d04e28-556f-4b58-84fd-5799257bf9bb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "contours.colors[np.array(zero_alpha_ixs)] = [1, 1, 1, 0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6754109-eff5-4cfd-8a2a-622c85f0a42c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "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", + " for i in ixs_visible:\n", + " contours.world_object.geometry.colors.data[(i * 75) + 1:(i * 75) + 74, -1] = 1\n", + " \n", + " for i in ixs_hide:\n", + " contours.world_object.geometry.colors.data[(i * 75) + 1:(i * 75) + 74, -1] = 0\n", + " \n", + " contours.world_object.geometry.colors.update_range()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d409b0a-32c3-4690-b181-648fa4b6101e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "selector.bounds.add_event_handler(set_visible_alpha)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "907e6c81-8ffc-475b-9a5f-2b2f9fc21916", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b4801a1e621557204234b5382388c505d2f89079 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 25 May 2023 19:14:29 -0400 Subject: [PATCH 35/96] Docs build fix (#210) * Update .readthedocs.yaml, install pygfx from main * Update .readthedocs.yaml * Update .readthedocs.yaml --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1de8fcd91..ae7598f41 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,6 +10,9 @@ build: - libxcb-xfixes0-dev - mesa-vulkan-drivers - libglfw3 + jobs: + pre_install: + - pip install git+https://github.com/pygfx/pygfx.git@main sphinx: configuration: docs/source/conf.py From 298f3878a3803135ea5eb416e550041c0319595b Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 26 May 2023 00:09:55 -0400 Subject: [PATCH 36/96] all feature docs (#212) * add all feature docs --- docs/source/api/graphic_features.rst | 70 ++++++++++++++++++++- fastplotlib/graphics/features/_base.py | 19 +++--- fastplotlib/graphics/features/_colors.py | 36 ++++++++++- fastplotlib/graphics/features/_present.py | 15 ++++- fastplotlib/graphics/features/_thickness.py | 13 +++- fastplotlib/graphics/image.py | 26 ++++++-- fastplotlib/graphics/line.py | 16 ++++- fastplotlib/graphics/line_collection.py | 65 +++++++++++++++++++ fastplotlib/graphics/scatter.py | 14 +++++ fastplotlib/graphics/selectors/_linear.py | 2 +- 10 files changed, 255 insertions(+), 21 deletions(-) diff --git a/docs/source/api/graphic_features.rst b/docs/source/api/graphic_features.rst index 449eaf297..2fe60ce24 100644 --- a/docs/source/api/graphic_features.rst +++ b/docs/source/api/graphic_features.rst @@ -1,5 +1,10 @@ -Features -******** +.. _api_graphic_features: + +Graphic Features +**************** + +Image +##### .. autoclass:: fastplotlib.graphics.features.ImageDataFeature :members: @@ -13,3 +18,64 @@ Features :exclude-members: __init__ :no-undoc-members: +Heatmap +####### + +.. autoclass:: fastplotlib.graphics.features.HeatmapDataFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +.. autoclass:: fastplotlib.graphics.features.HeatmapCmapFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +Line +#### + +.. autoclass:: fastplotlib.graphics.features.PositionsDataFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +.. autoclass:: fastplotlib.graphics.features.ColorsFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +.. autoclass:: fastplotlib.graphics.features.ThicknessFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +Scatter +####### + +.. autoclass:: fastplotlib.graphics.features.PositionsDataFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +.. autoclass:: fastplotlib.graphics.features.ColorsFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: + +Common +###### + +Features common to all graphics + +.. autoclass:: fastplotlib.graphics.features.PresentFeature + :members: + :inherited-members: + :exclude-members: __init__ + :no-undoc-members: diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 57cd15a1d..82fa2847c 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -48,12 +48,14 @@ class FeatureEvent: type: str, example "colors" pick_info: dict in the form: - ============== ======================================================================= - index indices where feature data was changed, ``range`` object or List[int] - ============== ======================================================================= - world_object world object the feature belongs to - new_data the new data for this feature - ============== ======================================================================= + + ============== ============================================================================= + key value + ============== ============================================================================= + "index" indices where feature data was changed, ``range`` object or ``List[int]`` + "world_object" world object the feature belongs to + "new_data: the new data for this feature + ============== ============================================================================= """ def __init__(self, type: str, pick_info: dict): @@ -105,8 +107,8 @@ def add_event_handler(self, handler: callable): """ Add an event handler. All added event handlers are called when this feature changes. The `handler` can optionally accept ``FeatureEvent`` as the first and only argument. - The ``FeatureEvent`` only has two attributes, `type` which denotes the type of event - as a str in the form of "-changed", such as "color-changed". + The ``FeatureEvent`` only has two attributes, ``type`` which denotes the type of event + as a ``str`` in the form of "", such as "color". Parameters ---------- @@ -289,6 +291,7 @@ def _update_range(self, key): @property @abstractmethod def buffer(self) -> Union[Buffer, Texture]: + """Underlying buffer for this feature""" pass @property diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index 5ff82ca72..d607d6397 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -6,6 +6,22 @@ class ColorFeature(GraphicFeatureIndexable): + """ + Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` + + **event pick info:** + + ==================== =============================== ========================================================================= + key type description + ==================== =============================== ========================================================================= + "index" ``numpy.ndarray`` or ``None`` changed indices in the buffer + "new_data" ``numpy.ndarray`` or ``None`` new buffer data at the changed indices + "collection-index" int the index of the graphic within the collection that triggered the event + "world_object" pygfx.WorldObject world object + ==================== =============================== ========================================================================= + + + """ @property def buffer(self): return self._parent.world_object.geometry.colors @@ -204,6 +220,8 @@ def _feature_changed(self, key, new_data): class CmapFeature(ColorFeature): """ Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. + + Same event pick info as :class:`ColorFeature` """ def __init__(self, parent, colors): super(ColorFeature, self).__init__(parent, colors) @@ -227,7 +245,19 @@ def __setitem__(self, key, value): class ImageCmapFeature(GraphicFeature): """ - Colormap for ImageGraphic + Colormap for :class:`ImageGraphic` + + **event pick info:** + + ================ =================== =============== + key type description + ================ =================== =============== + "index" ``None`` not used + "new_data" ``str`` colormap name + "world_object" pygfx.WorldObject world object + ================ =================== =============== + + """ def __init__(self, parent, cmap: str): cmap_texture_view = get_cmap_texture(cmap) @@ -257,7 +287,9 @@ def _feature_changed(self, key, new_data): class HeatmapCmapFeature(ImageCmapFeature): """ - Colormap for HeatmapGraphic + Colormap for :class:`HeatmapGraphic` + + Same event pick info as :class:`ImageCmapFeature` """ def _set(self, cmap_name: str): diff --git a/fastplotlib/graphics/features/_present.py b/fastplotlib/graphics/features/_present.py index 9bd439157..820c1d123 100644 --- a/fastplotlib/graphics/features/_present.py +++ b/fastplotlib/graphics/features/_present.py @@ -4,9 +4,20 @@ class PresentFeature(GraphicFeature): """ - Toggles if the object is present in the scene, different from visible \n + Toggles if the object is present in the scene, different from visible. Useful for computing bounding boxes from the Scene to only include graphics - that are present + that are present. + + **event pick info:** + + ==================== ======================== ========================================================================= + key type description + ==================== ======================== ========================================================================= + "index" ``None`` not used + "new_data" ``bool`` new data, ``True`` or ``False`` + "collection-index" int the index of the graphic within the collection that triggered the event + "world_object" pygfx.WorldObject world object + ==================== ======================== ======================================================================== """ def __init__(self, parent, present: bool = True, collection_index: int = False): self._scene = None diff --git a/fastplotlib/graphics/features/_thickness.py b/fastplotlib/graphics/features/_thickness.py index 5c7c3e00b..ce9c3cbc4 100644 --- a/fastplotlib/graphics/features/_thickness.py +++ b/fastplotlib/graphics/features/_thickness.py @@ -3,7 +3,18 @@ class ThicknessFeature(GraphicFeature): """ - Used by Line graphics for line material thickness + Used by Line graphics for line material thickness. + + **event pick info:** + + ===================== ======================== ========================================================================= + key type description + ==================== ======================== ========================================================================= + "index" ``None`` not used + "new_data" ``float`` new thickness value + "collection-index" int the index of the graphic within the collection that triggered the event + "world_object" pygfx.WorldObject world object + ==================== ======================== ======================================================================== """ def __init__(self, parent, thickness: float): self._scene = None diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 5adfde338..a5318a330 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -226,6 +226,7 @@ def __init__( **present**: :class:`.PresentFeature` Control the presence of the Graphic in the scene + Examples -------- .. code-block:: python @@ -396,6 +397,20 @@ def __init__( kwargs: additional keyword arguments passed to Graphic + + Features + -------- + + **data**: :class:`.HeatmapDataFeature` + Manages the data buffer displayed in the HeatmapGraphic + + **cmap**: :class:`.HeatmapCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + + Examples -------- .. code-block:: python @@ -403,10 +418,13 @@ def __init__( from fastplotlib import Plot # create a `Plot` instance plot = Plot() - # make some random 2D image data - data = np.random.rand(512, 512) - # plot the image data - plot.add_image(data=data) + + # make some random 2D heatmap data + data = np.random.rand(10_000, 8_000) + + # add a heatmap + plot.add_heatmap(data=data) + # show the plot plot.show() """ diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index dbfdfc40e..1d9db6d58 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -43,7 +43,7 @@ def __init__( thickness of the line colors: str, array, or iterable, default "w" - specify colors as a single human readable string, a single RGBA array, + specify colors as a single human-readable string, a single RGBA array, or an iterable of strings or RGBA arrays cmap: str, optional @@ -62,6 +62,20 @@ def __init__( kwargs passed to Graphic + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. + ex: ``scatter.data[:, 0] = 5```, ``scatter.data[xs > 5] = 3`` + + **colors**: :class:`.ColorFeature` + Manages the color buffer, allows regular and fancy indexing. + ex: ``scatter.data[:, 1] = 0.5``, ``scatter.colors[xs > 5] = "cyan"`` + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene, set to ``True`` or ``False`` + """ self.data = PointsDataFeature(self, data, collection_index=collection_index) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index be223677d..80a66e6bd 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -76,40 +76,76 @@ def __init__( kwargs passed to GraphicCollection + + Features + -------- + + Collections support the same features as the underlying graphic. You just have to slice the selection. + + .. code-block:: python + + # slice only the collection + line_collection[10:20].colors = "blue" + + # slice the collection and a feature + line_collection[20:30].colors[10:30] = "red" + + # the data feature also works like this + + See :class:`LineGraphic` details on the features. + + Examples -------- .. code-block:: python from fastplotlib import Plot from fastplotlib.graphics import LineCollection + # creating data for sine and cosine waves xs = np.linspace(-10, 10, 100) ys = np.sin(xs) + sine = np.dstack([xs, ys])[0] + ys = np.sin(xs) + 10 sine2 = np.dstack([xs, ys])[0] + ys = np.cos(xs) + 5 cosine = np.dstack([xs, ys])[0] + # creating plot plot = Plot() + # creating a line collection using the sine and cosine wave data line_collection = LineCollection(data=[sine, cosine, sine2], cmap=["Oranges", "Blues", "Reds"], thickness=20.0) + # add graphic to plot plot.add_graphic(line_collection) + # show plot plot.show() + # change the color of the sine wave to white line_collection[0].colors = "w" + # change certain color indexes of the cosine data to red line_collection[1].colors[0:15] = "r" + # toggle presence of sine2 and rescale graphics line_collection[2].present = False + plot.autoscale() + line_collection[2].present = True + plot.autoscale() + # can also do slicing line_collection[1:].colors[35:70] = "magenta" + """ + super(LineCollection, self).__init__(name) if not isinstance(z_position, float) and z_position is not None: @@ -481,30 +517,59 @@ def __init__( kwargs passed to LineCollection + + Features + -------- + + Collections support the same features as the underlying graphic. You just have to slice the selection. + + .. code-block:: python + + # slice only the collection + line_collection[10:20].colors = "blue" + + # slice the collection and a feature + line_collection[20:30].colors[10:30] = "red" + + # the data feature also works like this + + See :class:`LineGraphic` details on the features. + + Examples -------- .. code-block:: python from fastplotlib import Plot from fastplotlib.graphics import LineStack + # create plot plot = Plot() + # create line data xs = np.linspace(-10, 10, 100) ys = np.sin(xs) + sine = np.dstack([xs, ys])[0] + ys = np.sin(xs) cosine = np.dstack([xs, ys])[0] + # create line stack line_stack = LineStack(data=[sine, cosine], cmap=["Oranges", "Blues"], thickness=20.0, separation=5.0) + # add graphic to plot plot.add_graphic(line_stack) + # show plot plot.show() + # change the color of the sine wave to white line_stack[0].colors = "w" + # change certain color indexes of the cosine data to red line_stack[1].colors[0:15] = "r" + # more slicing line_stack[0].colors[35:70] = "magenta" diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index cfad8e7ce..5556b1de2 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -51,6 +51,20 @@ def __init__( kwargs passed to Graphic + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the scatter [x, y, z] positions data buffer, allows regular and fancy indexing. + ex: ``scatter.data[:, 0] = 5```, ``scatter.data[xs > 5] = 3`` + + **colors**: :class:`.ColorFeature` + Manages the color buffer, allows regular and fancy indexing. + ex: ``scatter.data[:, 1] = 0.5``, ``scatter.colors[xs > 5] = "cyan"`` + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene, set to ``True`` or ``False`` + """ self.data = PointsDataFeature(self, data) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 55b64b62f..8899f03b1 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -21,7 +21,7 @@ class LinearSelectionFeature(GraphicFeature): """ Manages the slider selection and callbacks - **pick info** + **event pick info** ================== ================================================================ key selection From c3389640a60e1984c8405e14eb13732555a7ac15 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 26 May 2023 22:55:45 -0400 Subject: [PATCH 37/96] Create examples README --- examples/README | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 examples/README diff --git a/examples/README b/examples/README new file mode 100644 index 000000000..73987cb02 --- /dev/null +++ b/examples/README @@ -0,0 +1,4 @@ +# Examples + +**IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi`, i.e. `pip install pygfx`, you will need to use the examples from the following commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples .** +The current examples will work if you installed `fastplotlib` and `pygfx` directly from github. Both `fastplotlib` and `pygfx` are rapidly evolving libraries, and we try to closely track `pygfx`. From 9f7c0da69308d397d2793cb0c585f45b6d9fdb37 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 26 May 2023 22:56:04 -0400 Subject: [PATCH 38/96] Update README --- examples/README | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/README b/examples/README index 73987cb02..e89153d54 100644 --- a/examples/README +++ b/examples/README @@ -1,4 +1,6 @@ # Examples **IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi`, i.e. `pip install pygfx`, you will need to use the examples from the following commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples .** -The current examples will work if you installed `fastplotlib` and `pygfx` directly from github. Both `fastplotlib` and `pygfx` are rapidly evolving libraries, and we try to closely track `pygfx`. +The current examples will work if you installed `fastplotlib` and `pygfx` directly from github.** + +Both `fastplotlib` and `pygfx` are rapidly evolving libraries, and we try to closely track `pygfx`. From 3c59b34bc17fc7347a3ebab839eca0702b85a63a Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 26 May 2023 22:56:33 -0400 Subject: [PATCH 39/96] Update and rename README to README.md --- examples/{README => README.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename examples/{README => README.md} (83%) diff --git a/examples/README b/examples/README.md similarity index 83% rename from examples/README rename to examples/README.md index e89153d54..1b79c879b 100644 --- a/examples/README +++ b/examples/README.md @@ -1,6 +1,6 @@ # Examples -**IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi`, i.e. `pip install pygfx`, you will need to use the examples from the following commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples .** -The current examples will work if you installed `fastplotlib` and `pygfx` directly from github.** +**IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi`, i.e. `pip install pygfx`, you will need to use the examples from the following commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples . +The current examples will work if you installed `fastplotlib` and `pygfx` directly from github** Both `fastplotlib` and `pygfx` are rapidly evolving libraries, and we try to closely track `pygfx`. From d2c23c8fa553f89a4ce66e6bcaeea6b08072d7d8 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:58:49 -0400 Subject: [PATCH 40/96] finish buttons for toolbar (#207) * working on toolbar * toolbar updates * basic toolbar for plot implemented * base logic in notebook for gridplot toolbar * gp toolbar working when subplots have names & mix names/no names * initial changes to simple plot toolbar * updates to plot toolbar, still need to refactor into separate file * fixed gridplot toolbar, still need to move to separate class * add flip button * adding click event handler for gp toolbar dropdown options * keep new clicked plot up-to-date with toggle button values * requested changes * adding record button * record button * silly miss in merge conflict resolve * wow I am struggling this am * imagewidget toolbar for single plot iw * toolbar for iw with gridplot * requested changes * update flip icon * toolbar updates * changes to linear selector event handler removal * Update fastplotlib/widgets/image.py * simplify toolbar stuff in show(), simplify image widget toolbar --------- Co-authored-by: Kushal Kolar --- examples/buttons.ipynb | 228 ++++++++++++++---- fastplotlib/graphics/features/_base.py | 10 +- fastplotlib/graphics/features/_colors.py | 3 + .../graphics/selectors/_base_selector.py | 8 +- .../graphics/selectors/_linear_region.py | 7 +- fastplotlib/layouts/_base.py | 3 + fastplotlib/layouts/_gridplot.py | 46 ++-- fastplotlib/layouts/_toolbar.py | 1 - fastplotlib/plot.py | 43 ++-- fastplotlib/widgets/image.py | 81 ++++++- 10 files changed, 333 insertions(+), 97 deletions(-) delete mode 100644 fastplotlib/layouts/_toolbar.py diff --git a/examples/buttons.ipynb b/examples/buttons.ipynb index 60419a229..b46e09d5f 100644 --- a/examples/buttons.ipynb +++ b/examples/buttons.ipynb @@ -4,11 +4,13 @@ "cell_type": "code", "execution_count": 1, "id": "6725ce7d-eea7-44f7-bedc-813e8ce5bf4f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "from fastplotlib import Plot\n", - "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button\n", + "from fastplotlib import Plot, GridPlot, ImageWidget\n", + "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button, Dropdown\n", "import numpy as np" ] }, @@ -16,12 +18,14 @@ "cell_type": "code", "execution_count": 2, "id": "33bf59c4-14e5-43a8-8a16-69b6859864c5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "95be7ab3326347359f783946ec8d9339", + "model_id": "5039947949ac4d5da76e561e082da8c2", "version_major": 2, "version_minor": 0 }, @@ -36,14 +40,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:33: UserWarning: converting float64 array to float32\n", + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", " warn(f\"converting {array.dtype} array to float32\")\n" ] }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -64,12 +68,14 @@ "cell_type": "code", "execution_count": 3, "id": "68ea8011-d6fd-448f-9bf6-34073164d271", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cecbc6a1fec54d03876ac5a8609e5200", + "model_id": "5819995040a04300b5ccf10abac8b69e", "version_major": 2, "version_minor": 0 }, @@ -89,24 +95,15 @@ { "cell_type": "code", "execution_count": 4, - "id": "0bbb459c-cb49-448e-b0b8-c541e55da313", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import GridPlot\n", - "from ipywidgets import Dropdown" - ] - }, - { - "cell_type": "code", - "execution_count": 5, "id": "91a31531-818b-46a2-9587-5d9ef5b59b93", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "05c6d9d2f62846e8ab6135a3d218964c", + "model_id": "2f318829d0a4419798a008c5fe2d6677", "version_major": 2, "version_minor": 0 }, @@ -126,9 +123,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "e96bbda7-3693-42f2-bd52-f668f39134f6", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img = np.random.rand(512,512)\n", @@ -138,14 +137,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "03b877ba-cf9c-47d9-a0e5-b3e694274a28", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9b792858ff24411db756810bb8eea00f", + "model_id": "ff5064077bb944c6a5248f135f052668", "version_major": 2, "version_minor": 0 }, @@ -153,7 +154,7 @@ "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -162,16 +163,63 @@ "gp.show()" ] }, + { + "cell_type": "code", + "execution_count": 7, + "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f21de0c607b24fd281364a7bec8ad837", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gp2 = GridPlot(shape=(1,2))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for subplot in gp2:\n", + " subplot.add_image(data=img)" + ] + }, { "cell_type": "code", "execution_count": 9, - "id": "afc0cd52-fb24-4561-9876-50fbdf784502", - "metadata": {}, + "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fcb816a9aaab42b3a7a7e443607ad127", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "(0, 1)" + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" ] }, "execution_count": 9, @@ -180,19 +228,33 @@ } ], "source": [ - "gp[0,1].position" + "gp2.show()" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", - "metadata": {}, + "execution_count": 2, + "id": "3958829a-1a2b-4aa2-8c9d-408dce9ccf30", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data = np.random.rand(500, 512, 512)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9f60c0b0-d0ee-4ea1-b961-708aff3b91ae", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f902391ceb614f2a812725371184ce81", + "model_id": "433485ec12b44aa68082a93c264e613c", "version_major": 2, "version_minor": 0 }, @@ -202,53 +264,115 @@ }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] } ], "source": [ - "gp2 = GridPlot(shape=(1,2))" + "iw = ImageWidget(data=data, vmin_vmax_sliders=True)" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "346f241c-4bd0-4f90-afd5-3fa617d96dad", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a1bdd59532f240948d33ae440d61c4a5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "for subplot in gp2:\n", - " subplot.add_image(data=img)" + "iw.show()" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", - "metadata": {}, + "execution_count": 5, + "id": "0563ddfb-8fd3-4a99-bcee-3a83ae5d0f32", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "65a2a30036f44e22a479c6edd215e25a", + "model_id": "79e924078e7f4cf69be71eaf12a94854", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "iw2 = ImageWidget(data=[data, data, data, data], grid_plot_kwargs={'controllers': np.array([[0, 1], [2, 3]])}, slider_dims=\"t\", vmin_vmax_sliders=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d7fb94f8-825f-4447-b161-c9dafa1a068a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bb3b7dd92b4d4f74ace4adfbca35aaf3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" ] }, - "execution_count": 10, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "gp2.show()" + "iw2.show()" ] }, { "cell_type": "code", "execution_count": null, - "id": "cd3e7b3c-f4d2-44c7-931c-d172c7bdad36", + "id": "3743219d-6702-468a-bea6-0e4c4549e9e4", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68bafb86-db5a-4681-8176-37ec72ce04a8", "metadata": {}, "outputs": [], "source": [] @@ -270,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 82fa2847c..360a4b0d1 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -140,6 +140,9 @@ def remove_event_handler(self, handler: callable): self._event_handlers.remove(handler) + def clear_event_handlers(self): + self._event_handlers.clear() + #TODO: maybe this can be implemented right here in the base class @abstractmethod def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): @@ -229,10 +232,12 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: # return slice(int(start), int(stop), int(step)) -def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: +def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]: """ Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. + Returns None if nothing to change. + Parameters ---------- key: np.ndarray @@ -256,6 +261,9 @@ def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: if key.dtype == bool: key = np.nonzero(key)[0] + if key.size < 1: + return None + # make sure indices within bounds of feature buffer range if key[-1] > upper_bound: raise IndexError(f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`") diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index d607d6397..f1ca3acb9 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -145,6 +145,9 @@ def __setitem__(self, key, value): elif isinstance(key, np.ndarray): key = cleanup_array_slice(key, self._upper_bound) + if key is None: + return + indices = key else: diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 64934b6fa..7d108efc6 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -324,4 +324,10 @@ def __del__(self): self._plot_area.renderer.remove_event_handler(self._key_up, "key_up") # remove animation func - self._plot_area.remove_animation(self._key_hold) \ No newline at end of file + self._plot_area.remove_animation(self._key_hold) + + if hasattr(self, "feature_events"): + feature_names = getattr(self, "feature_events") + for n in feature_names: + fea = getattr(self, n) + fea.clear_event_handlers() diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 30f223fad..17751f51e 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -11,6 +11,9 @@ class LinearBoundsFeature(GraphicFeature): + feature_events = ( + "data", + ) """ Feature for a linearly bounding region @@ -121,10 +124,6 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): class LinearRegionSelector(Graphic, BaseSelector): - feature_events = ( - "bounds" - ) - def __init__( self, bounds: Tuple[int, int], diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index f3781a4e7..105eabb64 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -394,6 +394,9 @@ def clear(self): for g in self.graphics: self.delete_graphic(g) + for s in self.selectors: + self.delete_graphic(s) + def __getitem__(self, name: str): for graphic in self.graphics: if graphic.name == name: diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 734d004d4..517bf5b53 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,4 +1,5 @@ -import itertools +import traceback +from datetime import datetime from itertools import product import numpy as np from typing import * @@ -169,8 +170,8 @@ def __init__( self._current_iter = None self._starting_size = size - - super(RecordMixin, self).__init__() + + RecordMixin.__init__(self) def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: if isinstance(index, str): @@ -277,24 +278,24 @@ def show(self, toolbar: bool = True): for subplot in self: subplot.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - # check if in jupyter notebook or not - if not isinstance(self.canvas, JupyterWgpuCanvas): + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.canvas, JupyterWgpuCanvas)) or (not toolbar): return self.canvas - if toolbar and self.toolbar is None: - self.toolbar = GridPlotToolBar(self).widget - return VBox([self.canvas, self.toolbar]) - elif toolbar and self.toolbar is not None: - return VBox([self.canvas, self.toolbar]) - else: - return self.canvas + if self.toolbar is None: + self.toolbar = GridPlotToolBar(self) + + return VBox([self.canvas, self.toolbar.widget]) def close(self): self.canvas.close() + if self.toolbar is not None: + self.toolbar.widget.close() + def _get_iterator(self): return product(range(self.shape[0]), range(self.shape[1])) @@ -331,9 +332,12 @@ def __init__(self, self.maintain_aspect_button = ToggleButton(value=True, disabled=False, description="1:1", layout=Layout(width='auto'), tooltip='maintain aspect') self.maintain_aspect_button.style.font_weight = "bold" - self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + self.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', layout=Layout(width='auto'), tooltip='flip') + self.record_button = ToggleButton(value=False, disabled=False, icon='video', + layout=Layout(width='auto'), tooltip='record') + positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) values = list() for pos in positions: @@ -341,13 +345,15 @@ def __init__(self, values.append(self.plot[pos].name) else: values.append(str(pos)) - self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:') + self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:', + layout=Layout(width='200px')) self.widget = HBox([self.autoscale_button, self.center_scene_button, self.panzoom_controller_button, self.maintain_aspect_button, self.flip_camera_button, + self.record_button, self.dropdown]) self.panzoom_controller_button.observe(self.panzoom_control, 'value') @@ -355,6 +361,7 @@ def __init__(self, self.center_scene_button.on_click(self.center_scene) self.maintain_aspect_button.observe(self.maintain_aspect, 'value') self.flip_camera_button.on_click(self.flip_camera) + self.record_button.observe(self.record_plot, 'value') self.plot.renderer.add_event_handler(self.update_current_subplot, "click") @@ -399,3 +406,12 @@ def update_current_subplot(self, ev): self.panzoom_controller_button.value = subplot.controller.enabled self.maintain_aspect_button.value = subplot.camera.maintain_aspect + def record_plot(self, obj): + if self.record_button.value: + try: + self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + except Exception: + traceback.print_exc() + self.record_button.value = False + else: + self.plot.record_stop() diff --git a/fastplotlib/layouts/_toolbar.py b/fastplotlib/layouts/_toolbar.py deleted file mode 100644 index 8b1378917..000000000 --- a/fastplotlib/layouts/_toolbar.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 6f8e1bc44..3514d95fa 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -1,11 +1,12 @@ from typing import * import pygfx from wgpu.gui.auto import WgpuCanvas - from .layouts._subplot import Subplot from ipywidgets import HBox, Layout, Button, ToggleButton, VBox from wgpu.gui.jupyter import JupyterWgpuCanvas from .layouts._record_mixin import RecordMixin +from datetime import datetime +import traceback class Plot(Subplot, RecordMixin): @@ -90,7 +91,7 @@ def __init__( controller=controller, **kwargs ) - super(RecordMixin, self).__init__() + RecordMixin.__init__(self) self._starting_size = size @@ -118,22 +119,22 @@ def show(self, autoscale: bool = True, toolbar: bool = True): self.canvas.set_logical_size(*self._starting_size) - # check if in jupyter notebook or not - if not isinstance(self.canvas, JupyterWgpuCanvas): + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.canvas, JupyterWgpuCanvas)) or (not toolbar): return self.canvas - if toolbar and self.toolbar is None: - self.toolbar = ToolBar(self).widget - return VBox([self.canvas, self.toolbar]) - elif toolbar and self.toolbar is not None: - return VBox([self.canvas, self.toolbar]) - else: - return self.canvas + if self.toolbar is None: + self.toolbar = ToolBar(self) + + return VBox([self.canvas, self.toolbar.widget]) def close(self): self.canvas.close() - + if self.toolbar is not None: + self.toolbar.widget.close() + + class ToolBar: def __init__(self, plot: Plot): @@ -156,20 +157,24 @@ def __init__(self, layout=Layout(width='auto'), tooltip='maintain aspect') self.maintain_aspect_button.style.font_weight = "bold" - self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + self.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', layout=Layout(width='auto'), tooltip='flip') + self.record_button = ToggleButton(value=False, disabled=False, icon='video', + layout=Layout(width='auto'), tooltip='record') self.widget = HBox([self.autoscale_button, self.center_scene_button, self.panzoom_controller_button, self.maintain_aspect_button, - self.flip_camera_button]) + self.flip_camera_button, + self.record_button]) self.panzoom_controller_button.observe(self.panzoom_control, 'value') self.autoscale_button.on_click(self.auto_scale) self.center_scene_button.on_click(self.center_scene) self.maintain_aspect_button.observe(self.maintain_aspect, 'value') self.flip_camera_button.on_click(self.flip_camera) + self.record_button.observe(self.record_plot, 'value') def auto_scale(self, obj): self.plot.auto_scale(maintain_aspect=self.plot.camera.maintain_aspect) @@ -185,3 +190,13 @@ def maintain_aspect(self, obj): def flip_camera(self, obj): self.plot.camera.world.scale_y *= -1 + + def record_plot(self, obj): + if self.record_button.value: + try: + self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + except Exception: + traceback.print_exc() + self.record_button.value = False + else: + self.plot.record_stop() diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 32f444a32..fc82a719c 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -1,15 +1,22 @@ +import traceback +from datetime import datetime +from itertools import product + +from ipywidgets import Dropdown +from wgpu.gui.jupyter import JupyterWgpuCanvas + +from ..layouts._subplot import Subplot from ..plot import Plot from ..layouts import GridPlot from ..graphics import ImageGraphic from ..utils import quick_min_max -from ipywidgets.widgets import IntSlider, VBox, HBox, Layout, FloatRangeSlider +from ipywidgets.widgets import IntSlider, VBox, HBox, Layout, FloatRangeSlider, Button, BoundedIntText, Play, jslink import numpy as np from typing import * from warnings import warn from functools import partial from copy import deepcopy - DEFAULT_DIMS_ORDER = \ { 2: "xy", @@ -238,6 +245,7 @@ def __init__( passed to fastplotlib.graphics.Image """ self._names = None + self.toolbar = None if isinstance(data, list): # verify that it's a list of np.ndarray @@ -335,7 +343,8 @@ def __init__( f"index {data_ix} out of bounds for `dims_order`, the bounds are 0 - {len(self.data)}" ) else: - raise TypeError(f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>") + raise TypeError( + f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>") if not len(self.dims_order[0]) == self.ndim: raise ValueError( @@ -589,8 +598,7 @@ def __init__( self.block_sliders: bool = False # TODO: So just stack everything vertically for now - self.widget = VBox([ - self.plot.canvas, + self._vbox_sliders = VBox([ *list(self._sliders.values()), *self.vmin_vmax_sliders ]) @@ -847,7 +855,7 @@ def reset_vmin_vmax(self): self.vmin_vmax_sliders[i].set_state(state) - def show(self): + def show(self, toolbar: bool = True): """ Show the widget @@ -856,7 +864,62 @@ def show(self): VBox ``ipywidgets.VBox`` stacking the plotter and sliders in a vertical layout """ - # start render loop - self.plot.show() - return self.widget + if not isinstance(self.plot.canvas, JupyterWgpuCanvas): + raise TypeError("ImageWidget is currently not supported outside of Jupyter") + + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.plot.canvas, JupyterWgpuCanvas)) or (not toolbar): + return VBox([self.plot.show(toolbar=False), self._vbox_sliders]) + + if self.toolbar is None: + self.toolbar = ImageWidgetToolbar(self) + + return VBox( + [ + self.plot.show(toolbar=True), + self.toolbar.widget, + self._vbox_sliders, + ] + ) + + +class ImageWidgetToolbar: + def __init__(self, + iw: ImageWidget): + """ + Basic toolbar for a ImageWidget instance. + + Parameters + ---------- + plot: + """ + self.iw = iw + self.plot = iw.plot + + self.reset_vminvmax_button = Button(value=False, disabled=False, icon='adjust', + layout=Layout(width='auto'), tooltip='reset vmin/vmax') + + self.step_size_setter = BoundedIntText(value=1, min=1, max=self.iw.sliders['t'].max, step=1, + description='Step Size:', disabled=False, + description_tooltip='set slider step', layout=Layout(width='150px')) + self.play_button = Play( + value=0, + min=iw.sliders["t"].min, + max=iw.sliders["t"].max, + step=iw.sliders["t"].step, + description="play/pause", + disabled=False) + + self.widget = HBox([self.reset_vminvmax_button, self.play_button, self.step_size_setter]) + + self.reset_vminvmax_button.on_click(self.reset_vminvmax) + self.step_size_setter.observe(self.change_stepsize, 'value') + jslink((self.play_button, 'value'), (self.iw.sliders["t"], 'value')) + + def reset_vminvmax(self, obj): + if len(self.iw.vmin_vmax_sliders) != 0: + self.iw.reset_vmin_vmax() + + def change_stepsize(self, obj): + self.iw.sliders['t'].step = self.step_size_setter.value From 1b7a827d7c9aa7e9b77ad5fd91f577446b2f546c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Fri, 2 Jun 2023 16:27:11 -0400 Subject: [PATCH 41/96] nbmake tests work --- .github/workflows/ci.yml | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 945635dad..34c2c66cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,6 @@ jobs: fail-fast: false matrix: include: - - name: Test py38 - pyversion: '3.8' - name: Test py39 pyversion: '3.9' - name: Test py310 @@ -68,33 +66,12 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest imageio + pip install pytest imageio nbmake scipy REGENERATE_SCREENSHOTS=1 pytest -v examples + pytest --nbmake notebooks/simple.ipynb - uses: actions/upload-artifact@v3 #if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs -# test-notebooks-build: -# name: Test notebooks -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# steps: -# - uses: actions/checkout@v3 -# - name: Set up Python -# uses: actions/setup-python@v3 -# with: -# python-version: '3.10' -# - name: Install dev dependencies -# run: | -# python -m pip install --upgrade pip -# # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving -# sed -i "/pygfx/d" ./setup.py -# pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 -# pip install -e . -# - name: Test notebooks -# run: | -# pip install pytest nbmake -# pytest --nbmake notebooks/simple.ipynb From fe0b8c9ef29120738160aef8025382f27dcb6892 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 5 Jun 2023 14:56:40 -0400 Subject: [PATCH 42/96] updating CI file --- .github/workflows/ci.yml | 27 +---- notebooks/garbage_collection.ipynb | 48 +++++---- notebooks/gridplot_simple.py | 53 ---------- notebooks/histogram.ipynb | 122 ---------------------- notebooks/text.ipynb | 161 ----------------------------- 5 files changed, 32 insertions(+), 379 deletions(-) delete mode 100644 notebooks/gridplot_simple.py delete mode 100644 notebooks/histogram.ipynb delete mode 100644 notebooks/text.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34c2c66cd..32c211d40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ on: - master jobs: - - test-linux-builds: - name: ${{ matrix.name }} + + test-examples-build: + name: Test examples runs-on: ubuntu-latest strategy: fail-fast: false @@ -25,29 +25,10 @@ jobs: pyversion: '3.11' steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.pyversion }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.pyversion }} - - name: Install package and dev dependencies - run: | - python -m pip install --upgrade pip - # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving - sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 - pip install -e . - - test-examples-build: - name: Test examples - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: - python-version: '3.10' + python-version: ${{ matrix.pyversion }} - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq diff --git a/notebooks/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb index 85744e6e0..4a066a912 100644 --- a/notebooks/garbage_collection.ipynb +++ b/notebooks/garbage_collection.ipynb @@ -203,7 +203,9 @@ "cell_type": "code", "execution_count": null, "id": "dd6a26c1-ea81-469d-ae7a-95839b1f9d5a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -278,7 +280,9 @@ "cell_type": "code", "execution_count": null, "id": "2599f430-8b00-4490-9e11-774897be6e77", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -307,7 +311,9 @@ "cell_type": "code", "execution_count": null, "id": "3ec10f26-6544-4ad3-80c1-aa34617dc826", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import weakref" @@ -317,7 +323,9 @@ "cell_type": "code", "execution_count": null, "id": "acc819a3-cd50-4fdd-a0b5-c442d80847e2", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img = make_image()\n", @@ -328,7 +336,9 @@ "cell_type": "code", "execution_count": null, "id": "f89da335-3372-486b-b773-9f103d6a9bbd", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img_ref()" @@ -338,7 +348,9 @@ "cell_type": "code", "execution_count": null, "id": "c22904ad-d674-43e6-83bb-7a2f7b277c06", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "del img" @@ -348,7 +360,9 @@ "cell_type": "code", "execution_count": null, "id": "573566d7-eb91-4690-958c-d00dd495b3e4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import gc" @@ -358,27 +372,21 @@ "cell_type": "code", "execution_count": null, "id": "aaef3e89-2bfd-43af-9b8f-824a3f89b85f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img_ref()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "3380f35e-fcc9-43f6-80d2-7e9348cd13b4", - "metadata": {}, - "outputs": [], - "source": [ - "draw()" - ] - }, { "cell_type": "code", "execution_count": null, "id": "a27bf7c7-f3ef-4ae8-8ecf-31507f8c0449", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "print(\n", @@ -411,7 +419,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/notebooks/gridplot_simple.py b/notebooks/gridplot_simple.py deleted file mode 100644 index 382f2b486..000000000 --- a/notebooks/gridplot_simple.py +++ /dev/null @@ -1,53 +0,0 @@ -import numpy as np -from wgpu.gui.auto import WgpuCanvas -import pygfx as gfx -from fastplotlib.layouts import GridPlot -from fastplotlib.graphics import ImageGraphic, LineGraphic, HistogramGraphic -from fastplotlib import run -from math import sin, cos, radians - -# GridPlot of shape 2 x 3 -grid_plot = GridPlot(shape=(2, 3)) - -image_graphics = list() - -hist_data1 = np.random.normal(0, 256, 2048) -hist_data2 = np.random.poisson(0, 256) - -# Make a random image graphic for each subplot -for i, subplot in enumerate(grid_plot): - img = np.random.rand(512, 512) * 255 - ig = ImageGraphic(data=img, vmin=0, vmax=255, cmap='gnuplot2') - image_graphics.append(ig) - - # add the graphic to the subplot - subplot.add_graphic(ig) - - histogram = HistogramGraphic(data=hist_data1, bins=100) - histogram.world_object.rotation.w = cos(radians(45)) - histogram.world_object.rotation.z = sin(radians(45)) - - histogram.world_object.scale.y = 1 - histogram.world_object.scale.x = 8 - - for dv_position in ["right", "top", "bottom", "left"]: - h2 = HistogramGraphic(data=hist_data1, bins=100) - - subplot.docked_viewports[dv_position].size = 60 - subplot.docked_viewports[dv_position].add_graphic(h2) -# - -# Define a function to update the image graphics -# with new randomly generated data -def set_random_frame(): - for ig in image_graphics: - new_data = np.random.rand(512, 512) * 255 - ig.update_data(data=new_data) - - -# add the animation -# grid_plot.add_animations(set_random_frame) - -grid_plot.show() - -run() diff --git a/notebooks/histogram.ipynb b/notebooks/histogram.ipynb deleted file mode 100644 index d78a38282..000000000 --- a/notebooks/histogram.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "77bd602d-aed3-4ddc-9917-46f3429d32b9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib import Plot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0dc85d4e-4554-44e5-bf05-96ceb339e57f", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "27e7e540a7be408997b143ac274b2b8e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "data = np.random.normal(0, 256, 2048)\n", - "\n", - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ef4fbdb5-f7a3-44c3-acef-38d127dccdc9", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushal/python-venvs/mescore/lib/python3.10/site-packages/pygfx/geometries/_plane.py:19: RuntimeWarning: invalid value encountered in true_divide\n", - " texcoords = (positions[..., :2] + dim / 2) / dim\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ff05c3706cfd401eb97f58b6e12e6d2a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.add_histogram(data=data, bins=100)\n", - "\n", - "plot.set_axes_visibility(True)\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c72d91a-0771-46c1-a2b8-d555ee47291f", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/text.ipynb b/notebooks/text.ipynb deleted file mode 100644 index 53260fbf8..000000000 --- a/notebooks/text.ipynb +++ /dev/null @@ -1,161 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "fd2167fe-04de-42fc-a7e2-c96dac665656", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import Plot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "bc5acda4-9533-4619-a655-ed5404e00f5e", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e17c38170e864bd3a61cc7e662854dbe", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ea3c2c127b3f414eb96e03b8b775c80c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot = Plot()\n", - "\n", - "text_graphic = plot.text(\"hello world\")\n", - "\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "71bb2a1b-fc85-44f1-941a-6fc53b8fb8a4", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_text(\"foo\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "aa8aba1e-ee37-49c6-b483-234241871855", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_face_color(\"r\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "757b6012-c06b-46cf-b5a8-a874f4b0b99a", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_size(5)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "e3b8ace2-a7ca-4576-983f-9e667e3d2ba3", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_size(15)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "616faaef-4cbb-4fee-a6e1-9e684c7b4ebe", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_outline_size(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ee963404-afe7-47c9-b8d9-a3ac3d8ba23f", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_outline_color(\"b\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "0baf70d9-ee5b-41f7-9e27-bb8a38f498df", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_position((5, -1, 10))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 980d2a4749daf802c7ad33572173e34634c05caa Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:09:03 -0400 Subject: [PATCH 43/96] all tests passing locally --- .github/workflows/ci.yml | 6 +- notebooks/garbage_collection.ipynb | 427 ----------------------------- 2 files changed, 3 insertions(+), 430 deletions(-) delete mode 100644 notebooks/garbage_collection.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32c211d40..fc4ea8707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ on: - master jobs: - - test-examples-build: + + test-build: name: Test examples runs-on: ubuntu-latest strategy: @@ -49,7 +49,7 @@ jobs: run: | pip install pytest imageio nbmake scipy REGENERATE_SCREENSHOTS=1 pytest -v examples - pytest --nbmake notebooks/simple.ipynb + pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 #if: ${{ failure() }} with: diff --git a/notebooks/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb deleted file mode 100644 index 4a066a912..000000000 --- a/notebooks/garbage_collection.ipynb +++ /dev/null @@ -1,427 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "1ef0578e-09e1-45ff-bd34-84472db3885e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from fastplotlib import Plot\n", - "import numpy as np\n", - "import sys\n", - "\n", - "import weakref\n", - "import gc\n", - "import os, psutil\n", - "process = psutil.Process(os.getpid())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1bb6bc6f-7786-4d23-9eb1-e30bbc66c798", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "def print_process_ram_mb():\n", - " print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b376676e-a7fe-4424-9ba6-fde5be03b649", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b23ba640-88ec-40d9-b53c-c8cbb3e39b0b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot = Plot()\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17f46f21-b29d-4dd3-9496-989bbb240f50", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27627cd4-c363-4eab-a121-f6c8abbbe5ae", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "graphic = \"scatter\"" - ] - }, - { - "cell_type": "markdown", - "id": "d9c10edc-169a-4dd2-bd5b-8a1b67baf3a9", - "metadata": {}, - "source": [ - "### Run the following cells repeatedly to add and remove the graphic" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e26d392f-6afd-4e89-a685-d618065d3caf", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "if graphic == \"line\":\n", - " a = np.random.rand(10_000_000)\n", - " g = plot.add_line(a)\n", - " \n", - "elif graphic == \"heatmap\":\n", - " a = np.random.rand(20_000, 20_000)\n", - " g = plot.add_heatmap(a)\n", - "\n", - "elif graphic == \"line_collection\":\n", - " a = np.random.rand(500, 50_000)\n", - " g = plot.add_line_collection(a)\n", - " \n", - "elif graphic == \"image\":\n", - " a = np.random.rand(7_000, 7_000)\n", - " g = plot.add_image(a)\n", - "\n", - "elif graphic == \"scatter\":\n", - " a = np.random.rand(10_000_000, 3)\n", - " g = plot.add_scatter(a)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31e3027a-56cf-4f7b-ba78-aed4f78eef47", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6518795c-98cf-405d-94ab-786ac3b2e1d6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "g" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee2481c9-82e3-4043-85fd-21a0cdf21187", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.auto_scale()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53170858-ae72-4451-8647-7d5b1f9da75e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4e0f73b-c58a-40e7-acf5-07a1f70d2821", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.delete_graphic(plot.graphics[0])" - ] - }, - { - "cell_type": "markdown", - "id": "47baa487-c66b-4c40-aa11-d819902870e3", - "metadata": {}, - "source": [ - "If there is no serious system memory leak, this value shouldn't really increase after repeated cycles" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56c64498-229e-48b7-9fb1-f7c327fff2ae", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd6a26c1-ea81-469d-ae7a-95839b1f9d5a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from wgpu.gui.auto import WgpuCanvas, run\n", - "import pygfx as gfx\n", - "import subprocess\n", - "\n", - "canvas = WgpuCanvas()\n", - "renderer = gfx.WgpuRenderer(canvas)\n", - "scene = gfx.Scene()\n", - "camera = gfx.OrthographicCamera(5000, 5000)\n", - "camera.position.x = 2048\n", - "camera.position.y = 2048\n", - "\n", - "\n", - "def make_image():\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - "\n", - " return gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )\n", - "\n", - "\n", - "class Graphic:\n", - " def __init__(self):\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - " self.wo = gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )\n", - "\n", - "\n", - "def draw():\n", - " renderer.render(scene, camera)\n", - " canvas.request_draw()\n", - "\n", - "\n", - "def print_nvidia(msg):\n", - " print(msg)\n", - " print(\n", - " subprocess.check_output([\"nvidia-smi\", \"--format=csv\", \"--query-gpu=memory.used\"]).decode().split(\"\\n\")[1]\n", - " )\n", - " print()\n", - "\n", - "\n", - "def add_img(*args):\n", - " print_nvidia(\"Before creating image\")\n", - " img = make_image()\n", - " print_nvidia(\"After creating image\")\n", - " scene.add(img)\n", - " img.add_event_handler(remove_img, \"click\")\n", - " draw()\n", - " print_nvidia(\"After add image to scene\")\n", - "\n", - "\n", - "def remove_img(*args):\n", - " img = scene.children[0]\n", - " scene.remove(img)\n", - " draw()\n", - " print_nvidia(\"After remove image from scene\")\n", - " del img\n", - " draw()\n", - " print_nvidia(\"After del image\")\n", - "\n", - "\n", - "renderer.add_event_handler(add_img, \"double_click\")\n", - "canvas" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2599f430-8b00-4490-9e11-774897be6e77", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from wgpu.gui.auto import WgpuCanvas, run\n", - "import pygfx as gfx\n", - "import subprocess\n", - "\n", - "canvas = WgpuCanvas()\n", - "renderer = gfx.WgpuRenderer(canvas)\n", - "scene = gfx.Scene()\n", - "camera = gfx.OrthographicCamera(5000, 5000)\n", - "camera.position.x = 2048\n", - "camera.position.y = 2048\n", - "\n", - "\n", - "def make_image():\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - "\n", - " return gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ec10f26-6544-4ad3-80c1-aa34617dc826", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import weakref" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acc819a3-cd50-4fdd-a0b5-c442d80847e2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img = make_image()\n", - "img_ref = weakref.ref(img)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f89da335-3372-486b-b773-9f103d6a9bbd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img_ref()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c22904ad-d674-43e6-83bb-7a2f7b277c06", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "del img" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "573566d7-eb91-4690-958c-d00dd495b3e4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import gc" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aaef3e89-2bfd-43af-9b8f-824a3f89b85f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img_ref()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a27bf7c7-f3ef-4ae8-8ecf-31507f8c0449", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(\n", - " subprocess.check_output([\"nvidia-smi\", \"--format=csv\", \"--query-gpu=memory.used\"]).decode().split(\"\\n\")[1]\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4bf2711-8a83-4d9c-a4f7-f50de7ae1715", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 2bece59138f8dd242a92f5e3447f9d08c9a3741c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:25:56 -0400 Subject: [PATCH 44/96] trying to get CI to run on ghub --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc4ea8707..6a428ee7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - fpl-tests pull_request: branches: - master From 26749f1736143208f3af9e989360859a798f24a1 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:37:45 -0400 Subject: [PATCH 45/96] change screenshots to be stored as png files --- .github/workflows/ci.yml | 3 +-- examples/tests/test_examples.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a428ee7d..6ef4c14c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - fpl-tests pull_request: branches: - master @@ -52,7 +51,7 @@ jobs: REGENERATE_SCREENSHOTS=1 pytest -v examples pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 - #if: ${{ failure() }} + if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 5aa78a164..b42a22bcc 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -67,16 +67,18 @@ def test_example_screenshots(module, force_offscreen): if not os.path.exists(screenshots_dir): os.mkdir(screenshots_dir) - screenshot_path = screenshots_dir / f"{module.stem}.npy" + screenshot_path = screenshots_dir / f"{module.stem}.png" if "REGENERATE_SCREENSHOTS" in os.environ.keys(): if os.environ["REGENERATE_SCREENSHOTS"] == "1": - np.save(screenshot_path, img) + iio.imwrite(screenshot_path, img) + #np.save(screenshot_path, img) assert ( screenshot_path.exists() ), "found # test_example = true but no reference screenshot available" - stored_img = np.load(screenshot_path) + #stored_img = np.load(screenshot_path) + stored_img = iio.imread(screenshot_path) is_similar = np.allclose(img, stored_img, atol=1) update_diffs(module.stem, is_similar, img, stored_img) assert is_similar, ( From 40557de83af77298f339a674f92192e022f26120 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:13:18 -0400 Subject: [PATCH 46/96] restarted tests, now work from the command line --- .gitignore | 3 + examples/image/cmap.py | 33 ++++++++ examples/image/rgb.py | 30 +++++++ examples/image/rgbvminvmax.py | 33 ++++++++ examples/image/simple.py | 32 ++++++++ examples/image/vminvmax.py | 34 ++++++++ examples/tests/__init__.py | 0 examples/tests/test_examples.py | 79 +++++++++++++++++++ examples/tests/testutils.py | 60 ++++++++++++++ .../garbage_collection.ipynb | 0 {examples => notebooks}/gridplot.ipynb | 0 {examples => notebooks}/gridplot_simple.ipynb | 0 {examples => notebooks}/gridplot_simple.py | 0 {examples => notebooks}/histogram.ipynb | 0 {examples => notebooks}/image_widget.ipynb | 0 .../line_collection_event.ipynb | 0 .../linear_region_selector.ipynb | 0 {examples => notebooks}/linear_selector.ipynb | 0 {examples => notebooks}/lineplot.ipynb | 0 {examples => notebooks}/scatter.ipynb | 0 {examples => notebooks}/simple.ipynb | 0 .../single_contour_event.ipynb | 0 {examples => notebooks}/text.ipynb | 0 23 files changed, 304 insertions(+) create mode 100644 examples/image/cmap.py create mode 100644 examples/image/rgb.py create mode 100644 examples/image/rgbvminvmax.py create mode 100644 examples/image/simple.py create mode 100644 examples/image/vminvmax.py create mode 100644 examples/tests/__init__.py create mode 100644 examples/tests/test_examples.py create mode 100644 examples/tests/testutils.py rename {examples => notebooks}/garbage_collection.ipynb (100%) rename {examples => notebooks}/gridplot.ipynb (100%) rename {examples => notebooks}/gridplot_simple.ipynb (100%) rename {examples => notebooks}/gridplot_simple.py (100%) rename {examples => notebooks}/histogram.ipynb (100%) rename {examples => notebooks}/image_widget.ipynb (100%) rename {examples => notebooks}/line_collection_event.ipynb (100%) rename {examples => notebooks}/linear_region_selector.ipynb (100%) rename {examples => notebooks}/linear_selector.ipynb (100%) rename {examples => notebooks}/lineplot.ipynb (100%) rename {examples => notebooks}/scatter.ipynb (100%) rename {examples => notebooks}/simple.ipynb (100%) rename {examples => notebooks}/single_contour_event.ipynb (100%) rename {examples => notebooks}/text.ipynb (100%) diff --git a/.gitignore b/.gitignore index f87eb1c51..98b21a799 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ dmypy.json # Pycharm .idea/ +# Binary files for testing +/examples/data +/examples/screenshots diff --git a/examples/image/cmap.py b/examples/image/cmap.py new file mode 100644 index 000000000..684d90f5e --- /dev/null +++ b/examples/image/cmap.py @@ -0,0 +1,33 @@ +""" +Simple Plot +============ +Example showing simple plot creation and subsequent cmap change with 512 x 512 pre-saved random image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +image_graphic.cmap = "viridis" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/rgb.py b/examples/image/rgb.py new file mode 100644 index 000000000..a8281df49 --- /dev/null +++ b/examples/image/rgb.py @@ -0,0 +1,30 @@ +""" +Simple Plot +============ +Example showing the simple plot creation with 512 x 512 2D RGB image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +import imageio.v3 as iio + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +im = iio.imread("imageio:astronaut.png") + +# plot the image data +image_graphic = plot.add_image(data=im, name="iio astronaut") + +plot.show() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) \ No newline at end of file diff --git a/examples/image/rgbvminvmax.py b/examples/image/rgbvminvmax.py new file mode 100644 index 000000000..8852b48a8 --- /dev/null +++ b/examples/image/rgbvminvmax.py @@ -0,0 +1,33 @@ +""" +Simple Plot +============ +Example showing the simple plot followed by changing the vmin/vmax with 512 x 512 2D RGB image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +import imageio.v3 as iio + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +im = iio.imread("imageio:astronaut.png") + +# plot the image data +image_graphic = plot.add_image(data=im, name="iio astronaut") + +plot.show() + +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/simple.py b/examples/image/simple.py new file mode 100644 index 000000000..2dacc5ecc --- /dev/null +++ b/examples/image/simple.py @@ -0,0 +1,32 @@ +""" +Simple Plot +============ +Example showing the simple plot creation with 512 x 512 pre-saved random image. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/image/vminvmax.py b/examples/image/vminvmax.py new file mode 100644 index 000000000..645c06b5c --- /dev/null +++ b/examples/image/vminvmax.py @@ -0,0 +1,34 @@ +""" +Simple Plot +============ +Example showing the simple plot creation followed by changing the vmin/vmax with 512 x 512 pre-saved random image. +""" +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +# plot the image data +image_graphic = plot.add_image(data=data, name="random-image") + +plot.show() + +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/tests/__init__.py b/examples/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py new file mode 100644 index 000000000..a0f5e2bf8 --- /dev/null +++ b/examples/tests/test_examples.py @@ -0,0 +1,79 @@ +""" +Test that examples run without error. +""" +import importlib +import runpy +import pytest +import os +import numpy as np + +from .testutils import ( + ROOT, + examples_dir, + screenshots_dir, + find_examples, + wgpu_backend, + is_lavapipe, +) + +# run all tests unless they opt-out +examples_to_run = find_examples(negative_query="# run_example = false") + +# only test output of examples that opt-in +examples_to_test = find_examples(query="# test_example = true") + + +@pytest.mark.parametrize("module", examples_to_run, ids=lambda x: x.stem) +def test_examples_run(module, force_offscreen): + """Run every example marked to see if they run without error.""" + + runpy.run_path(module, run_name="__main__") + + +@pytest.fixture +def force_offscreen(): + """Force the offscreen canvas to be selected by the auto gui module.""" + os.environ["WGPU_FORCE_OFFSCREEN"] = "true" + try: + yield + finally: + del os.environ["WGPU_FORCE_OFFSCREEN"] + + +def test_that_we_are_on_lavapipe(): + print(wgpu_backend) + if os.getenv("PYGFX_EXPECT_LAVAPIPE"): + assert is_lavapipe + + +@pytest.mark.parametrize("module", examples_to_test, ids=lambda x: x.stem) +def test_example_screenshots(module, force_offscreen, regenerate_screenshots=False): + """Make sure that every example marked outputs the expected.""" + # (relative) module name from project root + module_name = module.relative_to(ROOT/"examples").with_suffix("").as_posix().replace("/", ".") + + # import the example module + example = importlib.import_module(module_name) + + # render a frame + img = np.asarray(example.renderer.target.draw()) + + # check if _something_ was rendered + assert img is not None and img.size > 0 + + screenshot_path = screenshots_dir / f"{module.stem}.npy" + + if regenerate_screenshots: + np.save(screenshot_path, img) + + assert ( + screenshot_path.exists() + ), "found # test_example = true but no reference screenshot available" + stored_img = np.load(screenshot_path) + is_similar = np.allclose(img, stored_img, atol=1) + assert is_similar + + +if __name__ == "__main__": + test_examples_run() + test_example_screenshots() diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py new file mode 100644 index 000000000..b25209d71 --- /dev/null +++ b/examples/tests/testutils.py @@ -0,0 +1,60 @@ +""" +Test suite utilities. +""" + +from pathlib import Path +import subprocess +import sys +from itertools import chain + + +ROOT = Path(__file__).parents[2] # repo root +examples_dir = ROOT / "examples" +screenshots_dir = examples_dir / "screenshots" + +# examples live in themed sub-folders +# example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] +example_globs = ["image/*.py"] + + +def get_wgpu_backend(): + """ + Query the configured wgpu backend driver. + """ + code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.request_adapter_info(); print(info['adapter_type'], info['backend_type'])" + result = subprocess.run( + [ + sys.executable, + "-c", + code, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=ROOT, + ) + out = result.stdout.strip() + err = result.stderr.strip() + return err if "traceback" in err.lower() else out + + +wgpu_backend = get_wgpu_backend() +is_lavapipe = wgpu_backend.lower() == "cpu vulkan" + + +def find_examples(query=None, negative_query=None, return_stems=False): + """Finds all modules to be tested.""" + result = [] + for example_path in chain(*(examples_dir.glob(x) for x in example_globs)): + example_code = example_path.read_text(encoding="UTF-8") + query_match = query is None or query in example_code + negative_query_match = ( + negative_query is None or negative_query not in example_code + ) + if query_match and negative_query_match: + result.append(example_path) + result = list(sorted(result)) + if return_stems: + result = [r.stem for r in result] + return result + diff --git a/examples/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb similarity index 100% rename from examples/garbage_collection.ipynb rename to notebooks/garbage_collection.ipynb diff --git a/examples/gridplot.ipynb b/notebooks/gridplot.ipynb similarity index 100% rename from examples/gridplot.ipynb rename to notebooks/gridplot.ipynb diff --git a/examples/gridplot_simple.ipynb b/notebooks/gridplot_simple.ipynb similarity index 100% rename from examples/gridplot_simple.ipynb rename to notebooks/gridplot_simple.ipynb diff --git a/examples/gridplot_simple.py b/notebooks/gridplot_simple.py similarity index 100% rename from examples/gridplot_simple.py rename to notebooks/gridplot_simple.py diff --git a/examples/histogram.ipynb b/notebooks/histogram.ipynb similarity index 100% rename from examples/histogram.ipynb rename to notebooks/histogram.ipynb diff --git a/examples/image_widget.ipynb b/notebooks/image_widget.ipynb similarity index 100% rename from examples/image_widget.ipynb rename to notebooks/image_widget.ipynb diff --git a/examples/line_collection_event.ipynb b/notebooks/line_collection_event.ipynb similarity index 100% rename from examples/line_collection_event.ipynb rename to notebooks/line_collection_event.ipynb diff --git a/examples/linear_region_selector.ipynb b/notebooks/linear_region_selector.ipynb similarity index 100% rename from examples/linear_region_selector.ipynb rename to notebooks/linear_region_selector.ipynb diff --git a/examples/linear_selector.ipynb b/notebooks/linear_selector.ipynb similarity index 100% rename from examples/linear_selector.ipynb rename to notebooks/linear_selector.ipynb diff --git a/examples/lineplot.ipynb b/notebooks/lineplot.ipynb similarity index 100% rename from examples/lineplot.ipynb rename to notebooks/lineplot.ipynb diff --git a/examples/scatter.ipynb b/notebooks/scatter.ipynb similarity index 100% rename from examples/scatter.ipynb rename to notebooks/scatter.ipynb diff --git a/examples/simple.ipynb b/notebooks/simple.ipynb similarity index 100% rename from examples/simple.ipynb rename to notebooks/simple.ipynb diff --git a/examples/single_contour_event.ipynb b/notebooks/single_contour_event.ipynb similarity index 100% rename from examples/single_contour_event.ipynb rename to notebooks/single_contour_event.ipynb diff --git a/examples/text.ipynb b/notebooks/text.ipynb similarity index 100% rename from examples/text.ipynb rename to notebooks/text.ipynb From 086c162c97682f7a37abcfdc244c907989d97887 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:38:09 -0400 Subject: [PATCH 47/96] more tests --- examples/line/colorslice.py | 62 ++++++++++++++++++++++++++++++++ examples/line/dataslice.py | 54 ++++++++++++++++++++++++++++ examples/line/line.py | 50 ++++++++++++++++++++++++++ examples/line/present_scaling.py | 55 ++++++++++++++++++++++++++++ examples/scatter/scatter.py | 33 +++++++++++++++++ examples/tests/test_examples.py | 4 +-- examples/tests/testutils.py | 2 +- 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 examples/line/colorslice.py create mode 100644 examples/line/dataslice.py create mode 100644 examples/line/line.py create mode 100644 examples/line/present_scaling.py create mode 100644 examples/scatter/scatter.py diff --git a/examples/line/colorslice.py b/examples/line/colorslice.py new file mode 100644 index 000000000..d3fc918cf --- /dev/null +++ b/examples/line/colorslice.py @@ -0,0 +1,62 @@ +""" +Line Plot +============ +Example showing color slicing with cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +# indexing of colors +cosine_graphic.colors[:15] = "magenta" +cosine_graphic.colors[90:] = "red" +cosine_graphic.colors[60] = "w" + +# indexing to assign colormaps to entire lines or segments +sinc_graphic.cmap[10:50] = "gray" +sine_graphic.cmap = "seismic" + +# more complex indexing, set the blue value directly from an array +cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65) + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/dataslice.py b/examples/line/dataslice.py new file mode 100644 index 000000000..f46307cc7 --- /dev/null +++ b/examples/line/dataslice.py @@ -0,0 +1,54 @@ +""" +Line Plot +============ +Example showing data slicing with cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.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]]) + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/line.py b/examples/line/line.py new file mode 100644 index 000000000..b44d7bd4c --- /dev/null +++ b/examples/line/line.py @@ -0,0 +1,50 @@ +""" +Line Plot +============ +Example showing cosine, sine, sinc lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/line/present_scaling.py b/examples/line/present_scaling.py new file mode 100644 index 000000000..e5b063bc8 --- /dev/null +++ b/examples/line/present_scaling.py @@ -0,0 +1,55 @@ +""" +Line Plot +============ +Example showing present and scaling feature for lines. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# cosine wave +ys = np.cos(xs) + 5 +cosine = np.dstack([xs, ys])[0] + +# sinc function +a = 0.5 +ys = np.sinc(xs) * 3 + 8 +sinc = np.dstack([xs, ys])[0] + +sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") + +# you can also use colormaps for lines! +cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") + +# or a list of colors for each datapoint +colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 +sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) + +plot.show() + +sinc_graphic.present = False + +plot.center_scene() + +plot.auto_scale() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) + diff --git a/examples/scatter/scatter.py b/examples/scatter/scatter.py new file mode 100644 index 000000000..5a9706f04 --- /dev/null +++ b/examples/scatter/scatter.py @@ -0,0 +1,33 @@ +""" +Scatter Plot +============ +Example showing scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index a0f5e2bf8..ed9eb3a53 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -75,5 +75,5 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal if __name__ == "__main__": - test_examples_run() - test_example_screenshots() + test_examples_run("simple") + test_example_screenshots("simple", regenerate_screenshots=True) diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index b25209d71..48fd2cea4 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -14,7 +14,7 @@ # examples live in themed sub-folders # example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] -example_globs = ["image/*.py"] +example_globs = ["image/*.py", "scatter/*.py", "line/*.py"] def get_wgpu_backend(): From bd32f5b3aa2056e73c4bbc41e11f313987fde81c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 19 Apr 2023 17:46:06 -0400 Subject: [PATCH 48/96] more tests, still working on iw --- examples/gridplot/gridplot.py | 35 +++++++++++++++++++++++++++++ examples/imagewidget/imagewidget.py | 0 examples/tests/testutils.py | 3 +-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 examples/gridplot/gridplot.py create mode 100644 examples/imagewidget/imagewidget.py diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py new file mode 100644 index 000000000..59566f4b2 --- /dev/null +++ b/examples/gridplot/gridplot.py @@ -0,0 +1,35 @@ +""" +GridPlot Simple +============ +Example showing simple 2x3 GridPlot with pre-saved 512x512 random images. +""" + +# test_example = true + +from fastplotlib import GridPlot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = GridPlot(shape=(2,3), canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") +data = np.load(data_path) + +for subplot in plot: + subplot.add_image(data=data) + +plot.show() + +for subplot in plot: + subplot.center_scene() + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/imagewidget/imagewidget.py b/examples/imagewidget/imagewidget.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 48fd2cea4..1733481e2 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -13,8 +13,7 @@ screenshots_dir = examples_dir / "screenshots" # examples live in themed sub-folders -# example_globs = ["image/*.py", "gridplot/*.py", "imagewidget/*.py", "line/*.py", "scatter/*.py"] -example_globs = ["image/*.py", "scatter/*.py", "line/*.py"] +example_globs = ["image/*.py", "scatter/*.py", "line/*.py", "gridplot/*.py"] def get_wgpu_backend(): From 3ae3961b663a649771767ec73efd818a22db194c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 20 Apr 2023 09:25:21 -0400 Subject: [PATCH 49/96] ipywidget can only be tested w/ nbmake --- examples/imagewidget/imagewidget.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/imagewidget/imagewidget.py diff --git a/examples/imagewidget/imagewidget.py b/examples/imagewidget/imagewidget.py deleted file mode 100644 index e69de29bb..000000000 From d0ab79b388b86cd4b1199e788d6d0be19faa59a4 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 20 Apr 2023 10:05:08 -0400 Subject: [PATCH 50/96] updated tests --- .gitignore | 5 ++-- examples/gridplot/gridplot.py | 7 ++--- examples/image/cmap.py | 7 ++--- examples/scatter/scatter_cmap.py | 35 +++++++++++++++++++++++ examples/scatter/scatter_colorslice.py | 36 ++++++++++++++++++++++++ examples/scatter/scatter_dataslice.py | 39 ++++++++++++++++++++++++++ examples/scatter/scatter_present.py | 35 +++++++++++++++++++++++ 7 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 examples/scatter/scatter_cmap.py create mode 100644 examples/scatter/scatter_colorslice.py create mode 100644 examples/scatter/scatter_dataslice.py create mode 100644 examples/scatter/scatter_present.py diff --git a/.gitignore b/.gitignore index 98b21a799..603fab511 100644 --- a/.gitignore +++ b/.gitignore @@ -132,5 +132,6 @@ dmypy.json .idea/ # Binary files for testing -/examples/data -/examples/screenshots +examples/data +examples/screenshots + diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py index 59566f4b2..1c92a3440 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/gridplot/gridplot.py @@ -8,7 +8,7 @@ from fastplotlib import GridPlot import numpy as np -from pathlib import Path +import imageio.v3 as iio from wgpu.gui.offscreen import WgpuCanvas from pygfx import WgpuRenderer @@ -18,11 +18,10 @@ plot = GridPlot(shape=(2,3), canvas=canvas, renderer=renderer) -data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") -data = np.load(data_path) +im = iio.imread("imageio:clock.png") for subplot in plot: - subplot.add_image(data=data) + subplot.add_image(data=im) plot.show() diff --git a/examples/image/cmap.py b/examples/image/cmap.py index 684d90f5e..9fef516d5 100644 --- a/examples/image/cmap.py +++ b/examples/image/cmap.py @@ -7,7 +7,7 @@ from fastplotlib import Plot import numpy as np -from pathlib import Path +import imageio.v3 as iio from wgpu.gui.offscreen import WgpuCanvas from pygfx import WgpuRenderer @@ -17,11 +17,10 @@ plot = Plot(canvas=canvas, renderer=renderer) -data_path = Path(__file__).parent.parent.joinpath("data", "random.npy") -data = np.load(data_path) +im = iio.imread("imageio:clock.png") # plot the image data -image_graphic = plot.add_image(data=data, name="random-image") +image_graphic = plot.add_image(data=im, name="random-image") plot.show() diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py new file mode 100644 index 000000000..af9f8ad68 --- /dev/null +++ b/examples/scatter/scatter_cmap.py @@ -0,0 +1,35 @@ +""" +Scatter Plot +============ +Example showing cmap change for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.cmap = "viridis" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/scatter/scatter_colorslice.py b/examples/scatter/scatter_colorslice.py new file mode 100644 index 000000000..749f0fd37 --- /dev/null +++ b/examples/scatter/scatter_colorslice.py @@ -0,0 +1,36 @@ +""" +Scatter Plot +============ +Example showing color slice for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.colors[0:5000] = "red" +scatter_graphic.colors[10000:20000] = "white" + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) diff --git a/examples/scatter/scatter_dataslice.py b/examples/scatter/scatter_dataslice.py new file mode 100644 index 000000000..6831d3eca --- /dev/null +++ b/examples/scatter/scatter_dataslice.py @@ -0,0 +1,39 @@ +""" +Scatter Plot +============ +Example showing data slice for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.data[0] = np.array([[35, -20, -5]]) +scatter_graphic.data[1] = np.array([[30, -20, -5]]) +scatter_graphic.data[2] = np.array([[40, -20, -5]]) +scatter_graphic.data[3] = np.array([[25, -20, -5]]) + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) + \ No newline at end of file diff --git a/examples/scatter/scatter_present.py b/examples/scatter/scatter_present.py new file mode 100644 index 000000000..0dbae0077 --- /dev/null +++ b/examples/scatter/scatter_present.py @@ -0,0 +1,35 @@ +""" +Scatter Plot +============ +Example showing present feature for scatter plot. +""" + +# test_example = true + +from fastplotlib import Plot +import numpy as np +from pathlib import Path + +from wgpu.gui.offscreen import WgpuCanvas +from pygfx import WgpuRenderer + +canvas = WgpuCanvas() +renderer = WgpuRenderer(canvas) + +plot = Plot(canvas=canvas, renderer=renderer) + +data_path = Path(__file__).parent.parent.joinpath("data", "scatter.npy") +data = np.load(data_path) + +scatter_graphic = plot.add_scatter(data=data, sizes=3, alpha=0.7) + +plot.show() + +plot.center_scene() + +scatter_graphic.present = False + +img = np.asarray(plot.renderer.target.draw()) + +if __name__ == "__main__": + print(__doc__) From b2efe058ae6dc41bb83fde5eba55c357387a5544 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 10:22:10 -0400 Subject: [PATCH 51/96] adding fancy indexing implemented in #177 --- examples/line/colorslice.py | 7 +++++++ examples/line/dataslice.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/examples/line/colorslice.py b/examples/line/colorslice.py index d3fc918cf..1a4595196 100644 --- a/examples/line/colorslice.py +++ b/examples/line/colorslice.py @@ -54,6 +54,13 @@ # more complex indexing, set the blue value directly from an array cosine_graphic.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" +# +# key2 = np.array([True, False, True, False, True, True, True, True]) +# cosine_graphic.colors[key2] = "Green" + plot.center_scene() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/line/dataslice.py b/examples/line/dataslice.py index f46307cc7..b8a30cb01 100644 --- a/examples/line/dataslice.py +++ b/examples/line/dataslice.py @@ -46,6 +46,10 @@ cosine_graphic.data[90:, 1] = 7 cosine_graphic.data[0] = np.array([[-10, 0, 0]]) +# additional fancy indexing using numpy +# key2 = np.array([True, False, True, False, True, True, True, True]) +# sinc_graphic.data[key2] = np.array([[5, 1, 2]]) + plot.center_scene() img = np.asarray(plot.renderer.target.draw()) From 9a4ae2cb357daa3c7942703e6b90b77dac242183 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 17:37:58 -0400 Subject: [PATCH 52/96] requested changes --- .gitignore | 1 - examples/data/iris.npy | Bin 0 -> 4928 bytes examples/gridplot/gridplot.py | 15 ++++++++++----- examples/image/cmap.py | 4 ++-- examples/image/simple.py | 9 ++++----- examples/image/vminvmax.py | 8 ++++---- examples/scatter/scatter.py | 7 +++++-- examples/scatter/scatter_cmap.py | 7 +++++-- examples/scatter/scatter_colorslice.py | 12 ++++++++---- examples/scatter/scatter_dataslice.py | 17 +++++++++++------ examples/scatter/scatter_present.py | 10 ++++++++-- examples/tests/test_examples.py | 4 ++++ 12 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 examples/data/iris.npy diff --git a/.gitignore b/.gitignore index 603fab511..e46391f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,5 @@ dmypy.json .idea/ # Binary files for testing -examples/data examples/screenshots diff --git a/examples/data/iris.npy b/examples/data/iris.npy new file mode 100644 index 0000000000000000000000000000000000000000..a8c7e5ab0a48138e424a1ed78323e4a7f6158259 GIT binary patch literal 4928 zcmbW5y>4Sw6oqY%^XG&lc5KJ_nc&VKr2qjELP&N+2cn^%LJ^aQ0x_cm(IB3J3f?4- z5DBWO(joBzi86`?C5o72t#5DLbCm*ab9L@Md+)W^{yE3~^^31Q|MKf*sopL}=l;_%J-o%cU_Z-4yZ zyZhr`#{cu_96db!S>xeZnQ47J{r$K5=gjX7%NJ|%KmWLYdNb``nrZzi^kuK-?~Atc zI^^=(xBM~xgV_fU&zj#i)<2(OU-IR^r}fJz_K|Cy{)M}1y9=RT#;>?a{HMk-aQtu8 z;*;ME+h6v+e_@`GI8`{s2u_(3k7$?tOL!$103m;8c{`dYF5i-;e6y*pvQ@$md*eltJX zcNhBFNBr=g`9rRF%KV<2AN0Wo|7RgbU;eZI#BcVOcu4=!+lc-u-x#H`ALs+%d287w66Es6Y8{S#Le^AUoT}HSc%Yug<4= z>oYH2ncpqzb!->D=riWSlh`M}7tY_d>x=Uv?aP1P@y~hRn(bUif5=|L_?Q#SG3Gn< zoqa03k?l9Fj~slXz{fnH4)h)t^6Ufc-!Agg4gBZ}2j^q*ulm}I__aUr$$p7H^{e*V z!Jqnwa}fK?7v|xu^D{DD@}Ko1d|T$NZJb$u@}K(fCq2&Dt>91JyHTIg-?n|~JNeW8 zoxrE{M(_*&ReqUgH?E(|ul6N>(r13RjU($z`^i7`%^bOm{u8bz=7)3m!uM`! z75J2|Imbmm&|d@Ns@IS35g+qA`xAcjrav-%;h?`;#$V=#^N0DG^#MP6!(Ux4|I`on z$*ZV8>0>|VqwsCIUebP*KhB4i`RmMNkN)O%1o-m%oKa^x=QObzbtv z`38RK{Ia+oGM^J)H9y2Pa(wiA)|dJTKJLu#uJ_{?J{!iH{mDF0+_$dJ>`(HcdTpBT zy|7O|OTSUf3*ztD?~VK8X)zzR%=4}J!GFdJKD~FnpZibpqr7e#$5Ql{zJrhRm;M}h z-<|R8M10a?UNr2N{vbZ`4ZoYVM}C=4%nST8pYh+aU*f0#@ee7y8xwa)06eLHyXy`<3jD><51G zd{X_^&p+wWA1PPd%t!K9_m6(HKbYT7i~NI!-%)Qz{_qDstkqZPU*DV2zg2nWPv7CA zZ9nAe+Wl4bFZfzvUvcsOL7s=;Q@-l;Nj|Fll>C 0 + # if screenshots dir does not exist, will create and generate screenshots + if not os.path.exists(screenshots_dir): + os.mkdir(screenshots_dir) + screenshot_path = screenshots_dir / f"{module.stem}.npy" if regenerate_screenshots: From 70f2e4b6b28e952a34cb03fd102860ad031a446f Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 11 May 2023 17:48:51 -0400 Subject: [PATCH 53/96] adding diff storage for CI pipeline usage --- examples/tests/test_examples.py | 42 ++++++++++++++++++++++++++++++++- examples/tests/testutils.py | 1 + 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 58316c241..e79d98ef0 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -6,6 +6,7 @@ import pytest import os import numpy as np +import imageio.v3 as iio from .testutils import ( ROOT, @@ -14,6 +15,7 @@ find_examples, wgpu_backend, is_lavapipe, + diffs_dir ) # run all tests unless they opt-out @@ -75,7 +77,45 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal ), "found # test_example = true but no reference screenshot available" stored_img = np.load(screenshot_path) is_similar = np.allclose(img, stored_img, atol=1) - assert is_similar + update_diffs(module.stem, is_similar, img, stored_img) + assert is_similar, ( + f"rendered image for example {module.stem} changed, see " + f"the {diffs_dir.relative_to(ROOT).as_posix()} folder" + " for visual diffs (you can download this folder from" + " CI build artifacts as well)" + ) + + +def update_diffs(module, is_similar, img, stored_img): + diffs_dir.mkdir(exist_ok=True) + + diffs_rgba = None + + def get_diffs_rgba(slicer): + # lazily get and cache the diff computation + nonlocal diffs_rgba + if diffs_rgba is None: + # cast to float32 to avoid overflow + # compute absolute per-pixel difference + diffs_rgba = np.abs(stored_img.astype("f4") - img) + # magnify small values, making it easier to spot small errors + diffs_rgba = ((diffs_rgba / 255) ** 0.25) * 255 + # cast back to uint8 + diffs_rgba = diffs_rgba.astype("u1") + return diffs_rgba[..., slicer] + + # split into an rgb and an alpha diff + diffs = { + diffs_dir / f"{module}-rgb.png": slice(0, 3), + diffs_dir / f"{module}-alpha.png": 3, + } + + for path, slicer in diffs.items(): + if not is_similar: + diff = get_diffs_rgba(slicer) + iio.imwrite(path, diff) + elif path.exists(): + path.unlink() if __name__ == "__main__": diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 1733481e2..8e248e1e4 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).parents[2] # repo root examples_dir = ROOT / "examples" screenshots_dir = examples_dir / "screenshots" +diffs_dir = examples_dir / "diffs" # examples live in themed sub-folders example_globs = ["image/*.py", "scatter/*.py", "line/*.py", "gridplot/*.py"] From 37aa0a820cc0600db01b15e53ab8c2dd35ef7bff Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 14:32:35 -0400 Subject: [PATCH 54/96] tests changes, ready to start CI pipeline --- .github/workflows/ci.yml | 0 examples/tests/test_examples.py | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index e79d98ef0..85818ed05 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -49,7 +49,7 @@ def test_that_we_are_on_lavapipe(): @pytest.mark.parametrize("module", examples_to_test, ids=lambda x: x.stem) -def test_example_screenshots(module, force_offscreen, regenerate_screenshots=False): +def test_example_screenshots(module, force_offscreen): """Make sure that every example marked outputs the expected.""" # (relative) module name from project root module_name = module.relative_to(ROOT/"examples").with_suffix("").as_posix().replace("/", ".") @@ -63,14 +63,14 @@ def test_example_screenshots(module, force_offscreen, regenerate_screenshots=Fal # check if _something_ was rendered assert img is not None and img.size > 0 - # if screenshots dir does not exist, will create and generate screenshots + # if screenshots dir does not exist, will create if not os.path.exists(screenshots_dir): os.mkdir(screenshots_dir) screenshot_path = screenshots_dir / f"{module.stem}.npy" - if regenerate_screenshots: - np.save(screenshot_path, img) + # if regenerate_screenshots == "True": + # np.save(screenshot_path, img) assert ( screenshot_path.exists() @@ -120,4 +120,4 @@ def get_diffs_rgba(slicer): if __name__ == "__main__": test_examples_run("simple") - test_example_screenshots("simple", regenerate_screenshots=True) + test_example_screenshots("simple") From ebe121d09f2c67f549bf9a9001fe55806d92516a Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 15:28:19 -0400 Subject: [PATCH 55/96] allow for screenshots to be regenerated via os.environ key --- examples/tests/test_examples.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 85818ed05..5aa78a164 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -69,8 +69,9 @@ def test_example_screenshots(module, force_offscreen): screenshot_path = screenshots_dir / f"{module.stem}.npy" - # if regenerate_screenshots == "True": - # np.save(screenshot_path, img) + if "REGENERATE_SCREENSHOTS" in os.environ.keys(): + if os.environ["REGENERATE_SCREENSHOTS"] == "1": + np.save(screenshot_path, img) assert ( screenshot_path.exists() From 3161cdebfdd269b8b89cd0f9f95f69337db85a95 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 16:52:59 -0400 Subject: [PATCH 56/96] CI build --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29bb..7708aa2df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: + - master + - fpl-tests + pull_request: + branches: + - master + + jobs: + + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up Python '3.10' + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install package and dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + + test-examples-build: + name: Test examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: + REGENERATE_SCREENSHOTS=1 pytest -v examples + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: screenshot-diffs + path: examples/screenshots/diffs \ No newline at end of file From e2c76371f3c2d19d94ea01a5e630dfeb1b50cab6 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 17:28:02 -0400 Subject: [PATCH 57/96] linux-build passes, still working on examples passing --- .github/workflows/ci.yml | 98 ++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7708aa2df..ca427dccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,53 +9,55 @@ on: branches: - master - jobs: +jobs: - build: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - steps: - - uses: actions/checkout@v3 - - name: Set up Python '3.10' - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - name: Install package and dev dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -e . + linux-build: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up Python '3.10' + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install package and dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . - test-examples-build: - name: Test examples - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - name: Install llvmpipe and lavapipe for offscreen canvas - run: | - sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - - name: Install dev dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - - name: Show wgpu backend - run: - python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" - - name: Test examples - env: - PYGFX_EXPECT_LAVAPIPE: true - run: - REGENERATE_SCREENSHOTS=1 pytest -v examples - - uses: actions/upload-artifact@v3 - if: ${{ failure() }} - with: - name: screenshot-diffs - path: examples/screenshots/diffs \ No newline at end of file +# test-examples-build: +# name: Test examples +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# steps: +# - uses: actions/checkout@v3 +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: '3.10' +# - name: Install llvmpipe and lavapipe for offscreen canvas +# run: | +# sudo apt-get update -y -qq +# sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt +# pip install -e . +# - name: Show wgpu backend +# run: +# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" +# - name: Test examples +# env: +# PYGFX_EXPECT_LAVAPIPE: true +# run: +# pip install pytest +# REGENERATE_SCREENSHOTS=1 pytest -v examples +# - uses: actions/upload-artifact@v3 +# if: ${{ failure() }} +# with: +# name: screenshot-diffs +# path: examples/screenshots/diffs \ No newline at end of file From 94214d0d661c20ee0a2d2ccc79d15ccd18150943 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 19:19:42 -0400 Subject: [PATCH 58/96] progress on CI pipeline, still need to figure out test build --- .github/workflows/ci.yml | 6 ++++-- requirements.txt | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca427dccc..65b671973 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - fpl-tests pull_request: branches: - master @@ -47,13 +46,16 @@ jobs: # python -m pip install --upgrade pip # pip install -r requirements.txt # pip install -e . +# git clone https://github.com/pygfx/pygfx.git +# cd pygfx +# git reset --hard 88ed3f31ed14840eb84259c3b1bd1cac740ecf25 # - name: Show wgpu backend # run: # python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" # - name: Test examples # env: # PYGFX_EXPECT_LAVAPIPE: true -# run: +# run: | # pip install pytest # REGENERATE_SCREENSHOTS=1 pytest -v examples # - uses: actions/upload-artifact@v3 diff --git a/requirements.txt b/requirements.txt index 79375c66a..d0bc814f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy jupyterlab jupyter_rfb pygfx>=0.1.10 +imageio \ No newline at end of file From 373a53dc94493d0b3c3cf9afd48af556967511d4 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 14 May 2023 20:16:12 -0400 Subject: [PATCH 59/96] still failing test build, what I have for now --- .github/workflows/ci.yml | 72 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65b671973..95e3ea516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,40 +26,38 @@ jobs: pip install -r requirements.txt pip install -e . -# test-examples-build: -# name: Test examples -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# steps: -# - uses: actions/checkout@v3 -# - name: Set up Python -# uses: actions/setup-python@v3 -# with: -# python-version: '3.10' -# - name: Install llvmpipe and lavapipe for offscreen canvas -# run: | -# sudo apt-get update -y -qq -# sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers -# - name: Install dev dependencies -# run: | -# python -m pip install --upgrade pip -# pip install -r requirements.txt -# pip install -e . -# git clone https://github.com/pygfx/pygfx.git -# cd pygfx -# git reset --hard 88ed3f31ed14840eb84259c3b1bd1cac740ecf25 -# - name: Show wgpu backend -# run: -# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" -# - name: Test examples -# env: -# PYGFX_EXPECT_LAVAPIPE: true -# run: | -# pip install pytest -# REGENERATE_SCREENSHOTS=1 pytest -v examples -# - uses: actions/upload-artifact@v3 -# if: ${{ failure() }} -# with: -# name: screenshot-diffs -# path: examples/screenshots/diffs \ No newline at end of file + test-examples-build: + name: Test examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: | + pip install pytest + pip install git+https://github.com/pygfx/pygfx.git@e683ae4542ae96ae8dce59a17f74b50bf996a4fa + REGENERATE_SCREENSHOTS=1 pytest -v examples + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: screenshot-diffs + path: examples/screenshots/diffs \ No newline at end of file From 6244d474c9c9c41c9ea9d5866f95d0da420bbe23 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 00:59:50 -0400 Subject: [PATCH 60/96] Update ci.yml use pygfx before pylinalg refactor --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95e3ea516..ebca8790d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,10 +54,10 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest - pip install git+https://github.com/pygfx/pygfx.git@e683ae4542ae96ae8dce59a17f74b50bf996a4fa + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} with: name: screenshot-diffs - path: examples/screenshots/diffs \ No newline at end of file + path: examples/screenshots/diffs From e1ee831703947438df94dc471157a9c9b630a227 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:17:02 -0400 Subject: [PATCH 61/96] Update ci.yml sed to remove pygfx from setup.py --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebca8790d..15837db94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,9 @@ jobs: - name: Install package and dev dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 pip install -e . test-examples-build: @@ -44,7 +46,9 @@ jobs: - name: Install dev dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 pip install -e . - name: Show wgpu backend run: @@ -54,7 +58,6 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest - pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} From 10504e8a816a6047af8251a26b6f86db6f3ab461 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:19:57 -0400 Subject: [PATCH 62/96] Update ci.yml imageio --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15837db94..195d199cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest + pip install pytest imageio REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} From 805fa8711434639ccccb3fa3cebeb2753340dd8d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 18 May 2023 01:29:28 -0400 Subject: [PATCH 63/96] Update ci.yml testing upload screens --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 195d199cc..bbc47b7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: pip install pytest imageio REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 - if: ${{ failure() }} + #if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs From aa6dadc25f44a7533ae74c48b2bb82c7827fcaf0 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 22 May 2023 13:12:50 -0400 Subject: [PATCH 64/96] changing linux build to have python versions matrix --- .github/workflows/ci.yml | 42 +++- examples/gridplot/__init__.py | 0 examples/image/__init__.py | 0 examples/image/{cmap.py => image_cmap.py} | 0 examples/image/{rgb.py => image_rgb.py} | 0 .../{rgbvminvmax.py => image_rgbvminvmax.py} | 0 examples/image/{simple.py => image_simple.py} | 0 .../image/{vminvmax.py => image_vminvmax.py} | 0 examples/line/__init__.py | 0 .../{colorslice.py => line_colorslice.py} | 0 .../line/{dataslice.py => line_dataslice.py} | 0 ...ent_scaling.py => line_present_scaling.py} | 0 examples/scatter/__init__.py | 0 notebooks/line_collection_event.ipynb | 212 ----------------- notebooks/single_contour_event.ipynb | 225 ------------------ 15 files changed, 38 insertions(+), 441 deletions(-) create mode 100644 examples/gridplot/__init__.py create mode 100644 examples/image/__init__.py rename examples/image/{cmap.py => image_cmap.py} (100%) rename examples/image/{rgb.py => image_rgb.py} (100%) rename examples/image/{rgbvminvmax.py => image_rgbvminvmax.py} (100%) rename examples/image/{simple.py => image_simple.py} (100%) rename examples/image/{vminvmax.py => image_vminvmax.py} (100%) create mode 100644 examples/line/__init__.py rename examples/line/{colorslice.py => line_colorslice.py} (100%) rename examples/line/{dataslice.py => line_dataslice.py} (100%) rename examples/line/{present_scaling.py => line_present_scaling.py} (100%) create mode 100644 examples/scatter/__init__.py delete mode 100644 notebooks/line_collection_event.ipynb delete mode 100644 notebooks/single_contour_event.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc47b7ec..945635dad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,27 @@ on: jobs: - linux-build: + test-linux-builds: + name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: - max-parallel: 5 + fail-fast: false + matrix: + include: + - name: Test py38 + pyversion: '3.8' + - name: Test py39 + pyversion: '3.9' + - name: Test py310 + pyversion: '3.10' + - name: Test py311 + pyversion: '3.11' steps: - uses: actions/checkout@v3 - - name: Set up Python '3.10' + - name: Set up Python ${{ matrix.pyversion }} uses: actions/setup-python@v3 with: - python-version: '3.10' + python-version: ${{ matrix.pyversion }} - name: Install package and dev dependencies run: | python -m pip install --upgrade pip @@ -64,3 +75,26 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs + +# test-notebooks-build: +# name: Test notebooks +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# steps: +# - uses: actions/checkout@v3 +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: '3.10' +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip +# # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving +# sed -i "/pygfx/d" ./setup.py +# pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 +# pip install -e . +# - name: Test notebooks +# run: | +# pip install pytest nbmake +# pytest --nbmake notebooks/simple.ipynb diff --git a/examples/gridplot/__init__.py b/examples/gridplot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/image/__init__.py b/examples/image/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/image/cmap.py b/examples/image/image_cmap.py similarity index 100% rename from examples/image/cmap.py rename to examples/image/image_cmap.py diff --git a/examples/image/rgb.py b/examples/image/image_rgb.py similarity index 100% rename from examples/image/rgb.py rename to examples/image/image_rgb.py diff --git a/examples/image/rgbvminvmax.py b/examples/image/image_rgbvminvmax.py similarity index 100% rename from examples/image/rgbvminvmax.py rename to examples/image/image_rgbvminvmax.py diff --git a/examples/image/simple.py b/examples/image/image_simple.py similarity index 100% rename from examples/image/simple.py rename to examples/image/image_simple.py diff --git a/examples/image/vminvmax.py b/examples/image/image_vminvmax.py similarity index 100% rename from examples/image/vminvmax.py rename to examples/image/image_vminvmax.py diff --git a/examples/line/__init__.py b/examples/line/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/line/colorslice.py b/examples/line/line_colorslice.py similarity index 100% rename from examples/line/colorslice.py rename to examples/line/line_colorslice.py diff --git a/examples/line/dataslice.py b/examples/line/line_dataslice.py similarity index 100% rename from examples/line/dataslice.py rename to examples/line/line_dataslice.py diff --git a/examples/line/present_scaling.py b/examples/line/line_present_scaling.py similarity index 100% rename from examples/line/present_scaling.py rename to examples/line/line_present_scaling.py diff --git a/examples/scatter/__init__.py b/examples/scatter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notebooks/line_collection_event.ipynb b/notebooks/line_collection_event.ipynb deleted file mode 100644 index c88971042..000000000 --- a/notebooks/line_collection_event.ipynb +++ /dev/null @@ -1,212 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "f02f7349-ac1a-49b7-b304-d5b701723e0f", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d9f54448-7718-4212-ac6d-163a2d3be146", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib.graphics import LineGraphic, LineCollection, ImageGraphic\n", - "from fastplotlib.plot import Plot\n", - "import pickle" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b683c6d1-d926-43e3-a221-4ec6450e3677", - "metadata": {}, - "outputs": [], - "source": [ - "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "26ced005-c52f-4696-903d-a6974ae6cefc", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2bc75fa52d484cd1b03006a6530f10b3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", - "\n" - ] - } - ], - "source": [ - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "1aea0a61-db63-41e1-b330-5a922da4bac5", - "metadata": {}, - "outputs": [], - "source": [ - "line_collection = LineCollection(contours, cmap=\"Oranges\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "310fd5b3-49f2-4dbb-831e-cb9f369d58c8", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(line_collection)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "661a5991-0de3-44d7-a626-2ae72704dcec", - "metadata": {}, - "outputs": [], - "source": [ - "image = ImageGraphic(data=np.ones((180, 180)))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "816f6382-f4ea-4cd4-b9f3-2dc5c232b0a5", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(image)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8187fd25-18e1-451f-b2fe-8cd2e7785c8b", - "metadata": {}, - "outputs": [], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5ddd3c4-84e2-44a3-a14f-74871aa0bb8f", - "metadata": {}, - "outputs": [], - "source": [ - "black = np.array([0.0, 0.0, 0.0, 1.0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ac64f1c-21e0-4c21-b968-3953e7858848", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import *" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d0d0c971-aac9-4c77-b88c-de9d79c7d74e", - "metadata": {}, - "outputs": [], - "source": [ - "def callback_function(source: Any, target: Any, event, new_data: Any):\n", - " # calculate coms of line collection\n", - " indices = np.array(event.pick_info[\"index\"])\n", - " \n", - " coms = list()\n", - "\n", - " for contour in target.items:\n", - " coors = contour.data.feature_data[~np.isnan(contour.data.feature_data).any(axis=1)]\n", - " com = coors.mean(axis=0)\n", - " coms.append(com)\n", - "\n", - " # euclidean distance to find closest index of com \n", - " indices = np.append(indices, [0])\n", - " \n", - " ix = np.linalg.norm((coms - indices), axis=1).argsort()[0]\n", - " \n", - " ix = int(ix)\n", - " \n", - " print(ix)\n", - " \n", - " target._set_feature(feature=\"colors\", new_data=new_data, indices=ix)\n", - " \n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a46d148-3007-4c7a-bf2b-91057eba855d", - "metadata": {}, - "outputs": [], - "source": [ - "image.link(event_type=\"click\", target=line_collection, feature=\"colors\", new_data=black, callback_function=callback_function)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8040456e-24a5-423b-8822-99a20e7ea470", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/single_contour_event.ipynb b/notebooks/single_contour_event.ipynb deleted file mode 100644 index a4b7f1f91..000000000 --- a/notebooks/single_contour_event.ipynb +++ /dev/null @@ -1,225 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "3a992f41-b157-4b6f-9630-ef370389f318", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "589eaea4-e749-46ff-ac3d-e22aa4f75641", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib.graphics import LineGraphic\n", - "from fastplotlib.plot import Plot\n", - "import pickle" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "650279ac-e7df-4c6f-aac1-078ae4287028", - "metadata": {}, - "outputs": [], - "source": [ - "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "49dc6123-39d8-4f60-b14b-9cfd9a008940", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour = LineGraphic(data=contours[0], size=10.0, cmap=\"jet\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "776e916f-16c9-4114-b1ff-7ea209aa7b04", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b9c85f639ca34c6c882396fc4753818b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", - "\n" - ] - } - ], - "source": [ - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "203768f0-6bb4-4ba9-b099-395f2bdd2a8c", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(single_contour)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4e46d687-d81a-4b6f-bece-c9edf3606d4f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "730d202ce0424559b30cd4d7d5f3b77b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "dbcda1d6-3f21-4a5e-b60f-75bf9103fbe6", - "metadata": {}, - "outputs": [], - "source": [ - "white = np.ones(shape=single_contour.colors.feature_data.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6db453cf-5856-4a7b-879f-83032cb9e9ac", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour.link(event_type=\"click\", target=single_contour, feature=\"colors\", new_data=white, indices_mapper=None)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "712c3f43-3339-4d1b-9d64-fe4f4d6bd672", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'click': [CallbackData(target=fastplotlib.LineGraphic @ 0x7f516cc02880, feature='colors', new_data=array([[1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.],\n", - " [1., 1., 1., 1.]]), indices_mapper=None)]}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "single_contour.registered_callbacks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d353d7b-a0d0-4629-a8c0-87b767d99bd2", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 05f21b713f8993c8511d50eae8e3e13b40c23d28 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Fri, 2 Jun 2023 16:27:11 -0400 Subject: [PATCH 65/96] nbmake tests work --- .github/workflows/ci.yml | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 945635dad..34c2c66cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,6 @@ jobs: fail-fast: false matrix: include: - - name: Test py38 - pyversion: '3.8' - name: Test py39 pyversion: '3.9' - name: Test py310 @@ -68,33 +66,12 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest imageio + pip install pytest imageio nbmake scipy REGENERATE_SCREENSHOTS=1 pytest -v examples + pytest --nbmake notebooks/simple.ipynb - uses: actions/upload-artifact@v3 #if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs -# test-notebooks-build: -# name: Test notebooks -# runs-on: ubuntu-latest -# strategy: -# fail-fast: false -# steps: -# - uses: actions/checkout@v3 -# - name: Set up Python -# uses: actions/setup-python@v3 -# with: -# python-version: '3.10' -# - name: Install dev dependencies -# run: | -# python -m pip install --upgrade pip -# # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving -# sed -i "/pygfx/d" ./setup.py -# pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 -# pip install -e . -# - name: Test notebooks -# run: | -# pip install pytest nbmake -# pytest --nbmake notebooks/simple.ipynb From 34ea40d693598a27a29b70e6b4a166ef789b711e Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 5 Jun 2023 14:56:40 -0400 Subject: [PATCH 66/96] updating CI file --- .github/workflows/ci.yml | 27 +---- notebooks/garbage_collection.ipynb | 48 +++++---- notebooks/gridplot_simple.py | 53 ---------- notebooks/histogram.ipynb | 122 ---------------------- notebooks/text.ipynb | 161 ----------------------------- 5 files changed, 32 insertions(+), 379 deletions(-) delete mode 100644 notebooks/gridplot_simple.py delete mode 100644 notebooks/histogram.ipynb delete mode 100644 notebooks/text.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34c2c66cd..32c211d40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ on: - master jobs: - - test-linux-builds: - name: ${{ matrix.name }} + + test-examples-build: + name: Test examples runs-on: ubuntu-latest strategy: fail-fast: false @@ -25,29 +25,10 @@ jobs: pyversion: '3.11' steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.pyversion }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.pyversion }} - - name: Install package and dev dependencies - run: | - python -m pip install --upgrade pip - # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving - sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 - pip install -e . - - test-examples-build: - name: Test examples - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: - python-version: '3.10' + python-version: ${{ matrix.pyversion }} - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq diff --git a/notebooks/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb index 85744e6e0..4a066a912 100644 --- a/notebooks/garbage_collection.ipynb +++ b/notebooks/garbage_collection.ipynb @@ -203,7 +203,9 @@ "cell_type": "code", "execution_count": null, "id": "dd6a26c1-ea81-469d-ae7a-95839b1f9d5a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -278,7 +280,9 @@ "cell_type": "code", "execution_count": null, "id": "2599f430-8b00-4490-9e11-774897be6e77", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -307,7 +311,9 @@ "cell_type": "code", "execution_count": null, "id": "3ec10f26-6544-4ad3-80c1-aa34617dc826", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import weakref" @@ -317,7 +323,9 @@ "cell_type": "code", "execution_count": null, "id": "acc819a3-cd50-4fdd-a0b5-c442d80847e2", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img = make_image()\n", @@ -328,7 +336,9 @@ "cell_type": "code", "execution_count": null, "id": "f89da335-3372-486b-b773-9f103d6a9bbd", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img_ref()" @@ -338,7 +348,9 @@ "cell_type": "code", "execution_count": null, "id": "c22904ad-d674-43e6-83bb-7a2f7b277c06", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "del img" @@ -348,7 +360,9 @@ "cell_type": "code", "execution_count": null, "id": "573566d7-eb91-4690-958c-d00dd495b3e4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import gc" @@ -358,27 +372,21 @@ "cell_type": "code", "execution_count": null, "id": "aaef3e89-2bfd-43af-9b8f-824a3f89b85f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img_ref()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "3380f35e-fcc9-43f6-80d2-7e9348cd13b4", - "metadata": {}, - "outputs": [], - "source": [ - "draw()" - ] - }, { "cell_type": "code", "execution_count": null, "id": "a27bf7c7-f3ef-4ae8-8ecf-31507f8c0449", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "print(\n", @@ -411,7 +419,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/notebooks/gridplot_simple.py b/notebooks/gridplot_simple.py deleted file mode 100644 index 382f2b486..000000000 --- a/notebooks/gridplot_simple.py +++ /dev/null @@ -1,53 +0,0 @@ -import numpy as np -from wgpu.gui.auto import WgpuCanvas -import pygfx as gfx -from fastplotlib.layouts import GridPlot -from fastplotlib.graphics import ImageGraphic, LineGraphic, HistogramGraphic -from fastplotlib import run -from math import sin, cos, radians - -# GridPlot of shape 2 x 3 -grid_plot = GridPlot(shape=(2, 3)) - -image_graphics = list() - -hist_data1 = np.random.normal(0, 256, 2048) -hist_data2 = np.random.poisson(0, 256) - -# Make a random image graphic for each subplot -for i, subplot in enumerate(grid_plot): - img = np.random.rand(512, 512) * 255 - ig = ImageGraphic(data=img, vmin=0, vmax=255, cmap='gnuplot2') - image_graphics.append(ig) - - # add the graphic to the subplot - subplot.add_graphic(ig) - - histogram = HistogramGraphic(data=hist_data1, bins=100) - histogram.world_object.rotation.w = cos(radians(45)) - histogram.world_object.rotation.z = sin(radians(45)) - - histogram.world_object.scale.y = 1 - histogram.world_object.scale.x = 8 - - for dv_position in ["right", "top", "bottom", "left"]: - h2 = HistogramGraphic(data=hist_data1, bins=100) - - subplot.docked_viewports[dv_position].size = 60 - subplot.docked_viewports[dv_position].add_graphic(h2) -# - -# Define a function to update the image graphics -# with new randomly generated data -def set_random_frame(): - for ig in image_graphics: - new_data = np.random.rand(512, 512) * 255 - ig.update_data(data=new_data) - - -# add the animation -# grid_plot.add_animations(set_random_frame) - -grid_plot.show() - -run() diff --git a/notebooks/histogram.ipynb b/notebooks/histogram.ipynb deleted file mode 100644 index d78a38282..000000000 --- a/notebooks/histogram.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "77bd602d-aed3-4ddc-9917-46f3429d32b9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib import Plot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0dc85d4e-4554-44e5-bf05-96ceb339e57f", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "27e7e540a7be408997b143ac274b2b8e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "data = np.random.normal(0, 256, 2048)\n", - "\n", - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ef4fbdb5-f7a3-44c3-acef-38d127dccdc9", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushal/python-venvs/mescore/lib/python3.10/site-packages/pygfx/geometries/_plane.py:19: RuntimeWarning: invalid value encountered in true_divide\n", - " texcoords = (positions[..., :2] + dim / 2) / dim\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ff05c3706cfd401eb97f58b6e12e6d2a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.add_histogram(data=data, bins=100)\n", - "\n", - "plot.set_axes_visibility(True)\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c72d91a-0771-46c1-a2b8-d555ee47291f", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/text.ipynb b/notebooks/text.ipynb deleted file mode 100644 index 53260fbf8..000000000 --- a/notebooks/text.ipynb +++ /dev/null @@ -1,161 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "fd2167fe-04de-42fc-a7e2-c96dac665656", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import Plot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "bc5acda4-9533-4619-a655-ed5404e00f5e", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e17c38170e864bd3a61cc7e662854dbe", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ea3c2c127b3f414eb96e03b8b775c80c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot = Plot()\n", - "\n", - "text_graphic = plot.text(\"hello world\")\n", - "\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "71bb2a1b-fc85-44f1-941a-6fc53b8fb8a4", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_text(\"foo\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "aa8aba1e-ee37-49c6-b483-234241871855", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_face_color(\"r\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "757b6012-c06b-46cf-b5a8-a874f4b0b99a", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_size(5)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "e3b8ace2-a7ca-4576-983f-9e667e3d2ba3", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_size(15)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "616faaef-4cbb-4fee-a6e1-9e684c7b4ebe", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_outline_size(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ee963404-afe7-47c9-b8d9-a3ac3d8ba23f", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_outline_color(\"b\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "0baf70d9-ee5b-41f7-9e27-bb8a38f498df", - "metadata": {}, - "outputs": [], - "source": [ - "text_graphic.update_position((5, -1, 10))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 58c3e483fd4c0dc5442109bf694ad882be075aa8 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:09:03 -0400 Subject: [PATCH 67/96] all tests passing locally --- .github/workflows/ci.yml | 6 +- notebooks/garbage_collection.ipynb | 427 ----------------------------- 2 files changed, 3 insertions(+), 430 deletions(-) delete mode 100644 notebooks/garbage_collection.ipynb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32c211d40..fc4ea8707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ on: - master jobs: - - test-examples-build: + + test-build: name: Test examples runs-on: ubuntu-latest strategy: @@ -49,7 +49,7 @@ jobs: run: | pip install pytest imageio nbmake scipy REGENERATE_SCREENSHOTS=1 pytest -v examples - pytest --nbmake notebooks/simple.ipynb + pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 #if: ${{ failure() }} with: diff --git a/notebooks/garbage_collection.ipynb b/notebooks/garbage_collection.ipynb deleted file mode 100644 index 4a066a912..000000000 --- a/notebooks/garbage_collection.ipynb +++ /dev/null @@ -1,427 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "1ef0578e-09e1-45ff-bd34-84472db3885e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from fastplotlib import Plot\n", - "import numpy as np\n", - "import sys\n", - "\n", - "import weakref\n", - "import gc\n", - "import os, psutil\n", - "process = psutil.Process(os.getpid())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1bb6bc6f-7786-4d23-9eb1-e30bbc66c798", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "def print_process_ram_mb():\n", - " print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b376676e-a7fe-4424-9ba6-fde5be03b649", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b23ba640-88ec-40d9-b53c-c8cbb3e39b0b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot = Plot()\n", - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17f46f21-b29d-4dd3-9496-989bbb240f50", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27627cd4-c363-4eab-a121-f6c8abbbe5ae", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "graphic = \"scatter\"" - ] - }, - { - "cell_type": "markdown", - "id": "d9c10edc-169a-4dd2-bd5b-8a1b67baf3a9", - "metadata": {}, - "source": [ - "### Run the following cells repeatedly to add and remove the graphic" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e26d392f-6afd-4e89-a685-d618065d3caf", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "if graphic == \"line\":\n", - " a = np.random.rand(10_000_000)\n", - " g = plot.add_line(a)\n", - " \n", - "elif graphic == \"heatmap\":\n", - " a = np.random.rand(20_000, 20_000)\n", - " g = plot.add_heatmap(a)\n", - "\n", - "elif graphic == \"line_collection\":\n", - " a = np.random.rand(500, 50_000)\n", - " g = plot.add_line_collection(a)\n", - " \n", - "elif graphic == \"image\":\n", - " a = np.random.rand(7_000, 7_000)\n", - " g = plot.add_image(a)\n", - "\n", - "elif graphic == \"scatter\":\n", - " a = np.random.rand(10_000_000, 3)\n", - " g = plot.add_scatter(a)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31e3027a-56cf-4f7b-ba78-aed4f78eef47", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print_process_ram_mb()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6518795c-98cf-405d-94ab-786ac3b2e1d6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "g" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee2481c9-82e3-4043-85fd-21a0cdf21187", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.auto_scale()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53170858-ae72-4451-8647-7d5b1f9da75e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4e0f73b-c58a-40e7-acf5-07a1f70d2821", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.delete_graphic(plot.graphics[0])" - ] - }, - { - "cell_type": "markdown", - "id": "47baa487-c66b-4c40-aa11-d819902870e3", - "metadata": {}, - "source": [ - "If there is no serious system memory leak, this value shouldn't really increase after repeated cycles" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56c64498-229e-48b7-9fb1-f7c327fff2ae", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(process.memory_info().rss / 1024 / 1024)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd6a26c1-ea81-469d-ae7a-95839b1f9d5a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from wgpu.gui.auto import WgpuCanvas, run\n", - "import pygfx as gfx\n", - "import subprocess\n", - "\n", - "canvas = WgpuCanvas()\n", - "renderer = gfx.WgpuRenderer(canvas)\n", - "scene = gfx.Scene()\n", - "camera = gfx.OrthographicCamera(5000, 5000)\n", - "camera.position.x = 2048\n", - "camera.position.y = 2048\n", - "\n", - "\n", - "def make_image():\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - "\n", - " return gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )\n", - "\n", - "\n", - "class Graphic:\n", - " def __init__(self):\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - " self.wo = gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )\n", - "\n", - "\n", - "def draw():\n", - " renderer.render(scene, camera)\n", - " canvas.request_draw()\n", - "\n", - "\n", - "def print_nvidia(msg):\n", - " print(msg)\n", - " print(\n", - " subprocess.check_output([\"nvidia-smi\", \"--format=csv\", \"--query-gpu=memory.used\"]).decode().split(\"\\n\")[1]\n", - " )\n", - " print()\n", - "\n", - "\n", - "def add_img(*args):\n", - " print_nvidia(\"Before creating image\")\n", - " img = make_image()\n", - " print_nvidia(\"After creating image\")\n", - " scene.add(img)\n", - " img.add_event_handler(remove_img, \"click\")\n", - " draw()\n", - " print_nvidia(\"After add image to scene\")\n", - "\n", - "\n", - "def remove_img(*args):\n", - " img = scene.children[0]\n", - " scene.remove(img)\n", - " draw()\n", - " print_nvidia(\"After remove image from scene\")\n", - " del img\n", - " draw()\n", - " print_nvidia(\"After del image\")\n", - "\n", - "\n", - "renderer.add_event_handler(add_img, \"double_click\")\n", - "canvas" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2599f430-8b00-4490-9e11-774897be6e77", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from wgpu.gui.auto import WgpuCanvas, run\n", - "import pygfx as gfx\n", - "import subprocess\n", - "\n", - "canvas = WgpuCanvas()\n", - "renderer = gfx.WgpuRenderer(canvas)\n", - "scene = gfx.Scene()\n", - "camera = gfx.OrthographicCamera(5000, 5000)\n", - "camera.position.x = 2048\n", - "camera.position.y = 2048\n", - "\n", - "\n", - "def make_image():\n", - " data = np.random.rand(4096, 4096).astype(np.float32)\n", - "\n", - " return gfx.Image(\n", - " gfx.Geometry(grid=gfx.Texture(data, dim=2)),\n", - " gfx.ImageBasicMaterial(clim=(0, 1)),\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ec10f26-6544-4ad3-80c1-aa34617dc826", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import weakref" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acc819a3-cd50-4fdd-a0b5-c442d80847e2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img = make_image()\n", - "img_ref = weakref.ref(img)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f89da335-3372-486b-b773-9f103d6a9bbd", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img_ref()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c22904ad-d674-43e6-83bb-7a2f7b277c06", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "del img" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "573566d7-eb91-4690-958c-d00dd495b3e4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import gc" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aaef3e89-2bfd-43af-9b8f-824a3f89b85f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img_ref()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a27bf7c7-f3ef-4ae8-8ecf-31507f8c0449", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "print(\n", - " subprocess.check_output([\"nvidia-smi\", \"--format=csv\", \"--query-gpu=memory.used\"]).decode().split(\"\\n\")[1]\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4bf2711-8a83-4d9c-a4f7-f50de7ae1715", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 8102ad9ce1dcabc2959cfe4284f6ee2be45faa23 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:25:56 -0400 Subject: [PATCH 68/96] trying to get CI to run on ghub --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc4ea8707..6a428ee7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - fpl-tests pull_request: branches: - master From 77cb69e2ed6b07aa87041370aa59918d2f2d573c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 09:37:45 -0400 Subject: [PATCH 69/96] change screenshots to be stored as png files --- .github/workflows/ci.yml | 3 +-- examples/tests/test_examples.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a428ee7d..6ef4c14c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - fpl-tests pull_request: branches: - master @@ -52,7 +51,7 @@ jobs: REGENERATE_SCREENSHOTS=1 pytest -v examples pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 - #if: ${{ failure() }} + if: ${{ failure() }} with: name: screenshot-diffs path: examples/screenshots/diffs diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 5aa78a164..b42a22bcc 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -67,16 +67,18 @@ def test_example_screenshots(module, force_offscreen): if not os.path.exists(screenshots_dir): os.mkdir(screenshots_dir) - screenshot_path = screenshots_dir / f"{module.stem}.npy" + screenshot_path = screenshots_dir / f"{module.stem}.png" if "REGENERATE_SCREENSHOTS" in os.environ.keys(): if os.environ["REGENERATE_SCREENSHOTS"] == "1": - np.save(screenshot_path, img) + iio.imwrite(screenshot_path, img) + #np.save(screenshot_path, img) assert ( screenshot_path.exists() ), "found # test_example = true but no reference screenshot available" - stored_img = np.load(screenshot_path) + #stored_img = np.load(screenshot_path) + stored_img = iio.imread(screenshot_path) is_similar = np.allclose(img, stored_img, atol=1) update_diffs(module.stem, is_similar, img, stored_img) assert is_similar, ( From 302c397f4717d9e3ebe0e7908f83cf3596380499 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 6 Jun 2023 12:31:24 -0400 Subject: [PATCH 70/96] rebase with master and update CI --- .github/workflows/ci.yml | 4 ++-- examples/line/line_colorslice.py | 10 +++++----- examples/line/line_dataslice.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ef4c14c4..976da8e0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,8 @@ jobs: run: | python -m pip install --upgrade pip # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving - sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@88ed3f31ed14840eb84259c3b1bd1cac740ecf25 + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d pip install -e . - name: Show wgpu backend run: diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index 1a4595196..2ee02b84c 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -55,11 +55,11 @@ cosine_graphic.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" -# -# key2 = np.array([True, False, True, False, True, True, True, True]) -# cosine_graphic.colors[key2] = "Green" +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" + +key2 = np.array([True, False, True, False, True, True, True, True]) +cosine_graphic.colors[key2] = "Green" plot.center_scene() diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index b8a30cb01..975b7360a 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -47,8 +47,8 @@ cosine_graphic.data[0] = np.array([[-10, 0, 0]]) # additional fancy indexing using numpy -# key2 = np.array([True, False, True, False, True, True, True, True]) -# sinc_graphic.data[key2] = np.array([[5, 1, 2]]) +key2 = np.array([True, False, True, False, True, True, True, True]) +sinc_graphic.data[key2] = np.array([[5, 1, 2]]) plot.center_scene() From b18b1c5ba346d2de5b0693e946e1777eefe125b3 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Jun 2023 13:13:54 -0400 Subject: [PATCH 71/96] track *.png files using Git LFS --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..24a8e8793 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text From 42c52211f1163b4b7f8e572097b206fd7c6fcd20 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Jun 2023 13:15:37 -0400 Subject: [PATCH 72/96] yml for regenerating screenshots, updating CI yml to not regenerate --- .github/workflows/ci.yml | 2 +- .github/workflows/screenshots.yml | 42 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/screenshots.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d52dc2a0f..80a271f73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest imageio nbmake scipy - REGENERATE_SCREENSHOTS=1 pytest -v examples + pytest -v examples pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 if: ${{ failure() }} diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml new file mode 100644 index 000000000..735b65516 --- /dev/null +++ b/.github/workflows/screenshots.yml @@ -0,0 +1,42 @@ +name: Screenshots + +on: + pull_request: + branches: + - master + +jobs: + screenshots: + name: Regenerate + runs-on: 'ubuntu-latest' + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d + pip install -e . + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: | + pip install pytest imageio scipy + REGENERATE_SCREENSHOTS=1 pytest -v -k test_examples_screenshots examples + - uses: actions/upload-artifact@v3 + if: always() + with: + name: screenshots + path: examples/screenshots/ \ No newline at end of file From de62e04f8dc08acd71046371e589fc729500472f Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Jun 2023 14:22:17 -0400 Subject: [PATCH 73/96] update .gitignore and regen screenshots yml --- .github/workflows/screenshots.yml | 2 +- .gitignore | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 735b65516..d8e3d2740 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -34,7 +34,7 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest imageio scipy - REGENERATE_SCREENSHOTS=1 pytest -v -k test_examples_screenshots examples + REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: always() with: diff --git a/.gitignore b/.gitignore index e46391f7c..f87eb1c51 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,3 @@ dmypy.json # Pycharm .idea/ -# Binary files for testing -examples/screenshots - From 90a0e011c3c1591150b1c36f59ee37bc88938798 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Jun 2023 14:45:25 -0400 Subject: [PATCH 74/96] hopefully works --- .github/workflows/screenshots.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index d8e3d2740..c2259ee06 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -34,6 +34,7 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pip install pytest imageio scipy + # regenerate screenshots REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: always() From 5c5569dbabf8aa0847af1a4cb550b9a61e29440e Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Jun 2023 15:00:41 -0400 Subject: [PATCH 75/96] add screenshots --- examples/screenshots/gridplot.png | 3 +++ examples/screenshots/image_cmap.png | 3 +++ examples/screenshots/image_rgb.png | 3 +++ examples/screenshots/image_rgbvminvmax.png | 3 +++ examples/screenshots/image_simple.png | 3 +++ examples/screenshots/image_vminvmax.png | 3 +++ examples/screenshots/line.png | 3 +++ examples/screenshots/line_colorslice.png | 3 +++ examples/screenshots/line_dataslice.png | 3 +++ examples/screenshots/line_present_scaling.png | 3 +++ examples/screenshots/scatter.png | 3 +++ examples/screenshots/scatter_cmap.png | 3 +++ examples/screenshots/scatter_colorslice.png | 3 +++ examples/screenshots/scatter_dataslice.png | 3 +++ examples/screenshots/scatter_present.png | 3 +++ 15 files changed, 45 insertions(+) create mode 100644 examples/screenshots/gridplot.png create mode 100644 examples/screenshots/image_cmap.png create mode 100644 examples/screenshots/image_rgb.png create mode 100644 examples/screenshots/image_rgbvminvmax.png create mode 100644 examples/screenshots/image_simple.png create mode 100644 examples/screenshots/image_vminvmax.png create mode 100644 examples/screenshots/line.png create mode 100644 examples/screenshots/line_colorslice.png create mode 100644 examples/screenshots/line_dataslice.png create mode 100644 examples/screenshots/line_present_scaling.png create mode 100644 examples/screenshots/scatter.png create mode 100644 examples/screenshots/scatter_cmap.png create mode 100644 examples/screenshots/scatter_colorslice.png create mode 100644 examples/screenshots/scatter_dataslice.png create mode 100644 examples/screenshots/scatter_present.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png new file mode 100644 index 000000000..f746e57a9 --- /dev/null +++ b/examples/screenshots/gridplot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:619f679d58483882ade3163ba54abe35fe2ff5bea2c64d6592faf4889f1c5590 +size 178043 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png new file mode 100644 index 000000000..0485152e4 --- /dev/null +++ b/examples/screenshots/image_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c +size 196267 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png new file mode 100644 index 000000000..0485152e4 --- /dev/null +++ b/examples/screenshots/image_rgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c +size 196267 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png new file mode 100644 index 000000000..b41adf788 --- /dev/null +++ b/examples/screenshots/image_rgbvminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 +size 30165 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png new file mode 100644 index 000000000..5945a9957 --- /dev/null +++ b/examples/screenshots/image_simple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f527e760eb905266557b296e00b1aa36c74afa7986f98a5c9f5f36268bbf813a +size 186638 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png new file mode 100644 index 000000000..b41adf788 --- /dev/null +++ b/examples/screenshots/image_vminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 +size 30165 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png new file mode 100644 index 000000000..a680a39c2 --- /dev/null +++ b/examples/screenshots/line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:725427d4b4bb97ac72c0c6a96b91a57fecf61421106042e608878f862119e8d2 +size 28311 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png new file mode 100644 index 000000000..3f9f6defe --- /dev/null +++ b/examples/screenshots/line_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1b2e37f6e4b577e4411f8cc4edbf7520251290e2c33db895331dea2394ba9bc +size 31890 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png new file mode 100644 index 000000000..0bd31928c --- /dev/null +++ b/examples/screenshots/line_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333df52503cf880e58275df3d9e4b750ece425237857cbd975771fde2d821324 +size 41477 diff --git a/examples/screenshots/line_present_scaling.png b/examples/screenshots/line_present_scaling.png new file mode 100644 index 000000000..307e5d562 --- /dev/null +++ b/examples/screenshots/line_present_scaling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:097c661c8bae4b49096169d56ed0d21c47877cfd3dcaa808a8798011f50dbb17 +size 20072 diff --git a/examples/screenshots/scatter.png b/examples/screenshots/scatter.png new file mode 100644 index 000000000..3f804e284 --- /dev/null +++ b/examples/screenshots/scatter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b28abaccc6875eef9d0c551ed2515e850d3ad1161ba7bee640e3fe8c9622d64c +size 18261 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png new file mode 100644 index 000000000..493fd858d --- /dev/null +++ b/examples/screenshots/scatter_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac7903576054e8e465da144a98af6d1ec2220f29046da334aa5bc975cf3090f +size 19640 diff --git a/examples/screenshots/scatter_colorslice.png b/examples/screenshots/scatter_colorslice.png new file mode 100644 index 000000000..a62ebe37a --- /dev/null +++ b/examples/screenshots/scatter_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6cc3cb3c342d7b69ad733440ac5fed88dc497d194a88ba21b32d4768e469a02 +size 16770 diff --git a/examples/screenshots/scatter_dataslice.png b/examples/screenshots/scatter_dataslice.png new file mode 100644 index 000000000..ade5f2cfb --- /dev/null +++ b/examples/screenshots/scatter_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a37c7216d3dc514e6580659041e37f0f672effa2f1437a605d698fdb2bc28c9 +size 19482 diff --git a/examples/screenshots/scatter_present.png b/examples/screenshots/scatter_present.png new file mode 100644 index 000000000..63a79c987 --- /dev/null +++ b/examples/screenshots/scatter_present.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b6ca8b21ec58dac6610d78676a47ecd5975d83b6f9a5fee039667e2d2d20842 +size 15504 From 035d2d0ce9000a0fda09d3b04b5a4f6db1d23014 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:07:44 -0400 Subject: [PATCH 76/96] remove screenshots from git --- examples/screenshots/gridplot.png | 3 --- examples/screenshots/image_cmap.png | 3 --- examples/screenshots/image_rgb.png | 3 --- examples/screenshots/image_rgbvminvmax.png | 3 --- examples/screenshots/image_simple.png | 3 --- examples/screenshots/image_vminvmax.png | 3 --- examples/screenshots/line.png | 3 --- examples/screenshots/line_colorslice.png | 3 --- examples/screenshots/line_dataslice.png | 3 --- examples/screenshots/line_present_scaling.png | 3 --- examples/screenshots/scatter.png | 3 --- examples/screenshots/scatter_cmap.png | 3 --- examples/screenshots/scatter_colorslice.png | 3 --- examples/screenshots/scatter_dataslice.png | 3 --- examples/screenshots/scatter_present.png | 3 --- 15 files changed, 45 deletions(-) delete mode 100644 examples/screenshots/gridplot.png delete mode 100644 examples/screenshots/image_cmap.png delete mode 100644 examples/screenshots/image_rgb.png delete mode 100644 examples/screenshots/image_rgbvminvmax.png delete mode 100644 examples/screenshots/image_simple.png delete mode 100644 examples/screenshots/image_vminvmax.png delete mode 100644 examples/screenshots/line.png delete mode 100644 examples/screenshots/line_colorslice.png delete mode 100644 examples/screenshots/line_dataslice.png delete mode 100644 examples/screenshots/line_present_scaling.png delete mode 100644 examples/screenshots/scatter.png delete mode 100644 examples/screenshots/scatter_cmap.png delete mode 100644 examples/screenshots/scatter_colorslice.png delete mode 100644 examples/screenshots/scatter_dataslice.png delete mode 100644 examples/screenshots/scatter_present.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png deleted file mode 100644 index f746e57a9..000000000 --- a/examples/screenshots/gridplot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:619f679d58483882ade3163ba54abe35fe2ff5bea2c64d6592faf4889f1c5590 -size 178043 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png deleted file mode 100644 index 0485152e4..000000000 --- a/examples/screenshots/image_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c -size 196267 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png deleted file mode 100644 index 0485152e4..000000000 --- a/examples/screenshots/image_rgb.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c -size 196267 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png deleted file mode 100644 index b41adf788..000000000 --- a/examples/screenshots/image_rgbvminvmax.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 -size 30165 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png deleted file mode 100644 index 5945a9957..000000000 --- a/examples/screenshots/image_simple.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f527e760eb905266557b296e00b1aa36c74afa7986f98a5c9f5f36268bbf813a -size 186638 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png deleted file mode 100644 index b41adf788..000000000 --- a/examples/screenshots/image_vminvmax.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 -size 30165 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png deleted file mode 100644 index a680a39c2..000000000 --- a/examples/screenshots/line.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:725427d4b4bb97ac72c0c6a96b91a57fecf61421106042e608878f862119e8d2 -size 28311 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png deleted file mode 100644 index 3f9f6defe..000000000 --- a/examples/screenshots/line_colorslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1b2e37f6e4b577e4411f8cc4edbf7520251290e2c33db895331dea2394ba9bc -size 31890 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png deleted file mode 100644 index 0bd31928c..000000000 --- a/examples/screenshots/line_dataslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:333df52503cf880e58275df3d9e4b750ece425237857cbd975771fde2d821324 -size 41477 diff --git a/examples/screenshots/line_present_scaling.png b/examples/screenshots/line_present_scaling.png deleted file mode 100644 index 307e5d562..000000000 --- a/examples/screenshots/line_present_scaling.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:097c661c8bae4b49096169d56ed0d21c47877cfd3dcaa808a8798011f50dbb17 -size 20072 diff --git a/examples/screenshots/scatter.png b/examples/screenshots/scatter.png deleted file mode 100644 index 3f804e284..000000000 --- a/examples/screenshots/scatter.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b28abaccc6875eef9d0c551ed2515e850d3ad1161ba7bee640e3fe8c9622d64c -size 18261 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png deleted file mode 100644 index 493fd858d..000000000 --- a/examples/screenshots/scatter_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ac7903576054e8e465da144a98af6d1ec2220f29046da334aa5bc975cf3090f -size 19640 diff --git a/examples/screenshots/scatter_colorslice.png b/examples/screenshots/scatter_colorslice.png deleted file mode 100644 index a62ebe37a..000000000 --- a/examples/screenshots/scatter_colorslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6cc3cb3c342d7b69ad733440ac5fed88dc497d194a88ba21b32d4768e469a02 -size 16770 diff --git a/examples/screenshots/scatter_dataslice.png b/examples/screenshots/scatter_dataslice.png deleted file mode 100644 index ade5f2cfb..000000000 --- a/examples/screenshots/scatter_dataslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a37c7216d3dc514e6580659041e37f0f672effa2f1437a605d698fdb2bc28c9 -size 19482 diff --git a/examples/screenshots/scatter_present.png b/examples/screenshots/scatter_present.png deleted file mode 100644 index 63a79c987..000000000 --- a/examples/screenshots/scatter_present.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b6ca8b21ec58dac6610d78676a47ecd5975d83b6f9a5fee039667e2d2d20842 -size 15504 From 8ee72f58665d4c4d2992e8100830ce29991be37b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:13:15 -0400 Subject: [PATCH 77/96] add screenshots, yet again --- examples/screenshots/gridplot.png | 3 +++ examples/screenshots/image_cmap.png | 3 +++ examples/screenshots/image_rgb.png | 3 +++ examples/screenshots/image_rgbvminvmax.png | 3 +++ examples/screenshots/image_simple.png | 3 +++ examples/screenshots/image_vminvmax.png | 3 +++ examples/screenshots/line.png | 3 +++ examples/screenshots/line_colorslice.png | 3 +++ examples/screenshots/line_dataslice.png | 3 +++ examples/screenshots/line_present_scaling.png | 3 +++ examples/screenshots/scatter.png | 3 +++ examples/screenshots/scatter_cmap.png | 3 +++ examples/screenshots/scatter_colorslice.png | 3 +++ examples/screenshots/scatter_dataslice.png | 3 +++ examples/screenshots/scatter_present.png | 3 +++ 15 files changed, 45 insertions(+) create mode 100644 examples/screenshots/gridplot.png create mode 100644 examples/screenshots/image_cmap.png create mode 100644 examples/screenshots/image_rgb.png create mode 100644 examples/screenshots/image_rgbvminvmax.png create mode 100644 examples/screenshots/image_simple.png create mode 100644 examples/screenshots/image_vminvmax.png create mode 100644 examples/screenshots/line.png create mode 100644 examples/screenshots/line_colorslice.png create mode 100644 examples/screenshots/line_dataslice.png create mode 100644 examples/screenshots/line_present_scaling.png create mode 100644 examples/screenshots/scatter.png create mode 100644 examples/screenshots/scatter_cmap.png create mode 100644 examples/screenshots/scatter_colorslice.png create mode 100644 examples/screenshots/scatter_dataslice.png create mode 100644 examples/screenshots/scatter_present.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png new file mode 100644 index 000000000..f746e57a9 --- /dev/null +++ b/examples/screenshots/gridplot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:619f679d58483882ade3163ba54abe35fe2ff5bea2c64d6592faf4889f1c5590 +size 178043 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png new file mode 100644 index 000000000..0485152e4 --- /dev/null +++ b/examples/screenshots/image_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c +size 196267 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png new file mode 100644 index 000000000..0485152e4 --- /dev/null +++ b/examples/screenshots/image_rgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c +size 196267 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png new file mode 100644 index 000000000..b41adf788 --- /dev/null +++ b/examples/screenshots/image_rgbvminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 +size 30165 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png new file mode 100644 index 000000000..5945a9957 --- /dev/null +++ b/examples/screenshots/image_simple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f527e760eb905266557b296e00b1aa36c74afa7986f98a5c9f5f36268bbf813a +size 186638 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png new file mode 100644 index 000000000..b41adf788 --- /dev/null +++ b/examples/screenshots/image_vminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 +size 30165 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png new file mode 100644 index 000000000..a680a39c2 --- /dev/null +++ b/examples/screenshots/line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:725427d4b4bb97ac72c0c6a96b91a57fecf61421106042e608878f862119e8d2 +size 28311 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png new file mode 100644 index 000000000..3f9f6defe --- /dev/null +++ b/examples/screenshots/line_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1b2e37f6e4b577e4411f8cc4edbf7520251290e2c33db895331dea2394ba9bc +size 31890 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png new file mode 100644 index 000000000..0bd31928c --- /dev/null +++ b/examples/screenshots/line_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333df52503cf880e58275df3d9e4b750ece425237857cbd975771fde2d821324 +size 41477 diff --git a/examples/screenshots/line_present_scaling.png b/examples/screenshots/line_present_scaling.png new file mode 100644 index 000000000..307e5d562 --- /dev/null +++ b/examples/screenshots/line_present_scaling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:097c661c8bae4b49096169d56ed0d21c47877cfd3dcaa808a8798011f50dbb17 +size 20072 diff --git a/examples/screenshots/scatter.png b/examples/screenshots/scatter.png new file mode 100644 index 000000000..3f804e284 --- /dev/null +++ b/examples/screenshots/scatter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b28abaccc6875eef9d0c551ed2515e850d3ad1161ba7bee640e3fe8c9622d64c +size 18261 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png new file mode 100644 index 000000000..493fd858d --- /dev/null +++ b/examples/screenshots/scatter_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ac7903576054e8e465da144a98af6d1ec2220f29046da334aa5bc975cf3090f +size 19640 diff --git a/examples/screenshots/scatter_colorslice.png b/examples/screenshots/scatter_colorslice.png new file mode 100644 index 000000000..a62ebe37a --- /dev/null +++ b/examples/screenshots/scatter_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6cc3cb3c342d7b69ad733440ac5fed88dc497d194a88ba21b32d4768e469a02 +size 16770 diff --git a/examples/screenshots/scatter_dataslice.png b/examples/screenshots/scatter_dataslice.png new file mode 100644 index 000000000..ade5f2cfb --- /dev/null +++ b/examples/screenshots/scatter_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a37c7216d3dc514e6580659041e37f0f672effa2f1437a605d698fdb2bc28c9 +size 19482 diff --git a/examples/screenshots/scatter_present.png b/examples/screenshots/scatter_present.png new file mode 100644 index 000000000..63a79c987 --- /dev/null +++ b/examples/screenshots/scatter_present.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b6ca8b21ec58dac6610d78676a47ecd5975d83b6f9a5fee039667e2d2d20842 +size 15504 From 0950aab914599c6209fc1e9ac4048a6a7b3e7dc2 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:35:19 -0400 Subject: [PATCH 78/96] setup options --- setup.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 47b9b4877..0591d90fb 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,7 @@ install_requires = [ 'numpy', - 'pygfx>=0.1.10', - 'jupyterlab', - 'jupyter-rfb', + 'pygfx>=0.1.13', ] @@ -16,8 +14,26 @@ "pydata-sphinx-theme<0.10.0", "glfw" ], + + "notebook": + [ + 'jupyterlab', + 'jupyter-rfb', + ], + + "tests": + [ + "pytest", + "nbmake", + "scipy", + "imageio", + "imageio-ffmpeg>=0.4.7", + "jupyterlab", + "jupyter-rfb", + ] } + with open(Path(__file__).parent.joinpath("README.md")) as f: readme = f.read() From 81b520aca7418df5c16671a29775bcb559f7db23 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:35:48 -0400 Subject: [PATCH 79/96] remove type annotation --- fastplotlib/graphics/selectors/_linear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 8899f03b1..5e3c33f1c 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -92,7 +92,7 @@ def __init__( parent: Graphic = None, end_points: Tuple[int, int] = None, arrow_keys_modifier: str = "Shift", - ipywidget_slider: ipywidgets.IntSlider = None, + ipywidget_slider = None, thickness: float = 2.5, color: Any = "w", name: str = None, From 4ac8cc5fcad13583915e81edb06f8fc72619753c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:36:03 -0400 Subject: [PATCH 80/96] change workflow to use setup optiosn --- .github/workflows/ci.yml | 4 +--- .github/workflows/screenshots.yml | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a271f73..0d5e55593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d - pip install -e . + pip install -e ".["tests"]" - name: Show wgpu backend run: python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" @@ -47,7 +47,6 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest imageio nbmake scipy pytest -v examples pytest --nbmake notebooks/ - uses: actions/upload-artifact@v3 @@ -55,4 +54,3 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs - diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index c2259ee06..8707b44bd 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -25,7 +25,7 @@ jobs: # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d - pip install -e . + pip install -e ".["tests"]" - name: Show wgpu backend run: python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" @@ -33,11 +33,10 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | - pip install pytest imageio scipy # regenerate screenshots REGENERATE_SCREENSHOTS=1 pytest -v examples - uses: actions/upload-artifact@v3 if: always() with: name: screenshots - path: examples/screenshots/ \ No newline at end of file + path: examples/screenshots/ From a9ee40090d0a558afaeb42191f19e03ccfc2323f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:42:50 -0400 Subject: [PATCH 81/96] add pyav to setup for tests --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0591d90fb..c1adda299 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "imageio-ffmpeg>=0.4.7", "jupyterlab", "jupyter-rfb", + "av", ] } From cbaaf9836a0d0a40699974eb1cb3de7529d58b29 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:50:13 -0400 Subject: [PATCH 82/96] add scikit-image --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c1adda299..7868e6862 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "jupyterlab", "jupyter-rfb", "av", + "scikit-image" ] } From 493b2f521938c770b57702668aa4c8c8903e1236 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:54:26 -0400 Subject: [PATCH 83/96] opencv instead of pyav --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7868e6862..08e6d2931 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ "imageio-ffmpeg>=0.4.7", "jupyterlab", "jupyter-rfb", - "av", + "opencv-python-headless", "scikit-image" ] } From 5edf06148209aeb2ff54cf2b5c15bba0b4d96d1f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 15:58:49 -0400 Subject: [PATCH 84/96] add libpng-dev --- .github/workflows/ci.yml | 2 +- .github/workflows/screenshots.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d5e55593..89dc06cb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers libpng-dev - name: Install dev dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 8707b44bd..57845bf42 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -18,7 +18,7 @@ jobs: - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers libpng-dev - name: Install dev dependencies run: | python -m pip install --upgrade pip From f731b09c19f8f9170705b73d085037e06c7be053 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 16:10:47 -0400 Subject: [PATCH 85/96] try pillow, better work ffs --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 08e6d2931..d07f01949 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,7 @@ "imageio-ffmpeg>=0.4.7", "jupyterlab", "jupyter-rfb", - "opencv-python-headless", - "scikit-image" + "Pillow", ] } From 494b5af1a02b2cedb75ded121ccc90b3dc83b96b Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 16:16:52 -0400 Subject: [PATCH 86/96] bah --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89dc06cb5..72098bfba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,3 +54,6 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs + - name: Setup tmate session + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 From 5a1c3398f56743cca9cb9bf41d36b3ad6390ae2d Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 16:53:41 -0400 Subject: [PATCH 87/96] add git-lfs to apt install for github actions --- .github/workflows/ci.yml | 5 +---- .github/workflows/screenshots.yml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72098bfba..a1f3e1a88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers libpng-dev + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs - name: Install dev dependencies run: | python -m pip install --upgrade pip @@ -54,6 +54,3 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs - - name: Setup tmate session - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 57845bf42..6115e75d3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -18,7 +18,7 @@ jobs: - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers libpng-dev + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs - name: Install dev dependencies run: | python -m pip install --upgrade pip From 35f5e88accf1b192ce631d534c53dca1efcb2de8 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 16:57:34 -0400 Subject: [PATCH 88/96] add interactive again --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1f3e1a88..b7226ff6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,3 +54,6 @@ jobs: with: name: screenshot-diffs path: examples/screenshots/diffs + - name: Setup tmate session + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 From 4bed70acea5b200971569a21c11719e1f9396904 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:04:40 -0400 Subject: [PATCH 89/96] install git lfs before checkout repo --- .github/workflows/ci.yml | 3 +++ .github/workflows/screenshots.yml | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7226ff6e..85226c1a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,9 @@ jobs: - name: Test py311 pyversion: '3.11' steps: + - name: Install git-lfs + run: | + sudo apt install --no-install-recommends -y git-lfs - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 6115e75d3..bb555c2f3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -10,6 +10,9 @@ jobs: name: Regenerate runs-on: 'ubuntu-latest' steps: + - name: Install git-lfs + run: | + sudo apt install --no-install-recommends -y git-lfs - uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v4 @@ -18,7 +21,7 @@ jobs: - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq - sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - name: Install dev dependencies run: | python -m pip install --upgrade pip From a8e545fd6c4de4743a22b12022be412f059b5d54 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:11:00 -0400 Subject: [PATCH 90/96] add git lfs fetch pull --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85226c1a8..0b6b4a3fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,10 @@ jobs: - name: Show wgpu backend run: python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: fetch git lfs files + run: | + git lfs fetch --all + git lfs pull - name: Test examples env: PYGFX_EXPECT_LAVAPIPE: true From f2202c1811da201ab5952ffa55addb7909feb104 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:18:06 -0400 Subject: [PATCH 91/96] diffs --- .github/workflows/ci.yml | 2 +- examples/tests/test_examples.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b6b4a3fb..ce17a62bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: if: ${{ failure() }} with: name: screenshot-diffs - path: examples/screenshots/diffs + path: examples/screenshots - name: Setup tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index b42a22bcc..650c7e8cd 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -109,8 +109,8 @@ def get_diffs_rgba(slicer): # split into an rgb and an alpha diff diffs = { - diffs_dir / f"{module}-rgb.png": slice(0, 3), - diffs_dir / f"{module}-alpha.png": 3, + diffs_dir / f"diff-{module}-rgb.png": slice(0, 3), + diffs_dir / f"diff-{module}-alpha.png": 3, } for path, slicer in diffs.items(): From cba928c16c5d1d22aeab6af2057dea0f8c7e3157 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:24:43 -0400 Subject: [PATCH 92/96] diffs path --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce17a62bb..961f8a043 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: if: ${{ failure() }} with: name: screenshot-diffs - path: examples/screenshots + path: examples/diffs - name: Setup tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 From 80249a7e423100509e02394f9221c92263274486 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:41:50 -0400 Subject: [PATCH 93/96] update examples for canvas size and autoscale --- examples/gridplot/gridplot.py | 4 +++- examples/image/image_cmap.py | 6 +++++- examples/image/image_rgb.py | 4 ++++ examples/image/image_rgbvminvmax.py | 4 ++++ examples/image/image_simple.py | 4 ++++ examples/image/image_vminvmax.py | 4 ++++ examples/line/line.py | 4 +++- examples/line/line_colorslice.py | 4 +++- examples/line/line_dataslice.py | 4 +++- examples/line/line_present_scaling.py | 2 +- examples/scatter/scatter.py | 4 +++- examples/scatter/scatter_cmap.py | 4 +++- examples/scatter/scatter_colorslice.py | 4 +++- examples/scatter/scatter_dataslice.py | 4 +++- examples/scatter/scatter_present.py | 4 +++- 15 files changed, 49 insertions(+), 11 deletions(-) diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py index ba884b89c..211c671f7 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/gridplot/gridplot.py @@ -30,8 +30,10 @@ plot.show() +plot.canvas.set_logical_size(800, 800) + for subplot in plot: - subplot.center_scene() + subplot.auto_scale() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/image/image_cmap.py b/examples/image/image_cmap.py index 4f5e8dba7..3f061c9d4 100644 --- a/examples/image/image_cmap.py +++ b/examples/image/image_cmap.py @@ -17,13 +17,17 @@ plot = Plot(canvas=canvas, renderer=renderer) -im = iio.imread("imageio:astronaut.png") +im = iio.imread("imageio:camera.png") # plot the image data image_graphic = plot.add_image(data=im, name="random-image") plot.show() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() + image_graphic.cmap = "viridis" img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/image/image_rgb.py b/examples/image/image_rgb.py index a8281df49..fbd4cf24a 100644 --- a/examples/image/image_rgb.py +++ b/examples/image/image_rgb.py @@ -24,6 +24,10 @@ plot.show() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() + img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": diff --git a/examples/image/image_rgbvminvmax.py b/examples/image/image_rgbvminvmax.py index 8852b48a8..f8018fea2 100644 --- a/examples/image/image_rgbvminvmax.py +++ b/examples/image/image_rgbvminvmax.py @@ -24,6 +24,10 @@ plot.show() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() + image_graphic.vmin = 0.5 image_graphic.vmax = 0.75 diff --git a/examples/image/image_simple.py b/examples/image/image_simple.py index ad786b44e..afe5a608e 100644 --- a/examples/image/image_simple.py +++ b/examples/image/image_simple.py @@ -25,6 +25,10 @@ plot.show() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() + img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": diff --git a/examples/image/image_vminvmax.py b/examples/image/image_vminvmax.py index 715ceb229..52c6b64bc 100644 --- a/examples/image/image_vminvmax.py +++ b/examples/image/image_vminvmax.py @@ -25,6 +25,10 @@ plot.show() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() + image_graphic.vmin = 0.5 image_graphic.vmax = 0.75 diff --git a/examples/line/line.py b/examples/line/line.py index b44d7bd4c..008b0147d 100644 --- a/examples/line/line.py +++ b/examples/line/line.py @@ -42,7 +42,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/line/line_colorslice.py b/examples/line/line_colorslice.py index 2ee02b84c..68ce5b71c 100644 --- a/examples/line/line_colorslice.py +++ b/examples/line/line_colorslice.py @@ -61,7 +61,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) cosine_graphic.colors[key2] = "Green" -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/line/line_dataslice.py b/examples/line/line_dataslice.py index 975b7360a..ed9b542b6 100644 --- a/examples/line/line_dataslice.py +++ b/examples/line/line_dataslice.py @@ -50,7 +50,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) sinc_graphic.data[key2] = np.array([[5, 1, 2]]) -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/line/line_present_scaling.py b/examples/line/line_present_scaling.py index e5b063bc8..6f40bee49 100644 --- a/examples/line/line_present_scaling.py +++ b/examples/line/line_present_scaling.py @@ -44,7 +44,7 @@ sinc_graphic.present = False -plot.center_scene() +plot.canvas.set_logical_size(800, 800) plot.auto_scale() diff --git a/examples/scatter/scatter.py b/examples/scatter/scatter.py index c09b1e251..c866c4907 100644 --- a/examples/scatter/scatter.py +++ b/examples/scatter/scatter.py @@ -28,7 +28,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() img = np.asarray(plot.renderer.target.draw()) diff --git a/examples/scatter/scatter_cmap.py b/examples/scatter/scatter_cmap.py index 002e66257..8b52da767 100644 --- a/examples/scatter/scatter_cmap.py +++ b/examples/scatter/scatter_cmap.py @@ -28,7 +28,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() scatter_graphic.cmap = "viridis" diff --git a/examples/scatter/scatter_colorslice.py b/examples/scatter/scatter_colorslice.py index 41e13bd54..d3dd681a2 100644 --- a/examples/scatter/scatter_colorslice.py +++ b/examples/scatter/scatter_colorslice.py @@ -28,7 +28,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() scatter_graphic.colors[0:75] = "red" scatter_graphic.colors[75:150] = "white" diff --git a/examples/scatter/scatter_dataslice.py b/examples/scatter/scatter_dataslice.py index 5a6fef285..c522ca729 100644 --- a/examples/scatter/scatter_dataslice.py +++ b/examples/scatter/scatter_dataslice.py @@ -28,7 +28,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) diff --git a/examples/scatter/scatter_present.py b/examples/scatter/scatter_present.py index ab02ec415..8770a1b92 100644 --- a/examples/scatter/scatter_present.py +++ b/examples/scatter/scatter_present.py @@ -31,7 +31,9 @@ plot.show() -plot.center_scene() +plot.canvas.set_logical_size(800, 800) + +plot.auto_scale() scatter_graphic.present = False From 4176207cd2cdc27f47b75f32cf50265e422772a4 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:42:36 -0400 Subject: [PATCH 94/96] remove gpu screenshots --- examples/screenshots/gridplot.png | 3 --- examples/screenshots/image_cmap.png | 3 --- examples/screenshots/image_rgb.png | 3 --- examples/screenshots/image_rgbvminvmax.png | 3 --- examples/screenshots/image_simple.png | 3 --- examples/screenshots/image_vminvmax.png | 3 --- examples/screenshots/line.png | 3 --- examples/screenshots/line_colorslice.png | 3 --- examples/screenshots/line_dataslice.png | 3 --- examples/screenshots/line_present_scaling.png | 3 --- examples/screenshots/scatter.png | 3 --- examples/screenshots/scatter_cmap.png | 3 --- examples/screenshots/scatter_colorslice.png | 3 --- examples/screenshots/scatter_dataslice.png | 3 --- examples/screenshots/scatter_present.png | 3 --- 15 files changed, 45 deletions(-) delete mode 100644 examples/screenshots/gridplot.png delete mode 100644 examples/screenshots/image_cmap.png delete mode 100644 examples/screenshots/image_rgb.png delete mode 100644 examples/screenshots/image_rgbvminvmax.png delete mode 100644 examples/screenshots/image_simple.png delete mode 100644 examples/screenshots/image_vminvmax.png delete mode 100644 examples/screenshots/line.png delete mode 100644 examples/screenshots/line_colorslice.png delete mode 100644 examples/screenshots/line_dataslice.png delete mode 100644 examples/screenshots/line_present_scaling.png delete mode 100644 examples/screenshots/scatter.png delete mode 100644 examples/screenshots/scatter_cmap.png delete mode 100644 examples/screenshots/scatter_colorslice.png delete mode 100644 examples/screenshots/scatter_dataslice.png delete mode 100644 examples/screenshots/scatter_present.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png deleted file mode 100644 index f746e57a9..000000000 --- a/examples/screenshots/gridplot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:619f679d58483882ade3163ba54abe35fe2ff5bea2c64d6592faf4889f1c5590 -size 178043 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png deleted file mode 100644 index 0485152e4..000000000 --- a/examples/screenshots/image_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c -size 196267 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png deleted file mode 100644 index 0485152e4..000000000 --- a/examples/screenshots/image_rgb.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2df9d382b4adc9b11c9ab9a9f37c5a3c10d991d88929b73544afeb91886f4c -size 196267 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png deleted file mode 100644 index b41adf788..000000000 --- a/examples/screenshots/image_rgbvminvmax.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 -size 30165 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png deleted file mode 100644 index 5945a9957..000000000 --- a/examples/screenshots/image_simple.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f527e760eb905266557b296e00b1aa36c74afa7986f98a5c9f5f36268bbf813a -size 186638 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png deleted file mode 100644 index b41adf788..000000000 --- a/examples/screenshots/image_vminvmax.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57cf9c7dd1509638a62e870db65315377786f3255dc03e6dcb98b499417a6ae9 -size 30165 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png deleted file mode 100644 index a680a39c2..000000000 --- a/examples/screenshots/line.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:725427d4b4bb97ac72c0c6a96b91a57fecf61421106042e608878f862119e8d2 -size 28311 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png deleted file mode 100644 index 3f9f6defe..000000000 --- a/examples/screenshots/line_colorslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1b2e37f6e4b577e4411f8cc4edbf7520251290e2c33db895331dea2394ba9bc -size 31890 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png deleted file mode 100644 index 0bd31928c..000000000 --- a/examples/screenshots/line_dataslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:333df52503cf880e58275df3d9e4b750ece425237857cbd975771fde2d821324 -size 41477 diff --git a/examples/screenshots/line_present_scaling.png b/examples/screenshots/line_present_scaling.png deleted file mode 100644 index 307e5d562..000000000 --- a/examples/screenshots/line_present_scaling.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:097c661c8bae4b49096169d56ed0d21c47877cfd3dcaa808a8798011f50dbb17 -size 20072 diff --git a/examples/screenshots/scatter.png b/examples/screenshots/scatter.png deleted file mode 100644 index 3f804e284..000000000 --- a/examples/screenshots/scatter.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b28abaccc6875eef9d0c551ed2515e850d3ad1161ba7bee640e3fe8c9622d64c -size 18261 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png deleted file mode 100644 index 493fd858d..000000000 --- a/examples/screenshots/scatter_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ac7903576054e8e465da144a98af6d1ec2220f29046da334aa5bc975cf3090f -size 19640 diff --git a/examples/screenshots/scatter_colorslice.png b/examples/screenshots/scatter_colorslice.png deleted file mode 100644 index a62ebe37a..000000000 --- a/examples/screenshots/scatter_colorslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6cc3cb3c342d7b69ad733440ac5fed88dc497d194a88ba21b32d4768e469a02 -size 16770 diff --git a/examples/screenshots/scatter_dataslice.png b/examples/screenshots/scatter_dataslice.png deleted file mode 100644 index ade5f2cfb..000000000 --- a/examples/screenshots/scatter_dataslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a37c7216d3dc514e6580659041e37f0f672effa2f1437a605d698fdb2bc28c9 -size 19482 diff --git a/examples/screenshots/scatter_present.png b/examples/screenshots/scatter_present.png deleted file mode 100644 index 63a79c987..000000000 --- a/examples/screenshots/scatter_present.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b6ca8b21ec58dac6610d78676a47ecd5975d83b6f9a5fee039667e2d2d20842 -size 15504 From 00ed1e05cf1d6fde01c3bd5cdff7dde484dcca3c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:49:02 -0400 Subject: [PATCH 95/96] add screenshot ground truth files from github actions generated screenshots --- examples/screenshots/gridplot.png | 3 +++ examples/screenshots/image_cmap.png | 3 +++ examples/screenshots/image_rgb.png | 3 +++ examples/screenshots/image_rgbvminvmax.png | 3 +++ examples/screenshots/image_simple.png | 3 +++ examples/screenshots/image_vminvmax.png | 3 +++ examples/screenshots/line.png | 3 +++ examples/screenshots/line_colorslice.png | 3 +++ examples/screenshots/line_dataslice.png | 3 +++ examples/screenshots/line_present_scaling.png | 3 +++ examples/screenshots/scatter.png | 3 +++ examples/screenshots/scatter_cmap.png | 3 +++ examples/screenshots/scatter_colorslice.png | 3 +++ examples/screenshots/scatter_dataslice.png | 3 +++ examples/screenshots/scatter_present.png | 3 +++ 15 files changed, 45 insertions(+) create mode 100644 examples/screenshots/gridplot.png create mode 100644 examples/screenshots/image_cmap.png create mode 100644 examples/screenshots/image_rgb.png create mode 100644 examples/screenshots/image_rgbvminvmax.png create mode 100644 examples/screenshots/image_simple.png create mode 100644 examples/screenshots/image_vminvmax.png create mode 100644 examples/screenshots/line.png create mode 100644 examples/screenshots/line_colorslice.png create mode 100644 examples/screenshots/line_dataslice.png create mode 100644 examples/screenshots/line_present_scaling.png create mode 100644 examples/screenshots/scatter.png create mode 100644 examples/screenshots/scatter_cmap.png create mode 100644 examples/screenshots/scatter_colorslice.png create mode 100644 examples/screenshots/scatter_dataslice.png create mode 100644 examples/screenshots/scatter_present.png diff --git a/examples/screenshots/gridplot.png b/examples/screenshots/gridplot.png new file mode 100644 index 000000000..dbc8cd5b2 --- /dev/null +++ b/examples/screenshots/gridplot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:220c8e26502fec371bab3d860f405d53c32f56ed848a2e27a45074f1bb943acd +size 351714 diff --git a/examples/screenshots/image_cmap.png b/examples/screenshots/image_cmap.png new file mode 100644 index 000000000..e2b9e7016 --- /dev/null +++ b/examples/screenshots/image_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2b1c0c3c2df2e897c43d0263d66d05663b7007c43bcb8bdaf1f3857daa65f79 +size 274669 diff --git a/examples/screenshots/image_rgb.png b/examples/screenshots/image_rgb.png new file mode 100644 index 000000000..2ca90a7fb --- /dev/null +++ b/examples/screenshots/image_rgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:558f81f9e62244b89add9b5a84e58e70219e6a6495c3c9a9ea90ef22e5922c33 +size 319491 diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/screenshots/image_rgbvminvmax.png new file mode 100644 index 000000000..e7d32475c --- /dev/null +++ b/examples/screenshots/image_rgbvminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d919e3a3b9fb87fd64072f818aad07637fb82c82f95ef188c8eb0362ded2baf +size 44805 diff --git a/examples/screenshots/image_simple.png b/examples/screenshots/image_simple.png new file mode 100644 index 000000000..e89c0a6de --- /dev/null +++ b/examples/screenshots/image_simple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e46eba536bc4b6904f9df590b8c2e9b73226f22e37e920cf65e4c6720cd6634 +size 272624 diff --git a/examples/screenshots/image_vminvmax.png b/examples/screenshots/image_vminvmax.png new file mode 100644 index 000000000..e7d32475c --- /dev/null +++ b/examples/screenshots/image_vminvmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d919e3a3b9fb87fd64072f818aad07637fb82c82f95ef188c8eb0362ded2baf +size 44805 diff --git a/examples/screenshots/line.png b/examples/screenshots/line.png new file mode 100644 index 000000000..a38008ab9 --- /dev/null +++ b/examples/screenshots/line.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe996cd43013ff2e616e8a549933137529c13ad8e320331420e9c64f6ed1690 +size 49738 diff --git a/examples/screenshots/line_colorslice.png b/examples/screenshots/line_colorslice.png new file mode 100644 index 000000000..003f86e44 --- /dev/null +++ b/examples/screenshots/line_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0033fc23cda3f07cdd7db642d4d1af710319d56a1690339354a9df27bf51c381 +size 57146 diff --git a/examples/screenshots/line_dataslice.png b/examples/screenshots/line_dataslice.png new file mode 100644 index 000000000..20c777212 --- /dev/null +++ b/examples/screenshots/line_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a169b359a0e56bb48a625069e73e53b26e061e6bcb83d6eb613fbdd1a43cdac +size 75385 diff --git a/examples/screenshots/line_present_scaling.png b/examples/screenshots/line_present_scaling.png new file mode 100644 index 000000000..c4a41ac2e --- /dev/null +++ b/examples/screenshots/line_present_scaling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c67d65b0a5120c014b34dcfc326079113cee22b849c14a0284fc7881dac5d43c +size 43446 diff --git a/examples/screenshots/scatter.png b/examples/screenshots/scatter.png new file mode 100644 index 000000000..e35fd9e3c --- /dev/null +++ b/examples/screenshots/scatter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2efb81fb8b6b11bb1fc3136394c0f6bf1c38972d03dabd07949928f4e53cf71 +size 25240 diff --git a/examples/screenshots/scatter_cmap.png b/examples/screenshots/scatter_cmap.png new file mode 100644 index 000000000..8fefd0f91 --- /dev/null +++ b/examples/screenshots/scatter_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2be58a41b54f29db4683692b73993309ff561c47388af667a95bb33aed65f219 +size 26894 diff --git a/examples/screenshots/scatter_colorslice.png b/examples/screenshots/scatter_colorslice.png new file mode 100644 index 000000000..cd5a1f00d --- /dev/null +++ b/examples/screenshots/scatter_colorslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d3afcf293e425c2369d93745cb933afb15d971866620f160629e394f50cd9b6 +size 23747 diff --git a/examples/screenshots/scatter_dataslice.png b/examples/screenshots/scatter_dataslice.png new file mode 100644 index 000000000..8ed7ad590 --- /dev/null +++ b/examples/screenshots/scatter_dataslice.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:974111967bc5e4197b6b38c7a00950ee013ce4e689162c9d91e902d37240221a +size 26001 diff --git a/examples/screenshots/scatter_present.png b/examples/screenshots/scatter_present.png new file mode 100644 index 000000000..335191d91 --- /dev/null +++ b/examples/screenshots/scatter_present.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb5b0bf6fb7b7dbfafc1b2553dfff87f329ec5070fb69061313acce46364df52 +size 24627 From 6276b284ed44abd611764a02298d0d71aa382e04 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 14 Jun 2023 17:53:26 -0400 Subject: [PATCH 96/96] remove interactive session stuff --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 961f8a043..cd6aa5320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,3 @@ jobs: with: name: screenshot-diffs path: examples/diffs - - name: Setup tmate session - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3