diff --git a/docs/source/api/graphics/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst index 6da6f6531..3bd2f2baa 100644 --- a/docs/source/api/graphics/HeatmapGraphic.rst +++ b/docs/source/api/graphics/HeatmapGraphic.rst @@ -26,8 +26,6 @@ Properties HeatmapGraphic.position_y HeatmapGraphic.position_z HeatmapGraphic.visible - HeatmapGraphic.vmax - HeatmapGraphic.vmin HeatmapGraphic.world_object Methods diff --git a/docs/source/api/layouts/gridplot.rst b/docs/source/api/layouts/gridplot.rst index 63f1516cf..b5b03bfa4 100644 --- a/docs/source/api/layouts/gridplot.rst +++ b/docs/source/api/layouts/gridplot.rst @@ -22,6 +22,8 @@ Properties GridPlot.canvas GridPlot.renderer + GridPlot.toolbar + GridPlot.widget Methods ~~~~~~~ @@ -36,4 +38,5 @@ Methods GridPlot.remove_animation GridPlot.render GridPlot.show + GridPlot.start_render diff --git a/docs/source/api/layouts/plot.rst b/docs/source/api/layouts/plot.rst index a0be9287b..bd38720b4 100644 --- a/docs/source/api/layouts/plot.rst +++ b/docs/source/api/layouts/plot.rst @@ -31,7 +31,9 @@ Properties Plot.renderer Plot.scene Plot.selectors + Plot.toolbar Plot.viewport + Plot.widget Methods ~~~~~~~ @@ -67,4 +69,5 @@ Methods Plot.set_title Plot.set_viewport_rect Plot.show + Plot.start_render diff --git a/docs/source/api/selectors/Synchronizer.rst b/docs/source/api/selectors/Synchronizer.rst index d0fa0c2a8..2b28fe351 100644 --- a/docs/source/api/selectors/Synchronizer.rst +++ b/docs/source/api/selectors/Synchronizer.rst @@ -28,5 +28,6 @@ Methods :toctree: Synchronizer_api Synchronizer.add + Synchronizer.clear Synchronizer.remove diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst index 4e779f20b..08bce8d7a 100644 --- a/docs/source/api/widgets/ImageWidget.rst +++ b/docs/source/api/widgets/ImageWidget.rst @@ -29,6 +29,7 @@ Properties ImageWidget.ndim ImageWidget.slider_dims ImageWidget.sliders + ImageWidget.widget ImageWidget.window_funcs Methods @@ -38,6 +39,7 @@ Methods ImageWidget.close ImageWidget.reset_vmin_vmax + ImageWidget.reset_vmin_vmax_frame ImageWidget.set_data ImageWidget.show diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py new file mode 100644 index 000000000..a41bcd9f4 --- /dev/null +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -0,0 +1,34 @@ +""" +GridPlot Simple +============ +Example showing simple 2x2 GridPlot with Standard images from imageio. +""" + +# test_example = true + +import fastplotlib as fpl +import imageio.v3 as iio + + +plot = fpl.GridPlot(shape=(2, 2), controllers="sync") +# to force a specific framework such as glfw: +# plot = fpl.GridPlot(canvas="glfw") + +im = iio.imread("imageio:clock.png") +im2 = iio.imread("imageio:astronaut.png") +im3 = iio.imread("imageio:coffee.png") + +plot[0, 0].add_image(data=im) +plot[0, 1].add_image(data=im2) +plot[1, 0].add_image(data=im3) + +plot.show() + +plot.canvas.set_logical_size(800, 800) + +for subplot in plot: + subplot.auto_scale() + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/__init__.py b/examples/desktop/heatmap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py new file mode 100644 index 000000000..45c340cbd --- /dev/null +++ b/examples/desktop/heatmap/heatmap.py @@ -0,0 +1,37 @@ +""" +Simple Heatmap +============== +Example showing how to plot a heatmap +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") + +xs = np.linspace(0, 1_000, 10_000) + +sine = np.sin(xs) +cosine = np.cos(xs) + +# alternating sines and cosines +data = np.zeros((10_000, 10_000), dtype=np.float32) +data[::2] = sine +data[1::2] = cosine + +# plot the image data +heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") + +plot.show() + +plot.canvas.set_logical_size(1500, 1500) + +plot.auto_scale() + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py new file mode 100644 index 000000000..afc67f5b8 --- /dev/null +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -0,0 +1,39 @@ +""" +Heatmap change cmap +=================== +Change the cmap of a heatmap +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") + +xs = np.linspace(0, 1_000, 10_000) + +sine = np.sin(xs) +cosine = np.cos(xs) + +# alternating sines and cosines +data = np.zeros((10_000, 10_000), dtype=np.float32) +data[::2] = sine +data[1::2] = cosine + +# plot the image data +heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") + +plot.show() + +plot.canvas.set_logical_size(1500, 1500) + +plot.auto_scale() + +heatmap_graphic.cmap = "viridis" + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py new file mode 100644 index 000000000..78e819ab8 --- /dev/null +++ b/examples/desktop/heatmap/heatmap_data.py @@ -0,0 +1,41 @@ +""" +Heatmap change data +=================== +Change the data of a heatmap +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") + +xs = np.linspace(0, 1_000, 10_000) + +sine = np.sin(xs) +cosine = np.cos(xs) + +# alternating sines and cosines +data = np.zeros((10_000, 10_000), dtype=np.float32) +data[::2] = sine +data[1::2] = cosine + +# plot the image data +heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") + +plot.show() + +plot.canvas.set_logical_size(1500, 1500) + +plot.auto_scale() + +heatmap_graphic.data[:5_000] = sine +heatmap_graphic.data[5_000:] = cosine + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py new file mode 100644 index 000000000..7aae1d6d3 --- /dev/null +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -0,0 +1,40 @@ +""" +Heatmap change vmin vmax +======================== +Change the vmin vmax of a heatmap +""" + +# test_example = true + +import fastplotlib as fpl +import numpy as np + +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") + +xs = np.linspace(0, 1_000, 10_000) + +sine = np.sin(xs) +cosine = np.cos(xs) + +# alternating sines and cosines +data = np.zeros((10_000, 10_000), dtype=np.float32) +data[::2] = sine +data[1::2] = cosine + +# plot the image data +heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") + +plot.show() + +plot.canvas.set_logical_size(1500, 1500) + +plot.auto_scale() + +heatmap_graphic.cmap.vmin = -0.5 +heatmap_graphic.cmap.vmax = 0.5 + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png new file mode 100644 index 000000000..7b534aef9 --- /dev/null +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abf936904d1b5e2018a72311c510108925f2972dfdf59166580ad27876f9e2be +size 220140 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png new file mode 100644 index 000000000..d0df1510a --- /dev/null +++ b/examples/desktop/screenshots/heatmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c19d6454e79d92074bac01175dfbb8506e882ea55b626c0b2357960ed6e294f +size 163655 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png new file mode 100644 index 000000000..db3038dee --- /dev/null +++ b/examples/desktop/screenshots/heatmap_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0328eec32f13042e3e2f243793317e180bba1353fe961604ecad3f38463b8809 +size 156419 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png new file mode 100644 index 000000000..96169ec77 --- /dev/null +++ b/examples/desktop/screenshots/heatmap_data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a376c24fa123088be69f807ec5212cb5ed5680b146ce9d62df584790c632845 +size 20838 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png new file mode 100644 index 000000000..2a809d545 --- /dev/null +++ b/examples/desktop/screenshots/heatmap_vmin_vmax.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3eb12ad590aa8260f2cf722abadf5b51bcb7d5a8d8d1cb05b7711b50331da07a +size 165937 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 6155f8763..5f6772fb7 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -16,6 +16,7 @@ # examples live in themed sub-folders example_globs = [ "image/*.py", + "heatmap/*.py", "scatter/*.py", "line/*.py", "line_collection/*.py", diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index 2952df5a6..a0ad4fd5d 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.1.0.a14 +0.1.0.a15 diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index 85014155d..b6723b34b 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -401,8 +401,29 @@ class HeatmapCmapFeature(ImageCmapFeature): """ def _set(self, cmap_name: str): + # in heatmap we use one material for all ImageTiles self._parent._material.map.data[:] = make_colors(256, cmap_name) self._parent._material.map.update_range((0, 0, 0), size=(256, 1, 1)) - self.name = cmap_name + self._name = cmap_name self._feature_changed(key=None, new_data=self.name) + + @property + def vmin(self) -> float: + """Minimum contrast limit.""" + return self._parent._material.clim[0] + + @vmin.setter + def vmin(self, value: float): + """Minimum contrast limit.""" + self._parent._material.clim = (value, self._parent._material.clim[1]) + + @property + def vmax(self) -> float: + """Maximum contrast limit.""" + return self._parent._material.clim[1] + + @vmax.setter + def vmax(self, value: float): + """Maximum contrast limit.""" + self._parent._material.clim = (self._parent._material.clim[0], value) \ No newline at end of file diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 12ac9e41d..10f09eefb 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -480,26 +480,6 @@ def __init__( # set it with the actual data self.data = data - @property - def vmin(self) -> float: - """Minimum contrast limit.""" - return self._material.clim[0] - - @vmin.setter - def vmin(self, value: float): - """Minimum contrast limit.""" - self._material.clim = (value, self._material.clim[1]) - - @property - def vmax(self) -> float: - """Maximum contrast limit.""" - return self._material.clim[1] - - @vmax.setter - def vmax(self, value: float): - """Maximum contrast limit.""" - self._material.clim = (self._material.clim[0], value) - def set_feature(self, feature: str, new_data: Any, indices: Any): pass diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index e892ca32d..e6796f270 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -252,7 +252,8 @@ def _move_end(self, ev): # restore the initial controller state # if it was disabled, keep it disabled - self._plot_area.controller.enabled = self._initial_controller_state + if self._initial_controller_state is not None: + self._plot_area.controller.enabled = self._initial_controller_state def _move_to_pointer(self, ev): """ diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 2060850c2..7590bad10 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -495,7 +495,9 @@ 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 we decided if we want to put selectors in their own scene + if not len(self.scene.children) > 0: + return + # hacky workaround for now until we decide 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) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 3013559d5..8d1e8694f 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -26,6 +26,23 @@ def get_cmap(name: str, alpha: float = 1.0) -> np.ndarray: + """ + Get a colormap as numpy array + + Parameters + ---------- + name: str + name of colormap + alpha: float + alpha, 0.0 - 1.0 + + Returns + ------- + np.ndarray + [n_colors, 4], i.e. [n_colors, RGBA] + + """ + cmap_path = Path(__file__).absolute().parent.joinpath("colormaps", name) if cmap_path.is_file(): cmap = np.loadtxt(cmap_path) @@ -214,6 +231,9 @@ def calculate_gridshape(n_subplots: int) -> Tuple[int, int]: def normalize_min_max(a): """normalize an array between 0 - 1""" + if np.unique(a).size == 1: + return np.zeros(a.size) + return (a - np.min(a)) / (np.max(a - np.min(a))) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index a9ebfafb4..da256207f 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -225,6 +225,7 @@ def __init__( grid_shape: Tuple[int, int] = None, names: List[str] = None, grid_plot_kwargs: dict = None, + histogram_widget: bool = True, **kwargs, ): """ @@ -288,6 +289,9 @@ def __init__( names: Optional[str] gives names to the subplots + histogram_widget: bool, default False + make histogram LUT widget for each subplot + kwargs: Any passed to fastplotlib.graphics.Image @@ -556,16 +560,17 @@ def __init__( subplot.name = name subplot.set_title(name) - hlut = HistogramLUT( - data=d, - image_graphic=ig, - name="histogram_lut" - ) + if histogram_widget: + hlut = HistogramLUT( + data=d, + image_graphic=ig, + name="histogram_lut" + ) - subplot.docks["right"].add_graphic(hlut) - subplot.docks["right"].size = 80 - subplot.docks["right"].auto_scale(maintain_aspect=False) - subplot.docks["right"].controller.enabled = False + subplot.docks["right"].add_graphic(hlut) + subplot.docks["right"].size = 80 + subplot.docks["right"].auto_scale(maintain_aspect=False) + subplot.docks["right"].controller.enabled = False self.block_sliders = False self._image_widget_toolbar = None @@ -869,9 +874,6 @@ def set_data( # force graphics to update self.current_index = self.current_index - # if reset_vmin_vmax: - # self.reset_vmin_vmax() - def show(self, toolbar: bool = True, sidecar: bool = False, sidecar_kwargs: dict = None): """ Show the widget