Created
September 29, 2022 15:22
-
-
Save rgerum/a729b68dbaac71ca45707b636a1d1fa0 to your computer and use it in GitHub Desktop.
Gradient Patches for Matplotlib
This file contains hidden or 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
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from matplotlib.patches import Polygon | |
| def add_gradient_patch(polygon, start, end, color1=None, color2=None, stops=None, ax=None, count=100, debug=False, clip_on=True, **kwargs): | |
| import matplotlib as mpl | |
| import matplotlib.transforms as mtransforms | |
| # get the axes | |
| if ax is None: | |
| ax = plt.gca() | |
| # a rotation transform with a rotation center | |
| def transform(points, angle_rad, origin): | |
| origin = np.asarray(origin) | |
| rot = np.array([[np.cos(angle_rad), -np.sin(angle_rad)], | |
| [np.sin(angle_rad), np.cos(angle_rad)]]) | |
| return (points - origin) @ rot + origin | |
| # convert points and start&end to array | |
| polygon = np.asarray(polygon, dtype=float) | |
| startend = np.asarray([start, end], dtype=float) | |
| # the angle of the gradient | |
| start_end_diff = np.diff(startend, axis=0)[0] | |
| scale = np.linalg.norm(start_end_diff) | |
| angle_rad = np.arctan2(start_end_diff[1], start_end_diff[0]) | |
| # rotate points and start&end | |
| points_rotate = transform(polygon, angle_rad, startend[0]) | |
| startend_rot = transform(startend, angle_rad, startend[0]) | |
| scale_y = np.max(points_rotate[:, 1]) - np.min(points_rotate[:, 1]) | |
| # the offset the gradient image has to be shifted perpendicular to the gradient to cover the while polygon | |
| diff_rot = np.array([0, -startend_rot[0, 1] + np.min(points_rotate, axis=0)[1]]) | |
| diff = transform(diff_rot, -angle_rad, [0, 0]) | |
| # the parallel distance of the bounding box of the polygon to the start of the gradient (to fill with color1) | |
| diff_x1 = np.array([-startend_rot[0, 0] + np.min(points_rotate, axis=0)[0], 0]) | |
| # the parallel distance of the bounding box of the polygon to the end of the gradient (to fill with color2) | |
| diff_x2 = np.array([-startend_rot[1, 0] + np.max(points_rotate, axis=0)[0], 0]) | |
| # shift the gradient points to the edge of the bounding box | |
| startend += diff | |
| startend_rot += diff_rot | |
| # how much to add before the start of the gradient | |
| if diff_x1[0] < 0: | |
| offset_start = int(np.ceil(-diff_x1[0]/np.linalg.norm(start_end_diff)*count)) | |
| else: | |
| offset_start = 0 | |
| # how much to add after the start of the gradient | |
| if diff_x2[0] > 0: | |
| offset_end = int(np.ceil(diff_x2[0] / np.linalg.norm(start_end_diff) * count)) | |
| else: | |
| offset_end = 0 | |
| image = np.zeros((1, offset_start+count+offset_end, 4)) | |
| if stops is None: | |
| stops = [(color1, 0), (color2, 1)] | |
| fraction = np.hstack((np.zeros(offset_start), np.linspace(0, 1, count, dtype=float), np.ones(offset_end))) | |
| for i in range(0, len(stops)): | |
| color = np.array(mpl.colors.to_rgba(stops[i][0]))[None, None, :] | |
| def start_end(start, end, invert=False): | |
| f = (fraction - start) / (end - start) | |
| if invert: | |
| f = 1 - f | |
| f[fraction < start] = 0 | |
| if end < 1: | |
| f[fraction >= end] = 0 | |
| return f | |
| if i > 0: | |
| image += start_end(stops[i-1][1], stops[i][1], False)[None, :, None] * color | |
| if i < len(stops)-1: | |
| image += start_end(stops[i][1], stops[i+1][1], True)[None, :, None] * color | |
| # show the image with interpolation | |
| im = ax.imshow(image, extent=[0, 1, 0, 1], interpolation="bilinear", aspect='auto') | |
| # transformed image to cover the whole polygon | |
| offset = startend[0]-start_end_diff*offset_start/count | |
| im.set_transform(mtransforms.Affine2D().scale(scale*(1+offset_start/count+offset_end/count), scale_y) | |
| + mtransforms.Affine2D().rotate_deg(np.rad2deg(angle_rad)) | |
| + mtransforms.Affine2D().translate(*offset) | |
| + ax.transData) | |
| # optionally show the start and end of the gradient | |
| if debug: | |
| startend -= diff | |
| plt.plot(startend[:, 0], startend[:, 1], "o--k", mfc="none") | |
| # generate the polygon and clip the image to it | |
| patch = Polygon(polygon, transform=ax.transData, facecolor="none", clip_on=clip_on, **kwargs) | |
| ax.add_patch(patch) | |
| im.set_clip_path(patch) | |
| if clip_on is False: | |
| im.set_clip_box(ax.figure.bbox) | |
| def add_gradient(points, start, end, color1=None, color2=[1, 1, 1, 0], ax=None, **kwargs): | |
| if ax is None: | |
| ax = plt.gca() | |
| points2 = [] | |
| def trans_point(p): | |
| if isinstance(p, tuple) and getattr(p[1], "transform"): | |
| p = p[1].transform(p[0]) | |
| p = ax.transData.inverted().transform(p) | |
| return p | |
| return p | |
| for p in points: | |
| points2.append(trans_point(p)) | |
| start = trans_point(start) | |
| end = trans_point(end) | |
| add_gradient_patch(points2, start, end, color1, color2, ax=ax, clip_on=False, **kwargs)#, debug=True, edgecolor="k") | |
| if __name__ == "__main__": | |
| np.random.seed(42) | |
| # add axes | |
| ax1 = plt.axes() | |
| ax1.spines[["right", "top"]].set_visible(False) | |
| plt.xlabel("time") | |
| plt.ylabel("amplitude") | |
| # set limits | |
| plt.xlim(-8, 8) | |
| plt.ylim(-1.4, 1.2) | |
| # add gradient triangle to indicate noise | |
| add_gradient_patch([[-6, -1.3], [6, -1.3], [6, -1.1]], [-6, -1.3], [6, -1.2], stops=[([70 / 255., 170 / 255., 70 / 255.], 0), ("orange", 0.7), ([1, 0, 0], 1)], edgecolor="k") | |
| plt.text(6.1, -1.2, "noise", ha="left", va="center") | |
| # add gradient to show start | |
| add_gradient_patch([[-7.5, -0.3], [-2*np.pi, 0], [-6.5, -0.4]], [-7, -0.35], [-7.0, 0], | |
| color1="C0", color2="C1") | |
| plt.text(-7.5, -0.5, "start", ha="left", va="center") | |
| # crate and plot "data" | |
| x = np.linspace(-2*np.pi, 2 * np.pi, 360*2) | |
| y = np.sin(x)+np.random.normal(0, np.linspace(0, 0.1, 360*2)) | |
| plt.plot(x, y) | |
| # create inset | |
| ax2 = plt.axes([0.75, 0.75, 0.2, 0.2]) | |
| plt.xlim(-0.11, 0.11) | |
| plt.ylim(-0.11, 0.11) | |
| ax2.spines[["right", "top"]].set_visible(False) | |
| # plot "data" | |
| plt.plot(x, y) | |
| add_gradient([ | |
| ([-0.1, -0.1], ax1.transData), | |
| ([1, 0], ax2.transAxes), | |
| ([0, 1], ax2.transAxes), | |
| ([0.1, 0.1], ax1.transData), | |
| ], ([0, 0], ax1.transData), | |
| ([0, 0.5], ax2.transAxes), [1, 1, 1, 0.3], [0.6, 0.6, 0.6], ax=ax1) | |
| plt.show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment