from math import pi
import numpy as np
import branca
from ipywidgets import VBox, IntSlider
from ipycanvas import Canvas, MultiCanvas, hold_canvas
class Plot(MultiCanvas):
def __init__(self, x, y, color=None, scheme=branca.colormap.linear.RdBu_11):
super(Plot, self).__init__(3, width=800, height=600, sync_image_data=True)
self.color = color
self.scheme = scheme
self.background_color = "#f7f7f7"
self.init_plot(x, y)
def init_plot(self, x, y, color=None, scheme=None):
self.x = x
self.y = y
self.color = color if color is not None else self.color
self.scheme = scheme if scheme is not None else self.scheme
padding = 0.1
padding_x = padding * self.width
padding_y = padding * self.height
# TODO Fix drawarea max: It should be (canvas.size - padding)
self.drawarea = (
drawarea_min_x,
drawarea_min_y,
drawarea_max_x,
drawarea_max_y,
) = (
padding_x,
padding_y,
self.width - 2 * padding_x,
self.height - 2 * padding_y,
)
min_x, min_y, max_x, max_y = np.min(x), np.min(y), np.max(x), np.max(y)
dx = max_x - min_x
dy = max_y - min_y
# Turns a data coordinate into pixel coordinate
self.scale_x = lambda x: drawarea_max_x * (x - min_x) / dx + drawarea_min_x
self.scale_y = (
lambda y: drawarea_max_y * (1 - (y - min_y) / dy) + drawarea_min_y
)
# Turns a pixel coordinate into data coordinate
self.unscale_x = lambda sx: (sx - drawarea_min_x) * dx / drawarea_max_x + min_x
self.unscale_y = (
lambda sy: (1 - ((sy - drawarea_min_y) / drawarea_max_y)) * dy + min_y
)
self.colormap = None
if self.color is not None:
self.colormap = self.scheme.scale(np.min(self.color), np.max(self.color))
def draw_background(self):
drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y = self.drawarea
background = self[0]
# Draw background
background.fill_style = self.background_color
background.global_alpha = 0.3
background.fill_rect(
drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y
)
background.global_alpha = 1
# Draw grid and ticks
n_lines = 10
background.fill_style = "black"
background.stroke_style = "#8c8c8c"
background.line_width = 1
for i in range(n_lines):
j = i / (n_lines - 1)
line_x = drawarea_max_x * j + drawarea_min_x
line_y = drawarea_max_y * j + drawarea_min_y
# Line on the y axis
background.stroke_line(
line_x, drawarea_min_y, line_x, drawarea_max_y + drawarea_min_y
)
# Line on the x axis
background.stroke_line(
drawarea_min_x, line_y, drawarea_max_x + drawarea_min_x, line_y
)
# Draw y tick
background.text_align = "right"
background.text_baseline = "middle"
background.fill_text(
"{0:.2e}".format(self.unscale_y(line_y)), drawarea_min_x * 0.95, line_y
)
# Draw x tick
background.text_align = "center"
background.text_baseline = "top"
background.fill_text(
"{0:.2e}".format(self.unscale_x(line_x)),
line_x,
drawarea_max_y + drawarea_min_y + drawarea_min_y * 0.05,
)
class ScatterPlot(Plot):
def __init__(
self,
x,
y,
size,
color,
scheme=branca.colormap.linear.RdBu_11,
stroke_color="black",
):
super(ScatterPlot, self).__init__(x, y, color, scheme)
self.dragging = False
self.sizes = size
self.stroke_color = stroke_color
self.n_marks = min(x.shape[0], y.shape[0], size.shape[0], color.shape[0])
# Index of the dragged point
self.i_mark = -1
self[2].on_mouse_down(self.mouse_down_handler)
self[2].on_mouse_move(self.mouse_move_handler)
self[2].on_mouse_up(self.mouse_up_handler)
self.draw()
def draw(self):
with hold_canvas():
self.clear()
plot_layer = self[1]
plot_layer.save()
self.draw_background()
# Draw scatter
plot_layer.stroke_style = self.stroke_color
for idx in range(self.n_marks):
plot_layer.fill_style = self.colormap(self.color[idx])
mark_x = self.scale_x(self.x[idx])
mark_y = self.scale_y(self.y[idx])
mark_size = self.sizes[idx]
plot_layer.fill_circle(mark_x, mark_y, mark_size)
plot_layer.stroke_circle(mark_x, mark_y, mark_size)
plot_layer.restore()
def mouse_down_handler(self, pixel_x, pixel_y):
plot_layer = self[1]
for idx in range(self.n_marks):
mark_x = self.x[idx]
mark_y = self.y[idx]
mark_size = self.sizes[idx]
if (
pixel_x > self.scale_x(mark_x) - mark_size
and pixel_x < self.scale_x(mark_x) + mark_size
and pixel_y > self.scale_y(mark_y) - mark_size
and pixel_y < self.scale_y(mark_y) + mark_size
):
self.i_mark = idx
self.dragging = True
with hold_canvas():
plot_layer.fill_style = self.background_color
plot_layer.stroke_style = self.colormap(self.color[self.i_mark])
plot_layer.fill_circle(
self.scale_x(mark_x), self.scale_y(mark_y), mark_size
)
plot_layer.stroke_circle(
self.scale_x(mark_x), self.scale_y(mark_y), mark_size
)
break
def mouse_move_handler(self, pixel_x, pixel_y):
if self.dragging and self.i_mark != -1:
interaction_layer = self[2]
unscaled_x = self.unscale_x(pixel_x)
unscaled_y = self.unscale_y(pixel_y)
with hold_canvas():
interaction_layer.clear()
interaction_layer.fill_style = self.colormap(self.color[self.i_mark])
interaction_layer.stroke_style = self.stroke_color
self.x[self.i_mark] = unscaled_x
self.y[self.i_mark] = unscaled_y
interaction_layer.fill_circle(pixel_x, pixel_y, self.sizes[self.i_mark])
interaction_layer.stroke_circle(
pixel_x, pixel_y, self.sizes[self.i_mark]
)
def mouse_up_handler(self, pixel_x, pixel_y):
self.dragging = False
self.draw()
interaction_layer = self[2]
interaction_layer.clear()
class LinePlot(Plot):
def __init__(self, x, y, line_color="#749cb8", line_width=2):
super(LinePlot, self).__init__(x, y)
self.line_color = line_color
self.line_width = line_width
self.draw()
def update(self, x, y, line_color=None, line_width=None):
self.init_plot(x, y)
self.line_color = line_color if line_color is not None else self.line_color
self.line_width = line_width if line_width is not None else self.line_width
self.draw()
def draw(self):
with hold_canvas():
self.clear()
plot_layer = self[1]
plot_layer.save()
self.draw_background()
# Draw lines
n_points = min(self.x.shape[0], self.y.shape[0])
plot_layer.stroke_style = self.line_color
plot_layer.line_width = self.line_width
plot_layer.line_join = "bevel"
plot_layer.line_cap = "round"
plot_layer.stroke_lines(
np.stack((self.scale_x(self.x), self.scale_y(self.y)), axis=1)
)
plot_layer.restore()
class HeatmapPlot(Plot):
def __init__(self, x, y, color, scheme=branca.colormap.linear.RdBu_11):
super(HeatmapPlot, self).__init__(x, y, color, scheme)
self.draw()
def draw(self):
outof_x_bound = lambda idx: True if idx >= x.shape[0] or idx < 0 else False
outof_y_bound = lambda idx: True if idx >= y.shape[0] or idx < 0 else False
with hold_canvas():
self.clear()
plot_layer = self[1]
plot_layer.save()
self.draw_background()
# Draw heatmap
n_marks = min(self.x.shape[0], self.y.shape[0])
for x_idx in range(1, self.color.shape[0] - 1):
for y_idx in range(1, self.color.shape[1] - 1):
plot_layer.fill_style = self.colormap(self.color[x_idx][y_idx])
rect_center = (
self.scale_x(self.x[x_idx]),
self.scale_y(self.y[y_idx]),
)
neighbours_x = (
self.scale_x(self.x[x_idx - 1]),
self.scale_x(self.x[x_idx + 1]),
)
neighbours_y = (
self.scale_y(self.y[y_idx - 1]),
self.scale_y(self.y[y_idx + 1]),
)
rect_top_left_corner = (
(neighbours_x[0] + rect_center[0]) / 2,
(neighbours_y[0] + rect_center[1]) / 2,
)
rect_low_right_corner = (
(neighbours_x[1] + rect_center[0]) / 2,
(neighbours_y[1] + rect_center[1]) / 2,
)
width = rect_low_right_corner[0] - rect_top_left_corner[0] + 0.5
height = rect_low_right_corner[1] - rect_top_left_corner[1] - 0.5
plot_layer.fill_rect(
rect_top_left_corner[0], rect_top_left_corner[1], width, height
)
plot_layer.restore()
n_points = 1_000
x = np.random.rand(n_points)
y = np.random.rand(n_points)
sizes = np.random.randint(2, 8, n_points)
colors = np.random.rand(n_points) * 10 - 2
plot = ScatterPlot(
x, y, sizes, colors, branca.colormap.linear.viridis, stroke_color="white"
)
plot
Canvas
or a subpart of it using the get_image_data
method¶arr = plot.get_image_data(200, 300, 50, 100)
arr.shape
plot[1].stroke_style = "red"
plot[1].line_width = 2
plot[1].stroke_rect(200, 300, 50, 100)
c = Canvas(width=50, height=100)
c.put_image_data(arr, 0, 0)
c
to_file
¶plot.to_file("my_scatter.png")
from ipywidgets import Image
Image.from_file("my_scatter.png")
x = np.linspace(0, 20, 500)
y = np.sin(x)
LinePlot(x, y, line_width=3)
slider = IntSlider(description="Pow:", min=1, max=10, step=1)
x = np.linspace(-20, 20, 500)
y = np.power(x, slider.value)
power_plot = LinePlot(x, y, line_color="#32a852", line_width=3)
def on_slider_change(change):
y = np.power(x, slider.value)
power_plot.update(x, y)
slider.observe(on_slider_change, "value")
VBox((power_plot, slider))
n = 2_000
x = np.linspace(0, 100, n)
y = np.cumsum(np.random.randn(n))
LinePlot(x, y, line_width=3)
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x_grid, y_grid = np.meshgrid(x, y)
color = np.sin(x_grid + y_grid**2) + np.cos(x_grid**2 + y_grid**2)
HeatmapPlot(x, y, color, scheme=branca.colormap.linear.RdYlBu_05)