TikzTable: styling and drawing

This page continues from TikzTable basics. TikzTable supports every formatter and table-level option shown in the TexTable guide, then adds a TikZ lattice for formatting that depends on cell geometry: framing selected cells, drawing across several cells, and controlling the space around their contents.

import numpy as np
import pandas as pd

from gerrytools.latex import TikzTable

districts = pd.DataFrame(
    {
        "District": [f"CD {i}" for i in range(1, 9)],
        "BVAP share": [0.12, 0.18, 0.22, 0.31, 0.38, 0.44, 0.52, 0.58],
        "Dem share": [0.35, 0.41, 0.44, 0.47, 0.50, 0.55, 0.61, 0.66],
        "Polsby-Popper": [0.18, 0.22, 0.27, 0.31, 0.33, 0.35, 0.41, 0.44],
        "Pop. deviation": [0.004, 0.002, np.nan, 0.006, 0.001, 0.008, 0.003, 0.005],
    }
)
districts
District BVAP share Dem share Polsby-Popper Pop. deviation
0 CD 1 0.12 0.35 0.18 0.004
1 CD 2 0.18 0.41 0.22 0.002
2 CD 3 0.22 0.44 0.27 NaN
3 CD 4 0.31 0.47 0.31 0.006
4 CD 5 0.38 0.50 0.33 0.001
5 CD 6 0.44 0.55 0.35 0.008
6 CD 7 0.52 0.61 0.41 0.003
7 CD 8 0.58 0.66 0.44 0.005

Cell borders

set_cell_border(row, col, sides) uses 1-based coordinates in the rendered table. In this example, row 1 is the column header, row 2 is DataFrame row 0, and column 1 is District. A group header adds another rendered row, while include_index() adds a leading column. The accepted sides are "top", "bottom", "left", "right", and "all".

Coordinate systems

highlight_rows() uses zero-based DataFrame row positions. set_cell_border() and add_draw() use one-based rendered coordinates because they operate after headers and index columns have been laid out.

table = TikzTable(districts)
table.set_decimal_count(2)
table.set_cell_space_limits("2pt")
table.highlight_rows([3, 4], color="lightblue!20!white")
table.set_cell_border(5, [2, 3, 4, 5], "top")
table.set_cell_border(6, [2, 3, 4, 5], "bottom")
table.set_cell_border([5, 6], 2, "left")
table.set_cell_border([5, 6], 5, "right")
print(table)
\documentclass[border=2pt]{standalone}
\usepackage{latexcolors, colortbl, tikz, nicematrix}
\usetikzlibrary{calc}
\begin{document}
\begin{NiceTabular}{ccccc}[name=table, cell-space-limits=2pt]
\CodeBefore
  \rowcolor{lightblue!20!white}{5}
  \rowcolor{lightblue!20!white}{6}
\Body
\textbf{District} & \textbf{BVAP share} & \textbf{Dem share} & \textbf{Polsby-Popper} & \textbf{Pop. deviation}{\rule[-\dimexpr1\doublerulesep+\arrayrulewidth\relax]{0pt}{0pt}} \\
\hline
CD 1 & 0.12 & 0.35 & 0.18 & 0.00 \\
CD 2 & 0.18 & 0.41 & 0.22 & 0.00 \\
CD 3 & 0.22 & 0.44 & 0.27 & NaN \\
CD 4 & 0.31 & 0.47 & 0.31 & 0.01 \\
CD 5 & 0.38 & 0.50 & 0.33 & 0.00 \\
CD 6 & 0.44 & 0.55 & 0.35 & 0.01 \\
CD 7 & 0.52 & 0.61 & 0.41 & 0.00 \\
CD 8 & 0.58 & 0.66 & 0.44 & 0.01 \\
\CodeAfter
\begin{tikzpicture}
  \draw[line width=\arrayrulewidth] ([yshift=1\doublerulesep]row-2-|col-1) -- ([yshift=1\doublerulesep]row-2-|col-6);
  \draw (row-5-|col-2) -- (row-5-|col-3);
  \draw (row-7-|col-2) -- (row-7-|col-3);
  \draw (row-5-|col-3) -- (row-5-|col-4);
  \draw (row-7-|col-3) -- (row-7-|col-4);
  \draw (row-5-|col-4) -- (row-5-|col-5);
  \draw (row-7-|col-4) -- (row-7-|col-5);
  \draw (row-5-|col-5) -- (row-5-|col-6);
  \draw (row-7-|col-5) -- (row-7-|col-6);
  \draw (row-5-|col-2) -- (row-6-|col-2);
  \draw (row-5-|col-6) -- (row-6-|col-6);
  \draw (row-6-|col-2) -- (row-7-|col-2);
  \draw (row-6-|col-6) -- (row-7-|col-6);
\end{tikzpicture}
\end{NiceTabular}
\end{document}

A bordered range over two highlighted rows

Passing row or column lists applies the requested sides to every selected cell. The four calls above draw only the outside of the range. Use "all" when each cell needs its own box. set_cell_space_limits() increases the minimum vertical breathing room without changing the DataFrame or wrapping its values.

Combine formatters with cell geometry

The shared formatter pipeline still controls which cells receive formatting. TikZ then paints those fills across the complete cell. For diverging_gradient_formatter(), pass command_name=None to select that full-cell path. Fixing lo, mid, and hi keeps the colors comparable across several tables rather than rescaling them to each DataFrame.

compose_formatters() runs from right to left: this example first renders two decimal places, then chooses a background from the original numeric value.

from gerrytools.latex.commands import tex_twocolor_gradient_command
from gerrytools.latex.formatters import (
    compose_formatters,
    diverging_gradient_formatter,
    highlight_ge,
    round_decimals,
    wrap_with_tex_command,
)

table = TikzTable(districts)
table.set_decimal_count(2)
table.set_column_formatter(
    "Dem share",
    compose_formatters(
        diverging_gradient_formatter(
            lo=0.35,
            mid=0.50,
            hi=0.65,
            color_lo="alizarin",
            color_mid="white",
            color_hi="denim",
            command_name=None,
        ),
        round_decimals(2),
    ),
)
print(table)
\documentclass[border=2pt]{standalone}
\usepackage{xcolor, colortbl, tikz, nicematrix}
\usetikzlibrary{calc}
\begin{document}
\begin{NiceTabular}{ccccc}[name=table, cell-space-limits=1pt]
\CodeBefore
  \cellcolor[HTML]{D11A42}{2-3}
  \cellcolor[HTML]{E3768E}{3-3}
  \cellcolor[HTML]{EDA3B3}{4-3}
  \cellcolor[HTML]{F6D1D9}{5-3}
  \cellcolor[HTML]{FFFFFF}{6-3}
  \cellcolor[HTML]{B1CAE9}{7-3}
  \cellcolor[HTML]{538ACF}{8-3}
  \cellcolor[HTML]{1560BD}{9-3}
\Body
\textbf{District} & \textbf{BVAP share} & \textbf{Dem share} & \textbf{Polsby-Popper} & \textbf{Pop. deviation}{\rule[-\dimexpr1\doublerulesep+\arrayrulewidth\relax]{0pt}{0pt}} \\
\hline
CD 1 & 0.12 & 0.35 & 0.18 & 0.00 \\
CD 2 & 0.18 & 0.41 & 0.22 & 0.00 \\
CD 3 & 0.22 & 0.44 & 0.27 & NaN \\
CD 4 & 0.31 & 0.47 & 0.31 & 0.01 \\
CD 5 & 0.38 & 0.50 & 0.33 & 0.00 \\
CD 6 & 0.44 & 0.55 & 0.35 & 0.01 \\
CD 7 & 0.52 & 0.61 & 0.41 & 0.00 \\
CD 8 & 0.58 & 0.66 & 0.44 & 0.01 \\
\CodeAfter
\begin{tikzpicture}
  \draw[line width=\arrayrulewidth] ([yshift=1\doublerulesep]row-2-|col-1) -- ([yshift=1\doublerulesep]row-2-|col-6);
\end{tikzpicture}
\end{NiceTabular}
\end{document}

Diverging gradient in a TikzTable

Format every numeric cell

A column formatter is appropriate when one measure has a meaningful scale. A number formatter applies to every numeric cell, which is useful for a compact diagnostic matrix. Missing values bypass the formatter and use the string configured by set_nan_string().

