Coverage for /usr/lib/python3/dist-packages/matplotlib/table.py: 20%
336 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1# Original code by:
2# John Gill <jng@europe.renre.com>
3# Copyright 2004 John Gill and John Hunter
4#
5# Subsequent changes:
6# The Matplotlib development team
7# Copyright The Matplotlib development team
9"""
10Tables drawing.
12.. note::
13 The table implementation in Matplotlib is lightly maintained. For a more
14 featureful table implementation, you may wish to try `blume
15 <https://github.com/swfiua/blume>`_.
17Use the factory function `~matplotlib.table.table` to create a ready-made
18table from texts. If you need more control, use the `.Table` class and its
19methods.
21The table consists of a grid of cells, which are indexed by (row, column).
22The cell (0, 0) is positioned at the top left.
24Thanks to John Gill for providing the class and table.
25"""
27from . import _api, _docstring
28from .artist import Artist, allow_rasterization
29from .patches import Rectangle
30from .text import Text
31from .transforms import Bbox
32from .path import Path
35class Cell(Rectangle):
36 """
37 A cell is a `.Rectangle` with some associated `.Text`.
39 As a user, you'll most likely not creates cells yourself. Instead, you
40 should use either the `~matplotlib.table.table` factory function or
41 `.Table.add_cell`.
42 """
44 PAD = 0.1
45 """Padding between text and rectangle."""
47 _edges = 'BRTL'
48 _edge_aliases = {'open': '',
49 'closed': _edges, # default
50 'horizontal': 'BT',
51 'vertical': 'RL'
52 }
54 @_api.make_keyword_only("3.6", name="edgecolor")
55 def __init__(self, xy, width, height,
56 edgecolor='k', facecolor='w',
57 fill=True,
58 text='',
59 loc=None,
60 fontproperties=None,
61 *,
62 visible_edges='closed',
63 ):
64 """
65 Parameters
66 ----------
67 xy : 2-tuple
68 The position of the bottom left corner of the cell.
69 width : float
70 The cell width.
71 height : float
72 The cell height.
73 edgecolor : color
74 The color of the cell border.
75 facecolor : color
76 The cell facecolor.
77 fill : bool
78 Whether the cell background is filled.
79 text : str
80 The cell text.
81 loc : {'left', 'center', 'right'}, default: 'right'
82 The alignment of the text within the cell.
83 fontproperties : dict
84 A dict defining the font properties of the text. Supported keys and
85 values are the keyword arguments accepted by `.FontProperties`.
86 visible_edges : str, default: 'closed'
87 The cell edges to be drawn with a line: a substring of 'BRTL'
88 (bottom, right, top, left), or one of 'open' (no edges drawn),
89 'closed' (all edges drawn), 'horizontal' (bottom and top),
90 'vertical' (right and left).
91 """
93 # Call base
94 super().__init__(xy, width=width, height=height, fill=fill,
95 edgecolor=edgecolor, facecolor=facecolor)
96 self.set_clip_on(False)
97 self.visible_edges = visible_edges
99 # Create text object
100 if loc is None:
101 loc = 'right'
102 self._loc = loc
103 self._text = Text(x=xy[0], y=xy[1], clip_on=False,
104 text=text, fontproperties=fontproperties,
105 horizontalalignment=loc, verticalalignment='center')
107 def set_transform(self, trans):
108 super().set_transform(trans)
109 # the text does not get the transform!
110 self.stale = True
112 def set_figure(self, fig):
113 super().set_figure(fig)
114 self._text.set_figure(fig)
116 def get_text(self):
117 """Return the cell `.Text` instance."""
118 return self._text
120 def set_fontsize(self, size):
121 """Set the text fontsize."""
122 self._text.set_fontsize(size)
123 self.stale = True
125 def get_fontsize(self):
126 """Return the cell fontsize."""
127 return self._text.get_fontsize()
129 def auto_set_font_size(self, renderer):
130 """Shrink font size until the text fits into the cell width."""
131 fontsize = self.get_fontsize()
132 required = self.get_required_width(renderer)
133 while fontsize > 1 and required > self.get_width():
134 fontsize -= 1
135 self.set_fontsize(fontsize)
136 required = self.get_required_width(renderer)
138 return fontsize
140 @allow_rasterization
141 def draw(self, renderer):
142 if not self.get_visible():
143 return
144 # draw the rectangle
145 super().draw(renderer)
146 # position the text
147 self._set_text_position(renderer)
148 self._text.draw(renderer)
149 self.stale = False
151 def _set_text_position(self, renderer):
152 """Set text up so it is drawn in the right place."""
153 bbox = self.get_window_extent(renderer)
154 # center vertically
155 y = bbox.y0 + bbox.height / 2
156 # position horizontally
157 loc = self._text.get_horizontalalignment()
158 if loc == 'center':
159 x = bbox.x0 + bbox.width / 2
160 elif loc == 'left':
161 x = bbox.x0 + bbox.width * self.PAD
162 else: # right.
163 x = bbox.x0 + bbox.width * (1 - self.PAD)
164 self._text.set_position((x, y))
166 def get_text_bounds(self, renderer):
167 """
168 Return the text bounds as *(x, y, width, height)* in table coordinates.
169 """
170 return (self._text.get_window_extent(renderer)
171 .transformed(self.get_data_transform().inverted())
172 .bounds)
174 def get_required_width(self, renderer):
175 """Return the minimal required width for the cell."""
176 l, b, w, h = self.get_text_bounds(renderer)
177 return w * (1.0 + (2.0 * self.PAD))
179 @_docstring.dedent_interpd
180 def set_text_props(self, **kwargs):
181 """
182 Update the text properties.
184 Valid keyword arguments are:
186 %(Text:kwdoc)s
187 """
188 self._text._internal_update(kwargs)
189 self.stale = True
191 @property
192 def visible_edges(self):
193 """
194 The cell edges to be drawn with a line.
196 Reading this property returns a substring of 'BRTL' (bottom, right,
197 top, left').
199 When setting this property, you can use a substring of 'BRTL' or one
200 of {'open', 'closed', 'horizontal', 'vertical'}.
201 """
202 return self._visible_edges
204 @visible_edges.setter
205 def visible_edges(self, value):
206 if value is None:
207 self._visible_edges = self._edges
208 elif value in self._edge_aliases:
209 self._visible_edges = self._edge_aliases[value]
210 else:
211 if any(edge not in self._edges for edge in value):
212 raise ValueError('Invalid edge param {}, must only be one of '
213 '{} or string of {}'.format(
214 value,
215 ", ".join(self._edge_aliases),
216 ", ".join(self._edges)))
217 self._visible_edges = value
218 self.stale = True
220 def get_path(self):
221 """Return a `.Path` for the `.visible_edges`."""
222 codes = [Path.MOVETO]
223 codes.extend(
224 Path.LINETO if edge in self._visible_edges else Path.MOVETO
225 for edge in self._edges)
226 if Path.MOVETO not in codes[1:]: # All sides are visible
227 codes[-1] = Path.CLOSEPOLY
228 return Path(
229 [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
230 codes,
231 readonly=True
232 )
235CustomCell = Cell # Backcompat. alias.
238class Table(Artist):
239 """
240 A table of cells.
242 The table consists of a grid of cells, which are indexed by (row, column).
244 For a simple table, you'll have a full grid of cells with indices from
245 (0, 0) to (num_rows-1, num_cols-1), in which the cell (0, 0) is positioned
246 at the top left. However, you can also add cells with negative indices.
247 You don't have to add a cell to every grid position, so you can create
248 tables that have holes.
250 *Note*: You'll usually not create an empty table from scratch. Instead use
251 `~matplotlib.table.table` to create a table from data.
252 """
253 codes = {'best': 0,
254 'upper right': 1, # default
255 'upper left': 2,
256 'lower left': 3,
257 'lower right': 4,
258 'center left': 5,
259 'center right': 6,
260 'lower center': 7,
261 'upper center': 8,
262 'center': 9,
263 'top right': 10,
264 'top left': 11,
265 'bottom left': 12,
266 'bottom right': 13,
267 'right': 14,
268 'left': 15,
269 'top': 16,
270 'bottom': 17,
271 }
272 """Possible values where to place the table relative to the Axes."""
274 FONTSIZE = 10
276 AXESPAD = 0.02
277 """The border between the Axes and the table edge in Axes units."""
279 def __init__(self, ax, loc=None, bbox=None, **kwargs):
280 """
281 Parameters
282 ----------
283 ax : `matplotlib.axes.Axes`
284 The `~.axes.Axes` to plot the table into.
285 loc : str
286 The position of the cell with respect to *ax*. This must be one of
287 the `~.Table.codes`.
288 bbox : `.Bbox` or None
289 A bounding box to draw the table into. If this is not *None*, this
290 overrides *loc*.
292 Other Parameters
293 ----------------
294 **kwargs
295 `.Artist` properties.
296 """
298 super().__init__()
300 if isinstance(loc, str):
301 if loc not in self.codes:
302 raise ValueError(
303 "Unrecognized location {!r}. Valid locations are\n\t{}"
304 .format(loc, '\n\t'.join(self.codes)))
305 loc = self.codes[loc]
306 self.set_figure(ax.figure)
307 self._axes = ax
308 self._loc = loc
309 self._bbox = bbox
311 # use axes coords
312 ax._unstale_viewLim()
313 self.set_transform(ax.transAxes)
315 self._cells = {}
316 self._edges = None
317 self._autoColumns = []
318 self._autoFontsize = True
319 self._internal_update(kwargs)
321 self.set_clip_on(False)
323 def add_cell(self, row, col, *args, **kwargs):
324 """
325 Create a cell and add it to the table.
327 Parameters
328 ----------
329 row : int
330 Row index.
331 col : int
332 Column index.
333 *args, **kwargs
334 All other parameters are passed on to `Cell`.
336 Returns
337 -------
338 `.Cell`
339 The created cell.
341 """
342 xy = (0, 0)
343 cell = Cell(xy, visible_edges=self.edges, *args, **kwargs)
344 self[row, col] = cell
345 return cell
347 def __setitem__(self, position, cell):
348 """
349 Set a custom cell in a given position.
350 """
351 _api.check_isinstance(Cell, cell=cell)
352 try:
353 row, col = position[0], position[1]
354 except Exception as err:
355 raise KeyError('Only tuples length 2 are accepted as '
356 'coordinates') from err
357 cell.set_figure(self.figure)
358 cell.set_transform(self.get_transform())
359 cell.set_clip_on(False)
360 self._cells[row, col] = cell
361 self.stale = True
363 def __getitem__(self, position):
364 """Retrieve a custom cell from a given position."""
365 return self._cells[position]
367 @property
368 def edges(self):
369 """
370 The default value of `~.Cell.visible_edges` for newly added
371 cells using `.add_cell`.
373 Notes
374 -----
375 This setting does currently only affect newly created cells using
376 `.add_cell`.
378 To change existing cells, you have to set their edges explicitly::
380 for c in tab.get_celld().values():
381 c.visible_edges = 'horizontal'
383 """
384 return self._edges
386 @edges.setter
387 def edges(self, value):
388 self._edges = value
389 self.stale = True
391 def _approx_text_height(self):
392 return (self.FONTSIZE / 72.0 * self.figure.dpi /
393 self._axes.bbox.height * 1.2)
395 @allow_rasterization
396 def draw(self, renderer):
397 # docstring inherited
399 # Need a renderer to do hit tests on mouseevent; assume the last one
400 # will do
401 if renderer is None:
402 renderer = self.figure._get_renderer()
403 if renderer is None:
404 raise RuntimeError('No renderer defined')
406 if not self.get_visible():
407 return
408 renderer.open_group('table', gid=self.get_gid())
409 self._update_positions(renderer)
411 for key in sorted(self._cells):
412 self._cells[key].draw(renderer)
414 renderer.close_group('table')
415 self.stale = False
417 def _get_grid_bbox(self, renderer):
418 """
419 Get a bbox, in axes coordinates for the cells.
421 Only include those in the range (0, 0) to (maxRow, maxCol).
422 """
423 boxes = [cell.get_window_extent(renderer)
424 for (row, col), cell in self._cells.items()
425 if row >= 0 and col >= 0]
426 bbox = Bbox.union(boxes)
427 return bbox.transformed(self.get_transform().inverted())
429 def contains(self, mouseevent):
430 # docstring inherited
431 inside, info = self._default_contains(mouseevent)
432 if inside is not None:
433 return inside, info
434 # TODO: Return index of the cell containing the cursor so that the user
435 # doesn't have to bind to each one individually.
436 renderer = self.figure._get_renderer()
437 if renderer is not None:
438 boxes = [cell.get_window_extent(renderer)
439 for (row, col), cell in self._cells.items()
440 if row >= 0 and col >= 0]
441 bbox = Bbox.union(boxes)
442 return bbox.contains(mouseevent.x, mouseevent.y), {}
443 else:
444 return False, {}
446 def get_children(self):
447 """Return the Artists contained by the table."""
448 return list(self._cells.values())
450 def get_window_extent(self, renderer=None):
451 # docstring inherited
452 if renderer is None:
453 renderer = self.figure._get_renderer()
454 self._update_positions(renderer)
455 boxes = [cell.get_window_extent(renderer)
456 for cell in self._cells.values()]
457 return Bbox.union(boxes)
459 def _do_cell_alignment(self):
460 """
461 Calculate row heights and column widths; position cells accordingly.
462 """
463 # Calculate row/column widths
464 widths = {}
465 heights = {}
466 for (row, col), cell in self._cells.items():
467 height = heights.setdefault(row, 0.0)
468 heights[row] = max(height, cell.get_height())
469 width = widths.setdefault(col, 0.0)
470 widths[col] = max(width, cell.get_width())
472 # work out left position for each column
473 xpos = 0
474 lefts = {}
475 for col in sorted(widths):
476 lefts[col] = xpos
477 xpos += widths[col]
479 ypos = 0
480 bottoms = {}
481 for row in sorted(heights, reverse=True):
482 bottoms[row] = ypos
483 ypos += heights[row]
485 # set cell positions
486 for (row, col), cell in self._cells.items():
487 cell.set_x(lefts[col])
488 cell.set_y(bottoms[row])
490 def auto_set_column_width(self, col):
491 """
492 Automatically set the widths of given columns to optimal sizes.
494 Parameters
495 ----------
496 col : int or sequence of ints
497 The indices of the columns to auto-scale.
498 """
499 # check for col possibility on iteration
500 try:
501 iter(col)
502 except (TypeError, AttributeError):
503 self._autoColumns.append(col)
504 else:
505 for cell in col:
506 self._autoColumns.append(cell)
508 self.stale = True
510 def _auto_set_column_width(self, col, renderer):
511 """Automatically set width for column."""
512 cells = [cell for key, cell in self._cells.items() if key[1] == col]
513 max_width = max((cell.get_required_width(renderer) for cell in cells),
514 default=0)
515 for cell in cells:
516 cell.set_width(max_width)
518 def auto_set_font_size(self, value=True):
519 """Automatically set font size."""
520 self._autoFontsize = value
521 self.stale = True
523 def _auto_set_font_size(self, renderer):
525 if len(self._cells) == 0:
526 return
527 fontsize = next(iter(self._cells.values())).get_fontsize()
528 cells = []
529 for key, cell in self._cells.items():
530 # ignore auto-sized columns
531 if key[1] in self._autoColumns:
532 continue
533 size = cell.auto_set_font_size(renderer)
534 fontsize = min(fontsize, size)
535 cells.append(cell)
537 # now set all fontsizes equal
538 for cell in self._cells.values():
539 cell.set_fontsize(fontsize)
541 def scale(self, xscale, yscale):
542 """Scale column widths by *xscale* and row heights by *yscale*."""
543 for c in self._cells.values():
544 c.set_width(c.get_width() * xscale)
545 c.set_height(c.get_height() * yscale)
547 def set_fontsize(self, size):
548 """
549 Set the font size, in points, of the cell text.
551 Parameters
552 ----------
553 size : float
555 Notes
556 -----
557 As long as auto font size has not been disabled, the value will be
558 clipped such that the text fits horizontally into the cell.
560 You can disable this behavior using `.auto_set_font_size`.
562 >>> the_table.auto_set_font_size(False)
563 >>> the_table.set_fontsize(20)
565 However, there is no automatic scaling of the row height so that the
566 text may exceed the cell boundary.
567 """
568 for cell in self._cells.values():
569 cell.set_fontsize(size)
570 self.stale = True
572 def _offset(self, ox, oy):
573 """Move all the artists by ox, oy (axes coords)."""
574 for c in self._cells.values():
575 x, y = c.get_x(), c.get_y()
576 c.set_x(x + ox)
577 c.set_y(y + oy)
579 def _update_positions(self, renderer):
580 # called from renderer to allow more precise estimates of
581 # widths and heights with get_window_extent
583 # Do any auto width setting
584 for col in self._autoColumns:
585 self._auto_set_column_width(col, renderer)
587 if self._autoFontsize:
588 self._auto_set_font_size(renderer)
590 # Align all the cells
591 self._do_cell_alignment()
593 bbox = self._get_grid_bbox(renderer)
594 l, b, w, h = bbox.bounds
596 if self._bbox is not None:
597 # Position according to bbox
598 rl, rb, rw, rh = self._bbox
599 self.scale(rw / w, rh / h)
600 ox = rl - l
601 oy = rb - b
602 self._do_cell_alignment()
603 else:
604 # Position using loc
605 (BEST, UR, UL, LL, LR, CL, CR, LC, UC, C,
606 TR, TL, BL, BR, R, L, T, B) = range(len(self.codes))
607 # defaults for center
608 ox = (0.5 - w / 2) - l
609 oy = (0.5 - h / 2) - b
610 if self._loc in (UL, LL, CL): # left
611 ox = self.AXESPAD - l
612 if self._loc in (BEST, UR, LR, R, CR): # right
613 ox = 1 - (l + w + self.AXESPAD)
614 if self._loc in (BEST, UR, UL, UC): # upper
615 oy = 1 - (b + h + self.AXESPAD)
616 if self._loc in (LL, LR, LC): # lower
617 oy = self.AXESPAD - b
618 if self._loc in (LC, UC, C): # center x
619 ox = (0.5 - w / 2) - l
620 if self._loc in (CL, CR, C): # center y
621 oy = (0.5 - h / 2) - b
623 if self._loc in (TL, BL, L): # out left
624 ox = - (l + w)
625 if self._loc in (TR, BR, R): # out right
626 ox = 1.0 - l
627 if self._loc in (TR, TL, T): # out top
628 oy = 1.0 - b
629 if self._loc in (BL, BR, B): # out bottom
630 oy = - (b + h)
632 self._offset(ox, oy)
634 def get_celld(self):
635 r"""
636 Return a dict of cells in the table mapping *(row, column)* to
637 `.Cell`\s.
639 Notes
640 -----
641 You can also directly index into the Table object to access individual
642 cells::
644 cell = table[row, col]
646 """
647 return self._cells
650@_docstring.dedent_interpd
651def table(ax,
652 cellText=None, cellColours=None,
653 cellLoc='right', colWidths=None,
654 rowLabels=None, rowColours=None, rowLoc='left',
655 colLabels=None, colColours=None, colLoc='center',
656 loc='bottom', bbox=None, edges='closed',
657 **kwargs):
658 """
659 Add a table to an `~.axes.Axes`.
661 At least one of *cellText* or *cellColours* must be specified. These
662 parameters must be 2D lists, in which the outer lists define the rows and
663 the inner list define the column values per row. Each row must have the
664 same number of elements.
666 The table can optionally have row and column headers, which are configured
667 using *rowLabels*, *rowColours*, *rowLoc* and *colLabels*, *colColours*,
668 *colLoc* respectively.
670 For finer grained control over tables, use the `.Table` class and add it to
671 the axes with `.Axes.add_table`.
673 Parameters
674 ----------
675 cellText : 2D list of str, optional
676 The texts to place into the table cells.
678 *Note*: Line breaks in the strings are currently not accounted for and
679 will result in the text exceeding the cell boundaries.
681 cellColours : 2D list of colors, optional
682 The background colors of the cells.
684 cellLoc : {'left', 'center', 'right'}, default: 'right'
685 The alignment of the text within the cells.
687 colWidths : list of float, optional
688 The column widths in units of the axes. If not given, all columns will
689 have a width of *1 / ncols*.
691 rowLabels : list of str, optional
692 The text of the row header cells.
694 rowColours : list of colors, optional
695 The colors of the row header cells.
697 rowLoc : {'left', 'center', 'right'}, default: 'left'
698 The text alignment of the row header cells.
700 colLabels : list of str, optional
701 The text of the column header cells.
703 colColours : list of colors, optional
704 The colors of the column header cells.
706 colLoc : {'left', 'center', 'right'}, default: 'left'
707 The text alignment of the column header cells.
709 loc : str, optional
710 The position of the cell with respect to *ax*. This must be one of
711 the `~.Table.codes`.
713 bbox : `.Bbox`, optional
714 A bounding box to draw the table into. If this is not *None*, this
715 overrides *loc*.
717 edges : substring of 'BRTL' or {'open', 'closed', 'horizontal', 'vertical'}
718 The cell edges to be drawn with a line. See also
719 `~.Cell.visible_edges`.
721 Returns
722 -------
723 `~matplotlib.table.Table`
724 The created table.
726 Other Parameters
727 ----------------
728 **kwargs
729 `.Table` properties.
731 %(Table:kwdoc)s
732 """
734 if cellColours is None and cellText is None:
735 raise ValueError('At least one argument from "cellColours" or '
736 '"cellText" must be provided to create a table.')
738 # Check we have some cellText
739 if cellText is None:
740 # assume just colours are needed
741 rows = len(cellColours)
742 cols = len(cellColours[0])
743 cellText = [[''] * cols] * rows
745 rows = len(cellText)
746 cols = len(cellText[0])
747 for row in cellText:
748 if len(row) != cols:
749 raise ValueError("Each row in 'cellText' must have {} columns"
750 .format(cols))
752 if cellColours is not None:
753 if len(cellColours) != rows:
754 raise ValueError("'cellColours' must have {} rows".format(rows))
755 for row in cellColours:
756 if len(row) != cols:
757 raise ValueError("Each row in 'cellColours' must have {} "
758 "columns".format(cols))
759 else:
760 cellColours = ['w' * cols] * rows
762 # Set colwidths if not given
763 if colWidths is None:
764 colWidths = [1.0 / cols] * cols
766 # Fill in missing information for column
767 # and row labels
768 rowLabelWidth = 0
769 if rowLabels is None:
770 if rowColours is not None:
771 rowLabels = [''] * rows
772 rowLabelWidth = colWidths[0]
773 elif rowColours is None:
774 rowColours = 'w' * rows
776 if rowLabels is not None:
777 if len(rowLabels) != rows:
778 raise ValueError("'rowLabels' must be of length {0}".format(rows))
780 # If we have column labels, need to shift
781 # the text and colour arrays down 1 row
782 offset = 1
783 if colLabels is None:
784 if colColours is not None:
785 colLabels = [''] * cols
786 else:
787 offset = 0
788 elif colColours is None:
789 colColours = 'w' * cols
791 # Set up cell colours if not given
792 if cellColours is None:
793 cellColours = ['w' * cols] * rows
795 # Now create the table
796 table = Table(ax, loc, bbox, **kwargs)
797 table.edges = edges
798 height = table._approx_text_height()
800 # Add the cells
801 for row in range(rows):
802 for col in range(cols):
803 table.add_cell(row + offset, col,
804 width=colWidths[col], height=height,
805 text=cellText[row][col],
806 facecolor=cellColours[row][col],
807 loc=cellLoc)
808 # Do column labels
809 if colLabels is not None:
810 for col in range(cols):
811 table.add_cell(0, col,
812 width=colWidths[col], height=height,
813 text=colLabels[col], facecolor=colColours[col],
814 loc=colLoc)
816 # Do row labels
817 if rowLabels is not None:
818 for row in range(rows):
819 table.add_cell(row + offset, -1,
820 width=rowLabelWidth or 1e-15, height=height,
821 text=rowLabels[row], facecolor=rowColours[row],
822 loc=rowLoc)
823 if rowLabelWidth == 0:
824 table.auto_set_column_width(-1)
826 ax.add_table(table)
827 return table