Quick Start Guide 🚀

This notebook goes through the basic components of the fastplotlib API, images, image updates, line plots, scatter plots, and grid plots.

NOTE: This quick start guide in the docs is NOT interactive. Download the examples from the repo and try them on your own computer. You can run the desktop examples directly if you have ``glfw`` installed, or try the notebook demos: https://github.com/kushalkolar/fastplotlib/tree/master/examples

It will not be possible to have live demos on the docs until someone can figure out how to get pygfx to work with wgpu in the browser, perhaps through pyodide or something :D.

The example images are from ``imageio`` so you will need to install it for this example notebook. But ``imageio`` is not required to use ``fasptlotlib``

[1]:
!pip install imageio
Collecting imageio
  Downloading imageio-2.34.1-py3-none-any.whl.metadata (4.9 kB)
Requirement already satisfied: numpy in /home/docs/checkouts/readthedocs.org/user_builds/fastplotlib/envs/latest/lib/python3.11/site-packages (from imageio) (1.26.4)
Collecting pillow>=8.3.2 (from imageio)
  Downloading pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (9.2 kB)
Downloading imageio-2.34.1-py3-none-any.whl (313 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 313.5/313.5 kB 7.0 MB/s eta 0:00:00
Downloading pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl (4.5 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 63.4 MB/s eta 0:00:00
Installing collected packages: pillow, imageio
Successfully installed imageio-2.34.1 pillow-10.3.0
[2]:
import imageio.v3 as iio
[3]:
import fastplotlib as fpl
from ipywidgets import VBox, HBox, IntSlider
import numpy as np
error: XDG_RUNTIME_DIR not set in the environment.
No windowing system present. Using surfaceless platform
No config found!
No config found!
Available devices:
❗ (default) | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)
❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | OpenGL |
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.

Images

[4]:
# create a `Figure` instance
fig = fpl.Figure()

# 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()
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
error: XDG_RUNTIME_DIR not set in the environment.
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/docs/.imageio/images/camera.png.
[4]:
snapshot

In live notebooks or desktop applications, you can use the handle on the bottom right corner of the canvas to resize it. You can also pan and zoom using your mouse!

This is how you can take a snapshot of the canvas. Snapshots are shown throughout this doc page for the purposes of documentation, they are NOT necessary for real interactive usage. Download the notebooks to run live demos.

[5]:
fig.canvas.snapshot()
[5]:
snapshot

Changing graphic “features”

[6]:
image_graphic.cmap = "viridis"
[7]:
fig.canvas.snapshot()
[7]:
snapshot

Slicing data

Most features, such as ``data`` support slicing!

Out image data is of shape [n_rows, n_cols]

[8]:
image_graphic.data().shape
[8]:
(512, 512)
[9]:
image_graphic.data[::15, :] = 1
image_graphic.data[:, ::15] = 1
[10]:
fig.canvas.snapshot()
[10]:
snapshot

Fancy indexing

[11]:
image_graphic.data[data > 175] = 255
[12]:
fig.canvas.snapshot()
[12]:
snapshot

Adjust vmin vmax

[13]:
image_graphic.cmap.vmin = 50
image_graphic.cmap.vmax = 150
[14]:
fig.canvas.snapshot()
[14]:
snapshot

Set the entire data array again

Note: The shape of the new data array must match the current data shown in the Graphic.

[15]:
new_data = iio.imread("imageio:astronaut.png")
new_data.shape
Imageio: 'astronaut.png' was not found on your computer; downloading it now.
Try 1. Download from https://github.com/imageio/imageio-binaries/raw/master/images/astronaut.png (773 kB)
Downloading: 8192/791555 bytes (1.0%)791555/791555 bytes (100.0%)
  Done
File saved as /home/docs/.imageio/images/astronaut.png.
[15]:
(512, 512, 3)

This is an RGB image, convert to grayscale to maintain the shape of (512, 512)

[16]:
gray = new_data.dot([0.3, 0.6, 0.1])
gray.shape
[16]:
(512, 512)
[17]:
image_graphic.data = gray
/home/docs/checkouts/readthedocs.org/user_builds/fastplotlib/envs/latest/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:34: UserWarning: converting float64 array to float32
  warn(f"converting {array.dtype} array to float32")
[18]:
fig.canvas.snapshot()
[18]:
snapshot

reset vmin vmax

[19]:
image_graphic.cmap.reset_vmin_vmax()
[20]:
fig.canvas.snapshot()
[20]:
snapshot

Indexing plots

Plots are indexable and give you their graphics by name

[21]:
fig
[21]:
fastplotlib.Figure @ 0x7fd087f10550
  Subplots:
        unnamed: Subplot @ 0x7fd087f111d0
[22]:
fig[0, 0]["sample-image"]
[22]:
<weakproxy at 0x7fd053e046d0 to ImageGraphic at 0x7fd053e08dd0>

You can also use numerical indexing on ``plot.graphics``

[23]:
fig[0, 0].graphics
[23]:
(<weakproxy at 0x7fd053e046d0 to ImageGraphic at 0x7fd053e08dd0>,)
[24]:
fig[0, 0].graphics[0]
[24]:
<weakproxy at 0x7fd053e046d0 to ImageGraphic at 0x7fd053e08dd0>

The Graphic instance is also returned when you call plot.add_<graphic_type>.

[25]:
image_graphic
[25]:
<weakproxy at 0x7fd053e046d0 to ImageGraphic at 0x7fd053e08dd0>
[26]:
image_graphic is fig[0, 0]["sample-image"]
[26]:
True

RGB images

cmap arguments are ignored for rgb images, but vmin vmax still works

[27]:
fig_rgb = fpl.Figure()

fig_rgb[0, 0].add_image(new_data, name="rgb-image")

fig_rgb.show()
[27]:
snapshot
[28]:
fig_rgb.canvas.snapshot()
[28]:
snapshot

vmin and vmax are still applicable to rgb images

[29]:
fig_rgb[0, 0]["rgb-image"].cmap.vmin = 100
[30]:
fig_rgb.canvas.snapshot()
[30]:
snapshot

Image updates

This examples show how you can define animation functions that run on every render cycle.

[31]:
# create another `Figure` instance
fig_vid = fpl.Figure()

# make some random data again
data = np.random.rand(512, 512)

# plot the data
fig_vid[0, 0].add_image(data=data, name="random-image")

# a function to update the image_graphic
# a subplot will pass its instance to the animation function as an argument
def update_data(subplot):
    new_data = np.random.rand(512, 512)
    subplot["random-image"].data = new_data

#add this as an animation function to the subplot
fig_vid[0, 0].add_animations(update_data)

# show the plot
fig_vid.show()
[31]:
snapshot

Share controllers across plots

This example creates a new plot, but it synchronizes the pan-zoom controller

[32]:
fig_sync = fpl.Figure(controllers=fig_vid.controllers)

data = np.random.rand(512, 512)

image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap="viridis")

