Last active
June 9, 2018 14:59
-
-
Save asherp/1ccffa5886cd5d3696b635c43e9f9d50 to your computer and use it in GitHub Desktop.
Filled line color plot [plotly]. Uses interpolating bezier shape to mask a color contour plot.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'''Bezier interpolation method based on | |
http://www.antigrain.com/research/bezier_interpolation/ | |
''' | |
import numpy as np | |
import plotly.graph_objs as go | |
import colorlover as cl | |
def roll_forward(a, steps = 1, axis = 0): | |
return np.roll(a, steps, axis = axis) | |
def roll_back(a, steps = -1, axis = 0): | |
return np.roll(a, steps, axis = axis) | |
def get_segment_ratios(points): | |
segments = points - roll_forward(points) | |
segment_lengths = np.linalg.norm(segments, axis=1) | |
if np.isclose(segment_lengths, 0).any(): | |
segment_lengths = np.ones(len(segment_lengths)) | |
return roll_back(segment_lengths)/segment_lengths | |
def get_midpoint_segments(points): | |
midpoints = (points + roll_forward(points))/2 | |
return midpoints - roll_back(midpoints) | |
def get_interpolating_bezier(points, smoothing = .5, validate = False): | |
ratios = get_segment_ratios(points) | |
dm = get_midpoint_segments(points) | |
dm_lengths = np.linalg.norm(dm, axis = 1) | |
if np.isclose(dm_lengths, 0).any(): | |
dm_lengths = np.ones(len(dm_lengths)) | |
dm_norm = (dm.T/dm_lengths.reshape(1,-1)).T | |
if validate: | |
assert np.isclose(np.linalg.norm(dm_norm, axis =1), 1).all() | |
d_mag = dm_lengths/(1+ratios) | |
d1 = (dm_norm.T*d_mag.reshape(1,-1)).T | |
if validate: | |
assert (np.linalg.norm(d1, axis = 1) < dm_lengths).all() | |
d2 = d1 - dm | |
if validate: | |
assert (np.linalg.norm(d2, axis = 1) < dm_lengths).all() | |
return points+smoothing*d1, points+smoothing*d2 | |
def get_smoothed_svg_path(points, path = '', smoothing = .1): | |
c1, c2 = get_interpolating_bezier(points, smoothing) | |
for i in range(len(points)-1): | |
path += 'C {:f} {:f} '.format(*c2[i]) | |
path += '{:f} {:f} '.format(*c1[i+1]) | |
path += '{:f} {:f} '.format(*points[i+1]) | |
return path | |
def bezier_shape(points, portion = 'upper', smoothing = .75, ymax = None, ymin = None): | |
if portion == 'upper': | |
if ymax is None: | |
extremum = max([p[1] for p in points]) | |
else: | |
extremum = ymax | |
else: | |
if ymin is None: | |
extremum = min([p[1] for p in points]) | |
else: | |
extremum = ymin | |
corner_left = points[0][0], extremum | |
corner_right = points[-1][0], extremum | |
path = 'M {:f} {:f} '.format(*corner_left) | |
path += 'L {:f} {:f}'.format(*points[0]) | |
path += get_smoothed_svg_path(points, path, smoothing) | |
path += 'L {:f} {:f} '.format(*corner_right) | |
if portion == 'upper': | |
path += 'Z' | |
return path | |
def filled_line_shapes(points, bgcolor = 'white', fillcolor = 'rgba(0, 0, 0, .2)', opacity = 1, line_width = 15, line_color = 'white', ymin = None, ymax = None): | |
upper = bezier_shape(points, portion = 'upper', ymax = ymax) | |
lower = bezier_shape(points, portion = 'lower', ymin = ymin) | |
path = get_smoothed_svg_path(points) | |
mask = dict(type = 'path', | |
path = upper, | |
fillcolor = bgcolor, | |
line = dict(width = 2, color = bgcolor)) | |
fill = dict(type = 'path', | |
path = lower, | |
fillcolor = fillcolor, | |
opacity = opacity, | |
line = dict(width = 0, color = line_color)) | |
return [mask, fill] | |
def interp_color(x, colorscale, x_range = None): | |
if x_range is not None: | |
x = (x - x_range[0])/(x_range[1] - x_range[0]) | |
rgb = np.array(cl.to_numeric([scale[1] for scale in colorscale])) | |
vals = np.array([scale[0] for scale in colorscale]) | |
return 'rgb({},{},{})'.format(*[int(np.interp(x, vals, rgb[:,i])) for i in range(3)]) | |
def filled_line_plot(points, colorscale = None, line_color = None, zmin = None, zmax = None, **layout_kwargs): | |
'''filled line plot supporting colormaps | |
array must be a function with no gaps (use array.fillna(0)) | |
''' | |
if colorscale is None: | |
scale = cl.scales['9']['seq']['PuBuGn'] | |
colorscale = zip(np.linspace(0,1, len(scale)), scale) | |
bgcolor = layout_kwargs.get('paper_bgcolor', 'white') | |
if line_color is None: | |
line_color = bgcolor | |
y = points[:,1] | |
if zmin is not None: | |
ymin = zmin | |
else: | |
ymin = y.min() | |
if zmax is not None: | |
ymax = zmax | |
else: | |
ymax = y.max() | |
data = [] | |
layout = dict(**layout_kwargs) | |
if len(points) == 1: | |
point_color = interp_color(points[0][1], colorscale, (ymin, ymax)) | |
print 'point color:', point_color | |
data.append(go.Scatter( | |
x = [points[0][0], points[0][0]], | |
y = [ymin, points[0][1]], | |
mode = 'lines+markers', | |
marker = dict( | |
size = 0, | |
cmin = ymin, | |
cmax = ymax, | |
color = [points[0][1], points[0][1]], | |
colorscale = colorscale, | |
showscale = True), | |
showlegend = False, | |
line = dict( | |
width = 10, | |
color = point_color, | |
))) | |
else: | |
points_repeated = np.repeat(y, 2).reshape((len(y),2)).T | |
data.append(go.Contour( | |
z = points_repeated, | |
x = points[:,0], | |
y = [ymin, ymax], | |
zmin = zmin, | |
zmax = zmax, | |
colorscale = colorscale, | |
contours = dict(coloring = 'heatmap'), | |
line = dict(width = 0))) | |
shapes = filled_line_shapes(points, bgcolor = bgcolor, line_color = line_color, ymin = zmin, ymax = zmax) | |
layout['shapes'] = shapes | |
return go.Figure(data = data, layout = layout) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment