Spotlighting Data in Simple Little Tables | by Michael Demastrie | Towards Data Science

When presenting tabular data, we often want to bring attention to a particular cell. This focus improves the visual support we want from our tables and keeps the audience from feeling overwhelmed by the breadth of the data contained in the whole set of values. Still, we want to present the key information in the context of the whole set. With tables, we can highlight the desired cells by making them different from others in size, weight, color, shape, spacing, or borders.
This article expands on my earlier article, “Simple Little Tables with Matplotlib,” by providing a set of techniques we can use to highlight cells within a Matplotlib table. These techniques include ways to change the border width, background color, font weight, and font color of a cell. We will also see how to fade the cells not highlighted. Along the way, we’ll discover some related style techniques like changing the table grid line color and using open-source color palettes. I will annotate each step of the transformation to provide insight into pyplot.table behaviors that we might reuse to accomplish other tasks or for you to extend the discoveries I am sharing. I will present the full source code at the end of the article, so you don't need to piece it together, line by line.
The starting point is the final code from the “Simple Little Tables with Matplotlib” article. These techniques should also work on any table derived from pyplot.table with a likely need for adjustments.
The first thing we’ll do is change out the color palette from the colormap inherited from the pyplot.table example. I used a pleasant color palette called "Outcomes" by TaraLynn1221 on colourlovers.com. Using open sourced palettes from artistic communities is one way we design-challenged programmers can put together colors that at least match in some context.
# colors from "Outcomes" palette by TaraLynn1221# https://www.colourlovers.com/palette/4751687/Outcomescl_outcomes = {    'white':'#FFFFFF',    'gray': '#AAA9AD',    'black':'#313639',    'purple':'#AD688E',    'orange':'#D18F77',    'yellow':'#E8E190',    'ltgreen':'#CCD9C7',    'dkgreen':'#96ABA0',    }...# Get some lists of color specs for row and column headersrcolors = np.full(len(row_headers), cl_outcomes['ltgreen'])ccolors = np.full(len(column_headers), cl_outcomes['ltgreen'])
I also created some color variables for the highlighted cells that we’ll create and for some other components in the drawing.
grid_color = cl_outcomes['dkgreen']fig_background_color = cl_outcomes['white']fig_border = cl_outcomes['dkgreen']label_text_color = cl_outcomes['black']highlight_text_color = cl_outcomes['yellow']faded_text_color = cl_outcomes['gray']highlight_color = cl_outcomes['purple']highlight_border = cl_outcomes['dkgreen']title_text_color = cl_outcomes['black']
While I was at it, I updated and added some variables for text elements. In this example, we’ll be adding a text annotation and a subtitle to the figure.
title_text = 'Loss by Disaster'subtitle_text = '(In millions of dollars)'annotation_text = 'The ten-year loss due to floods is $78.0M.'footer_text = 'July 28, 2020'
Highlighting is accomplished by setting the fill and edge (border) colors of the cell. This procedure can be repeated for several cells, if you want.
A Table object is made up of Cell objects. The cells are referenced using row, column indexing with (0,0) starting in the upper-left corner of the table. The column headers are within this matrix, so (0,0) refers to the first column header. Row headers are indeed part of this matrix, but are indexed as column -1.
The following code will highlight the cell in the second row and third column. Remember that we have to consider that row 0 is the column header row. The column index of the row headers is -1. The top row header is (1,-1).
To do the highlighting, we retrieve the cell from the table. Then we set the face color, edge color, and line width. To control the font styles, we get the Text object from the Cell. There are limitations on what Text attributes can be controlled. For example, setting the font size of a single cell has no effect. All cells in the data grid need to have the same font size, which is controlled at the Table level.
Once we have made all the highlighting style changes we want, we call ax.add_patch() passing in a reference to the cell. You might think that simply changing the attributes would be enough to set the styles, but it does not seem to reposition the cell in the z-order and adjacent cell styles display on top of the highlighted cell. Adding the cell to the Axes object as a patch fixes that problem. If you find a more elegant way, let us know in the comments.
# Highlight the cell to draw attention to itthe_cell = the_table[2,2]the_cell.set_facecolor(highlight_color)the_cell.set_edgecolor(highlight_border)the_cell.set_linewidth(2)the_text = the_cell.get_text()#the_text.set_weight('bold')the_text.set_fontstyle('italic')the_text.set_color(highlight_text_color)ax.add_patch(the_cell)
Cell objects are subclasses of Matplotib Rectangles. Rectangles are subclasses of Patches. This inheritance means that you can reference the attributes and functions of Rectangle and Patches, giving you a lot of control over the formatting.
To draw more attention to the highlighted cell, we can fade them. We’ll set them to a lighter color using a technique similar to highlighting. We can call get_children() to get a list of cells and loop through them. A table only contains cells, so there is no need to type-check the elements. For each cell, we get its text object. Excluding headers from the process is as simple as checking the cell text for its presence in the header label lists. Of course, if you have cells with values that match the labels, you'll need to come up with another way. This looping technique is also how we set table border color, so let's add cell.set_edgecolor(grid_color) here. There is no attribute on the Table object to set this color, so looping is how we do it.
# Fade the cellsfor cell in the_table.get_children():    cell_text = cell.get_text().get_text()    cell.set_edgecolor(grid_color)    if cell_text not in row_headers\       and cell_text not in column_headers:        cell.get_text().set_color(faded_text_color)    else:        cell.get_text().set_weight('bold')        cell.get_text().set_color(label_text_color)
We could have added code to exclude the highlighted cells. It’s easier just to fade them all before highlighting, though. So let’s just insert the fading code routine earlier in the flow.
An alternative to get_children() is get_celld(). While get_children() returns a list of Artist objects that happen to only be Cells here, get_celld() will return a dictionary of Cells with keys that are row and column tuples.
print(the_table.get_children())[<matplotlib.table.CustomCell object at 0x7fb7b430f150>, <matplotlib.table.CustomCell object at 0x7fb7b6bf2890>, <matplotlib.table.CustomCell object at 0x7fb7b6bf26d0>,...print(the_table.get_celld()){(1, 0): <matplotlib.table.CustomCell object at 0x7fb7b4313390>, (1, 1): <matplotlib.table.CustomCell object at 0x7fb7b4313e50>, (1, 2): <matplotlib.table.CustomCell object at 0x7fb7b4313150>,...
Tip: get_celld() can be used to generate an index table of cell text to use in troubleshooting or manually referencing cells by index.
for key, val in the_table.get_celld().items():	# call get_text() on Cell and again on returned Text    print(f'{key}\t{val.get_text().get_text()}')(1, 0)	66.4(1, 1)	174.3(1, 2)	75.1...
If we’re going to use our table image in a presentation or report, we may want to add a text annotation to it to help the reader to understand why we are calling out the cell value. A subtitle can help the reader understand the figure, too. In our case, we have not let the reader know what the units of measure are in our table, so that information might make a good subtitle.
There are several ways to create annotations in Matplotlib. We have a good amount of whitespace surrounding the table, so let’s use a simple line of figure text below the chart. We’ll include some styling, too.
# Add annotationplt.figtext(0.5, 0.2,            annotation_text,            horizontalalignment='center',            size=9, weight='light',            color=title_text_color           )
Matplotlib does not give us a first-class subtitle function, so we’ll use figure text, too. Unfortunately, this means we may need to tweak it’s positioning if we change the title or overall figure structure later. We could go through heroics to calculate the subtitle position relative to the figure and title, but we’ll keep it simple and hard-code the subtitle position based on experimentation.
# Add subtitleplt.figtext(0.5, 0.9,            subtitle_text,            horizontalalignment='center',            size=9, style='italic',            color=title_text_color           )
Note: Remember that suptitle() is the function to set the title on the figure and not a subtitle. Unfortunate naming.
Here is the final source code. I also added some explicit styling to the title and footer. I have a designer friend who taught me that the best black in graphic design is not pure black, so we’re using #313639 instead of #000000.
Here is the final product, from which you can clip to your heart’s extent.
import numpy as npimport matplotlib.pyplot as plt# colors from "Outcomes" palette by TaraLynn1221# https://www.colourlovers.com/palette/4751687/Outcomescl_outcomes = {    'white':'#FFFFFF',    'gray': '#AAA9AD',    'black':'#313639',    'purple':'#AD688E',    'orange':'#D18F77',    'yellow':'#E8E190',    'ltgreen':'#CCD9C7',    'dkgreen':'#96ABA0',    }title_text = 'Loss by Disaster'subtitle_text = '(In millions of dollars)'annotation_text = 'The ten-year loss due to floods is $78.0M.'footer_text = 'July 28, 2020'grid_color = cl_outcomes['dkgreen']fig_background_color = cl_outcomes['white']fig_border = cl_outcomes['dkgreen']label_text_color = cl_outcomes['black']highlight_text_color = cl_outcomes['yellow']faded_text_color = cl_outcomes['gray']highlight_color = cl_outcomes['purple']highlight_border = cl_outcomes['dkgreen']title_text_color = cl_outcomes['black']data =  [            [         'Freeze', 'Wind', 'Flood', 'Quake', 'Hail'],            [ '5 year',  66386, 174296,   75131,  577908,  32015],            ['10 year',  58230, 381139,   78045,   99308, 160454],            ['20 year',  89135,  80552,  152558,  497981, 603535],            ['30 year',  78415,  81858,  150656,  193263,  69638],            ['40 year', 139361, 331509,  343164,  781380,  52269],        ]# Pop the headers from the data arraycolumn_headers = data.pop(0)row_headers = [x.pop(0) for x in data]# Table data needs to be non-numeric text. Format the data# while I'm at it.cell_text = []for row in data:    cell_text.append([f'{x/1000:1.1f}' for x in row])# Get some lists of color specs for row and column headersrcolors = np.full(len(row_headers), cl_outcomes['ltgreen'])ccolors = np.full(len(column_headers), cl_outcomes['ltgreen'])# Create the figure. Setting a small pad on tight_layout# seems to better regulate white space. Sometimes experimenting# with an explicit figsize here can produce better outcome.plt.figure(linewidth=1,           edgecolor=fig_border,           facecolor=fig_background_color,           tight_layout={'pad':1},           #figsize=(5,3)          )# Add a table at the bottom of the axesthe_table = plt.table(cellText=cell_text,                      rowLabels=row_headers,                      rowColours=rcolors,                      rowLoc='right',                      colColours=ccolors,                      colLabels=column_headers,                      loc='center')# Scaling is the only influence we have over top and bottom cell padding.# Make the rows taller (i.e., make cell y scale larger).the_table.scale(1, 1.5)# Hide axesax = plt.gca()ax.get_xaxis().set_visible(False)ax.get_yaxis().set_visible(False)# Hide axes borderplt.box(on=None)# Add titleplt.suptitle(title_text,             weight='bold',             size=14,             color=title_text_color)# Add subtitleplt.figtext(0.5, 0.9,            subtitle_text,            horizontalalignment='center',            size=9, style='italic',            color=title_text_color           )# Add footerplt.figtext(0.95, 0.05, footer_text,            horizontalalignment='right',            size=6,            weight='light',            color=title_text_color           )# Add annotationplt.figtext(0.5, 0.2,            annotation_text,            horizontalalignment='center',            size=9, weight='light',            color=title_text_color           )# Fade the cellsfor cell in the_table.get_children():    cell_text = cell.get_text().get_text()    cell.set_edgecolor(grid_color)    if cell_text not in row_headers\       and cell_text not in column_headers:        cell.get_text().set_color(faded_text_color)    else:        cell.get_text().set_weight('bold')        cell.get_text().set_color(label_text_color)# Highlight the cell to draw attention to itthe_cell = the_table[2,2]the_cell.set_facecolor(highlight_color)the_cell.set_edgecolor(highlight_border)the_cell.set_linewidth(2)the_text = the_cell.get_text()#the_text.set_weight('bold')the_text.set_fontstyle('italic')the_text.set_color(highlight_text_color)ax.add_patch(the_cell)# Force the figure to update, so backends center objects correctly within the figure.# Without plt.draw() here, the title will center on the axes and not the figure.plt.draw()# Create image. plt.savefig ignores figure edge and face colors, so map them.fig = plt.gcf()plt.savefig('pyplot-table-highlight.png',            bbox='tight',            edgecolor=fig.get_edgecolor(),            facecolor=fig.get_facecolor(),            dpi=300            )
We have taken our first little table and added some highlighting to a cell to make it more visible for our audiences. We learned how to harvest color palettes from open design sites. We saw highlighting techniques based on size, color, relative fading, and borders. We learned an easy way to add an annotation and subtitle. Matplotlib tables are a flexible and powerful tool for presenting your tabular summary data.