# you will need to define a new animation function for this graphic
def update_data_2():
    new_data = np.random.rand(512, 512)
    # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot
    image_graphic_instance.data = new_data

# add the animation function to the figure instead of the subplot
fig_sync.add_animations(update_data_2)

fig_sync.show()
[32]:
snapshot

Keeping a reference to the Graphic instance, as shown above image_graphic_instance, is useful if you’re creating something where you need flexibility in the naming of the graphics

You can also use ipywidgets.VBox and HBox to stack plots. See the gridplot notebooks for a proper gridplot interface for more automated subplotting

Not shown in the docs, try the live demo for this feature

[33]:
#VBox([plot_v.canvas, plot_sync.show()])
[34]:
#HBox([plot_v.show(), plot_sync.show()])

Line plots

2D line plots

This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how Graphic Features can be modified by slicing!

Generate some data.

[35]:
# 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.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]

Plot all of it on the same plot. Each line plot will be an individual Graphic, you can have any combination of graphics on a plot.

[36]:
# Create a plot instance
fig_line = fpl.Figure()

# plot sine wave, use a single color
sine_graphic = fig_line[0, 0].add_line(data=sine, thickness=5, colors="magenta")

# you can also use colormaps for lines!
cosine_graphic = fig_line[0, 0].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 = fig_line[0, 0].add_line(data=sinc, thickness=5, colors = colors)

fig_line.show()
[36]:
snapshot

“stretching” the camera, useful for large timeseries data

Set maintain_aspect = False on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!

You can also click the ``1:1`` button to toggle this.

[37]:
fig_line[0, 0].camera.maintain_aspect = False

reset the plot area

[38]:
fig_line[0, 0].auto_scale(maintain_aspect=True)

Graphic features support slicing! :D

[39]:
# 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)

Make a snapshot of the canvas after slicing

[40]:
fig_line.canvas.snapshot()
[40]:
snapshot

You can capture changes to a graphic feature as events

[41]:
def callback_func(event_data):
    print(event_data)

# Will print event data when the color changes
cosine_graphic.colors.add_event_handler(callback_func)
[42]:
# more complex indexing of colors
# from point 15 - 30, set every 3rd point as "cyan"
cosine_graphic.colors[15:50:3] = "cyan"
FeatureEvent @ 0x7fd053e2cb50
type: colors
pick_info: {'index': range(15, 50, 3), 'collection-index': None, 'world_object': <weakproxy at 0x7fd053e47fb0 to Line at 0x7fd053e1b290>, 'new_data': array([[0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.],
       [0., 1., 1., 1.]], dtype=float32)}

