This notebook walks through Glue's new ability to build custom, user-defined visualizations.
%matplotlib inline
from skimage.io import imread
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from IPython.html.widgets import interact
from IPython.display import YouTubeVideo, HTML
plt.rcParams.update(**{'figure.figsize':(8,6),
'figure.dpi':500,
'font.size':12})
We'll be working with data from this video -- time trial data of many people playing a level in a Super Mario Bros. clone.
YouTubeVideo('cTiJaWCaKas')
The two data files can be downloaded here:
data = pd.read_csv('mario_data.csv')
df = pd.read_csv('timelines.csv')
df.head()
run_id | time | x | y | animationdirection | animationstate | finaltime | |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 2.625000 | 12.25 | right | idle | 67.633333 |
1 | 1 | 100 | 2.629444 | 12.25 | right | running | 67.633333 |
2 | 1 | 200 | 2.629444 | 12.25 | right | running | 67.633333 |
3 | 1 | 300 | 2.629444 | 12.25 | right | running | 67.633333 |
4 | 1 | 400 | 2.629444 | 12.25 | right | running | 67.633333 |
The columns in this dataset are:
The timelines.csv
file is a subset of the datapoints in mario_data.csv
, resampled onto a uniform time grid.
Here's the typical kind of exploration we might do using static visualizations in the notebook:
# how long does each player take to finish?
df.finaltime.hist(fc='#575757')
plt.xlabel("Time to finish level (s)")
<matplotlib.text.Text at 0x10a6d96d0>
# Path through the level
plt.plot(df.x, -df.y, '.k', alpha=0.3)
plt.xlim(0, 100)
(0, 100)
# restack timelines so each run_id is a row
timelines = df.set_index(['run_id', 'time']).unstack()
timelines.head()
x | ... | finaltime | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
time | 0 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | ... | 157100 | 157200 | 157300 | 157400 | 157500 | 157600 | 157700 | 157800 | 157900 | 158000 |
run_id | |||||||||||||||||||||
1 | 2.625 | 2.629444 | 2.629444 | 2.629444 | 2.629444 | 2.718333 | 2.888333 | 2.921111 | 3.010000 | 3.263333 | ... | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 | 67.633333 |
10 | 2.625 | 2.749444 | 3.029444 | 3.469444 | 4.042778 | 4.618778 | 5.088111 | 5.357778 | 5.350778 | 5.172111 | ... | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 | 90.733333 |
11 | 2.625 | 2.718333 | 2.971667 | 3.385000 | 3.953889 | 4.567222 | 5.180556 | 5.714444 | 5.996111 | 5.989444 | ... | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 | 67.066667 |
12 | 2.625 | 2.718333 | 2.918333 | 3.385000 | 3.807222 | 4.313889 | 4.883667 | 5.127556 | 5.156889 | 4.961667 | ... | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 | 63.900000 |
13 | 2.625 | 2.749444 | 3.029444 | 3.469444 | 4.069444 | 4.250000 | 4.250000 | 4.250000 | 4.250000 | 4.263333 | ... | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 | 79.200000 |
5 rows × 7905 columns
# plot path on top of an image of the level
plt.figure(figsize=(25, 15), dpi=200)
im = imread('bg.png')
im = im[:, :2000]
plt.imshow(im, origin='upper')
plt.plot(df.x * 16, df.y * 16, '.k', alpha=0.3)
plt.xlim(0, 1000)
(0, 1000)
Static visualizations can sometime feel cumbersome when slicing and dicing data. The IPython widget features gives us some ability to add interactivity, with almost no extra effort
im = imread('bg.png')
def explore(run, time):
# show path of a single run (red line), emphasizing a particular timestamp (black dot)
plt.figure(figsize=(15, 7), dpi=100)
ax = plt.gca()
show_image(ax)
draw_timeline(ax, run, time)
def show_image(axes):
axes.imshow(im, interpolation='nearest', origin='upper', extent=[0, im.shape[1] / 16., im.shape[0] / 16., 0])
axes.set_xlim(0, 75)
axes.set_ylim(14, -5)
def draw_timeline(axes, run, time):
sub = timelines.iloc[run]
x = sub.x
y = sub.y
xt = x.values[time]
yt = y.values[time]
axes.plot(x, y, 'r-', # plot the timeline
[xt], [yt], 'ko', # overplot the black circle
lw=3, ms=20)
explore(run=5, time=20)
# make run and time interactive parameters
interact(explore, run=(0, 110), time=(0, 350))
<function __main__.explore>
Custom viewers in Glue are inspired by IPython widgets. Glue lets you specify some adjustable parameters, along with some functions to perform specific tasks like visualizing a dataset, performing selection, etc. Glue then uses these functions to build a specialized viewer.
It's worth noting a few things about this code:
run
and time
keywords to custom_viewer
behave like IPython's interact feature -- they create slider widgets.
You can also create checkboxes, dropdown menus, etc.x
, y
, and runid
keywords give the functions below access to data attributes (ie particular columns of whatever dataset is loaded into Glue)setup
, settings_changed
, and draw_subset
functions are copied directly from above.See the documentation for more information about how Custom Viewers in Glue work.
from glue import custom_viewer, qglue
mario_viewer = custom_viewer('Mario',
run=(0, 110),
time=(0, 350),
x='att(x)',
y='att(y)',
full_path=False,
runid='att(run_id)')
@mario_viewer.setup
def show_image(axes):
axes.imshow(im, interpolation='nearest', origin='upper',
extent=[0, im.shape[1] / 16., im.shape[0] / 16., 0])
axes.set_xlim(0, 75)
axes.set_ylim(14, -5)
@mario_viewer.settings_changed
def draw_timeline(axes, run, time):
sub = timelines.iloc[run]
x = sub.x
y = sub.y
xt = x.values[time]
yt = y.values[time]
axes.plot(x, y, 'r-', # plot the timeline
[xt], [yt], 'ko', # overplot the black circle
lw=3, ms=20)
@mario_viewer.plot_subset
def draw_subset(axes, x, y, style):
axes.plot(x, y, 'o', mec='none', mfc=style.color)
@mario_viewer.select
def select(roi, x, y, runid, full_path):
in_roi = roi.contains(x, y)
if not full_path:
# just select points inside the ROI
return in_roi
# return all points inside any run which passes through the ROI
return np.in1d(runid, runid[in_roi])
# startup Glue
qglue(data=data)
<glue.qt.glue_application.GlueApplication at 0x11328bcc8>
Custom viewers in Glue combine the functionality of IPython widgets with Glue's ability to select data and build linked-view visualizations.
Here are some interesting ways to explore the data in the viewer:
styles = open('custom.css', 'r').read()
HTML(styles)