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()andadd_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}

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}

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}

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}

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}

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.