Note
Click here to download the full example code
Introduction to fastplotlib
fastplotlib
API
1. Graphics - objects that are drawn
Image
,Line
,Scatter
-
Collections -
LineCollection
,LineStack
(ex: neural timeseries data)a) Graphic Properties
- Common:
Name
,Offset
,Rotation
,Visible
,Deleted
- Graphic Specific:
ImageVmin
,ImageVmax
,VertexColors
, etc.
b) Selectors
LinearSelector
- horizontal or vertical line sliderLinearRegionSelector
- horizontal or vertical resizable region selection
- Common:
2. Layouts
Figure
- a single plot or a grid of subplots
3. Widgets - high level widgets to make repetitive UIs easier
ImageWidget
- widget forImageGraphic
data with dims:xy
,txy
,tzxy
- Sliders, support window functions, etc.
This notebook will go through some basic components of the fastplotlib
API including how to instantiate a plot,
add graphics to a plot, and subsequently interact with the plot.
By default, fastplotlib
will enumerate the available devices and highlight which device has been selected by
default when importing the module.
import fastplotlib as fpl
import numpy as np
import imageio.v3 as iio
import workshop_utils
Simple Image
# create a `Figure` instance
# by default the figure will have 1 subplot
fig = fpl.Figure(size=(600, 500))
# get a grayscale image
data = iio.imread("imageio:camera.png")
# plot the image data
image_graphic = fig[0, 0].add_image(data=data, name="sample-image")
# show the plot
fig.show(sidecar=True)
Out:
Imageio: 'camera.png' was not found on your computer; downloading it now.
Try 1. Download from https://github.com/imageio/imageio-binaries/raw/master/images/camera.png (136 kB)
Downloading: 8192/139512 bytes (5.9%)139512/139512 bytes (100.0%)
Done
File saved as /home/runner/.imageio/images/camera.png.
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f87291b8a10>
Use the handle on the bottom right corner of the canvas to resize it. You can also pan and zoom using your mouse!
Changing graphic properties
image_graphic.cmap = "viridis"
Slicing data
Most properties, such as data, support slicing!
Our image data is of shape [n_rows, n_columns]
print(image_graphic.data.value.shape)
Out:
(512, 512)
image_graphic.data[::15, :] = 1
image_graphic.data[:, ::15] = 1
Fancy indexing
image_graphic.data[data > 175] = 255
Adjust vmin vmax NOTE: vmin and vmax values range from (0, 255)
image_graphic.vmin = 50
image_graphic.vmax = 150
Reset the vmin vmax
image_graphic.reset_vmin_vmax()
Change graphic properties
Now that we have a better idea of how graphic properties can be dynamically changed, let's practice :D
Question: Can you change the data of the image to be random data of the same shape? Hint: Use np.random.rand()
to create the new data.
Note: At first you should see a solid image. You will need to reset the vmin/vmax of the image.
# create new random data of the same shape as the original image
new_data = np.random.rand(*image_graphic.data.value.shape)
# update the data
image_graphic.data = new_data
Question: Can you change the colormap of the image? See here for a list of colormaps.
image_graphic.cmap = "hsv"
#
# close the plot
fig.close()
This examples show how you can define animation functions that run on every render cycle.
# create another `Figure` instance
fig_v = fpl.Figure(size=(600, 500))
fig.canvas.max_buffered_frames = 1
# make some random data again
data = np.random.rand(512, 512)
# plot the data
fig_v[0, 0].add_image(data=data, name="random-image")
# a function to update the image_graphic
# a plot will pass its plot instance to the animation function as an argument
def update_data(plot_instance):
new_data = np.random.rand(512, 512)
plot_instance[0, 0]["random-image"].data = new_data
# add this as an animation function
fig_v.add_animations(update_data)
fig_v.show(sidecar=True)
Out:
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728b64090>
# close the plot
fig_v.close()
Image Practice
Question: Can you do the following:
-
create a new plot called
practice_fig
-
add an
ImageGraphic
with the following characteristics to the figure:-
random data in the shape (512, 512)
-
colormap "viridis"
-
name "random-image"
-
-
set the top-left and bottom-right quadrants of the data to 1 using slicing :D
-
add an animation function that updates the top-right and bottom-left quadrants with new random data
practice_fig = fpl.Figure()
data = np.random.rand(512, 512)
practice_fig[0, 0].add_image(data=data, name="random-image", cmap="viridis")
# set the top-left and bottom-right quadrants of the data to 1
practice_fig[0, 0]["random-image"].data[:256, :256] = 1 # top-left
practice_fig[0, 0]["random-image"].data[256:, 256:] = 1 # bottom-right
# define an animation function to toggle the data
def update_data(plot_instance):
# set the top-right and bottom-left quadrants with new random data
plot_instance[0, 0]["random-image"].data[:256, 256:] = np.random.rand(256, 256) # bottom-left
plot_instance[0, 0]["random-image"].data[256:, :256] = np.random.rand(256, 256) # top-right
# add the animation function
practice_fig.add_animations(update_data)
practice_fig.show()
Out:
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f87289ec350>
# close the plot
practice_fig.close()
ImageWidget
Great for looking at multi-dimensional image data.
# get zfish data
path = workshop_utils.fetch_zfish()
Out:
0%| | 0.00/26.2M [00:00<?, ?iB/s]
100%|##########| 26.2M/26.2M [00:00<00:00, 101MiB/s]
# load subsampled snippet of zebrafish data from Martin Haesemeyer
zfish_data = np.load(path, allow_pickle=False)
# data is tzxy
print(zfish_data.shape)
Out:
(100, 4, 256, 256)
# get the number of planes
n_planes = zfish_data.shape[1]
print(n_planes)
Out:
4
iw_zfish = fpl.ImageWidget(
data=[zfish_data[:, i] for i in range(n_planes)],
window_funcs={"t": (np.mean, 5)},
names=[f"plane-{i}" for i in range(n_planes)],
cmap="gnuplot2",
)
iw_zfish.show(sidecar=True)
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728b665d0>
Apply a window function
#iw_zfish.window_funcs["t"].func = np.max
# close the plot
iw_zfish.close()
z-slider
ImageWidget will also give you a slider for "z" in addition to "t" if necessary
This example uses the same example data shown above, but displays them in a single subplot and ImageWidget provides a z-slider. You can use window_funcs, frame_apply funcs, etc. There is no difference in ImageWidget behavior with the z-slider.
iw_z = fpl.ImageWidget(
data=zfish_data, # you can also provide a list of tzxy arrays
window_funcs={"t": (np.mean, 5)},
cmap="gnuplot2",
)
iw_z.show()
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728d461d0>
# close the plot
iw_z.close()
2D Line Plots
This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how Graphic properties can be modified by slicing!
First generate some data
# linspace, create 100 evenly spaced x values from -10 to 10
xs = np.linspace(-10, 10, 100)
# sine wave
ys = np.sin(xs)
sine = np.column_stack([xs, ys])
# cosine wave
ys = np.cos(xs)
cosine = np.column_stack([xs, ys])
# sinc function
a = 0.5
ys = np.sinc(xs) * 3
sinc = np.column_stack([xs, ys])
We will plot all of it on the same plot. Each line plot will be an individual Graphic
, you can have any
combination of graphics in a plot.
# Create a figure
fig_lines = fpl.Figure()
# we will add all the lines to the same subplot
subplot = fig_lines[0, 0]
# plot sine wave, use a single color
sine_graphic = subplot.add_line(data=sine, thickness=5, colors="magenta")
# you can also use colormaps for lines!
cosine_graphic = subplot.add_line(data=cosine, thickness=12, cmap="autumn", offset=(0, 5, 0))
# or a list of colors for each datapoint
colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25
sinc_graphic = subplot.add_line(data=sinc, thickness=5, colors=colors, offset=(0, 8, 0))
# show the plot
fig_lines.show(sidecar=True, sidecar_kwargs={"title": "lines"})
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728728ed0>
Graphic properties support slicing! :D
# 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"
# setting the colormap based on particular values
sine_graphic.cmap = "plasma"
# set the cmap transform
sine_graphic.cmap.transform = [4] * 25 + [1] * 25 + [3] * 25 + [8] * 25
# close the plot
fig_lines.close()
We are going to use the same sine and cosine data from before and do some practice!
# Create a figure
fig_lines = fpl.Figure()
# we will add all the lines to the same subplot
subplot = fig_lines[0, 0]
# plot sine wave, use a single color
sine_graphic = subplot.add_line(data=sine, thickness=5, colors="magenta")
# you can also use colormaps for lines!
cosine_graphic = subplot.add_line(data=cosine, thickness=12, cmap="autumn", offset=(0, 5, 0))
# show the plot
fig_lines.show(sidecar=True, sidecar_kwargs={"title": "lines"})
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728895ed0>
2D Lines Practice
Question: Can you change the colormap of the sine_graphic to "hsv"?
sine_graphic.cmap = "hsv"
Question: Can you change the color of first 50 data points of the sinc_graphic to green?
sinc_graphic.colors[:50] = "g"
Question: Can you to change the color of last 50 points of the cosine_graphic to equal the colors of the last 50 points of the sine_graphic?
cosine_graphic.colors[50:] = sine_graphic.colors[50:]
Capture changes to graphic properties as events
Two ways to add events in fastplotlib
:
1) Using graphic.add_event_handler(callback_func, "property")
2) Using a decorator:
@graphic.add_event_handler("property")
def callback_func(ev):
pass
Examples:
# first set the color of the sine and cosine wave to white so that we can better see the changes
cosine_graphic.colors = "w"
sine_graphic.colors = "w"
# will print event data when the color of the cosine graphic changes
@cosine_graphic.add_event_handler("colors")
def callback_func(ev):
print(ev.info)
# when the cosine graphic colors change, will also update the sine_graphic colors
def change_colors(ev):
sine_graphic.colors[ev.info["key"]] = "magenta"
cosine_graphic.add_event_handler(change_colors, "colors")
Out:
<function change_colors at 0x7f8728c2ae80>
cosine_graphic.colors[:10] = "g"
Out:
{'key': slice(None, 10, None), 'value': array([[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.],
[0., 1., 0., 1.]], dtype=float32), 'user_value': 'g'}
fig_lines.close()
More Events :D
# generate some circles
def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray:
theta = np.linspace(0, 2 * np.pi, n_points)
xs = radius * np.sin(theta)
ys = radius * np.cos(theta)
return np.column_stack([xs, ys]) + center
# this makes 5 circles
circles = list()
for x in range(0, 50, 10):
circles.append(make_circle(center=(x, 0), radius=4, n_points=100))
fig = fpl.Figure()
circles_graphic = fig[0, 0].add_line_collection(data=circles, cmap="tab10", thickness=10)
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
fig.show()
Out:
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728a79950>
# get the nearest graphic that is clicked and change the color
def click_event(ev):
# reset colors
circles_graphic.cmap = "tab10"
# map the click position to world coordinates
xy = fig[0, 0].map_screen_to_world(ev)[:-1]
# get the nearest graphic to the position
nearest = fpl.utils.get_nearest_graphics(xy, circles_graphic)[0]
# change the closest graphic color to white
nearest.colors = "w"
# add event handler to the renderer
fig[0, 0].renderer.add_event_handler(click_event, "click")
Out:
<function click_event at 0x7f8728ce0400>
Events Practice
Question: Can you add another event handler (using either method) to the circles_graphic that will change the thickness of a circle to 5 when you hover over it?
Hint: the event type should be "pointer_move" :)
@circles_graphic.add_event_handler("pointer_move")
def hover_event(ev):
# reset the thickness of all the circles
circles_graphic.thickness = [10] * len(circles_graphic)
# change the thickness of the circle that was hovered over
ev.graphic.thickness = 5
# close the plot
fig.close()
Selectors
LinearSelector
fig = fpl.Figure()
# same sine data from before
sine_graphic = fig[0, 0].add_line(data=sine, colors="w")
# add a linear selector the sine wave
selector = sine_graphic.add_linear_selector()
fig[0, 0].auto_scale()
fig.show(maintain_aspect=False)
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f87290bec50>
# change the color of the sine wave based on the location of the linear selector
@selector.add_event_handler("selection")
def set_color_at_index(ev):
# get the selected index
ix = ev.get_selected_index()
# get the sine graphic
g = ev.graphic.parent
# change the color of the sine graphic at the index of the selector
g.colors[ix] = "green"
# close the plot
fig.close()
LinearRegionSelector
fig = fpl.Figure((2, 1))
# data to plot
xs = np.linspace(0, 10 * np.pi, 1_000)
sine = np.sin(xs)
sine += 100
# make sine along x axis
sine_graphic_x = fig[0, 0].add_line(np.column_stack([xs, sine]), offset=(10, 0, 0))
# add a linear region selector
ls_x = sine_graphic_x.add_linear_region_selector() # default axis is "x"
# get the initial selected date of the linear region selector
zoomed_init = ls_x.get_selected_data()
# make a line graphic for displaying zoomed data
zoomed_x = fig[1, 0].add_line(zoomed_init)
@ls_x.add_event_handler("selection")
def set_zoom_x(ev):
"""sets zoomed x selector data"""
# get the selected data
selected_data = ev.get_selected_data()
# remove the current zoomed data
# and update with new selected data
global zoomed_x
fig[1, 0].remove_graphic(zoomed_x)
zoomed_x = fig[1, 0].add_line(selected_data)
fig[1, 0].auto_scale()
fig.show(maintain_aspect=False)
Out:
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32
warn(f"casting {array.dtype} array to float32")
<wgpu.gui.offscreen.WgpuManualOffscreenCanvas object at 0x7f8728f94f90>
# close the plot
fig.close()
For more examples, please see our gallery:
https://fastplotlib.readthedocs.io/en/latest/_gallery/index.html
Total running time of the script: ( 0 minutes 2.413 seconds)
Download Python source code: 03_fpl_intro.py