123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- """
- Conventions:
- "constrain_x" means to constrain the variable with either
- another kiwisolver variable, or a float. i.e. `constrain_width(0.2)`
- will set a constraint that the width has to be 0.2 and this constraint is
- permanent - i.e. it will not be removed if it becomes obsolete.
- "edit_x" means to set x to a value (just a float), and that this value can
- change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)`
- will allow it to change to 0.3 later. Note that these values are still just
- "suggestions" in `kiwisolver` parlance, and could be over-ridden by
- other constrains.
- """
- import itertools
- import kiwisolver as kiwi
- import logging
- import numpy as np
- _log = logging.getLogger(__name__)
- # renderers can be complicated
- def get_renderer(fig):
- if fig._cachedRenderer:
- renderer = fig._cachedRenderer
- else:
- canvas = fig.canvas
- if canvas and hasattr(canvas, "get_renderer"):
- renderer = canvas.get_renderer()
- else:
- # not sure if this can happen
- # seems to with PDF...
- _log.info("constrained_layout : falling back to Agg renderer")
- from matplotlib.backends.backend_agg import FigureCanvasAgg
- canvas = FigureCanvasAgg(fig)
- renderer = canvas.get_renderer()
- return renderer
- class LayoutBox:
- """
- Basic rectangle representation using kiwi solver variables
- """
- def __init__(self, parent=None, name='', tightwidth=False,
- tightheight=False, artist=None,
- lower_left=(0, 0), upper_right=(1, 1), pos=False,
- subplot=False, h_pad=None, w_pad=None):
- Variable = kiwi.Variable
- self.parent = parent
- self.name = name
- sn = self.name + '_'
- if parent is None:
- self.solver = kiwi.Solver()
- self.constrained_layout_called = 0
- else:
- self.solver = parent.solver
- self.constrained_layout_called = None
- # parent wants to know about this child!
- parent.add_child(self)
- # keep track of artist associated w/ this layout. Can be none
- self.artist = artist
- # keep track if this box is supposed to be a pos that is constrained
- # by the parent.
- self.pos = pos
- # keep track of whether we need to match this subplot up with others.
- self.subplot = subplot
- # we need the str below for Py 2 which complains the string is unicode
- self.top = Variable(str(sn + 'top'))
- self.bottom = Variable(str(sn + 'bottom'))
- self.left = Variable(str(sn + 'left'))
- self.right = Variable(str(sn + 'right'))
- self.width = Variable(str(sn + 'width'))
- self.height = Variable(str(sn + 'height'))
- self.h_center = Variable(str(sn + 'h_center'))
- self.v_center = Variable(str(sn + 'v_center'))
- self.min_width = Variable(str(sn + 'min_width'))
- self.min_height = Variable(str(sn + 'min_height'))
- self.pref_width = Variable(str(sn + 'pref_width'))
- self.pref_height = Variable(str(sn + 'pref_height'))
- # margins are only used for axes-position layout boxes. maybe should
- # be a separate subclass:
- self.left_margin = Variable(str(sn + 'left_margin'))
- self.right_margin = Variable(str(sn + 'right_margin'))
- self.bottom_margin = Variable(str(sn + 'bottom_margin'))
- self.top_margin = Variable(str(sn + 'top_margin'))
- # mins
- self.left_margin_min = Variable(str(sn + 'left_margin_min'))
- self.right_margin_min = Variable(str(sn + 'right_margin_min'))
- self.bottom_margin_min = Variable(str(sn + 'bottom_margin_min'))
- self.top_margin_min = Variable(str(sn + 'top_margin_min'))
- right, top = upper_right
- left, bottom = lower_left
- self.tightheight = tightheight
- self.tightwidth = tightwidth
- self.add_constraints()
- self.children = []
- self.subplotspec = None
- if self.pos:
- self.constrain_margins()
- self.h_pad = h_pad
- self.w_pad = w_pad
- def constrain_margins(self):
- """
- Only do this for pos. This sets a variable distance
- margin between the position of the axes and the outer edge of
- the axes.
- Margins are variable because they change with the figure size.
- Margin minimums are set to make room for axes decorations. However,
- the margins can be larger if we are mathicng the position size to
- other axes.
- """
- sol = self.solver
- # left
- if not sol.hasEditVariable(self.left_margin_min):
- sol.addEditVariable(self.left_margin_min, 'strong')
- sol.suggestValue(self.left_margin_min, 0.0001)
- c = (self.left_margin == self.left - self.parent.left)
- self.solver.addConstraint(c | 'required')
- c = (self.left_margin >= self.left_margin_min)
- self.solver.addConstraint(c | 'strong')
- # right
- if not sol.hasEditVariable(self.right_margin_min):
- sol.addEditVariable(self.right_margin_min, 'strong')
- sol.suggestValue(self.right_margin_min, 0.0001)
- c = (self.right_margin == self.parent.right - self.right)
- self.solver.addConstraint(c | 'required')
- c = (self.right_margin >= self.right_margin_min)
- self.solver.addConstraint(c | 'required')
- # bottom
- if not sol.hasEditVariable(self.bottom_margin_min):
- sol.addEditVariable(self.bottom_margin_min, 'strong')
- sol.suggestValue(self.bottom_margin_min, 0.0001)
- c = (self.bottom_margin == self.bottom - self.parent.bottom)
- self.solver.addConstraint(c | 'required')
- c = (self.bottom_margin >= self.bottom_margin_min)
- self.solver.addConstraint(c | 'required')
- # top
- if not sol.hasEditVariable(self.top_margin_min):
- sol.addEditVariable(self.top_margin_min, 'strong')
- sol.suggestValue(self.top_margin_min, 0.0001)
- c = (self.top_margin == self.parent.top - self.top)
- self.solver.addConstraint(c | 'required')
- c = (self.top_margin >= self.top_margin_min)
- self.solver.addConstraint(c | 'required')
- def add_child(self, child):
- self.children += [child]
- def remove_child(self, child):
- try:
- self.children.remove(child)
- except ValueError:
- _log.info("Tried to remove child that doesn't belong to parent")
- def add_constraints(self):
- sol = self.solver
- # never let width and height go negative.
- for i in [self.min_width, self.min_height]:
- sol.addEditVariable(i, 1e9)
- sol.suggestValue(i, 0.0)
- # define relation ships between things thing width and right and left
- self.hard_constraints()
- # self.soft_constraints()
- if self.parent:
- self.parent_constrain()
- # sol.updateVariables()
- def parent_constrain(self):
- parent = self.parent
- hc = [self.left >= parent.left,
- self.bottom >= parent.bottom,
- self.top <= parent.top,
- self.right <= parent.right]
- for c in hc:
- self.solver.addConstraint(c | 'required')
- def hard_constraints(self):
- hc = [self.width == self.right - self.left,
- self.height == self.top - self.bottom,
- self.h_center == (self.left + self.right) * 0.5,
- self.v_center == (self.top + self.bottom) * 0.5,
- self.width >= self.min_width,
- self.height >= self.min_height]
- for c in hc:
- self.solver.addConstraint(c | 'required')
- def soft_constraints(self):
- sol = self.solver
- if self.tightwidth:
- suggest = 0.
- else:
- suggest = 20.
- c = (self.pref_width == suggest)
- for i in c:
- sol.addConstraint(i | 'required')
- if self.tightheight:
- suggest = 0.
- else:
- suggest = 20.
- c = (self.pref_height == suggest)
- for i in c:
- sol.addConstraint(i | 'required')
- c = [(self.width >= suggest),
- (self.height >= suggest)]
- for i in c:
- sol.addConstraint(i | 150000)
- def set_parent(self, parent):
- """Replace the parent of this with the new parent."""
- self.parent = parent
- self.parent_constrain()
- def constrain_geometry(self, left, bottom, right, top, strength='strong'):
- hc = [self.left == left,
- self.right == right,
- self.bottom == bottom,
- self.top == top]
- for c in hc:
- self.solver.addConstraint(c | strength)
- # self.solver.updateVariables()
- def constrain_same(self, other, strength='strong'):
- """
- Make the layoutbox have same position as other layoutbox
- """
- hc = [self.left == other.left,
- self.right == other.right,
- self.bottom == other.bottom,
- self.top == other.top]
- for c in hc:
- self.solver.addConstraint(c | strength)
- def constrain_left_margin(self, margin, strength='strong'):
- c = (self.left == self.parent.left + margin)
- self.solver.addConstraint(c | strength)
- def edit_left_margin_min(self, margin):
- self.solver.suggestValue(self.left_margin_min, margin)
- def constrain_right_margin(self, margin, strength='strong'):
- c = (self.right == self.parent.right - margin)
- self.solver.addConstraint(c | strength)
- def edit_right_margin_min(self, margin):
- self.solver.suggestValue(self.right_margin_min, margin)
- def constrain_bottom_margin(self, margin, strength='strong'):
- c = (self.bottom == self.parent.bottom + margin)
- self.solver.addConstraint(c | strength)
- def edit_bottom_margin_min(self, margin):
- self.solver.suggestValue(self.bottom_margin_min, margin)
- def constrain_top_margin(self, margin, strength='strong'):
- c = (self.top == self.parent.top - margin)
- self.solver.addConstraint(c | strength)
- def edit_top_margin_min(self, margin):
- self.solver.suggestValue(self.top_margin_min, margin)
- def get_rect(self):
- return (self.left.value(), self.bottom.value(),
- self.width.value(), self.height.value())
- def update_variables(self):
- '''
- Update *all* the variables that are part of the solver this LayoutBox
- is created with
- '''
- self.solver.updateVariables()
- def edit_height(self, height, strength='strong'):
- '''
- Set the height of the layout box.
- This is done as an editable variable so that the value can change
- due to resizing.
- '''
- sol = self.solver
- for i in [self.height]:
- if not sol.hasEditVariable(i):
- sol.addEditVariable(i, strength)
- sol.suggestValue(self.height, height)
- def constrain_height(self, height, strength='strong'):
- '''
- Constrain the height of the layout box. height is
- either a float or a layoutbox.height.
- '''
- c = (self.height == height)
- self.solver.addConstraint(c | strength)
- def constrain_height_min(self, height, strength='strong'):
- c = (self.height >= height)
- self.solver.addConstraint(c | strength)
- def edit_width(self, width, strength='strong'):
- sol = self.solver
- for i in [self.width]:
- if not sol.hasEditVariable(i):
- sol.addEditVariable(i, strength)
- sol.suggestValue(self.width, width)
- def constrain_width(self, width, strength='strong'):
- """
- Constrain the width of the layout box. *width* is
- either a float or a layoutbox.width.
- """
- c = (self.width == width)
- self.solver.addConstraint(c | strength)
- def constrain_width_min(self, width, strength='strong'):
- c = (self.width >= width)
- self.solver.addConstraint(c | strength)
- def constrain_left(self, left, strength='strong'):
- c = (self.left == left)
- self.solver.addConstraint(c | strength)
- def constrain_bottom(self, bottom, strength='strong'):
- c = (self.bottom == bottom)
- self.solver.addConstraint(c | strength)
- def constrain_right(self, right, strength='strong'):
- c = (self.right == right)
- self.solver.addConstraint(c | strength)
- def constrain_top(self, top, strength='strong'):
- c = (self.top == top)
- self.solver.addConstraint(c | strength)
- def _is_subplotspec_layoutbox(self):
- '''
- Helper to check if this layoutbox is the layoutbox of a
- subplotspec
- '''
- name = (self.name).split('.')[-1]
- return name[:2] == 'ss'
- def _is_gridspec_layoutbox(self):
- '''
- Helper to check if this layoutbox is the layoutbox of a
- gridspec
- '''
- name = (self.name).split('.')[-1]
- return name[:8] == 'gridspec'
- def find_child_subplots(self):
- '''
- Find children of this layout box that are subplots. We want to line
- poss up, and this is an easy way to find them all.
- '''
- if self.subplot:
- subplots = [self]
- else:
- subplots = []
- for child in self.children:
- subplots += child.find_child_subplots()
- return subplots
- def layout_from_subplotspec(self, subspec,
- name='', artist=None, pos=False):
- """
- Make a layout box from a subplotspec. The layout box is
- constrained to be a fraction of the width/height of the parent,
- and be a fraction of the parent width/height from the left/bottom
- of the parent. Therefore the parent can move around and the
- layout for the subplot spec should move with it.
- The parent is *usually* the gridspec that made the subplotspec.??
- """
- lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos)
- gs = subspec.get_gridspec()
- nrows, ncols = gs.get_geometry()
- parent = self.parent
- # OK, now, we want to set the position of this subplotspec
- # based on its subplotspec parameters. The new gridspec will inherit
- # from gridspec. prob should be new method in gridspec
- left = 0.0
- right = 1.0
- bottom = 0.0
- top = 1.0
- totWidth = right-left
- totHeight = top-bottom
- hspace = 0.
- wspace = 0.
- # calculate accumulated heights of columns
- cellH = totHeight / (nrows + hspace * (nrows - 1))
- sepH = hspace * cellH
- if gs._row_height_ratios is not None:
- netHeight = cellH * nrows
- tr = sum(gs._row_height_ratios)
- cellHeights = [netHeight * r / tr for r in gs._row_height_ratios]
- else:
- cellHeights = [cellH] * nrows
- sepHeights = [0] + ([sepH] * (nrows - 1))
- cellHs = np.cumsum(np.column_stack([sepHeights, cellHeights]).flat)
- # calculate accumulated widths of rows
- cellW = totWidth / (ncols + wspace * (ncols - 1))
- sepW = wspace * cellW
- if gs._col_width_ratios is not None:
- netWidth = cellW * ncols
- tr = sum(gs._col_width_ratios)
- cellWidths = [netWidth * r / tr for r in gs._col_width_ratios]
- else:
- cellWidths = [cellW] * ncols
- sepWidths = [0] + ([sepW] * (ncols - 1))
- cellWs = np.cumsum(np.column_stack([sepWidths, cellWidths]).flat)
- figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)]
- figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)]
- figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)]
- figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)]
- rowNum1, colNum1 = divmod(subspec.num1, ncols)
- rowNum2, colNum2 = divmod(subspec.num2, ncols)
- figBottom = min(figBottoms[rowNum1], figBottoms[rowNum2])
- figTop = max(figTops[rowNum1], figTops[rowNum2])
- figLeft = min(figLefts[colNum1], figLefts[colNum2])
- figRight = max(figRights[colNum1], figRights[colNum2])
- # These are numbers relative to (0, 0, 1, 1). Need to constrain
- # relative to parent.
- width = figRight - figLeft
- height = figTop - figBottom
- parent = self.parent
- cs = [self.left == parent.left + parent.width * figLeft,
- self.bottom == parent.bottom + parent.height * figBottom,
- self.width == parent.width * width,
- self.height == parent.height * height]
- for c in cs:
- self.solver.addConstraint(c | 'required')
- return lb
- def __repr__(self):
- args = (self.name, self.left.value(), self.bottom.value(),
- self.right.value(), self.top.value())
- return ('LayoutBox: %25s, (left: %1.3f) (bot: %1.3f) '
- '(right: %1.3f) (top: %1.3f) ') % args
- # Utility functions that act on layoutboxes...
- def hstack(boxes, padding=0, strength='strong'):
- '''
- Stack LayoutBox instances from left to right.
- *padding* is in figure-relative units.
- '''
- for i in range(1, len(boxes)):
- c = (boxes[i-1].right + padding <= boxes[i].left)
- boxes[i].solver.addConstraint(c | strength)
- def hpack(boxes, padding=0, strength='strong'):
- '''
- Stack LayoutBox instances from left to right.
- '''
- for i in range(1, len(boxes)):
- c = (boxes[i-1].right + padding == boxes[i].left)
- boxes[i].solver.addConstraint(c | strength)
- def vstack(boxes, padding=0, strength='strong'):
- '''
- Stack LayoutBox instances from top to bottom
- '''
- for i in range(1, len(boxes)):
- c = (boxes[i-1].bottom - padding >= boxes[i].top)
- boxes[i].solver.addConstraint(c | strength)
- def vpack(boxes, padding=0, strength='strong'):
- '''
- Stack LayoutBox instances from top to bottom
- '''
- for i in range(1, len(boxes)):
- c = (boxes[i-1].bottom - padding >= boxes[i].top)
- boxes[i].solver.addConstraint(c | strength)
- def match_heights(boxes, height_ratios=None, strength='medium'):
- '''
- Stack LayoutBox instances from top to bottom
- '''
- if height_ratios is None:
- height_ratios = np.ones(len(boxes))
- for i in range(1, len(boxes)):
- c = (boxes[i-1].height ==
- boxes[i].height*height_ratios[i-1]/height_ratios[i])
- boxes[i].solver.addConstraint(c | strength)
- def match_widths(boxes, width_ratios=None, strength='medium'):
- '''
- Stack LayoutBox instances from top to bottom
- '''
- if width_ratios is None:
- width_ratios = np.ones(len(boxes))
- for i in range(1, len(boxes)):
- c = (boxes[i-1].width ==
- boxes[i].width*width_ratios[i-1]/width_ratios[i])
- boxes[i].solver.addConstraint(c | strength)
- def vstackeq(boxes, padding=0, height_ratios=None):
- vstack(boxes, padding=padding)
- match_heights(boxes, height_ratios=height_ratios)
- def hstackeq(boxes, padding=0, width_ratios=None):
- hstack(boxes, padding=padding)
- match_widths(boxes, width_ratios=width_ratios)
- def align(boxes, attr, strength='strong'):
- cons = []
- for box in boxes[1:]:
- cons = (getattr(boxes[0], attr) == getattr(box, attr))
- boxes[0].solver.addConstraint(cons | strength)
- def match_top_margins(boxes, levels=1):
- box0 = boxes[0]
- top0 = box0
- for n in range(levels):
- top0 = top0.parent
- for box in boxes[1:]:
- topb = box
- for n in range(levels):
- topb = topb.parent
- c = (box0.top-top0.top == box.top-topb.top)
- box0.solver.addConstraint(c | 'strong')
- def match_bottom_margins(boxes, levels=1):
- box0 = boxes[0]
- top0 = box0
- for n in range(levels):
- top0 = top0.parent
- for box in boxes[1:]:
- topb = box
- for n in range(levels):
- topb = topb.parent
- c = (box0.bottom-top0.bottom == box.bottom-topb.bottom)
- box0.solver.addConstraint(c | 'strong')
- def match_left_margins(boxes, levels=1):
- box0 = boxes[0]
- top0 = box0
- for n in range(levels):
- top0 = top0.parent
- for box in boxes[1:]:
- topb = box
- for n in range(levels):
- topb = topb.parent
- c = (box0.left-top0.left == box.left-topb.left)
- box0.solver.addConstraint(c | 'strong')
- def match_right_margins(boxes, levels=1):
- box0 = boxes[0]
- top0 = box0
- for n in range(levels):
- top0 = top0.parent
- for box in boxes[1:]:
- topb = box
- for n in range(levels):
- topb = topb.parent
- c = (box0.right-top0.right == box.right-topb.right)
- box0.solver.addConstraint(c | 'strong')
- def match_width_margins(boxes, levels=1):
- match_left_margins(boxes, levels=levels)
- match_right_margins(boxes, levels=levels)
- def match_height_margins(boxes, levels=1):
- match_top_margins(boxes, levels=levels)
- match_bottom_margins(boxes, levels=levels)
- def match_margins(boxes, levels=1):
- match_width_margins(boxes, levels=levels)
- match_height_margins(boxes, levels=levels)
- _layoutboxobjnum = itertools.count()
- def seq_id():
- """Generate a short sequential id for layoutbox objects."""
- return '%06d' % next(_layoutboxobjnum)
- def print_children(lb):
- """Print the children of the layoutbox."""
- print(lb)
- for child in lb.children:
- print_children(child)
- def nonetree(lb):
- """
- Make all elements in this tree None, signalling not to do any more layout.
- """
- if lb is not None:
- if lb.parent is None:
- # Clear the solver. Hopefully this garbage collects.
- lb.solver.reset()
- nonechildren(lb)
- else:
- nonetree(lb.parent)
- def nonechildren(lb):
- for child in lb.children:
- nonechildren(child)
- lb.artist._layoutbox = None
- lb = None
- def print_tree(lb):
- '''
- Print the tree of layoutboxes
- '''
- if lb.parent is None:
- print('LayoutBox Tree\n')
- print('==============\n')
- print_children(lb)
- print('\n')
- else:
- print_tree(lb.parent)
- def plot_children(fig, box, level=0, printit=True):
- '''
- Simple plotting to show where boxes are
- '''
- import matplotlib
- import matplotlib.pyplot as plt
- if isinstance(fig, matplotlib.figure.Figure):
- ax = fig.add_axes([0., 0., 1., 1.])
- ax.set_facecolor([1., 1., 1., 0.7])
- ax.set_alpha(0.3)
- fig.draw(fig.canvas.get_renderer())
- else:
- ax = fig
- import matplotlib.patches as patches
- colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
- if printit:
- print("Level:", level)
- for child in box.children:
- if printit:
- print(child)
- ax.add_patch(
- patches.Rectangle(
- (child.left.value(), child.bottom.value()), # (x, y)
- child.width.value(), # width
- child.height.value(), # height
- fc='none',
- alpha=0.8,
- ec=colors[level]
- )
- )
- if level > 0:
- name = child.name.split('.')[-1]
- if level % 2 == 0:
- ax.text(child.left.value(), child.bottom.value(), name,
- size=12-level, color=colors[level])
- else:
- ax.text(child.right.value(), child.top.value(), name,
- ha='right', va='top', size=12-level,
- color=colors[level])
- plot_children(ax, child, level=level+1, printit=printit)
|