Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/_layoutbox.py : 16%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
3Conventions:
5"constrain_x" means to constrain the variable with either
6another kiwisolver variable, or a float. i.e. `constrain_width(0.2)`
7will set a constraint that the width has to be 0.2 and this constraint is
8permanent - i.e. it will not be removed if it becomes obsolete.
10"edit_x" means to set x to a value (just a float), and that this value can
11change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)`
12will allow it to change to 0.3 later. Note that these values are still just
13"suggestions" in `kiwisolver` parlance, and could be over-ridden by
14other constrains.
16"""
18import itertools
19import kiwisolver as kiwi
20import logging
21import numpy as np
24_log = logging.getLogger(__name__)
27# renderers can be complicated
28def get_renderer(fig):
29 if fig._cachedRenderer:
30 renderer = fig._cachedRenderer
31 else:
32 canvas = fig.canvas
33 if canvas and hasattr(canvas, "get_renderer"):
34 renderer = canvas.get_renderer()
35 else:
36 # not sure if this can happen
37 # seems to with PDF...
38 _log.info("constrained_layout : falling back to Agg renderer")
39 from matplotlib.backends.backend_agg import FigureCanvasAgg
40 canvas = FigureCanvasAgg(fig)
41 renderer = canvas.get_renderer()
43 return renderer
46class LayoutBox:
47 """
48 Basic rectangle representation using kiwi solver variables
49 """
51 def __init__(self, parent=None, name='', tightwidth=False,
52 tightheight=False, artist=None,
53 lower_left=(0, 0), upper_right=(1, 1), pos=False,
54 subplot=False, h_pad=None, w_pad=None):
55 Variable = kiwi.Variable
56 self.parent = parent
57 self.name = name
58 sn = self.name + '_'
59 if parent is None:
60 self.solver = kiwi.Solver()
61 self.constrained_layout_called = 0
62 else:
63 self.solver = parent.solver
64 self.constrained_layout_called = None
65 # parent wants to know about this child!
66 parent.add_child(self)
67 # keep track of artist associated w/ this layout. Can be none
68 self.artist = artist
69 # keep track if this box is supposed to be a pos that is constrained
70 # by the parent.
71 self.pos = pos
72 # keep track of whether we need to match this subplot up with others.
73 self.subplot = subplot
75 # we need the str below for Py 2 which complains the string is unicode
76 self.top = Variable(str(sn + 'top'))
77 self.bottom = Variable(str(sn + 'bottom'))
78 self.left = Variable(str(sn + 'left'))
79 self.right = Variable(str(sn + 'right'))
81 self.width = Variable(str(sn + 'width'))
82 self.height = Variable(str(sn + 'height'))
83 self.h_center = Variable(str(sn + 'h_center'))
84 self.v_center = Variable(str(sn + 'v_center'))
86 self.min_width = Variable(str(sn + 'min_width'))
87 self.min_height = Variable(str(sn + 'min_height'))
88 self.pref_width = Variable(str(sn + 'pref_width'))
89 self.pref_height = Variable(str(sn + 'pref_height'))
90 # margins are only used for axes-position layout boxes. maybe should
91 # be a separate subclass:
92 self.left_margin = Variable(str(sn + 'left_margin'))
93 self.right_margin = Variable(str(sn + 'right_margin'))
94 self.bottom_margin = Variable(str(sn + 'bottom_margin'))
95 self.top_margin = Variable(str(sn + 'top_margin'))
96 # mins
97 self.left_margin_min = Variable(str(sn + 'left_margin_min'))
98 self.right_margin_min = Variable(str(sn + 'right_margin_min'))
99 self.bottom_margin_min = Variable(str(sn + 'bottom_margin_min'))
100 self.top_margin_min = Variable(str(sn + 'top_margin_min'))
102 right, top = upper_right
103 left, bottom = lower_left
104 self.tightheight = tightheight
105 self.tightwidth = tightwidth
106 self.add_constraints()
107 self.children = []
108 self.subplotspec = None
109 if self.pos:
110 self.constrain_margins()
111 self.h_pad = h_pad
112 self.w_pad = w_pad
114 def constrain_margins(self):
115 """
116 Only do this for pos. This sets a variable distance
117 margin between the position of the axes and the outer edge of
118 the axes.
120 Margins are variable because they change with the figure size.
122 Margin minimums are set to make room for axes decorations. However,
123 the margins can be larger if we are mathicng the position size to
124 other axes.
125 """
126 sol = self.solver
128 # left
129 if not sol.hasEditVariable(self.left_margin_min):
130 sol.addEditVariable(self.left_margin_min, 'strong')
131 sol.suggestValue(self.left_margin_min, 0.0001)
132 c = (self.left_margin == self.left - self.parent.left)
133 self.solver.addConstraint(c | 'required')
134 c = (self.left_margin >= self.left_margin_min)
135 self.solver.addConstraint(c | 'strong')
137 # right
138 if not sol.hasEditVariable(self.right_margin_min):
139 sol.addEditVariable(self.right_margin_min, 'strong')
140 sol.suggestValue(self.right_margin_min, 0.0001)
141 c = (self.right_margin == self.parent.right - self.right)
142 self.solver.addConstraint(c | 'required')
143 c = (self.right_margin >= self.right_margin_min)
144 self.solver.addConstraint(c | 'required')
145 # bottom
146 if not sol.hasEditVariable(self.bottom_margin_min):
147 sol.addEditVariable(self.bottom_margin_min, 'strong')
148 sol.suggestValue(self.bottom_margin_min, 0.0001)
149 c = (self.bottom_margin == self.bottom - self.parent.bottom)
150 self.solver.addConstraint(c | 'required')
151 c = (self.bottom_margin >= self.bottom_margin_min)
152 self.solver.addConstraint(c | 'required')
153 # top
154 if not sol.hasEditVariable(self.top_margin_min):
155 sol.addEditVariable(self.top_margin_min, 'strong')
156 sol.suggestValue(self.top_margin_min, 0.0001)
157 c = (self.top_margin == self.parent.top - self.top)
158 self.solver.addConstraint(c | 'required')
159 c = (self.top_margin >= self.top_margin_min)
160 self.solver.addConstraint(c | 'required')
162 def add_child(self, child):
163 self.children += [child]
165 def remove_child(self, child):
166 try:
167 self.children.remove(child)
168 except ValueError:
169 _log.info("Tried to remove child that doesn't belong to parent")
171 def add_constraints(self):
172 sol = self.solver
173 # never let width and height go negative.
174 for i in [self.min_width, self.min_height]:
175 sol.addEditVariable(i, 1e9)
176 sol.suggestValue(i, 0.0)
177 # define relation ships between things thing width and right and left
178 self.hard_constraints()
179 # self.soft_constraints()
180 if self.parent:
181 self.parent_constrain()
182 # sol.updateVariables()
184 def parent_constrain(self):
185 parent = self.parent
186 hc = [self.left >= parent.left,
187 self.bottom >= parent.bottom,
188 self.top <= parent.top,
189 self.right <= parent.right]
190 for c in hc:
191 self.solver.addConstraint(c | 'required')
193 def hard_constraints(self):
194 hc = [self.width == self.right - self.left,
195 self.height == self.top - self.bottom,
196 self.h_center == (self.left + self.right) * 0.5,
197 self.v_center == (self.top + self.bottom) * 0.5,
198 self.width >= self.min_width,
199 self.height >= self.min_height]
200 for c in hc:
201 self.solver.addConstraint(c | 'required')
203 def soft_constraints(self):
204 sol = self.solver
205 if self.tightwidth:
206 suggest = 0.
207 else:
208 suggest = 20.
209 c = (self.pref_width == suggest)
210 for i in c:
211 sol.addConstraint(i | 'required')
212 if self.tightheight:
213 suggest = 0.
214 else:
215 suggest = 20.
216 c = (self.pref_height == suggest)
217 for i in c:
218 sol.addConstraint(i | 'required')
220 c = [(self.width >= suggest),
221 (self.height >= suggest)]
222 for i in c:
223 sol.addConstraint(i | 150000)
225 def set_parent(self, parent):
226 """Replace the parent of this with the new parent."""
227 self.parent = parent
228 self.parent_constrain()
230 def constrain_geometry(self, left, bottom, right, top, strength='strong'):
231 hc = [self.left == left,
232 self.right == right,
233 self.bottom == bottom,
234 self.top == top]
235 for c in hc:
236 self.solver.addConstraint(c | strength)
237 # self.solver.updateVariables()
239 def constrain_same(self, other, strength='strong'):
240 """
241 Make the layoutbox have same position as other layoutbox
242 """
243 hc = [self.left == other.left,
244 self.right == other.right,
245 self.bottom == other.bottom,
246 self.top == other.top]
247 for c in hc:
248 self.solver.addConstraint(c | strength)
250 def constrain_left_margin(self, margin, strength='strong'):
251 c = (self.left == self.parent.left + margin)
252 self.solver.addConstraint(c | strength)
254 def edit_left_margin_min(self, margin):
255 self.solver.suggestValue(self.left_margin_min, margin)
257 def constrain_right_margin(self, margin, strength='strong'):
258 c = (self.right == self.parent.right - margin)
259 self.solver.addConstraint(c | strength)
261 def edit_right_margin_min(self, margin):
262 self.solver.suggestValue(self.right_margin_min, margin)
264 def constrain_bottom_margin(self, margin, strength='strong'):
265 c = (self.bottom == self.parent.bottom + margin)
266 self.solver.addConstraint(c | strength)
268 def edit_bottom_margin_min(self, margin):
269 self.solver.suggestValue(self.bottom_margin_min, margin)
271 def constrain_top_margin(self, margin, strength='strong'):
272 c = (self.top == self.parent.top - margin)
273 self.solver.addConstraint(c | strength)
275 def edit_top_margin_min(self, margin):
276 self.solver.suggestValue(self.top_margin_min, margin)
278 def get_rect(self):
279 return (self.left.value(), self.bottom.value(),
280 self.width.value(), self.height.value())
282 def update_variables(self):
283 '''
284 Update *all* the variables that are part of the solver this LayoutBox
285 is created with
286 '''
287 self.solver.updateVariables()
289 def edit_height(self, height, strength='strong'):
290 '''
291 Set the height of the layout box.
293 This is done as an editable variable so that the value can change
294 due to resizing.
295 '''
296 sol = self.solver
297 for i in [self.height]:
298 if not sol.hasEditVariable(i):
299 sol.addEditVariable(i, strength)
300 sol.suggestValue(self.height, height)
302 def constrain_height(self, height, strength='strong'):
303 '''
304 Constrain the height of the layout box. height is
305 either a float or a layoutbox.height.
306 '''
307 c = (self.height == height)
308 self.solver.addConstraint(c | strength)
310 def constrain_height_min(self, height, strength='strong'):
311 c = (self.height >= height)
312 self.solver.addConstraint(c | strength)
314 def edit_width(self, width, strength='strong'):
315 sol = self.solver
316 for i in [self.width]:
317 if not sol.hasEditVariable(i):
318 sol.addEditVariable(i, strength)
319 sol.suggestValue(self.width, width)
321 def constrain_width(self, width, strength='strong'):
322 """
323 Constrain the width of the layout box. *width* is
324 either a float or a layoutbox.width.
325 """
326 c = (self.width == width)
327 self.solver.addConstraint(c | strength)
329 def constrain_width_min(self, width, strength='strong'):
330 c = (self.width >= width)
331 self.solver.addConstraint(c | strength)
333 def constrain_left(self, left, strength='strong'):
334 c = (self.left == left)
335 self.solver.addConstraint(c | strength)
337 def constrain_bottom(self, bottom, strength='strong'):
338 c = (self.bottom == bottom)
339 self.solver.addConstraint(c | strength)
341 def constrain_right(self, right, strength='strong'):
342 c = (self.right == right)
343 self.solver.addConstraint(c | strength)
345 def constrain_top(self, top, strength='strong'):
346 c = (self.top == top)
347 self.solver.addConstraint(c | strength)
349 def _is_subplotspec_layoutbox(self):
350 '''
351 Helper to check if this layoutbox is the layoutbox of a
352 subplotspec
353 '''
354 name = (self.name).split('.')[-1]
355 return name[:2] == 'ss'
357 def _is_gridspec_layoutbox(self):
358 '''
359 Helper to check if this layoutbox is the layoutbox of a
360 gridspec
361 '''
362 name = (self.name).split('.')[-1]
363 return name[:8] == 'gridspec'
365 def find_child_subplots(self):
366 '''
367 Find children of this layout box that are subplots. We want to line
368 poss up, and this is an easy way to find them all.
369 '''
370 if self.subplot:
371 subplots = [self]
372 else:
373 subplots = []
374 for child in self.children:
375 subplots += child.find_child_subplots()
376 return subplots
378 def layout_from_subplotspec(self, subspec,
379 name='', artist=None, pos=False):
380 """
381 Make a layout box from a subplotspec. The layout box is
382 constrained to be a fraction of the width/height of the parent,
383 and be a fraction of the parent width/height from the left/bottom
384 of the parent. Therefore the parent can move around and the
385 layout for the subplot spec should move with it.
387 The parent is *usually* the gridspec that made the subplotspec.??
388 """
389 lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos)
390 gs = subspec.get_gridspec()
391 nrows, ncols = gs.get_geometry()
392 parent = self.parent
394 # OK, now, we want to set the position of this subplotspec
395 # based on its subplotspec parameters. The new gridspec will inherit
396 # from gridspec. prob should be new method in gridspec
397 left = 0.0
398 right = 1.0
399 bottom = 0.0
400 top = 1.0
401 totWidth = right-left
402 totHeight = top-bottom
403 hspace = 0.
404 wspace = 0.
406 # calculate accumulated heights of columns
407 cellH = totHeight / (nrows + hspace * (nrows - 1))
408 sepH = hspace * cellH
410 if gs._row_height_ratios is not None:
411 netHeight = cellH * nrows
412 tr = sum(gs._row_height_ratios)
413 cellHeights = [netHeight * r / tr for r in gs._row_height_ratios]
414 else:
415 cellHeights = [cellH] * nrows
417 sepHeights = [0] + ([sepH] * (nrows - 1))
418 cellHs = np.cumsum(np.column_stack([sepHeights, cellHeights]).flat)
420 # calculate accumulated widths of rows
421 cellW = totWidth / (ncols + wspace * (ncols - 1))
422 sepW = wspace * cellW
424 if gs._col_width_ratios is not None:
425 netWidth = cellW * ncols
426 tr = sum(gs._col_width_ratios)
427 cellWidths = [netWidth * r / tr for r in gs._col_width_ratios]
428 else:
429 cellWidths = [cellW] * ncols
431 sepWidths = [0] + ([sepW] * (ncols - 1))
432 cellWs = np.cumsum(np.column_stack([sepWidths, cellWidths]).flat)
434 figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)]
435 figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)]
436 figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)]
437 figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)]
439 rowNum1, colNum1 = divmod(subspec.num1, ncols)
440 rowNum2, colNum2 = divmod(subspec.num2, ncols)
441 figBottom = min(figBottoms[rowNum1], figBottoms[rowNum2])
442 figTop = max(figTops[rowNum1], figTops[rowNum2])
443 figLeft = min(figLefts[colNum1], figLefts[colNum2])
444 figRight = max(figRights[colNum1], figRights[colNum2])
446 # These are numbers relative to (0, 0, 1, 1). Need to constrain
447 # relative to parent.
449 width = figRight - figLeft
450 height = figTop - figBottom
451 parent = self.parent
452 cs = [self.left == parent.left + parent.width * figLeft,
453 self.bottom == parent.bottom + parent.height * figBottom,
454 self.width == parent.width * width,
455 self.height == parent.height * height]
456 for c in cs:
457 self.solver.addConstraint(c | 'required')
459 return lb
461 def __repr__(self):
462 args = (self.name, self.left.value(), self.bottom.value(),
463 self.right.value(), self.top.value())
464 return ('LayoutBox: %25s, (left: %1.3f) (bot: %1.3f) '
465 '(right: %1.3f) (top: %1.3f) ') % args
468# Utility functions that act on layoutboxes...
469def hstack(boxes, padding=0, strength='strong'):
470 '''
471 Stack LayoutBox instances from left to right.
472 *padding* is in figure-relative units.
473 '''
475 for i in range(1, len(boxes)):
476 c = (boxes[i-1].right + padding <= boxes[i].left)
477 boxes[i].solver.addConstraint(c | strength)
480def hpack(boxes, padding=0, strength='strong'):
481 '''
482 Stack LayoutBox instances from left to right.
483 '''
485 for i in range(1, len(boxes)):
486 c = (boxes[i-1].right + padding == boxes[i].left)
487 boxes[i].solver.addConstraint(c | strength)
490def vstack(boxes, padding=0, strength='strong'):
491 '''
492 Stack LayoutBox instances from top to bottom
493 '''
495 for i in range(1, len(boxes)):
496 c = (boxes[i-1].bottom - padding >= boxes[i].top)
497 boxes[i].solver.addConstraint(c | strength)
500def vpack(boxes, padding=0, strength='strong'):
501 '''
502 Stack LayoutBox instances from top to bottom
503 '''
505 for i in range(1, len(boxes)):
506 c = (boxes[i-1].bottom - padding >= boxes[i].top)
507 boxes[i].solver.addConstraint(c | strength)
510def match_heights(boxes, height_ratios=None, strength='medium'):
511 '''
512 Stack LayoutBox instances from top to bottom
513 '''
515 if height_ratios is None:
516 height_ratios = np.ones(len(boxes))
517 for i in range(1, len(boxes)):
518 c = (boxes[i-1].height ==
519 boxes[i].height*height_ratios[i-1]/height_ratios[i])
520 boxes[i].solver.addConstraint(c | strength)
523def match_widths(boxes, width_ratios=None, strength='medium'):
524 '''
525 Stack LayoutBox instances from top to bottom
526 '''
528 if width_ratios is None:
529 width_ratios = np.ones(len(boxes))
530 for i in range(1, len(boxes)):
531 c = (boxes[i-1].width ==
532 boxes[i].width*width_ratios[i-1]/width_ratios[i])
533 boxes[i].solver.addConstraint(c | strength)
536def vstackeq(boxes, padding=0, height_ratios=None):
537 vstack(boxes, padding=padding)
538 match_heights(boxes, height_ratios=height_ratios)
541def hstackeq(boxes, padding=0, width_ratios=None):
542 hstack(boxes, padding=padding)
543 match_widths(boxes, width_ratios=width_ratios)
546def align(boxes, attr, strength='strong'):
547 cons = []
548 for box in boxes[1:]:
549 cons = (getattr(boxes[0], attr) == getattr(box, attr))
550 boxes[0].solver.addConstraint(cons | strength)
553def match_top_margins(boxes, levels=1):
554 box0 = boxes[0]
555 top0 = box0
556 for n in range(levels):
557 top0 = top0.parent
558 for box in boxes[1:]:
559 topb = box
560 for n in range(levels):
561 topb = topb.parent
562 c = (box0.top-top0.top == box.top-topb.top)
563 box0.solver.addConstraint(c | 'strong')
566def match_bottom_margins(boxes, levels=1):
567 box0 = boxes[0]
568 top0 = box0
569 for n in range(levels):
570 top0 = top0.parent
571 for box in boxes[1:]:
572 topb = box
573 for n in range(levels):
574 topb = topb.parent
575 c = (box0.bottom-top0.bottom == box.bottom-topb.bottom)
576 box0.solver.addConstraint(c | 'strong')
579def match_left_margins(boxes, levels=1):
580 box0 = boxes[0]
581 top0 = box0
582 for n in range(levels):
583 top0 = top0.parent
584 for box in boxes[1:]:
585 topb = box
586 for n in range(levels):
587 topb = topb.parent
588 c = (box0.left-top0.left == box.left-topb.left)
589 box0.solver.addConstraint(c | 'strong')
592def match_right_margins(boxes, levels=1):
593 box0 = boxes[0]
594 top0 = box0
595 for n in range(levels):
596 top0 = top0.parent
597 for box in boxes[1:]:
598 topb = box
599 for n in range(levels):
600 topb = topb.parent
601 c = (box0.right-top0.right == box.right-topb.right)
602 box0.solver.addConstraint(c | 'strong')
605def match_width_margins(boxes, levels=1):
606 match_left_margins(boxes, levels=levels)
607 match_right_margins(boxes, levels=levels)
610def match_height_margins(boxes, levels=1):
611 match_top_margins(boxes, levels=levels)
612 match_bottom_margins(boxes, levels=levels)
615def match_margins(boxes, levels=1):
616 match_width_margins(boxes, levels=levels)
617 match_height_margins(boxes, levels=levels)
620_layoutboxobjnum = itertools.count()
623def seq_id():
624 """Generate a short sequential id for layoutbox objects."""
625 return '%06d' % next(_layoutboxobjnum)
628def print_children(lb):
629 """Print the children of the layoutbox."""
630 print(lb)
631 for child in lb.children:
632 print_children(child)
635def nonetree(lb):
636 """
637 Make all elements in this tree None, signalling not to do any more layout.
638 """
639 if lb is not None:
640 if lb.parent is None:
641 # Clear the solver. Hopefully this garbage collects.
642 lb.solver.reset()
643 nonechildren(lb)
644 else:
645 nonetree(lb.parent)
648def nonechildren(lb):
649 for child in lb.children:
650 nonechildren(child)
651 lb.artist._layoutbox = None
652 lb = None
655def print_tree(lb):
656 '''
657 Print the tree of layoutboxes
658 '''
660 if lb.parent is None:
661 print('LayoutBox Tree\n')
662 print('==============\n')
663 print_children(lb)
664 print('\n')
665 else:
666 print_tree(lb.parent)
669def plot_children(fig, box, level=0, printit=True):
670 '''
671 Simple plotting to show where boxes are
672 '''
673 import matplotlib
674 import matplotlib.pyplot as plt
676 if isinstance(fig, matplotlib.figure.Figure):
677 ax = fig.add_axes([0., 0., 1., 1.])
678 ax.set_facecolor([1., 1., 1., 0.7])
679 ax.set_alpha(0.3)
680 fig.draw(fig.canvas.get_renderer())
681 else:
682 ax = fig
684 import matplotlib.patches as patches
685 colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
686 if printit:
687 print("Level:", level)
688 for child in box.children:
689 if printit:
690 print(child)
691 ax.add_patch(
692 patches.Rectangle(
693 (child.left.value(), child.bottom.value()), # (x, y)
694 child.width.value(), # width
695 child.height.value(), # height
696 fc='none',
697 alpha=0.8,
698 ec=colors[level]
699 )
700 )
701 if level > 0:
702 name = child.name.split('.')[-1]
703 if level % 2 == 0:
704 ax.text(child.left.value(), child.bottom.value(), name,
705 size=12-level, color=colors[level])
706 else:
707 ax.text(child.right.value(), child.top.value(), name,
708 ha='right', va='top', size=12-level,
709 color=colors[level])
711 plot_children(ax, child, level=level+1, printit=printit)