Skip to content

The gates

This page lists every row the two checkers return, the threshold each one measures against, and what makes it fail.

audit(fig) returns (ok, rows) with one row per figure gate. check(colors) returns the same shape for a palette. Each row is a (name, status, detail) triple, and status is True, False, or "warn".

For what to type when a row fails, see the how-to guides. For why a threshold sits where it does, see the figure style guide.

Figure gates

check_figure.py renders the figure through an Agg canvas at MEASURE_DPI = 150, measures the result, and hands the figure back on the dpi it arrived on. audit() returns these 21 rows in this order.

Gate Threshold Fails when
Clipping canvas bounds a text artist's bbox extends past the canvas
Text collision oriented box overlap two text boxes overlap, rotation included, tick labels on a shared axis exempted
Text readability TEXT_CONTRAST_MIN = 4.5 text misses WCAG AA against the backdrop it actually got, or data ink crosses its glyphs
Contrast stack ALPHA_LEVELS_MAX = 3 nothing in the figure is opaque, or transparency uses more than 3 distinct levels
Mark ratio MARK_RATIO_MAX = 5.0 largest data mark exceeds 5x the smallest by area
Overplotting OVERPLOT_THRESHOLD = 0.5 over half a scatter's points sit close enough to some other point for the two marks to touch on the page (advisory)
Axis redundancy shared scale panels sharing limits, scale type and axis title repeat tick labels or axis titles
Type size TYPE_FLOOR_PT = 7.5 a string renders under 7.5pt on the printed page
Line weight LINE_FLOOR_PT = 1.0 a stroke renders under 1pt on the printed page
Banking BANKING_SLOPE_MAX = 10.0 a line panel's median segment slope is over 10 or under 1/10, so the aspect ratio puts the typical segment past 84 degrees or under 6 (advisory)
Ink coverage INK_MIN, INK_MAX = 0.02, 0.55 a panel's ink fraction falls outside the band (advisory)
Series color palette gates, MAX_SERIES_HUES = 6 the hues actually drawn fail CVD or normal-vision separation, or one panel carries more than 6
Dual axis none a twinx second scale carries data of its own
Form none pie, 3D, or bars on a truncated baseline
Identity channel none two or more series, no legend and no text in the axes (advisory)
Label attribution LABEL_MARGIN = 2.0 a label's nearest rival series, line or scatter or filled region, is closer than 2x its distance to the one it names
Style sheet 40 keys the rcParams in effect differ from figure.mplstyle (advisory)
Contour dash none a signed contour set dashes its negative levels (advisory)
Colormap kind CMAP_BACKTRAVEL_MAX = 0.02 a colormap classifies misc: its lightness reverses, or its span is flat, or its halves are monotone and its ends match neither cyclic nor diverging. Also when a qualitative map's levels fail all-pairs separation
Fonts Type 42 PDF or PS export would embed Type 3, or no named typeface resolves (advisory)
Alt text ALT_TEXT_MIN_CHARS = 60 no description is attached, or the attached one is under 60 characters (advisory)

Advisory rows

Eight of the 21 rows are advisory. An advisory row can return "warn" but never False, so it never fails a build. ADVISORY_GATES in check_figure.py is the list:

  • Overplotting
  • Banking
  • Ink coverage
  • Identity channel
  • Style sheet
  • Contour dash
  • Fonts
  • Alt text

Type size is the one row that does both. It fails under the floor, and warns on a figure placed under PLACED_FRAC_WARN = 0.35 of the content width.

Detail strings

A row's detail carries up to two marks:

[FIX]
introduces an action. Nothing else uses this mark.
[WHY]
introduces the reason the row fired: the published floor, the perceptual fact, or what a reader loses.

A detail may carry both, in that order:

under 1.0pt on page at scale 0.50: ['a stroke at 0.40pt']
  [FIX] set linewidth to at least 2.00 at this scale
  [WHY] SIAM: lines thinner than one point break up or disappear in print

Every gate except check_collisions names a fix. That gate names the two colliding strings and stops, because which of the pair is free to move is a fact about the layout it cannot see.

Palette gates

check_palette.py takes hex strings on the command line or through check(), and imports nothing outside the standard library. Distances are CAM02-UCS dE. The lightness and chroma rows stay in OKLab.

check(colors) returns these five rows:

Gate Threshold Fails when
Lightness band L_MIN, L_MAX = 0.43, 0.77 OKLab lightness outside the band
Chroma floor CHROMA_MIN = 0.10 OKLab chroma below it, so the color reads as gray
CVD separation CVD_TARGET = 10.5 two hues under 10.5 dE in protan or deutan simulation, at dichromacy or at any severity in ANOMALOUS_SEVERITIES
Normal-vision floor NORMAL_FLOOR = 21.0 two hues under 21 dE in full color
Contrast vs surface CONTRAST_MIN = 3.0 a hue under 3:1 on the page (advisory)

Ink tokens passed through ink= are exempt from the lightness and chroma rows.

The separation rows are named CVD separation (adjacent) or CVD separation (all-pairs) after the comparison they ran. all_pairs=False is the default and compares neighbours, which is what a line chart needs. all_pairs=True compares every pair, which is what a scatter needs.

Protanopia and deuteranopia are gated. Tritan separation is measured and printed in the detail string, but not gated.

Ordinal rows

ordinal=True swaps the five categorical rows for four that apply to an ordered ramp:

Gate Threshold Fails when
Lightness monotone direction lightness does not run one way along the ramp
Adjacent dL ORDINAL_DL_MIN = 0.06 two neighbouring steps are closer in lightness than the floor
Light-end contrast ORDINAL_LIGHT_END_CONTRAST_MIN = 2.0 the lightest step is under 2:1 against the surface
Step uniformity ORDINAL_STEP_RATIO_MAX = 2.0 the largest lightness step is over twice the smallest

What each row tells you to do

Eleven rows carry a remedy in suggest_fixes.py, and eight of those come with a runnable snippet. The other ten name their fix in the detail string and stop, mostly because the answer is to draw something else.

Row First move suggest()
Clipping Turn on constrained_layout, or widen the figure yes
Text collision Move one of the two named strings. Which one is free is yours to know
Text readability Move the label to clear ground, or case it against the ink under it
Contrast stack Take one artist to alpha 1, and keep to three alpha levels yes
Mark ratio Clip the size array so the largest mark is 5x the smallest yes
Overplotting Thin the counts, or switch to hexbin. Alpha does not move this row yes
Axis redundancy sharex/sharey at creation, or ax.label_outer() after yes
Type size Cut words. Do not shrink type
Line weight Raise linewidth to clear 1pt at the placed scale
Banking Set the panel aspect or the figure size to the ratio the row names
Ink coverage Look at the named panel: empty and saturated both read as a defect
Series color Fold the tail into "Other", or facet. Both need a redraw yes
Dual axis Split the two scales into two panels
Form Redraw: bars from zero, no pie, no 3D
Identity channel Direct labels, not a legend
Label attribution Move the label nearer the series it names, or rely on a leader line
Style sheet Apply the sheet in the same rc_context the figure is drawn in yes
Contour dash linestyles="solid" on signed contour data yes
Colormap kind viridis for sequential, RdBu for diverging, twilight for cyclic yes
Fonts Set pdf.fonttype and ps.fonttype to 42 yes
Alt text describe(fig, "..."), then pass alt_metadata(fig, path) to savefig yes

Thresholds

Every threshold is a module-level constant. To change one, see Change a threshold.

Thresholds cite a published floor where one exists: SIAM's one point, WCAG's 4.5:1, and the Nature, Science, and PNAS type minima. The rest were measured. The figure style guide records the measurement and the figure that motivated each one.