table = TikzTable(districts)
table.set_nan_string("---")
table.set_number_formatter(compose_formatters(wrap_with_tex_command("heatmap"), round_decimals(2)))
table.document.add_command(tex_twocolor_gradient_command("heatmap"))
print(table)
\documentclass[border=2pt]{standalone}
\usepackage{latexcolors, colortbl, siunitx, xfp, tikz, nicematrix}
\usetikzlibrary{calc}
\newcommand{\heatmap}[1]{%
  \begingroup%
  \edef\heatlo{0.0}\edef\heathi{1.0}%
  \edef\heatrange{\fpeval{max(\heathi-\heatlo, 1e-12)}}%
  \edef\heatt{\fpeval{min(1, max(0, (#1-\heatlo)/\heatrange))}}%
  \edef\heatpct{\fpeval{round(100*(1-\heatt),0)}}%
  \edef\heatcolorspec{denim!\heatpct!alizarin}%
  \expandafter\cellcolor\expandafter{\heatcolorspec}%
  \num[round-mode=places,round-precision=4]{#1}%
  \endgroup%
}
\begin{document}
\begin{NiceTabular}{ccccc}[name=table, cell-space-limits=1pt]
\textbf{District} & \textbf{BVAP share} & \textbf{Dem share} & \textbf{Polsby-Popper} & \textbf{Pop. deviation}{\rule[-\dimexpr1\doublerulesep+\arrayrulewidth\relax]{0pt}{0pt}} \\
\hline
CD 1 & \heatmap{0.12} & \heatmap{0.35} & \heatmap{0.18} & \heatmap{0.00} \\
CD 2 & \heatmap{0.18} & \heatmap{0.41} & \heatmap{0.22} & \heatmap{0.00} \\
CD 3 & \heatmap{0.22} & \heatmap{0.44} & \heatmap{0.27} & --- \\
CD 4 & \heatmap{0.31} & \heatmap{0.47} & \heatmap{0.31} & \heatmap{0.01} \\
CD 5 & \heatmap{0.38} & \heatmap{0.50} & \heatmap{0.33} & \heatmap{0.00} \\
CD 6 & \heatmap{0.44} & \heatmap{0.55} & \heatmap{0.35} & \heatmap{0.01} \\
CD 7 & \heatmap{0.52} & \heatmap{0.61} & \heatmap{0.41} & \heatmap{0.00} \\
CD 8 & \heatmap{0.58} & \heatmap{0.66} & \heatmap{0.44} & \heatmap{0.01} \\
\CodeAfter
\begin{tikzpicture}
  \draw[line width=\arrayrulewidth] ([yshift=1\doublerulesep]row-2-|col-1) -- ([yshift=1\doublerulesep]row-2-|col-6);
\end{tikzpicture}
\end{NiceTabular}
\end{document}

Two-color heatmap in a TikzTable

Draw across cells

Use add_draw() when the annotation spans a range or expresses a relationship that cannot be described as individual cell sides. It appends raw TikZ to the table’s \CodeAfter block. set_table_name() supplies the node prefix, and each cell is addressable as (<name>-<row>-<col>) through the usual TikZ anchors. Give tables distinct names when several are pasted into the same document so their nodes cannot collide.

table = TikzTable(districts)
table.set_decimal_count(2)
table.set_table_name("diagnostics")
table.document.add_color("framecolor", "denim")
table.add_draw(
    r"\draw[framecolor, rounded corners=2pt, line width=0.8pt] "
    r"(diagnostics-5-2.north west) rectangle "
    r"(diagnostics-6-5.south east);"
)
print(table)
\documentclass[border=2pt]{standalone}
\usepackage{latexcolors, tikz, nicematrix}
\colorlet{framecolor}{denim}
\usetikzlibrary{calc}
\begin{document}
\begin{NiceTabular}{ccccc}[name=diagnostics, cell-space-limits=1pt]
\textbf{District} & \textbf{BVAP share} & \textbf{Dem share} & \textbf{Polsby-Popper} & \textbf{Pop. deviation}{\rule[-\dimexpr1\doublerulesep+\arrayrulewidth\relax]{0pt}{0pt}} \\
\hline
CD 1 & 0.12 & 0.35 & 0.18 & 0.00 \\
CD 2 & 0.18 & 0.41 & 0.22 & 0.00 \\
CD 3 & 0.22 & 0.44 & 0.27 & NaN \\
CD 4 & 0.31 & 0.47 & 0.31 & 0.01 \\
CD 5 & 0.38 & 0.50 & 0.33 & 0.00 \\
CD 6 & 0.44 & 0.55 & 0.35 & 0.01 \\
CD 7 & 0.52 & 0.61 & 0.41 & 0.00 \\
CD 8 & 0.58 & 0.66 & 0.44 & 0.01 \\
\CodeAfter
\begin{tikzpicture}
  \draw[line width=\arrayrulewidth] ([yshift=1\doublerulesep]row-2-|col-1) -- ([yshift=1\doublerulesep]row-2-|col-6);
  \draw[framecolor, rounded corners=2pt, line width=0.8pt] (diagnostics-5-2.north west) rectangle (diagnostics-6-5.south east);
\end{tikzpicture}
\end{NiceTabular}
\end{document}

A rounded frame drawn around a block of cells

The command uses the north-west anchor of one cell and the south-east anchor of another, so the frame follows the table layout automatically. The boundary lattice is also available as (row-<i>) and (col-<j>). clear_extra_draws() removes the raw drawing commands, just as clear_cell_borders() removes the higher-level cell borders.

Put the pieces together

A finished table can combine the shared report formatting with TikZ-specific geometry. Here, grouped headers organize the measures, a fixed diverging scale shows vote share, and a threshold formatter identifies larger population deviations. The borders pick out those same cells after the extra group-header row has shifted the rendered coordinates.

table = TikzTable(districts)
table.set_header_groups(
    {
        "": ["District"],
        "Representation": ["BVAP share", "Dem share"],
        "Diagnostics": ["Polsby-Popper", "Pop. deviation"],
    }
)
table.set_cell_space_limits("1.5pt")
table.set_column_formatter(
    "Dem share",
    compose_formatters(
        diverging_gradient_formatter(
            lo=0.35,
            mid=0.50,
            hi=0.65,
            color_lo="alizarin",
            color_mid="white",
            color_hi="denim",
            command_name=None,
        ),
        round_decimals(2),
    ),
)
table.set_column_formatter(
    "Pop. deviation",
    compose_formatters(
        highlight_ge(0.006, color="amber!35!white"),
        round_decimals(3),
    ),
)
table.set_cell_border([6, 8], 5, "all")
table.add_toprule()
table.add_bottomrule()
print(table)
\documentclass[border=2pt]{standalone}
\usepackage{latexcolors, colortbl, tikz, nicematrix}
\usetikzlibrary{calc}
\begin{document}
\begin{NiceTabular}{ccccc}[name=table, cell-space-limits=1.5pt]
\CodeBefore
  \cellcolor[HTML]{D11A42}{3-3}
  \cellcolor[HTML]{E3768E}{4-3}
  \cellcolor[HTML]{EDA3B3}{5-3}
  \cellcolor[HTML]{F6D1D9}{6-3}
  \cellcolor{amber!35!white}{6-5}
  \cellcolor[HTML]{FFFFFF}{7-3}
  \cellcolor[HTML]{B1CAE9}{8-3}
  \cellcolor{amber!35!white}{8-5}
  \cellcolor[HTML]{538ACF}{9-3}
  \cellcolor[HTML]{1560BD}{10-3}
\Body
\hline
\multicolumn{1}{c}{} & \multicolumn{2}{c}{\textbf{Representation}} & \multicolumn{2}{c}{\textbf{Diagnostics}} \\
\textbf{District} & \textbf{BVAP share} & \textbf{Dem share} & \textbf{Polsby-Popper} & \textbf{Pop. deviation}{\rule[-\dimexpr1\doublerulesep+\arrayrulewidth\relax]{0pt}{0pt}} \\
\hline
CD 1 & 0.1200 & 0.35 & 0.1800 & 0.004 \\
CD 2 & 0.1800 & 0.41 & 0.2200 & 0.002 \\
CD 3 & 0.2200 & 0.44 & 0.2700 & NaN \\
CD 4 & 0.3100 & 0.47 & 0.3100 & 0.006 \\
CD 5 & 0.3800 & 0.50 & 0.3300 & 0.001 \\
CD 6 & 0.4400 & 0.55 & 0.3500 & 0.008 \\
CD 7 & 0.5200 & 0.61 & 0.4100 & 0.003 \\
CD 8 & 0.5800 & 0.66 & 0.4400 & 0.005 \\
\hline
\CodeAfter
\begin{tikzpicture}
  \draw[line width=\arrayrulewidth] ([yshift=1\doublerulesep]row-3-|col-1) -- ([yshift=1\doublerulesep]row-3-|col-6);
  \draw (row-6-|col-5) -- (row-6-|col-6);
  \draw (row-7-|col-5) -- (row-7-|col-6);
  \draw (row-8-|col-5) -- (row-8-|col-6);
  \draw (row-9-|col-5) -- (row-9-|col-6);
  \draw (row-6-|col-5) -- (row-7-|col-5);
  \draw (row-6-|col-6) -- (row-7-|col-6);
  \draw (row-8-|col-5) -- (row-9-|col-5);
  \draw (row-8-|col-6) -- (row-9-|col-6);
\end{tikzpicture}
\end{NiceTabular}
\end{document}

A complete TikzTable with grouped headers, gradients, highlights, and borders

Choose the narrowest formatting tool

  • Use highlight_rows() for a complete DataFrame row.

  • Use a formatter when the displayed value determines the treatment.

  • Use set_cell_border() for exact cells or rectangular boundaries.

  • Use add_draw() only for geometry that crosses cells or needs other TikZ primitives.

These layers compose: formatters and row highlights are resolved before borders and raw draw commands are added over the finished table.