|
| 1 | +# Chapter 10: Applications |
| 2 | + |
| 3 | +## Sloping Text |
| 4 | + |
| 5 | +Suppose you'd like to label a line. |
| 6 | + |
| 7 | +For a sloped line, you might rather the text sit parallel to the line instead of suffering the below. |
| 8 | + |
| 9 | +```python |
| 10 | +plt.plot([0,1], [0,1]) |
| 11 | +plt.text(0.65, 0.5, |
| 12 | + s = 'label', |
| 13 | + size = 30) |
| 14 | + |
| 15 | +ax = plt.gca() |
| 16 | +# Cosmetics |
| 17 | +ax.grid(False) |
| 18 | +ax.set_xticks([]) |
| 19 | +ax.set_yticks([]) |
| 20 | +``` |
| 21 | + |
| 22 | +```{figure} ../images/chapter10/no-slope.png |
| 23 | +:width: 60% |
| 24 | +:align: center |
| 25 | +``` |
| 26 | + |
| 27 | +The `rotation` argument can help if you know the right angle in degrees. Here the angle is 45 degrees or $\frac{\pi}{4}$ radians. So we modify the second line to be `plt.text(0.65, 0.5, 'label', size = 30, rotation = 45)`. |
| 28 | + |
| 29 | +But this doesn't do what we want! The plot coordinate system is stretched, because we didn't call `ax.set_aspect('equal')` and `text` doesn't recalculate the text angle to make it align. |
| 30 | + |
| 31 | +```{figure} ../images/chapter10/bad-slope.png |
| 32 | +:width: 60% |
| 33 | +:align: center |
| 34 | +``` |
| 35 | + |
| 36 | +Now let's solve it for good in the general case, using trigonometry and then `transform_angles`. This is a method that we'll use with the transformation `ax.transData`. Try experimenting by replacing the `x2,y2` values to see this works for any angle. |
| 37 | + |
| 38 | +```python |
| 39 | +x1, y1 = 0, 0 |
| 40 | +x2, y2 = 1, 1 |
| 41 | +x = (x1, x2) |
| 42 | +y = (y1, y2) |
| 43 | + |
| 44 | +# plot |
| 45 | +fig, ax = plt.figure(), plt.axes() |
| 46 | +ax.plot(x,y) |
| 47 | + |
| 48 | +# Find angles and then insert text |
| 49 | +slope = (y2 - y1) / (x2 - x1) |
| 50 | +true_angle = math.degrees(math.atan(slope)) |
| 51 | + |
| 52 | +# dummy_array is the point where the angles are anchored |
| 53 | +dummy_array = np.array([[0,0]]) # doesn't matter what pair you use |
| 54 | +# matplotlib.org/stable/api/transformations.html#matplotlib.transforms.Transform.transform_angles |
| 55 | + |
| 56 | +plot_angle = ax.transData.transform_angles( |
| 57 | + np.array((true_angle,)), |
| 58 | + dummy_array)[0] |
| 59 | + |
| 60 | +ax.text(np.mean(x), np.mean(y), |
| 61 | + s = 'label', |
| 62 | + rotation = plot_angle, |
| 63 | + fontsize = 30, |
| 64 | + va = 'top', |
| 65 | + ha = 'center') |
| 66 | + |
| 67 | +ax.grid(False) |
| 68 | +ax.set_xticks([]) |
| 69 | +ax.set_yticks([]) |
| 70 | +print(true_angle, plot_angle) |
| 71 | +``` |
| 72 | + |
| 73 | +```{figure} ../images/chapter10/slope-label.png |
| 74 | +:width: 60% |
| 75 | +:align: center |
| 76 | +``` |
| 77 | + |
| 78 | +## Circular Arrangements |
| 79 | + |
| 80 | +With our knowledge of the unit circle, we can arrange some points in a circle with no additional help beyond the math package. This might be useful if you want to avoid mixing polar and Cartesian axes. |
| 81 | + |
| 82 | +```python |
| 83 | +n_points = 10 |
| 84 | +pie_angle = 360/n_points # angle of each slice |
| 85 | +starting_angle = 90 |
| 86 | + |
| 87 | +fig, ax = plt.subplots() |
| 88 | + |
| 89 | +for i in range(n_points): |
| 90 | + |
| 91 | + angle = starting_angle + i*pie_angle |
| 92 | + angle = math.radians(angle) |
| 93 | + x = math.cos(angle) |
| 94 | + y = math.sin(angle) |
| 95 | + |
| 96 | + ax.plot([x],[y], 'o', markersize = 17 - i) |
| 97 | + |
| 98 | +ax.set_aspect('equal') |
| 99 | +ax.axis('off') |
| 100 | +``` |
| 101 | + |
| 102 | +This code produces the following. |
| 103 | + |
| 104 | +```{figure} ../images/chapter10/circle.png |
| 105 | +:width: 60% |
| 106 | +:align: center |
| 107 | +``` |
| 108 | + |
| 109 | +The below makes similar use of trigonometry to create a circle colored according to a gradient, like in [Chapter 6](../chapter6/index.md). I make use of `solid_capstyle = 'round'` to round the endpoints of the plotted line, creating a cleaner look compared to the default. |
| 110 | + |
| 111 | +```python |
| 112 | +# make a circle gradient |
| 113 | +start_color = 255/256, 59/256, 48/256 # red |
| 114 | +end_color = 255/256, 255/256, 85/256 # yellow |
| 115 | + |
| 116 | +# How many color changes |
| 117 | +segments = 130 |
| 118 | + |
| 119 | +# Create figure |
| 120 | +fig, ax = plt.figure(figsize = (8,8)), plt.axes() |
| 121 | + |
| 122 | +# Start at 90 degrees and return clockwise |
| 123 | +angles = np.linspace(2.5*np.pi, np.pi/2, segments + 1) |
| 124 | + |
| 125 | +# Create the intermediate colors |
| 126 | +colors = dict() |
| 127 | +for i in range(3): |
| 128 | + colors[i] = np.linspace(start_color[i], end_color[i], segments) |
| 129 | + |
| 130 | +# plot each arc |
| 131 | +for i in range(segments): |
| 132 | + |
| 133 | + start_angle = angles[i] |
| 134 | + end_angle = angles[i+1] |
| 135 | + angle_slice = np.linspace(start_angle, end_angle, 100) |
| 136 | + |
| 137 | + x_values = np.cos(angle_slice) |
| 138 | + y_values = np.sin(angle_slice) |
| 139 | + |
| 140 | + rgb = colors[0][i], colors[1][i], colors[2][i] |
| 141 | + |
| 142 | + ax.plot(x_values, y_values, |
| 143 | + color = rgb, |
| 144 | + linewidth = 20, |
| 145 | + solid_capstyle = 'round') |
| 146 | + |
| 147 | +ax.set_aspect('equal') |
| 148 | +ax.axis('off') |
| 149 | +``` |
| 150 | + |
| 151 | +```{figure} ../images/chapter10/circle-grad.png |
| 152 | +:width: 80% |
| 153 | +:align: center |
| 154 | +``` |
| 155 | + |
| 156 | +## Network Graphs |
| 157 | + |
| 158 | +Networks are represented mathematically as graphs—a set of vertices and edges between them. In drawing a graph, there are many drawing algorithms available. For large networks or sophisticated algorithms, you should use something off the shelf in a package like [nxviz](https://nxviz.readthedocs.io/en/latest/index.html). For a small network, you might avoid dealing with NetworkX and nxviz and do the drawing yourself. We will work through two simple layouts: arc diagrams and a circular layout for an undirected graph. |
| 159 | + |
| 160 | +An arc diagram places all points on a straight line. The links are drawn as arcs from one point to another. |
| 161 | + |
| 162 | +Let's consider the complete graph with four vertices, where every pair is connected. |
| 163 | + |
| 164 | +```python |
| 165 | +fig, ax = plt.figure(), plt.axes() |
| 166 | +x = np.linspace(0,1,4) |
| 167 | +ax.plot(x, np.zeros(4), |
| 168 | + marker = 'o', |
| 169 | + linestyle = '', |
| 170 | + markersize = 13) |
| 171 | + |
| 172 | +angles = np.linspace(0,np.pi,100) |
| 173 | +for point in x: |
| 174 | + # connect other points |
| 175 | + other_x = x[x > point] |
| 176 | + # construct a half circle |
| 177 | + unit_x, unit_y = np.cos(angles), np.sin(angles) |
| 178 | + for other in other_x: |
| 179 | + # arc is centered between the two points |
| 180 | + shift = np.mean([point,other]) |
| 181 | + r = (other - point)/2 |
| 182 | + new_x = r*unit_x + shift |
| 183 | + new_y = r*unit_y |
| 184 | + ax.plot(new_x, new_y, zorder = -1) |
| 185 | + |
| 186 | +ax.axis('off') |
| 187 | +ax.set_aspect(1.5) |
| 188 | +``` |
| 189 | + |
| 190 | +```{figure} ../images/chapter10/arc-graph.png |
| 191 | +:width: 70% |
| 192 | +:align: center |
| 193 | +``` |
| 194 | + |
| 195 | +Next we move on to a circular layout. This layout places each vertex along a circle. Spaced evenly and with just four vertices in our graph, this will in fact produce a square. We also label each edge. |
| 196 | + |
| 197 | +```python |
| 198 | +fig, ax = plt.figure(), plt.axes() |
| 199 | + |
| 200 | +n_points = 4 |
| 201 | + |
| 202 | +# Draw vertices |
| 203 | +angles = np.linspace(0, 2*np.pi, n_points + 1)[0:n_points] |
| 204 | +x = np.cos(angles) |
| 205 | +y = np.sin(angles) |
| 206 | +ax.plot(x, y, |
| 207 | + marker = 'o', |
| 208 | + linestyle = '', |
| 209 | + markersize = 13) |
| 210 | + |
| 211 | +# Draw Edges |
| 212 | +points = [p for p in zip(x,y)] |
| 213 | +counter = 1 |
| 214 | +for point, other in combinations(points,2): |
| 215 | + |
| 216 | + x = [p[0] for p in (point, other)] |
| 217 | + y = [p[1] for p in (point, other)] |
| 218 | + ax.plot(x, y, zorder = -1) |
| 219 | + |
| 220 | + # add a label |
| 221 | + label_point = .65*np.array(point) + .35*np.array(other) |
| 222 | + |
| 223 | + run = x[1]-x[0] |
| 224 | + rotation = 90 |
| 225 | + ha = 'left' |
| 226 | + if run != 0: |
| 227 | + line_slope = (y[1]-y[0])/(x[1]-x[0]) |
| 228 | + rotation = math.atan(line_slope) |
| 229 | + rotation = math.degrees(rotation) |
| 230 | + ha = 'center' |
| 231 | + else: |
| 232 | + print(point, other, rotation) |
| 233 | + |
| 234 | + # get rgb then blend with white |
| 235 | + line_color = mpl.colors.to_rgb("C"+str(counter)) |
| 236 | + lighter = .8*np.ones(3) + .2*np.array(line_color) |
| 237 | + ax.text(label_point[0], label_point[1], |
| 238 | + 'label', rotation = rotation, |
| 239 | + bbox = dict(facecolor = lighter), |
| 240 | + va = 'center', |
| 241 | + ha = 'center' |
| 242 | + ) |
| 243 | + counter += 1 |
| 244 | + |
| 245 | +ax.axis('off') |
| 246 | +ax.set_aspect('equal') |
| 247 | +``` |
| 248 | + |
| 249 | +```{figure} ../images/chapter10/circle-graph.png |
| 250 | +:width: 83% |
| 251 | +:align: center |
| 252 | +``` |
| 253 | + |
| 254 | +## Tony Hawk's Vertical Loop |
| 255 | + |
| 256 | +Tony Hawk became the first skateboarder to skate a vertical loop in 1998. We honor that accomplishment in two dimensions with the help of a rotation matrix. The unit circle is our vertical loop and we add two smaller circles to represent a skateboard. This is trigonometry. The small circles are placed along a ray from the origin of the unit circle to ensure they will lie tangent inside in the loop. In the first subplot, we place the skateboard at the bottom of the ramp. Though the same figure could be produced without using a rotation matrix, we use one so that the first subplot is essentially reused over and over by rotating the skateboard wheels up and around the loop. |
| 257 | + |
| 258 | +```python |
| 259 | +thetas = np.linspace(0,2*np.pi,8)[0:-1] |
| 260 | +fig = plt.figure(figsize = (12,3)) |
| 261 | + |
| 262 | +# Set radius for skateboard wheels |
| 263 | +radius = 0.1 |
| 264 | + |
| 265 | +# Make individual subplots |
| 266 | +for key, theta in enumerate(thetas): |
| 267 | + rotation_matrix = np.matrix([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) |
| 268 | + |
| 269 | + # Create panel for one frame |
| 270 | + ax = fig.add_subplot(1, len(thetas), key+1) |
| 271 | + ax.set_aspect('equal') |
| 272 | + |
| 273 | + # Plot the loop itself |
| 274 | + angles = np.linspace(0, 2*np.pi, 100) |
| 275 | + x = np.cos(angles) |
| 276 | + y = np.sin(angles) |
| 277 | + ax.plot(x,y) |
| 278 | + |
| 279 | + # Make skateboard wheels at bottom of the ramp |
| 280 | + # and then rotate them counter-clockwise according to theta |
| 281 | + centers = list() |
| 282 | + for ang in 1.5*np.pi, 1.6*np.pi: |
| 283 | + center = (1-radius)*np.cos(ang), (1-radius)*np.sin(ang) |
| 284 | + |
| 285 | + # rotate |
| 286 | + point = np.matrix(center).T |
| 287 | + rotated_point = rotation_matrix*point |
| 288 | + rotated_point = np.array(rotated_point).flatten() |
| 289 | + centers.append(rotated_point) |
| 290 | + |
| 291 | + # make wheel around new center |
| 292 | + wheel_x = radius*x + rotated_point[0] |
| 293 | + wheel_y = radius*y + rotated_point[1] |
| 294 | + |
| 295 | + ax.plot(wheel_x, wheel_y) |
| 296 | + |
| 297 | + # connect the two wheel centers |
| 298 | + c1, c2 = centers |
| 299 | + ax.plot([c1[0],c2[0]], [c1[1],c2[1]]) |
| 300 | + |
| 301 | + ax.axis('off') |
| 302 | + |
| 303 | + xlim = ax.get_xlim() |
| 304 | + ax.plot(xlim, [-1,-1], |
| 305 | + color = 'C0', |
| 306 | + zorder = -1) |
| 307 | +``` |
| 308 | + |
| 309 | +```{figure} ../images/chapter10/tony-hawk.png |
| 310 | +:width: 100% |
| 311 | +:align: center |
| 312 | +``` |
0 commit comments