[43]:
fig_line.canvas.snapshot()
[43]:
snapshot

Graphic data is also indexable

[44]:
cosine_graphic.data[10:50:5, :2] = sine[10:50:5]
cosine_graphic.data[90:, 1] = 7
[45]:
cosine_graphic.data[0] = np.array([[-10, 0, 0]])
/home/docs/checkouts/readthedocs.org/user_builds/fastplotlib/envs/latest/lib/python3.11/site-packages/fastplotlib/graphics/_features/_base.py:31: UserWarning: converting int64 array to int32
  warn(f"converting {array.dtype} array to int32")
[46]:
fig_line.canvas.snapshot()
[46]:
snapshot

Toggle the presence of a graphic within the scene

[47]:
sinc_graphic.present = False
[48]:
fig_line.canvas.snapshot()
[48]:
snapshot
[49]:
sinc_graphic.present = True
[50]:
fig_line.canvas.snapshot()
[50]:
snapshot

You can create callbacks to present too, for example to re-scale the plot w.r.t. graphics that are present in the scene

[51]:
sinc_graphic.present.add_event_handler(fig_line[0, 0].auto_scale)
[52]:
sinc_graphic.present = False
[53]:
fig_line.canvas.snapshot()
[53]:
snapshot
[54]:
sinc_graphic.present = True
[55]:
fig_line.canvas.snapshot()
[55]:
snapshot

You can set the z-positions of graphics to have them appear under or over other graphics

[56]:
img = np.random.rand(20, 100)

fig_line[0, 0].add_image(img, name="image", cmap="gray")

# z axis position -1 so it is below all the lines
fig_line[0, 0]["image"].position_z = -1
fig_line[0, 0]["image"].position_x = -50
[57]:
fig_line.canvas.snapshot()
[57]:
snapshot

3D line plot

[58]:
# just set the camera as "3d", the rest is basically the same :D
fig_line_3d = fpl.Figure(cameras='3d')

# create a spiral
phi = np.linspace(0, 30, 200)

xs = phi * np.cos(phi)
ys = phi * np.sin(phi)
zs = phi

# use 3D data
# note: you usually mix 3D and 2D graphics on the same plot
spiral = np.dstack([xs, ys, zs])[0]

fig_line_3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')

fig_line_3d.show()
[58]:
snapshot
[59]:
fig_line_3d[0, 0].auto_scale(maintain_aspect=True)

Scatter plots

Plot tens of thousands or millions of points

There might be a small delay for a few seconds before the plot shows, this is due to shaders being compiled and a few other things. The plot should be very fast and responsive once it is displayed and future modifications should also be fast!

[60]:
# create a random distribution
# only 1,000 points shown here in the docs, but it can be millions
n_points = 1_000

# if you have a good GPU go for 1.5 million points :D
# this is multiplied by 3
#n_points = 500_000

# dimensions always have to be [n_points, xyz]
dims = (n_points, 3)

clouds_offset = 15

# create some random clouds
normal = np.random.normal(size=dims, scale=5)
# stack the data into a single array
cloud = np.vstack(
    [
        normal - clouds_offset,
        normal,
        normal + clouds_offset,
    ]
)

# color each of them separately
colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points

# create plot
fig_scatter = fpl.Figure()

# use an alpha value since this will be a lot of points
scatter_graphic = fig_scatter[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)

fig_scatter.show()
[60]:
snapshot

Scatter graphic features work similarly to line graphic

[61]:
# half of the first cloud's points to red
scatter_graphic.colors[:n_points:2] = "r"
[62]:
fig_scatter.canvas.snapshot()
[62]:
snapshot
[63]:
# set the green value directly
scatter_graphic.colors[n_points:n_points * 2, 1] = 0.3
[64]:
fig_scatter.canvas.snapshot()
[64]:
snapshot
[65]:
# set color values directly using an array
scatter_graphic.colors[n_points * 2:] = np.repeat([[1, 1, 0, 0.5]], n_points, axis=0)
[66]:
fig_scatter.canvas.snapshot()
[66]:
snapshot
[67]:
# change the data, change y-values
scatter_graphic.data[n_points:n_points * 2, 1] += 15
[68]:
fig_scatter.canvas.snapshot()
[68]:
snapshot
[69]:
# set x values directly but using an array
scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)
[70]:
fig_scatter.canvas.snapshot()
[70]:
snapshot

ipywidget layouts

This just plots everything from these examples in a single output cell

