1
+ import matplotlib .pyplot as plt
2
+ import matplotlib .patches as patches
3
+
4
+ from matplotlib .transforms import Bbox , TransformedBbox
5
+
6
+ # TODO: Study this https://matplotlib.org/stable/tutorials/text/annotations.html#sphx-glr-tutorials-text-annotations-py
7
+
8
+
9
+ def override (d1 , ** d2 ):
10
+ """Add key-value pairs to d.
11
+
12
+ d1: dictionary
13
+ d2: keyword args to add to d
14
+
15
+ returns: new dict
16
+ """
17
+ d = d1 .copy ()
18
+ d .update (d2 )
19
+ return d
20
+
21
+ def underride (d1 , ** d2 ):
22
+ """Add key-value pairs to d only if key is not in d.
23
+
24
+ d1: dictionary
25
+ d2: keyword args to add to d
26
+
27
+ returns: new dict
28
+ """
29
+ d = d2 .copy ()
30
+ d .update (d1 )
31
+ return d
32
+
33
+ def diagram (width = 5 , height = 1 , ** options ):
34
+ fig , ax = plt .subplots (** options )
35
+
36
+ # TODO: dpi in the notebook should be 100, in the book it should be 300 or 600
37
+ fig .set_dpi (300 )
38
+
39
+ # Set figure size
40
+ fig .set_size_inches (width , height )
41
+
42
+ plt .rc ('font' , size = 8 )
43
+
44
+ # Set axes position
45
+ ax .set_position ([0 , 0 , 1 , 1 ])
46
+
47
+ # Set x and y limits
48
+ ax .set_xlim (0 , width )
49
+ ax .set_ylim (0 , height )
50
+
51
+ # Remove the spines, ticks, and labels
52
+ despine (ax )
53
+ return ax
54
+
55
+ def despine (ax ):
56
+ # Remove the spines
57
+ ax .spines ['right' ].set_visible (False )
58
+ ax .spines ['top' ].set_visible (False )
59
+ ax .spines ['bottom' ].set_visible (False )
60
+ ax .spines ['left' ].set_visible (False )
61
+
62
+ # Remove the axis labels
63
+ ax .set_xticklabels ([])
64
+ ax .set_yticklabels ([])
65
+
66
+ # Remove the tick marks
67
+ ax .tick_params (axis = 'both' , which = 'both' , length = 0 , width = 0 )
68
+
69
+ def adjust (x , y , bbox ):
70
+ """Adjust the coordinates of a point based on a bounding box.
71
+
72
+ x: x coordinate
73
+ y: y coordinate
74
+ bbox: Bbox object
75
+
76
+ returns: tuple of coordinates
77
+ """
78
+ width = bbox .width
79
+ height = bbox .height + 0.2
80
+ t = width , height , x - bbox .x0 , y - bbox .y0 + 0.1
81
+ return [round (x , 2 ) for x in t ]
82
+
83
+ def get_bbox (ax , handle ):
84
+ bbox = handle .get_window_extent ()
85
+ transformed = TransformedBbox (bbox , ax .transData .inverted ())
86
+ return transformed
87
+
88
+ def draw_bbox (ax , bbox , ** options ):
89
+ options = underride (options , facecolor = 'gray' , alpha = 0.1 , linewidth = 0 )
90
+ rect = patches .Rectangle ((bbox .xmin , bbox .ymin ), bbox .width , bbox .height , ** options )
91
+ handle = ax .add_patch (rect )
92
+ bbox = get_bbox (ax , handle )
93
+ return bbox
94
+
95
+ def draw_box_around (ax , bboxes , ** options ):
96
+ bbox = Bbox .union (bboxes )
97
+ return draw_bbox (ax , padded (bbox ), ** options )
98
+
99
+ def padded (bbox , dx = 0.1 , dy = 0.1 ):
100
+ """Add padding to a bounding box.
101
+ """
102
+ [x0 , y0 ], [x1 , y1 ] = bbox .get_points ()
103
+ return Bbox ([[x0 - dx , y0 - dy ], [x1 + dx , y1 + dy ]])
104
+
105
+ def make_binding (name , value , ** options ):
106
+ """Make a binding between a name and a value.
107
+
108
+ name: string
109
+ value: any type
110
+
111
+ returns: Binding object
112
+ """
113
+ if not isinstance (value , Frame ):
114
+ value = Value (repr (value ))
115
+
116
+ return Binding (Value (name ), value , ** options )
117
+
118
+ def make_mapping (key , value , ** options ):
119
+ """Make a binding between a key and a value.
120
+
121
+ key: any type
122
+ value: any type
123
+
124
+ returns: Binding object
125
+ """
126
+ return Binding (Value (repr (key )), Value (repr (value )), ** options )
127
+
128
+ def make_dict (d , name = 'dict' , ** options ):
129
+ """Make a Frame that represents a dictionary.
130
+
131
+ d: dictionary
132
+ name: string
133
+ options: passed to Frame
134
+ """
135
+ mappings = [make_mapping (key , value ) for key , value in d .items ()]
136
+ return Frame (mappings , name = name , ** options )
137
+
138
+ def make_frame (d , name = 'frame' , ** options ):
139
+ """Make a Frame that represents a stack frame.
140
+
141
+ d: dictionary
142
+ name: string
143
+ options: passed to Frame
144
+ """
145
+ bindings = [make_binding (key , value ) for key , value in d .items ()]
146
+ return Frame (bindings , name = name , ** options )
147
+
148
+ class Binding (object ):
149
+ def __init__ (self , name , value = None , ** options ):
150
+ """ Represents a binding between a name and a value.
151
+
152
+ name: Value object
153
+ value: Value object
154
+ """
155
+ self .name = name
156
+ self .value = value
157
+ self .options = options
158
+
159
+ def draw (self , ax , x , y , ** options ):
160
+ options = override (self .options , ** options )
161
+ dx = options .pop ('dx' , 0.4 )
162
+ dy = options .pop ('dy' , 0 )
163
+ draw_value = options .pop ('draw_value' , True )
164
+
165
+ bbox1 = self .name .draw (ax , x , y , ha = 'right' )
166
+ bboxes = [bbox1 ]
167
+
168
+ arrow = Arrow (dx = dx , dy = dy , ** options )
169
+ bbox2 = arrow .draw (ax , x , y )
170
+
171
+ if draw_value :
172
+ bbox3 = self .value .draw (ax , x + dx , y + dy )
173
+ # only include the arrow if we drew the value
174
+ bboxes .extend ([bbox2 , bbox3 ])
175
+
176
+ bbox = Bbox .union (bboxes )
177
+ # draw_bbox(ax, self.bbox)
178
+ self .bbox = bbox
179
+ return bbox
180
+
181
+
182
+ class Element (object ):
183
+ def __init__ (self , index , value , ** options ):
184
+ """ Represents a an element of a list.
185
+
186
+ index: integer
187
+ value: Value object
188
+ """
189
+ self .index = index
190
+ self .value = value
191
+ self .options = options
192
+
193
+ def draw (self , ax , x , y , dx = 0.15 , ** options ):
194
+ options = override (self .options , ** options )
195
+ draw_value = options .pop ('draw_value' , True )
196
+
197
+ bbox1 = self .index .draw (ax , x , y , ha = 'right' , fontsize = 6 , color = 'gray' )
198
+ bboxes = [bbox1 ]
199
+
200
+ if draw_value :
201
+ bbox2 = self .value .draw (ax , x + dx , y )
202
+ bboxes .append (bbox2 )
203
+
204
+ bbox = Bbox .union (bboxes )
205
+ self .bbox = bbox
206
+ # draw_bbox(ax, self.bbox)
207
+ return bbox
208
+
209
+
210
+ class Value (object ):
211
+ def __init__ (self , value ):
212
+ self .value = value
213
+ self .options = dict (ha = 'left' , va = 'center' )
214
+ self .bbox = None
215
+
216
+ def draw (self , ax , x , y , ** options ):
217
+ options = override (self .options , ** options )
218
+
219
+ handle = ax .text (x , y , self .value , ** options )
220
+ bbox = self .bbox = get_bbox (ax , handle )
221
+ # draw_bbox(ax, bbox)
222
+ self .bbox = bbox
223
+ return bbox
224
+
225
+
226
+ class Arrow (object ):
227
+ def __init__ (self , ** options ):
228
+ # Note for the future about dotted arrows
229
+ # self.arrowprops = dict(arrowstyle="->", ls=':')
230
+ arrowprops = dict (arrowstyle = "->" , color = 'gray' )
231
+ options = underride (options , arrowprops = arrowprops )
232
+ self .options = options
233
+
234
+ def draw (self , ax , x , y , ** options ):
235
+ options = override (self .options , ** options )
236
+ dx = options .pop ('dx' , 0.5 )
237
+ dy = options .pop ('dy' , 0 )
238
+ shim = options .pop ('shim' , 0.02 )
239
+
240
+ handle = ax .annotate ("" , [x + dx , y + dy ], [x + shim , y ], ** options )
241
+ bbox = get_bbox (ax , handle )
242
+ self .bbox = bbox
243
+ return bbox
244
+
245
+
246
+ class ReturnArrow (object ):
247
+ def __init__ (self , ** options ):
248
+ style = "Simple, tail_width=0.5, head_width=4, head_length=8"
249
+ options = underride (options , arrowstyle = style , color = "gray" )
250
+ self .options = options
251
+
252
+ def draw (self , ax , x , y , ** options ):
253
+ options = override (self .options , ** options )
254
+ value = options .pop ('value' , None )
255
+ dx = options .pop ('dx' , 0 )
256
+ dy = options .pop ('dy' , 0.4 )
257
+ shim = options .pop ('shim' , 0.02 )
258
+
259
+ x += shim
260
+ arrow = patches .FancyArrowPatch ((x , y ), (x + dx , y + dy ),
261
+ connectionstyle = "arc3,rad=.6" , ** options )
262
+ handle = ax .add_patch (arrow )
263
+ bbox = get_bbox (ax , handle )
264
+
265
+ if value is not None :
266
+ handle = plt .text (x + 0.15 , y + dy / 2 , str (value ), ha = 'left' , va = 'center' )
267
+ bbox2 = get_bbox (ax , handle )
268
+ bbox = Bbox .union ([bbox , bbox2 ])
269
+
270
+ self .bbox = bbox
271
+ return bbox
272
+
273
+
274
+ class Frame (object ):
275
+ def __init__ (self , bindings , ** options ):
276
+ self .bindings = bindings
277
+ self .options = options
278
+
279
+ def draw (self , ax , x , y , ** options ):
280
+ options = override (self .options , ** options )
281
+ name = options .pop ('name' , '' )
282
+ value = options .pop ('value' , None )
283
+ dx = options .pop ('dx' , 0 )
284
+ dy = options .pop ('dy' , 0 )
285
+ offsetx = options .pop ('offsetx' , 0 )
286
+ offsety = options .pop ('offsety' , 0 )
287
+ shim = options .pop ('shim' , 0 )
288
+ loc = options .pop ('loc' , 'top' )
289
+ box_around = options .pop ('box_around' , None )
290
+
291
+ x += offsetx
292
+ y += offsety
293
+ save_y = y
294
+
295
+ if len (self .bindings ) == 0 :
296
+ bbox = Bbox ([[x , y ], [x , y ]])
297
+ bboxes = [bbox ]
298
+ else :
299
+ bboxes = []
300
+
301
+ # draw the bindings
302
+ for binding in self .bindings :
303
+ bbox = binding .draw (ax , x , y )
304
+ bboxes .append (bbox )
305
+ x += dx
306
+ y += dy
307
+
308
+ if box_around :
309
+ bbox1 = draw_bbox (ax , box_around , ** options )
310
+ else :
311
+ bbox1 = draw_box_around (ax , bboxes , ** options )
312
+ bboxes .append (bbox1 )
313
+
314
+ if value is not None :
315
+ arrow = ReturnArrow (value = value )
316
+ x = bbox1 .xmax + shim
317
+ bbox2 = arrow .draw (ax , x , save_y , value = value )
318
+ bboxes .append (bbox2 )
319
+
320
+ if name :
321
+ if loc == 'top' :
322
+ x = bbox1 .xmin
323
+ y = bbox1 .ymax + 0.02
324
+ handle = plt .text (x , y , name , ha = 'left' , va = 'bottom' )
325
+ elif loc == 'left' :
326
+ x = bbox1 .xmin - 0.1
327
+ y = save_y
328
+ handle = plt .text (x , y , name , ha = 'right' , va = 'center' )
329
+ bbox3 = get_bbox (ax , handle )
330
+ bboxes .append (bbox3 )
331
+
332
+ bbox = Bbox .union (bboxes )
333
+ self .bbox = bbox
334
+ return bbox
335
+
336
+
337
+ class Stack (object ):
338
+ def __init__ (self , frames , ** options ):
339
+ self .frames = frames
340
+ self .options = options
341
+
342
+ def draw (self , ax , x , y , ** options ):
343
+ options = override (self .options , ** options )
344
+ dx = options .pop ('dx' , 0 )
345
+ dy = options .pop ('dy' , - 0.4 )
346
+
347
+ # draw the frames
348
+ bboxes = []
349
+ for frame in self .frames :
350
+ bbox = frame .draw (ax , x , y )
351
+ bboxes .append (bbox )
352
+ x += dx
353
+ y += dy
354
+
355
+ bbox = Bbox .union (bboxes )
356
+ self .bbox = bbox
357
+ return bbox
358
+
359
+ def make_rebind (name , seq ):
360
+ bindings = []
361
+ for i , value in enumerate (seq ):
362
+ dy = dy = - 0.3 * i
363
+ if i == len (seq )- 1 :
364
+ binding = make_binding (name , value , dy = dy )
365
+ else :
366
+ arrowprops = dict (arrowstyle = "->" , color = 'gray' , ls = ':' )
367
+ binding = make_binding ('' , value , dy = dy , arrowprops = arrowprops )
368
+ bindings .append (binding )
369
+
370
+ return bindings
371
+
372
+ def make_element (index , value ):
373
+ return Element (Value (index ), Value (repr (value )))
374
+
375
+ def make_list (seq , name = 'list' , ** options ):
376
+ elements = [make_element (index , value ) for index , value in enumerate (seq )]
377
+ return Frame (elements , name = name , ** options )
378
+
379
+ def draw_bindings (bindings , ax , x , y ):
380
+ bboxes = []
381
+ for binding in bindings :
382
+ bbox = binding .draw (ax , x , y )
383
+ bboxes .append (bbox )
384
+
385
+ bbox = Bbox .union (bboxes )
386
+ return bbox
0 commit comments