Skip to content

Instantly share code, notes, and snippets.

@asherp
Last active June 9, 2018 14:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asherp/1ccffa5886cd5d3696b635c43e9f9d50 to your computer and use it in GitHub Desktop.
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.
'''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