[71]:
# row1 = HBox([plot.show(), plot_v.show(), plot_sync.show()])
# row2 = HBox([plot_l.show(), plot_l3d.show(), plot_s.show()])

# VBox([row1, row2])

More subplots

[72]:
# Figure of shape 2 x 3 with all controllers synced
figure_grid = fpl.Figure(shape=(2, 3), controller_ids="sync")

# Make a random image graphic for each subplot
for subplot in figure_grid:
    # create image data
    data = np.random.rand(512, 512)
    # add an image to the subplot
    subplot.add_image(data, name="rand-img")

# Define a function to update the image graphics with new data
# add_animations will pass the gridplot to the animation function
def update_data(f):
    for subplot in f:
        new_data = np.random.rand(512, 512)
        # index the image graphic by name and set the data
        subplot["rand-img"].data = new_data

# add the animation function
figure_grid.add_animations(update_data)

# show the gridplot
figure_grid.show()
[72]:
snapshot

Slicing GridPlot

[73]:
# positional indexing
# row 0 and col 0
figure_grid[0, 0]
[73]:
unnamed: Subplot @ 0x7fd04fa34990
  parent: Figure @ 0x7fd04fbdbbd0
  Graphics:
        'rand-img': ImageGraphic @ 0x7fd04fa73d10

You can get the graphics within a subplot, just like with simple Plot

[74]:
figure_grid[0, 1].graphics
[74]:
(<weakproxy at 0x7fd04faea2f0 to ImageGraphic at 0x7fd04fab9b90>,)

and change their properties

[75]:
figure_grid[0, 1].graphics[0].vmax = 0.5

more slicing with GridPlot

[76]:
# you can give subplots human-readable string names
figure_grid[0, 2].name = "top-right-plot"
[77]:
figure_grid["top-right-plot"]
[77]:
top-right-plot: Subplot @ 0x7fd04fa06850
  parent: Figure @ 0x7fd04fbdbbd0
  Graphics:
        'rand-img': ImageGraphic @ 0x7fd04fae7950
[78]:
# view its position
figure_grid["top-right-plot"].position
[78]:
(0, 2)
[79]:
# these are really the same
figure_grid["top-right-plot"] is figure_grid[0, 2]
[79]:
True

Indexing with subplot name and graphic name

[80]:
figure_grid["top-right-plot"]["rand-img"].vmin = 0.5

Figure subplot customization

[81]:
# 2 rows and 3 columns
shape = (2, 3)

# pan-zoom controllers for each view
# views are synced if they have the
# same controller ID
controller_ids = [
    [0, 3, 1],  # id each controller with an integer
    [2, 2, 3]
]


# you can give string names for each subplot within the gridplot
names = [
    ["subplot0", "subplot1", "subplot2"],
    ["subplot3", "subplot4", "subplot5"]
]

# Create the grid plot
figure_grid = fpl.Figure(
    shape=shape,
    controller_ids=controller_ids,
    names=names,
)


# Make a random image graphic for each subplot
for subplot in figure_grid:
    data = np.random.rand(512, 512)
    # create and add an ImageGraphic
    subplot.add_image(data=data, name="rand-image")


# Define a function to update the image graphics
# with new randomly generated data
def set_random_frame(gp):
    for subplot in gp:
        new_data = np.random.rand(512, 512)
        subplot["rand-image"].data = new_data

# add the animation
figure_grid.add_animations(set_random_frame)
figure_grid.show()
[81]:
snapshot

Indexing the gridplot to access subplots

[82]:
# can access subplot by name
figure_grid["subplot0"]
[82]:
subplot0: Subplot @ 0x7fd04f98ce50
  parent: Figure @ 0x7fd04f95f750
  Graphics:
        'rand-image': ImageGraphic @ 0x7fd04fb85a90
[83]:
# can access subplot by index
figure_grid[0, 0]
[83]:
subplot0: Subplot @ 0x7fd04f98ce50
  parent: Figure @ 0x7fd04f95f750
  Graphics:
        'rand-image': ImageGraphic @ 0x7fd04fb85a90

subplots also support indexing!

this can be used to get graphics if they are named

[84]:
# can access graphic directly via name
figure_grid["subplot0"]["rand-image"]
[84]:
<weakproxy at 0x7fd04f976e30 to ImageGraphic at 0x7fd04fb85a90>
[85]:
figure_grid["subplot0"]["rand-image"].vmin = 0.6
figure_grid["subplot0"]["rand-image"].vmax = 0.8

positional indexing also works event if subplots have string names

[86]:
figure_grid[1, 0]["rand-image"].vim = 0.1
figure_grid[1, 0]["rand-image"].vmax = 0.3