diff --git a/doc/users/whats_new/updated_table.rst b/doc/users/whats_new/updated_table.rst new file mode 100644 index 000000000000..f00b00a8fbb2 --- /dev/null +++ b/doc/users/whats_new/updated_table.rst @@ -0,0 +1,11 @@ +Updated Table and to control edge visibility +-------------------------------------------- +Added the ability to toggle the visibility of lines in Tables. +Functionality added to the table() factory function under the keyword argument "edges". +Values can be the strings "open", "closed", "horizontal", "vertical" or combinations of the letters "L", "R", "T", "B" which represent left, right, top, and bottom respectively. + +Example: + table(..., edges="open") # No line visible + table(..., edges="closed") # All lines visible + table(..., edges="horizontal") # Only top and bottom lines visible + table(..., edges="LT") # Only left and top lines visible. diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 497c1eb49ad4..3b3cd02210cf 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -34,6 +34,7 @@ from matplotlib import docstring from .text import Text from .transforms import Bbox +from matplotlib.path import Path class Cell(Rectangle): @@ -146,6 +147,67 @@ def set_text_props(self, **kwargs): self._text.update(kwargs) +class CustomCell(Cell): + """ + A subclass of Cell where the sides may be visibly toggled. + + """ + + _edges = 'BRTL' + _edge_aliases = {'open': '', + 'closed': _edges, # default + 'horizontal': 'BT', + 'vertical': 'RL' + } + + def __init__(self, *args, **kwargs): + visible_edges = kwargs.pop('visible_edges') + Cell.__init__(self, *args, **kwargs) + self.visible_edges = visible_edges + + @property + def visible_edges(self): + 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: + for edge in value: + if edge not in self._edges: + msg = ('Invalid edge param {0}, must only be one of' + ' {1} or string of {2}.').format( + value, + ", ".join(self._edge_aliases.keys()), + ", ".join(self._edges), + ) + raise ValueError(msg) + self._visible_edges = value + + def get_path(self): + 'Return a path where the edges specificed by _visible_edges are drawn' + + codes = [Path.MOVETO] + + for edge in self._edges: + if edge in self._visible_edges: + codes.append(Path.LINETO) + else: + codes.append(Path.MOVETO) + + 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 + ) + + class Table(Artist): """ Create a table of cells. @@ -203,6 +265,7 @@ def __init__(self, ax, loc=None, bbox=None, **kwargs): self._texts = [] self._cells = {} + self._edges = None self._autoRows = [] self._autoColumns = [] self._autoFontsize = True @@ -216,13 +279,21 @@ def add_cell(self, row, col, *args, **kwargs): """ Add a cell to the table. """ xy = (0, 0) - cell = Cell(xy, *args, **kwargs) + cell = CustomCell(xy, visible_edges=self.edges, *args, **kwargs) cell.set_figure(self.figure) cell.set_transform(self.get_transform()) cell.set_clip_on(False) self._cells[(row, col)] = cell + @property + def edges(self): + return self._edges + + @edges.setter + def edges(self, value): + self._edges = value + def _approx_text_height(self): return (self.FONTSIZE / 72.0 * self.figure.dpi / self._axes.bbox.height * 1.2) @@ -246,8 +317,8 @@ def draw(self, renderer): keys.sort() for key in keys: self._cells[key].draw(renderer) - #for c in self._cells.itervalues(): - # c.draw(renderer) + # for c in self._cells.itervalues(): + # c.draw(renderer) renderer.close_group('table') def _get_grid_bbox(self, renderer): @@ -273,8 +344,8 @@ def contains(self, mouseevent): # doesn't have to bind to each one individually. if self._cachedRenderer is not None: boxes = [self._cells[pos].get_window_extent(self._cachedRenderer) - for pos in six.iterkeys(self._cells) - if pos[0] >= 0 and pos[1] >= 0] + for pos in six.iterkeys(self._cells) + if pos[0] >= 0 and pos[1] >= 0] bbox = Bbox.union(boxes) return bbox.contains(mouseevent.x, mouseevent.y), {} else: @@ -455,23 +526,24 @@ def get_celld(self): 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, - **kwargs): + 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): """ TABLE(cellText=None, cellColours=None, cellLoc='right', colWidths=None, rowLabels=None, rowColours=None, rowLoc='left', colLabels=None, colColours=None, colLoc='center', - loc='bottom', bbox=None) + loc='bottom', bbox=None, edges='closed') Factory function to generate a Table instance. Thanks to John Gill for providing the class and table. """ + # Check we have some cellText if cellText is None: # assume just colours are needed @@ -528,6 +600,7 @@ def table(ax, # Now create the table table = Table(ax, loc, bbox, **kwargs) + table.edges = edges height = table._approx_text_height() # Add the cells diff --git a/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png b/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png new file mode 100644 index 000000000000..e5d399a09e45 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_table/table_cell_manipulation.png differ diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index 817e05170b0e..be6d9b693184 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -7,6 +7,10 @@ import numpy as np from matplotlib.testing.decorators import image_comparison +from matplotlib.table import CustomCell +from matplotlib.path import Path +from nose.tools import assert_equal + @image_comparison(baseline_images=['table_zorder'], extensions=['png'], @@ -79,3 +83,41 @@ def test_label_colours(): colColours=colours, colLabels=['Header'] * dim, loc='best') + + +@image_comparison(baseline_images=['table_cell_manipulation'], + extensions=['png'], remove_text=True) +def test_diff_cell_table(): + cells = ('horizontal', 'vertical', 'open', 'closed', 'T', 'R', 'B', 'L') + cellText = [['1'] * len(cells)] * 2 + colWidths = [0.1] * len(cells) + + _, axes = plt.subplots(nrows=len(cells), figsize=(4, len(cells)+1)) + for ax, cell in zip(axes, cells): + ax.table( + colWidths=colWidths, + cellText=cellText, + loc='center', + edges=cell, + ) + ax.axis('off') + plt.tight_layout() + + +def test_customcell(): + types = ('horizontal', 'vertical', 'open', 'closed', 'T', 'R', 'B', 'L') + codes = ( + (Path.MOVETO, Path.LINETO, Path.MOVETO, Path.LINETO, Path.MOVETO), + (Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO, Path.LINETO), + (Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO), + (Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY), + (Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO), + (Path.MOVETO, Path.MOVETO, Path.LINETO, Path.MOVETO, Path.MOVETO), + (Path.MOVETO, Path.LINETO, Path.MOVETO, Path.MOVETO, Path.MOVETO), + (Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.MOVETO, Path.LINETO), + ) + + for t, c in zip(types, codes): + cell = CustomCell((0, 0), visible_edges=t, width=1, height=1) + code = tuple(s for _, s in cell.get_path().iter_segments()) + assert_equal(c, code)