diff options
Diffstat (limited to 'contrib/python/matplotlib/py3/matplotlib/table.py')
| -rw-r--r-- | contrib/python/matplotlib/py3/matplotlib/table.py | 831 |
1 files changed, 831 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/matplotlib/table.py b/contrib/python/matplotlib/py3/matplotlib/table.py new file mode 100644 index 00000000000..d42cdf878d6 --- /dev/null +++ b/contrib/python/matplotlib/py3/matplotlib/table.py @@ -0,0 +1,831 @@ +# Original code by: +# John Gill <[email protected]> +# Copyright 2004 John Gill and John Hunter +# +# Subsequent changes: +# The Matplotlib development team +# Copyright The Matplotlib development team + +""" +Tables drawing. + +.. note:: + The table implementation in Matplotlib is lightly maintained. For a more + featureful table implementation, you may wish to try `blume + <https://github.com/swfiua/blume>`_. + +Use the factory function `~matplotlib.table.table` to create a ready-made +table from texts. If you need more control, use the `.Table` class and its +methods. + +The table consists of a grid of cells, which are indexed by (row, column). +The cell (0, 0) is positioned at the top left. + +Thanks to John Gill for providing the class and table. +""" + +import numpy as np + +from . import _api, _docstring +from .artist import Artist, allow_rasterization +from .patches import Rectangle +from .text import Text +from .transforms import Bbox +from .path import Path + + +class Cell(Rectangle): + """ + A cell is a `.Rectangle` with some associated `.Text`. + + As a user, you'll most likely not creates cells yourself. Instead, you + should use either the `~matplotlib.table.table` factory function or + `.Table.add_cell`. + """ + + PAD = 0.1 + """Padding between text and rectangle.""" + + _edges = 'BRTL' + _edge_aliases = {'open': '', + 'closed': _edges, # default + 'horizontal': 'BT', + 'vertical': 'RL' + } + + def __init__(self, xy, width, height, *, + edgecolor='k', facecolor='w', + fill=True, + text='', + loc=None, + fontproperties=None, + visible_edges='closed', + ): + """ + Parameters + ---------- + xy : 2-tuple + The position of the bottom left corner of the cell. + width : float + The cell width. + height : float + The cell height. + edgecolor : color + The color of the cell border. + facecolor : color + The cell facecolor. + fill : bool + Whether the cell background is filled. + text : str + The cell text. + loc : {'left', 'center', 'right'}, default: 'right' + The alignment of the text within the cell. + fontproperties : dict + A dict defining the font properties of the text. Supported keys and + values are the keyword arguments accepted by `.FontProperties`. + visible_edges : str, default: 'closed' + The cell edges to be drawn with a line: a substring of 'BRTL' + (bottom, right, top, left), or one of 'open' (no edges drawn), + 'closed' (all edges drawn), 'horizontal' (bottom and top), + 'vertical' (right and left). + """ + + # Call base + super().__init__(xy, width=width, height=height, fill=fill, + edgecolor=edgecolor, facecolor=facecolor) + self.set_clip_on(False) + self.visible_edges = visible_edges + + # Create text object + if loc is None: + loc = 'right' + self._loc = loc + self._text = Text(x=xy[0], y=xy[1], clip_on=False, + text=text, fontproperties=fontproperties, + horizontalalignment=loc, verticalalignment='center') + + @_api.rename_parameter("3.8", "trans", "t") + def set_transform(self, t): + super().set_transform(t) + # the text does not get the transform! + self.stale = True + + def set_figure(self, fig): + super().set_figure(fig) + self._text.set_figure(fig) + + def get_text(self): + """Return the cell `.Text` instance.""" + return self._text + + def set_fontsize(self, size): + """Set the text fontsize.""" + self._text.set_fontsize(size) + self.stale = True + + def get_fontsize(self): + """Return the cell fontsize.""" + return self._text.get_fontsize() + + def auto_set_font_size(self, renderer): + """Shrink font size until the text fits into the cell width.""" + fontsize = self.get_fontsize() + required = self.get_required_width(renderer) + while fontsize > 1 and required > self.get_width(): + fontsize -= 1 + self.set_fontsize(fontsize) + required = self.get_required_width(renderer) + + return fontsize + + @allow_rasterization + def draw(self, renderer): + if not self.get_visible(): + return + # draw the rectangle + super().draw(renderer) + # position the text + self._set_text_position(renderer) + self._text.draw(renderer) + self.stale = False + + def _set_text_position(self, renderer): + """Set text up so it is drawn in the right place.""" + bbox = self.get_window_extent(renderer) + # center vertically + y = bbox.y0 + bbox.height / 2 + # position horizontally + loc = self._text.get_horizontalalignment() + if loc == 'center': + x = bbox.x0 + bbox.width / 2 + elif loc == 'left': + x = bbox.x0 + bbox.width * self.PAD + else: # right. + x = bbox.x0 + bbox.width * (1 - self.PAD) + self._text.set_position((x, y)) + + def get_text_bounds(self, renderer): + """ + Return the text bounds as *(x, y, width, height)* in table coordinates. + """ + return (self._text.get_window_extent(renderer) + .transformed(self.get_data_transform().inverted()) + .bounds) + + def get_required_width(self, renderer): + """Return the minimal required width for the cell.""" + l, b, w, h = self.get_text_bounds(renderer) + return w * (1.0 + (2.0 * self.PAD)) + + @_docstring.dedent_interpd + def set_text_props(self, **kwargs): + """ + Update the text properties. + + Valid keyword arguments are: + + %(Text:kwdoc)s + """ + self._text._internal_update(kwargs) + self.stale = True + + @property + def visible_edges(self): + """ + The cell edges to be drawn with a line. + + Reading this property returns a substring of 'BRTL' (bottom, right, + top, left'). + + When setting this property, you can use a substring of 'BRTL' or one + of {'open', 'closed', 'horizontal', 'vertical'}. + """ + return self._visible_edges + + @visible_edges.setter + def visible_edges(self, value): + if value is None: + self._visible_edges = self._edges + elif value in self._edge_aliases: + self._visible_edges = self._edge_aliases[value] + else: + if any(edge not in self._edges for edge in value): + raise ValueError('Invalid edge param {}, must only be one of ' + '{} or string of {}'.format( + value, + ", ".join(self._edge_aliases), + ", ".join(self._edges))) + self._visible_edges = value + self.stale = True + + def get_path(self): + """Return a `.Path` for the `.visible_edges`.""" + codes = [Path.MOVETO] + codes.extend( + Path.LINETO if edge in self._visible_edges else Path.MOVETO + for edge in self._edges) + if Path.MOVETO not in codes[1:]: # All sides are visible + codes[-1] = Path.CLOSEPOLY + return Path( + [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]], + codes, + readonly=True + ) + + +CustomCell = Cell # Backcompat. alias. + + +class Table(Artist): + """ + A table of cells. + + The table consists of a grid of cells, which are indexed by (row, column). + + For a simple table, you'll have a full grid of cells with indices from + (0, 0) to (num_rows-1, num_cols-1), in which the cell (0, 0) is positioned + at the top left. However, you can also add cells with negative indices. + You don't have to add a cell to every grid position, so you can create + tables that have holes. + + *Note*: You'll usually not create an empty table from scratch. Instead use + `~matplotlib.table.table` to create a table from data. + """ + codes = {'best': 0, + 'upper right': 1, # default + 'upper left': 2, + 'lower left': 3, + 'lower right': 4, + 'center left': 5, + 'center right': 6, + 'lower center': 7, + 'upper center': 8, + 'center': 9, + 'top right': 10, + 'top left': 11, + 'bottom left': 12, + 'bottom right': 13, + 'right': 14, + 'left': 15, + 'top': 16, + 'bottom': 17, + } + """Possible values where to place the table relative to the Axes.""" + + FONTSIZE = 10 + + AXESPAD = 0.02 + """The border between the Axes and the table edge in Axes units.""" + + def __init__(self, ax, loc=None, bbox=None, **kwargs): + """ + Parameters + ---------- + ax : `~matplotlib.axes.Axes` + The `~.axes.Axes` to plot the table into. + loc : str + The position of the cell with respect to *ax*. This must be one of + the `~.Table.codes`. + bbox : `.Bbox` or [xmin, ymin, width, height], optional + A bounding box to draw the table into. If this is not *None*, this + overrides *loc*. + + Other Parameters + ---------------- + **kwargs + `.Artist` properties. + """ + + super().__init__() + + if isinstance(loc, str): + if loc not in self.codes: + raise ValueError( + "Unrecognized location {!r}. Valid locations are\n\t{}" + .format(loc, '\n\t'.join(self.codes))) + loc = self.codes[loc] + self.set_figure(ax.figure) + self._axes = ax + self._loc = loc + self._bbox = bbox + + # use axes coords + ax._unstale_viewLim() + self.set_transform(ax.transAxes) + + self._cells = {} + self._edges = None + self._autoColumns = [] + self._autoFontsize = True + self._internal_update(kwargs) + + self.set_clip_on(False) + + def add_cell(self, row, col, *args, **kwargs): + """ + Create a cell and add it to the table. + + Parameters + ---------- + row : int + Row index. + col : int + Column index. + *args, **kwargs + All other parameters are passed on to `Cell`. + + Returns + ------- + `.Cell` + The created cell. + + """ + xy = (0, 0) + cell = Cell(xy, visible_edges=self.edges, *args, **kwargs) + self[row, col] = cell + return cell + + def __setitem__(self, position, cell): + """ + Set a custom cell in a given position. + """ + _api.check_isinstance(Cell, cell=cell) + try: + row, col = position[0], position[1] + except Exception as err: + raise KeyError('Only tuples length 2 are accepted as ' + 'coordinates') from err + cell.set_figure(self.figure) + cell.set_transform(self.get_transform()) + cell.set_clip_on(False) + self._cells[row, col] = cell + self.stale = True + + def __getitem__(self, position): + """Retrieve a custom cell from a given position.""" + return self._cells[position] + + @property + def edges(self): + """ + The default value of `~.Cell.visible_edges` for newly added + cells using `.add_cell`. + + Notes + ----- + This setting does currently only affect newly created cells using + `.add_cell`. + + To change existing cells, you have to set their edges explicitly:: + + for c in tab.get_celld().values(): + c.visible_edges = 'horizontal' + + """ + return self._edges + + @edges.setter + def edges(self, value): + self._edges = value + self.stale = True + + def _approx_text_height(self): + return (self.FONTSIZE / 72.0 * self.figure.dpi / + self._axes.bbox.height * 1.2) + + @allow_rasterization + def draw(self, renderer): + # docstring inherited + + # Need a renderer to do hit tests on mouseevent; assume the last one + # will do + if renderer is None: + renderer = self.figure._get_renderer() + if renderer is None: + raise RuntimeError('No renderer defined') + + if not self.get_visible(): + return + renderer.open_group('table', gid=self.get_gid()) + self._update_positions(renderer) + + for key in sorted(self._cells): + self._cells[key].draw(renderer) + + renderer.close_group('table') + self.stale = False + + def _get_grid_bbox(self, renderer): + """ + Get a bbox, in axes coordinates for the cells. + + Only include those in the range (0, 0) to (maxRow, maxCol). + """ + boxes = [cell.get_window_extent(renderer) + for (row, col), cell in self._cells.items() + if row >= 0 and col >= 0] + bbox = Bbox.union(boxes) + return bbox.transformed(self.get_transform().inverted()) + + def contains(self, mouseevent): + # docstring inherited + if self._different_canvas(mouseevent): + return False, {} + # TODO: Return index of the cell containing the cursor so that the user + # doesn't have to bind to each one individually. + renderer = self.figure._get_renderer() + if renderer is not None: + boxes = [cell.get_window_extent(renderer) + for (row, col), cell in self._cells.items() + if row >= 0 and col >= 0] + bbox = Bbox.union(boxes) + return bbox.contains(mouseevent.x, mouseevent.y), {} + else: + return False, {} + + def get_children(self): + """Return the Artists contained by the table.""" + return list(self._cells.values()) + + def get_window_extent(self, renderer=None): + # docstring inherited + if renderer is None: + renderer = self.figure._get_renderer() + self._update_positions(renderer) + boxes = [cell.get_window_extent(renderer) + for cell in self._cells.values()] + return Bbox.union(boxes) + + def _do_cell_alignment(self): + """ + Calculate row heights and column widths; position cells accordingly. + """ + # Calculate row/column widths + widths = {} + heights = {} + for (row, col), cell in self._cells.items(): + height = heights.setdefault(row, 0.0) + heights[row] = max(height, cell.get_height()) + width = widths.setdefault(col, 0.0) + widths[col] = max(width, cell.get_width()) + + # work out left position for each column + xpos = 0 + lefts = {} + for col in sorted(widths): + lefts[col] = xpos + xpos += widths[col] + + ypos = 0 + bottoms = {} + for row in sorted(heights, reverse=True): + bottoms[row] = ypos + ypos += heights[row] + + # set cell positions + for (row, col), cell in self._cells.items(): + cell.set_x(lefts[col]) + cell.set_y(bottoms[row]) + + def auto_set_column_width(self, col): + """ + Automatically set the widths of given columns to optimal sizes. + + Parameters + ---------- + col : int or sequence of ints + The indices of the columns to auto-scale. + """ + col1d = np.atleast_1d(col) + if not np.issubdtype(col1d.dtype, np.integer): + _api.warn_deprecated("3.8", name="col", + message="%(name)r must be an int or sequence of ints. " + "Passing other types is deprecated since %(since)s " + "and will be removed %(removal)s.") + return + for cell in col1d: + self._autoColumns.append(cell) + + self.stale = True + + def _auto_set_column_width(self, col, renderer): + """Automatically set width for column.""" + cells = [cell for key, cell in self._cells.items() if key[1] == col] + max_width = max((cell.get_required_width(renderer) for cell in cells), + default=0) + for cell in cells: + cell.set_width(max_width) + + def auto_set_font_size(self, value=True): + """Automatically set font size.""" + self._autoFontsize = value + self.stale = True + + def _auto_set_font_size(self, renderer): + + if len(self._cells) == 0: + return + fontsize = next(iter(self._cells.values())).get_fontsize() + cells = [] + for key, cell in self._cells.items(): + # ignore auto-sized columns + if key[1] in self._autoColumns: + continue + size = cell.auto_set_font_size(renderer) + fontsize = min(fontsize, size) + cells.append(cell) + + # now set all fontsizes equal + for cell in self._cells.values(): + cell.set_fontsize(fontsize) + + def scale(self, xscale, yscale): + """Scale column widths by *xscale* and row heights by *yscale*.""" + for c in self._cells.values(): + c.set_width(c.get_width() * xscale) + c.set_height(c.get_height() * yscale) + + def set_fontsize(self, size): + """ + Set the font size, in points, of the cell text. + + Parameters + ---------- + size : float + + Notes + ----- + As long as auto font size has not been disabled, the value will be + clipped such that the text fits horizontally into the cell. + + You can disable this behavior using `.auto_set_font_size`. + + >>> the_table.auto_set_font_size(False) + >>> the_table.set_fontsize(20) + + However, there is no automatic scaling of the row height so that the + text may exceed the cell boundary. + """ + for cell in self._cells.values(): + cell.set_fontsize(size) + self.stale = True + + def _offset(self, ox, oy): + """Move all the artists by ox, oy (axes coords).""" + for c in self._cells.values(): + x, y = c.get_x(), c.get_y() + c.set_x(x + ox) + c.set_y(y + oy) + + def _update_positions(self, renderer): + # called from renderer to allow more precise estimates of + # widths and heights with get_window_extent + + # Do any auto width setting + for col in self._autoColumns: + self._auto_set_column_width(col, renderer) + + if self._autoFontsize: + self._auto_set_font_size(renderer) + + # Align all the cells + self._do_cell_alignment() + + bbox = self._get_grid_bbox(renderer) + l, b, w, h = bbox.bounds + + if self._bbox is not None: + # Position according to bbox + if isinstance(self._bbox, Bbox): + rl, rb, rw, rh = self._bbox.bounds + else: + rl, rb, rw, rh = self._bbox + self.scale(rw / w, rh / h) + ox = rl - l + oy = rb - b + self._do_cell_alignment() + else: + # Position using loc + (BEST, UR, UL, LL, LR, CL, CR, LC, UC, C, + TR, TL, BL, BR, R, L, T, B) = range(len(self.codes)) + # defaults for center + ox = (0.5 - w / 2) - l + oy = (0.5 - h / 2) - b + if self._loc in (UL, LL, CL): # left + ox = self.AXESPAD - l + if self._loc in (BEST, UR, LR, R, CR): # right + ox = 1 - (l + w + self.AXESPAD) + if self._loc in (BEST, UR, UL, UC): # upper + oy = 1 - (b + h + self.AXESPAD) + if self._loc in (LL, LR, LC): # lower + oy = self.AXESPAD - b + if self._loc in (LC, UC, C): # center x + ox = (0.5 - w / 2) - l + if self._loc in (CL, CR, C): # center y + oy = (0.5 - h / 2) - b + + if self._loc in (TL, BL, L): # out left + ox = - (l + w) + if self._loc in (TR, BR, R): # out right + ox = 1.0 - l + if self._loc in (TR, TL, T): # out top + oy = 1.0 - b + if self._loc in (BL, BR, B): # out bottom + oy = - (b + h) + + self._offset(ox, oy) + + def get_celld(self): + r""" + Return a dict of cells in the table mapping *(row, column)* to + `.Cell`\s. + + Notes + ----- + You can also directly index into the Table object to access individual + cells:: + + cell = table[row, col] + + """ + return self._cells + + +@_docstring.dedent_interpd +def table(ax, + cellText=None, cellColours=None, + cellLoc='right', colWidths=None, + rowLabels=None, rowColours=None, rowLoc='left', + colLabels=None, colColours=None, colLoc='center', + loc='bottom', bbox=None, edges='closed', + **kwargs): + """ + Add a table to an `~.axes.Axes`. + + At least one of *cellText* or *cellColours* must be specified. These + parameters must be 2D lists, in which the outer lists define the rows and + the inner list define the column values per row. Each row must have the + same number of elements. + + The table can optionally have row and column headers, which are configured + using *rowLabels*, *rowColours*, *rowLoc* and *colLabels*, *colColours*, + *colLoc* respectively. + + For finer grained control over tables, use the `.Table` class and add it to + the axes with `.Axes.add_table`. + + Parameters + ---------- + cellText : 2D list of str, optional + The texts to place into the table cells. + + *Note*: Line breaks in the strings are currently not accounted for and + will result in the text exceeding the cell boundaries. + + cellColours : 2D list of colors, optional + The background colors of the cells. + + cellLoc : {'left', 'center', 'right'}, default: 'right' + The alignment of the text within the cells. + + colWidths : list of float, optional + The column widths in units of the axes. If not given, all columns will + have a width of *1 / ncols*. + + rowLabels : list of str, optional + The text of the row header cells. + + rowColours : list of colors, optional + The colors of the row header cells. + + rowLoc : {'left', 'center', 'right'}, default: 'left' + The text alignment of the row header cells. + + colLabels : list of str, optional + The text of the column header cells. + + colColours : list of colors, optional + The colors of the column header cells. + + colLoc : {'left', 'center', 'right'}, default: 'left' + The text alignment of the column header cells. + + loc : str, optional + The position of the cell with respect to *ax*. This must be one of + the `~.Table.codes`. + + bbox : `.Bbox` or [xmin, ymin, width, height], optional + A bounding box to draw the table into. If this is not *None*, this + overrides *loc*. + + edges : substring of 'BRTL' or {'open', 'closed', 'horizontal', 'vertical'} + The cell edges to be drawn with a line. See also + `~.Cell.visible_edges`. + + Returns + ------- + `~matplotlib.table.Table` + The created table. + + Other Parameters + ---------------- + **kwargs + `.Table` properties. + + %(Table:kwdoc)s + """ + + if cellColours is None and cellText is None: + raise ValueError('At least one argument from "cellColours" or ' + '"cellText" must be provided to create a table.') + + # Check we have some cellText + if cellText is None: + # assume just colours are needed + rows = len(cellColours) + cols = len(cellColours[0]) + cellText = [[''] * cols] * rows + + rows = len(cellText) + cols = len(cellText[0]) + for row in cellText: + if len(row) != cols: + raise ValueError(f"Each row in 'cellText' must have {cols} " + "columns") + + if cellColours is not None: + if len(cellColours) != rows: + raise ValueError(f"'cellColours' must have {rows} rows") + for row in cellColours: + if len(row) != cols: + raise ValueError("Each row in 'cellColours' must have " + f"{cols} columns") + else: + cellColours = ['w' * cols] * rows + + # Set colwidths if not given + if colWidths is None: + colWidths = [1.0 / cols] * cols + + # Fill in missing information for column + # and row labels + rowLabelWidth = 0 + if rowLabels is None: + if rowColours is not None: + rowLabels = [''] * rows + rowLabelWidth = colWidths[0] + elif rowColours is None: + rowColours = 'w' * rows + + if rowLabels is not None: + if len(rowLabels) != rows: + raise ValueError(f"'rowLabels' must be of length {rows}") + + # If we have column labels, need to shift + # the text and colour arrays down 1 row + offset = 1 + if colLabels is None: + if colColours is not None: + colLabels = [''] * cols + else: + offset = 0 + elif colColours is None: + colColours = 'w' * cols + + # Set up cell colours if not given + if cellColours is None: + cellColours = ['w' * cols] * rows + + # Now create the table + table = Table(ax, loc, bbox, **kwargs) + table.edges = edges + height = table._approx_text_height() + + # Add the cells + for row in range(rows): + for col in range(cols): + table.add_cell(row + offset, col, + width=colWidths[col], height=height, + text=cellText[row][col], + facecolor=cellColours[row][col], + loc=cellLoc) + # Do column labels + if colLabels is not None: + for col in range(cols): + table.add_cell(0, col, + width=colWidths[col], height=height, + text=colLabels[col], facecolor=colColours[col], + loc=colLoc) + + # Do row labels + if rowLabels is not None: + for row in range(rows): + table.add_cell(row + offset, -1, + width=rowLabelWidth or 1e-15, height=height, + text=rowLabels[row], facecolor=rowColours[row], + loc=rowLoc) + if rowLabelWidth == 0: + table.auto_set_column_width(-1) + + ax.add_table(table) + return table |
