|
3 | 3 | Multicolored lines
|
4 | 4 | ==================
|
5 | 5 |
|
6 |
| -This example shows how to make a multicolored line. In this example, the line |
7 |
| -is colored based on its derivative. |
| 6 | +The example shows two ways to plot a line with the a varying color defined by |
| 7 | +a third value. The first example defines the color at each (x, y) point. |
| 8 | +The second example defines the color between pairs of points, so the length |
| 9 | +of the color value list is one less than the length of the x and y lists. |
| 10 | +
|
| 11 | +Color values at points |
| 12 | +---------------------- |
| 13 | +
|
8 | 14 | """
|
9 | 15 |
|
| 16 | +import warnings |
| 17 | + |
10 | 18 | import matplotlib.pyplot as plt
|
11 | 19 | import numpy as np
|
12 | 20 |
|
13 | 21 | from matplotlib.collections import LineCollection
|
14 |
| -from matplotlib.colors import BoundaryNorm, ListedColormap |
15 | 22 |
|
| 23 | + |
| 24 | +def colored_line(x, y, c, ax, **lc_kwargs): |
| 25 | + """ |
| 26 | + Plot a line with a color specified along the line by a third value. |
| 27 | +
|
| 28 | + It does this by creating a collection of line segments. Each line segment is |
| 29 | + made up of two straight lines each connecting the current (x, y) point to the |
| 30 | + midpoints of the lines connecting the current point with its two neighbors. |
| 31 | + This creates a smooth line with no gaps between the line segments. |
| 32 | +
|
| 33 | + Parameters |
| 34 | + ---------- |
| 35 | + x, y : array-like |
| 36 | + The horizontal and vertical coordinates of the data points. |
| 37 | + c : array-like |
| 38 | + The color values, which should be the same size as x and y. |
| 39 | + ax : Axes |
| 40 | + Axis object on which to plot the colored line. |
| 41 | + **lc_kwargs |
| 42 | + Any additional arguments to pass to matplotlib.collections.LineCollection |
| 43 | + constructor. This should not include the array keyword argument because |
| 44 | + that is set to the color argument. If provided, it will be overridden. |
| 45 | +
|
| 46 | + Returns |
| 47 | + ------- |
| 48 | + matplotlib.collections.LineCollection |
| 49 | + The generated line collection representing the colored line. |
| 50 | + """ |
| 51 | + if "array" in lc_kwargs: |
| 52 | + warnings.warn('The provided "array" keyword argument will be overridden') |
| 53 | + |
| 54 | + # Default the capstyle to butt so that the line segments smoothly line up |
| 55 | + default_kwargs = {"capstyle": "butt"} |
| 56 | + default_kwargs.update(lc_kwargs) |
| 57 | + |
| 58 | + # Compute the midpoints of the line segments. Include the first and last points |
| 59 | + # twice so we don't need any special syntax later to handle them. |
| 60 | + x = np.asarray(x) |
| 61 | + y = np.asarray(y) |
| 62 | + x_midpts = np.hstack((x[0], 0.5 * (x[1:] + x[:-1]), x[-1])) |
| 63 | + y_midpts = np.hstack((y[0], 0.5 * (y[1:] + y[:-1]), y[-1])) |
| 64 | + |
| 65 | + # Determine the start, middle, and end coordinate pair of each line segment. |
| 66 | + # Use the reshape to add an extra dimension so each pair of points is in its |
| 67 | + # own list. Then concatenate them to create: |
| 68 | + # [ |
| 69 | + # [(x1_start, y1_start), (x1_mid, y1_mid), (x1_end, y1_end)], |
| 70 | + # [(x2_start, y2_start), (x2_mid, y2_mid), (x2_end, y2_end)], |
| 71 | + # ... |
| 72 | + # ] |
| 73 | + coord_start = np.column_stack((x_midpts[:-1], y_midpts[:-1]))[:, np.newaxis, :] |
| 74 | + coord_mid = np.column_stack((x, y))[:, np.newaxis, :] |
| 75 | + coord_end = np.column_stack((x_midpts[1:], y_midpts[1:]))[:, np.newaxis, :] |
| 76 | + segments = np.concatenate((coord_start, coord_mid, coord_end), axis=1) |
| 77 | + |
| 78 | + lc = LineCollection(segments, **default_kwargs) |
| 79 | + lc.set_array(c) # set the colors of each segment |
| 80 | + |
| 81 | + return ax.add_collection(lc) |
| 82 | + |
| 83 | + |
| 84 | +# -------------- Create and show plot -------------- |
| 85 | +# Some arbitrary function that gives x, y, and color values |
| 86 | +t = np.linspace(-7.4, -0.5, 200) |
| 87 | +x = 0.9 * np.sin(t) |
| 88 | +y = 0.9 * np.cos(1.6 * t) |
| 89 | +color = np.linspace(0, 2, t.size) |
| 90 | + |
| 91 | +# Create a figure and plot the line on it |
| 92 | +fig1, ax1 = plt.subplots() |
| 93 | +lines = colored_line(x, y, color, ax1, linewidth=10, cmap="plasma") |
| 94 | +fig1.colorbar(lines) # add a color legend |
| 95 | + |
| 96 | +# Set the axis limits and tick positions |
| 97 | +ax1.set_xlim(-1, 1) |
| 98 | +ax1.set_ylim(-1, 1) |
| 99 | +ax1.set_xticks((-1, 0, 1)) |
| 100 | +ax1.set_yticks((-1, 0, 1)) |
| 101 | +ax1.set_title("Color at each point") |
| 102 | + |
| 103 | +plt.show() |
| 104 | + |
| 105 | +#################################################################### |
| 106 | +# This method is designed to give a smooth impression when distances and color |
| 107 | +# differences between adjacent points are not too large. The following example |
| 108 | +# does not meet this criteria and by that serves to illustrate the segmentation |
| 109 | +# and coloring mechanism. |
| 110 | +x = [0, 1, 2, 3, 4] |
| 111 | +y = [0, 1, 2, 1, 1] |
| 112 | +c = [1, 2, 3, 4, 5] |
| 113 | +fig, ax = plt.subplots() |
| 114 | +ax.scatter(x, y, c=c, cmap='rainbow') |
| 115 | +colored_line(x, y, c=c, ax=ax, cmap='rainbow') |
| 116 | + |
| 117 | +plt.show() |
| 118 | + |
| 119 | +#################################################################### |
| 120 | +# Color values between points |
| 121 | +# --------------------------- |
| 122 | +# |
| 123 | + |
| 124 | + |
| 125 | +def colored_line_between_pts(x, y, c, ax, **lc_kwargs): |
| 126 | + """ |
| 127 | + Plot a line with a color specified between (x, y) points by a third value. |
| 128 | +
|
| 129 | + It does this by creating a collection of line segments between each pair of |
| 130 | + neighboring points. The color of each segment is determined by the |
| 131 | + made up of two straight lines each connecting the current (x, y) point to the |
| 132 | + midpoints of the lines connecting the current point with its two neighbors. |
| 133 | + This creates a smooth line with no gaps between the line segments. |
| 134 | +
|
| 135 | + Parameters |
| 136 | + ---------- |
| 137 | + x, y : array-like |
| 138 | + The horizontal and vertical coordinates of the data points. |
| 139 | + c : array-like |
| 140 | + The color values, which should have a size one less than that of x and y. |
| 141 | + ax : Axes |
| 142 | + Axis object on which to plot the colored line. |
| 143 | + **lc_kwargs |
| 144 | + Any additional arguments to pass to matplotlib.collections.LineCollection |
| 145 | + constructor. This should not include the array keyword argument because |
| 146 | + that is set to the color argument. If provided, it will be overridden. |
| 147 | +
|
| 148 | + Returns |
| 149 | + ------- |
| 150 | + matplotlib.collections.LineCollection |
| 151 | + The generated line collection representing the colored line. |
| 152 | + """ |
| 153 | + if "array" in lc_kwargs: |
| 154 | + warnings.warn('The provided "array" keyword argument will be overridden') |
| 155 | + |
| 156 | + # Check color array size (LineCollection still works, but values are unused) |
| 157 | + if len(c) != len(x) - 1: |
| 158 | + warnings.warn( |
| 159 | + "The c argument should have a length one less than the length of x and y. " |
| 160 | + "If it has the same length, use the colored_line function instead." |
| 161 | + ) |
| 162 | + |
| 163 | + # Create a set of line segments so that we can color them individually |
| 164 | + # This creates the points as an N x 1 x 2 array so that we can stack points |
| 165 | + # together easily to get the segments. The segments array for line collection |
| 166 | + # needs to be (numlines) x (points per line) x 2 (for x and y) |
| 167 | + points = np.array([x, y]).T.reshape(-1, 1, 2) |
| 168 | + segments = np.concatenate([points[:-1], points[1:]], axis=1) |
| 169 | + lc = LineCollection(segments, **lc_kwargs) |
| 170 | + |
| 171 | + # Set the values used for colormapping |
| 172 | + lc.set_array(c) |
| 173 | + |
| 174 | + return ax.add_collection(lc) |
| 175 | + |
| 176 | + |
| 177 | +# -------------- Create and show plot -------------- |
16 | 178 | x = np.linspace(0, 3 * np.pi, 500)
|
17 | 179 | y = np.sin(x)
|
18 | 180 | dydx = np.cos(0.5 * (x[:-1] + x[1:])) # first derivative
|
19 | 181 |
|
20 |
| -# Create a set of line segments so that we can color them individually |
21 |
| -# This creates the points as an N x 1 x 2 array so that we can stack points |
22 |
| -# together easily to get the segments. The segments array for line collection |
23 |
| -# needs to be (numlines) x (points per line) x 2 (for x and y) |
24 |
| -points = np.array([x, y]).T.reshape(-1, 1, 2) |
25 |
| -segments = np.concatenate([points[:-1], points[1:]], axis=1) |
26 |
| - |
27 |
| -fig, axs = plt.subplots(2, 1, sharex=True, sharey=True) |
28 |
| - |
29 |
| -# Create a continuous norm to map from data points to colors |
30 |
| -norm = plt.Normalize(dydx.min(), dydx.max()) |
31 |
| -lc = LineCollection(segments, cmap='viridis', norm=norm) |
32 |
| -# Set the values used for colormapping |
33 |
| -lc.set_array(dydx) |
34 |
| -lc.set_linewidth(2) |
35 |
| -line = axs[0].add_collection(lc) |
36 |
| -fig.colorbar(line, ax=axs[0]) |
37 |
| - |
38 |
| -# Use a boundary norm instead |
39 |
| -cmap = ListedColormap(['r', 'g', 'b']) |
40 |
| -norm = BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N) |
41 |
| -lc = LineCollection(segments, cmap=cmap, norm=norm) |
42 |
| -lc.set_array(dydx) |
43 |
| -lc.set_linewidth(2) |
44 |
| -line = axs[1].add_collection(lc) |
45 |
| -fig.colorbar(line, ax=axs[1]) |
46 |
| - |
47 |
| -axs[0].set_xlim(x.min(), x.max()) |
48 |
| -axs[0].set_ylim(-1.1, 1.1) |
| 182 | +fig2, ax2 = plt.subplots() |
| 183 | +line = colored_line_between_pts(x, y, dydx, ax2, linewidth=2, cmap="viridis") |
| 184 | +fig2.colorbar(line, ax=ax2, label="dy/dx") |
| 185 | + |
| 186 | +ax2.set_xlim(x.min(), x.max()) |
| 187 | +ax2.set_ylim(-1.1, 1.1) |
| 188 | +ax2.set_title("Color between points") |
| 189 | + |
49 | 190 | plt.show()
|
0 commit comments