Graphics Press CSS   v4.7.2 Documentation & Demo

Graphics Press CSS

A Tufte-inspired typography and data visualization CSS library

Graphics Press CSS encodes the design principles of Edward Tufte's five books into reusable CSS components. Every element can be traced to a specific passage or figure. The library requires no JavaScript, supports automatic dark mode via prefers-color-scheme, and ships as plain CSS, an npm package, or a Tailwind CSS plugin.


Installation

CDN (quickstart)

Drop a single <link> tag into your HTML:

<link rel="stylesheet" href="https://unpkg.com/@andrewxhill/[email protected]/css/graphics-press.css">

npm (raw CSS)

npm install @andrewxhill/graphics-press-css

Then either import in CSS:

@import '@andrewxhill/graphics-press-css';

Or link in HTML:

<link rel="stylesheet" href="node_modules/@andrewxhill/graphics-press-css/css/graphics-press.css">

Tailwind CSS plugin

npm install @andrewxhill/graphics-press-css tailwindcss

In tailwind.config.js:

module.exports = {
  plugins: [
    require('@andrewxhill/graphics-press-css/tailwind'),
  ],
}

Next.js font variation

For a sans-driven application mode, load Inter and JetBrains Mono with next/font/google and opt into the variation with .gp-font-inter or data-gp-font="inter".

import { Inter, JetBrains_Mono } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
  display: "swap",
});

const jetbrainsMono = JetBrains_Mono({
  subsets: ["latin"],
  variable: "--font-jetbrains-mono",
  display: "swap",
});

export default function RootLayout({ children }) {
  return (
    <html
      lang="en"
      className={`${inter.variable} ${jetbrainsMono.variable} gp-font-inter`}
    >
      <body>{children}</body>
    </html>
  );
}

No external font <link> tags are required.


Quick Start

The minimal HTML structure to use the library:

<!DOCTYPE html>
<html lang="en" class="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://unpkg.com/@andrewxhill/[email protected]/css/graphics-press.css">
</head>
<body>
  <article>
    <h1>Your Title</h1>
    <p class="subtitle">Your subtitle here</p>
    <section>
      <h2>Section heading</h2>
      <p>Your content...</p>
    </section>
  </article>
</body>
</html>

Dark Mode

Dark mode is supported three ways:

Automatic

The library responds to prefers-color-scheme: dark automatically. No classes needed — just ship the CSS.

Manual toggle via classes

Add .dark or .light to <html>:

<html class="dark">  <!-- force dark -->
<html class="light"> <!-- force light -->

JavaScript toggle

const html = document.documentElement;
let dark = false;

function setTheme(d) {
  dark = d;
  html.classList.toggle('dark', dark);
  html.classList.toggle('light', !dark);
}

document.getElementById('themeToggle')
  .addEventListener('click', () => setTheme(!dark));

Component Showcase

Every component below is a live example styled by the library itself. Click "Show code" to see the HTML markup pattern.


Dot Chart

"A table is nearly always better than a dumb pie chart; the dot chart is nearly always better than a dumb bar chart." — The Visual Display of Quantitative Information, p. 178

Life expectancy at birth by country, 2022. Scale: 50–90 years.

Japan Switzerland Australia Germany United States China Brazil Nigeria 84.3 83.2 82.8 80.9 76.1 78.2 72.8 53.4 50 yrs 90 yrs

World Health Organization, World Health Statistics 2023.

<svg viewBox="0 0 560 196">
  <!-- Track baseline lines -->
  <g style="stroke:var(--svg-light-rule)" stroke-width="0.75">
    <line x1="170" y1="14" x2="480" y2="14"/>
    <!-- ...one line per row... -->
  </g>
  <!-- Dots: x = 170 + ((val-50)/40)*310 -->
  <circle cx="436" cy="14" r="4" style="fill:var(--svg-ink)"/>
  <circle cx="359" cy="110" r="4" style="fill:var(--svg-red)"/>
  <!-- Labels (right-aligned) -->
  <g text-anchor="end" font-family="Gill Sans,Calibri,sans-serif" font-size="10">
    <text x="162" y="18">Japan</text>
  </g>
  <!-- Values -->
  <g font-family="Gill Sans,Calibri,sans-serif" font-size="10">
    <text x="492" y="18">84.3</text>
  </g>
</svg>

Dumbbell Chart

Renewable electricity share, 2010 vs 2023 (%). Each item shows start, end, and magnitude of change simultaneously.

Denmark Germany United Kingdom United States China 23 → 83% 19 → 61% 11 → 48% 10 → 22% 19 → 31% 0% 100% 2010 2023

IEA World Energy Statistics 2024.

<svg viewBox="0 0 560 170">
  <!-- Connector lines -->
  <line x1="241" y1="14" x2="427" y2="14" style="stroke:var(--svg-ghost)" stroke-width="1"/>
  <!-- Blue dots (before) -->
  <g style="fill:var(--svg-blue)">
    <circle cx="241" cy="14" r="5"/>
  </g>
  <!-- Red dots (after) -->
  <g style="fill:var(--svg-red)">
    <circle cx="427" cy="14" r="5"/>
  </g>
  <!-- Labels and values -->
</svg>

Strip Plot

Daily high temperatures in four cities, July 2023. Each dot is one day. The strip plot shows all observations — no invented distribution shape.

Phoenix Miami Chicago Seattle 60°F 120°F

NOAA Daily Climate Report. n=12 per city shown.

<svg viewBox="0 0 560 118">
  <!-- Track lines -->
  <g style="stroke:var(--svg-light-rule)" stroke-width="0.75">
    <line x1="170" y1="16" x2="510" y2="16"/>
  </g>
  <!-- Dots per city -->
  <g style="fill:var(--svg-red)" opacity="0.7">
    <circle cx="181" cy="16" r="3.5"/>
    <!-- ...more dots... -->
  </g>
  <!-- Row labels, axis -->
</svg>

Stem-and-Leaf

"The stem-and-leaf plot constructs the distribution of a variable with the numbers themselves." — The Visual Display of Quantitative Information, p. 140

Marathon finish times. Left = Women, Right = Men. Each digit is one runner's tens digit of minutes past the listed hour.

Women h Men
3 2  4  7  8
8  5  3  1 4 0  1  2  3  5  6  8  9
9  8  7  6  4  4  3  2  1  0 5 0  1  2  3  4  6  7  8
9  8  8  7  5  4  3  2  1  1 6 0  2  3  5  6  8
8  7  6  5  5  3  2 7 1  4  5  8
9  7  4  2 8 2  6
5  3 9 4

h = hour · leaf = tens digit of minutes · stem 5, leaf 3 = 53 min

<table class="stem-leaf">
  <thead>
    <tr>
      <th class="sl-col-left">Women</th>
      <th class="sl-col-stem">h</th>
      <th class="sl-col-right">Men</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="sl-col-left">8  5  3  1</td>
      <td class="sl-col-stem">4</td>
      <td class="sl-col-right">0  1  2  3  5  6  8  9</td>
    </tr>
  </tbody>
</table>
<p class="sl-unit">h = hour ...</p>

Bullet Graph

Tufte's replacement for gauge and speedometer charts. Shows current value, target, and qualitative ranges in a compact horizontal form. — Beautiful Evidence, p. 176

Revenue $71M Gross margin 52% Customer NPS 42 New customers 1,840 Poor Satisfactory Good

Bar = actual. Thin tick = target. Gray bands = performance ranges.

<svg viewBox="0 0 560 136">
  <!-- Qualitative range bands -->
  <rect x="148" y="6" width="113" height="18"
        style="fill:var(--svg-ghost)" opacity="0.30"/>
  <!-- Featured bar -->
  <rect x="148" y="10" width="241" height="10"
        style="fill:var(--svg-green)"/>
  <!-- Target tick -->
  <line x1="420" y1="6" x2="420" y2="24"
        style="stroke:var(--svg-ink)" stroke-width="2"/>
</svg>

Sparkline Table

"The point of sparklines is to be small enough to embed in text or tables, with enough resolution to show trends and variation at a glance." — Beautiful Evidence, pp. 47-63

Monthly active users by product line, Jan-Dec 2023 (thousands)
ProductTrendLastMinMaxChange
Core API 1,8407201,840+155%
Dashboard 842604842+39%
Mobile SDK 223223512-56%
Analytics 1,1048901,104+24%

Min   Last

<table class="sparkline-table">
  <thead>
    <tr><th>Product</th><th>Trend</th><th class="num">Last</th>...</tr>
  </thead>
  <tbody>
    <tr>
      <td>Core API</td>
      <td class="sp-cell">
        <svg viewBox="0 0 100 18" width="100" height="18">
          <polyline points="0,16 9,15 ..." stroke-width="1.2" fill="none"/>
          <circle cx="0" cy="16" r="2" style="fill:var(--svg-blue)"/>
          <circle cx="100" cy="2" r="2.5" style="fill:var(--svg-red)"/>
        </svg>
      </td>
      <td class="num">1,840</td>
    </tr>
  </tbody>
</table>

Heat Table

Mean monthly temperature by city. Cell color encodes magnitude. Toggle dark mode to see the palette adapt.

Cool
Warm
Mean monthly temperature (°C)
CityJanFebMarAprMayJunJulAugSepOctNovDec
Bangkok272931333230292929282827
London5681114171919161286
New York1271318232625211593
Sydney282825211714131417202326
Reykjavik-1-11481112129620
<table class="heat-table">
  <thead>...</thead>
  <tbody>
    <tr>
      <td>Bangkok</td>
      <td class="num heat"
          style="background:oklch(57% 0.09 72)">27</td>
      <td class="num heat"
          style="background:oklch(47% 0.10 67)"
          data-dark>33</td>
      <!-- data-dark flips text to light on dark cells -->
    </tr>
  </tbody>
</table>

Ranked Table

Dotted leaders guide the eye from label to value. Rank numbers are recessive.

Programming languages by developer adoption, 2024
#LanguageShareYoY
1JavaScript65.8%-0.4pp
2Python51.2%+1.8pp
3TypeScript43.9%+3.1pp
4Rust13.1%+2.2pp
5Go12.7%+0.9pp
6Java30.6%-2.4pp

Rust highlighted: fastest-growing in absolute percentage points.

Stack Overflow Developer Survey 2024.

<table class="ranked-table">
  <thead>
    <tr><th class="rank">#</th><th>Language</th>
        <th class="num rank-value">Share</th></tr>
  </thead>
  <tbody>
    <tr>
      <td class="rank">1</td>
      <td class="rank-label"><span>JavaScript</span></td>
      <td class="num rank-value">65.8%</td>
    </tr>
    <tr class="rank-highlight">...</tr>
  </tbody>
</table>

Interactive Chart — Hover to Isolate Series

+1.5° 0 -0.5° 1950 1985 2020 Northern Southern Ocean Historical +/-1s Temperature anomaly (°C), 1950-2020 — hover to isolate
Range frame axes. Three series directly labeled at endpoints. Reference band: historical range. No legend box.
<div class="chart-wrap">
  <div class="chart-tooltip" id="tt"></div>
  <svg id="chart" viewBox="0 0 560 190">
    <!-- Reference band -->
    <rect x="52" y="70" width="408" height="50"
          style="fill:var(--svg-ghost)" fill-opacity="0.12"/>
    <!-- Each series is a <g class="sg" data-n="Name"> -->
    <g class="sg" data-n="Northern">
      <polyline points="..." style="stroke:var(--svg-red)"
              stroke-width="1.8" fill="none"/>
      <text>Northern</text>
    </g>
  </svg>
</div>
<!-- JS hover isolation code at bottom of page -->

Evidence Layout

Image and analysis belong together. — Beautiful Evidence, pp. 82-97

+1.5° -0.5° 1950 1985 2020 +1.2°C pre-industrial range
Global mean temperature anomaly, 1950-2020. Baseline: 1850-1900.

The trajectory is unambiguous. From 1950 to 1980, anomalies remained within the pre-industrial reference band. After 1980 the trend accelerates sharply.

The 1.2°C anomaly reached in 2020 is extreme not by geological standards, but by the scale of human civilization. Every major agricultural system was designed for a climate that no longer exists.

The rate matters as much as the level. Roughly 0.2°C per decade since 1980 — faster than any rate in the 800,000-year ice core record.

Evidence layout from Beautiful Evidence: image and analysis as a unified analytical object. No page-turning between chart and interpretation.

<div class="evidence">
  <figure class="evidence__image">
    <svg>...</svg>
    <figcaption>...</figcaption>
  </figure>
  <div class="evidence__analysis">
    <p>Analysis text...</p>
  </div>
  <p class="evidence__caption">...</p>
</div>

Timeline

AI milestones, 2017-2024. Periods as colored bars, events as dots on the baseline.

Transformer era GPT-3 wave Foundation models Attention GPT-2 GPT-3 ChatGPT GPT-4 Claude 3 2015 2017 2019 2021 2023 2025
<svg viewBox="0 0 560 90">
  <!-- Period bands -->
  <rect x="40" y="52" width="103" height="10"
        style="fill:var(--svg-blue)" opacity="0.45" rx="1"/>
  <!-- Baseline -->
  <line x1="40" y1="62" x2="520" y2="62"
        style="stroke:var(--svg-ink)" stroke-width="1"/>
  <!-- Event dots -->
  <circle cx="40" cy="62" r="3.5" style="fill:var(--svg-ink)"/>
  <!-- Event labels below -->
  <text x="40" y="75" text-anchor="middle">Attention</text>
</svg>

Small Multiples

"At the heart of quantitative reasoning is a single question: Compared to what?" — Envisioning Information, p. 67

Germany
Denmark
United States
China

■ Solar■ Wind   capacity growth 2000-2023 · same scale all panels

<div class="small-multiples small-multiples-4">
  <figure>
    <svg viewBox="0 0 80 52" width="100%">
      <line x1="5" y1="46" x2="75" y2="46"
            style="stroke:var(--svg-ink)" stroke-width="0.75"/>
      <polyline points="5,44 20,40 35,33 50,22 65,12 75,6"
              style="stroke:var(--svg-red)" stroke-width="1.2"
              fill="none"/>
    </svg>
    <figcaption>Germany</figcaption>
  </figure>
  <!-- repeat for each panel -->
  <p class="small-multiples-xlabel">...</p>
</div>

Slopegraph

Invented by Tufte in The Visual Display of Quantitative Information, p. 158. Two time points, one line per entity. The slope is the data.

Government expenditure as % of GDP, 1995 and 2023. Hover any slope to inspect.

1995 2023 Sweden 47 Sweden 48 France 38 France 45 UK 28 UK 26 Japan 14 Japan 18 United States 10 United States 19

Government expenditure as % of GDP, selected countries.

<div class="slopegraph-wrap">
  <svg id="slopeChart" viewBox="0 0 500 310">
    <g class="slope-g" data-l="47" data-r="48"
       data-label="Sweden">
      <line x1="150" y1="50" x2="330" y2="46"/>
      <circle cx="150" cy="50" r="2.5"/>
      <text x="142" y="54">Sweden</text>
      <text x="114" y="54">47</text>
    </g>
  </svg>
</div>

Parallel Coordinates

Shows many variables simultaneously for many observations. Each vertical axis is one variable; each line crosses all axes at its values. Denmark (red) leads across all five welfare dimensions.

HDI Life exp. Education Income Press freedom High Low Denmark others ghosted

UN Human Development Report 2023; Reporters Without Borders 2023. Axes normalized 0-1.

<div class="parallel-coords">
  <svg viewBox="0 0 560 180">
    <!-- Ghost lines (all countries) -->
    <g opacity="0.18" fill="none"
       style="stroke:var(--svg-ink)" stroke-width="0.75">
      <polyline points="60,40  160,55  260,70  360,45  460,80"/>
    </g>
    <!-- Highlighted country -->
    <polyline fill="none" style="stroke:var(--svg-red)"
              stroke-width="2"
              points="60,35 160,30 260,40 360,38 460,32"/>
    <!-- Axes -->
    <g style="stroke:var(--svg-rule-mid)" stroke-width="0.75">
      <line x1="60" y1="20" x2="60" y2="145"/>
    </g>
  </svg>
</div>

Minimal Bar Chart

The default variant shows only a leader line and endpoint tick. The filled variant is available when visual weight aids legibility.

Endpoint tick only (default)

Solar Wind Hydro Nuclear Natural gas 78% 65% 54% 38% 22%

Filled variant

Solar Wind Hydro Nuclear Natural gas 78% 65% 54% 38% 22%
<!-- Tick variant -->
<ul class="bar-chart">
  <li>
    <span class="bar-label">Solar</span>
    <span class="bar red" style="--bar-value:78%"></span>
    <span class="bar-value">78%</span>
  </li>
</ul>

<!-- Filled variant -->
<ul class="bar-chart filled">
  <li>
    <span class="bar-label">Solar</span>
    <span class="bar red" style="--bar-value:78%"></span>
    <span class="bar-value">78%</span>
  </li>
</ul>

Pull Statistics & Stat Grid

When a single number is the finding, treat it as a finding. Large italic type, Gill Sans label beneath.

412 ppm Atmospheric CO2 — Mauna Loa Observatory, 2020 Up from 316 ppm in 1960, a 30% increase over 60 years
+2.5°C Projected warming by 2100, median
-40% Arctic sea ice summer minimum, 1979-2020
+18% Renewable growth global annual rate
<div class="pull-stat">
  <span class="pull-stat__number">412<span class="unit"> ppm</span></span>
  <span class="pull-stat__label">Atmospheric CO2</span>
  <span class="pull-stat__context">Up from 316 ppm...</span>
</div>

<div class="stat-grid">
  <div class="pull-stat" style="margin:0">
    <span class="pull-stat__number gp-red">+2.5<span class="unit">°C</span></span>
    <span class="pull-stat__label">Projected warming</span>
  </div>
  <!-- more items -->
</div>

Ghosting & Emphasis

Tufte's "smallest effective difference": reduce context data to near-invisible gray so the focal data pops. .ghost · .near-ghost · .focal

Korea Others 1990 2020 R&D spending, % of GDP
Five comparison countries at opacity: 0.22 via .ghost. Focal country at full ink. The eye goes immediately to the argument.
<!-- Ghost lines -->
<g opacity="0.22" fill="none"
   style="stroke:var(--svg-dim)" stroke-width="1">
  <polyline points="..."/>
</g>
<!-- Focal line -->
<polyline fill="none" style="stroke:var(--svg-red)"
          stroke-width="2.5" points="..."/>

<!-- Or use CSS classes: -->
<div class="ghost">...context...</div>
<div class="focal">...highlight...</div>

Figure Compare & Before/After Columns

Two layout components for side-by-side comparisons.

A B C
Conventional bar chart — box border, grid lines, filled bars, high non-data-ink.
C B A 55 33 45 0 55
Tufte minimal — range frame, endpoint ticks, direct labels. Data-ink ratio near 1.0.
Before

Bullet points fragment continuous reasoning into disconnected fragments. Each item competes equally for attention regardless of importance.

After

Prose allows the argument to unfold in sequence. Relationships between ideas are expressed by the structure of the sentences, not by the structure of the slide.

<div class="figure-compare">
  <figure>
    <svg>...</svg>
    <figcaption>Conventional</figcaption>
  </figure>
  <figure>
    <svg>...</svg>
    <figcaption>Tufte minimal</figcaption>
  </figure>
</div>

<div class="compare-wrap">
  <div class="compare-col before">
    <div class="compare-col__header">Before</div>
    <p>...</p>
  </div>
  <div class="compare-col after">
    <div class="compare-col__header">After</div>
    <p>...</p>
  </div>
</div>

Margin Notes & Margin Figures

The right margin carries three kinds of content. Numbered sidenotes Sidenotes appear beside the text they annotate. This note uses position: sticky; top: 2rem — it stays visible while you scroll through the section. appear with a superscript. Unnumbered margin notes Margin note A free commentary in the margin. Cross-references, glosses, small figures. No number, no anchor — it floats beside the relevant text. are free glosses. Margin figures Solar Wind A small chart in the margin. Spatial adjacency does the work. place small charts directly beside the text that references them. On narrow screens, the ⊕ button reveals them inline.

<!-- Numbered sidenote -->
<label for="sn-1" class="sidenote-toggle sidenote-number"></label>
<input type="checkbox" id="sn-1" class="sidenote-toggle">
<span class="sidenote">Content of the sidenote.</span>

<!-- Unnumbered margin note -->
<label for="mn-1" class="sidenote-toggle">&#8853;</label>
<input type="checkbox" id="mn-1" class="sidenote-toggle">
<span class="marginnote">
  <span class="marginnote-label">Margin note</span>
  Content of the margin note.
</span>

<!-- Margin figure -->
<label for="mf-1" class="sidenote-toggle">&#8853;</label>
<input type="checkbox" id="mf-1" class="sidenote-toggle">
<span class="marginnote">
  <svg viewBox="0 0 100 66" width="100%">...</svg>
  Caption text.
</span>

Datum & Data-Note

The .datum class treats inline numbers with slightly more typographic attention — lining figures, Gill Sans, tight tracking — making them visually distinct without disrupting the sentence.

In 2023, global renewable electricity capacity reached 3,382 GW, up from 2,802 GW in 2021 — a +20.7% increase in two years. The cost of utility-scale solar fell from $0.38/kWh in 2010 to $0.049/kWh in 2023, a -87% reduction.

Source & methodology

Capacity figures: IRENA Renewable Capacity Statistics 2024. Cost figures: IRENA Renewable Power Generation Costs 2023. All figures are global aggregates; regional variation is substantial. The cost series uses weighted average levelized cost of energy (LCOE) for utility-scale solar PV, 2010 constant USD.

<p>Capacity reached <span class="datum">3,382 GW</span>,
  a <span class="datum gp-green">+20.7%</span> increase.</p>

<details class="data-note">
  <summary>Source & methodology</summary>
  <p>IRENA Renewable Capacity Statistics 2024...</p>
</details>

Slippy Map

Good basemaps should recede; the argument should sit on top. A hosted vector style plus a few direct analytical marks is the cleanest public example for this library.

This example uses MapLibre GL JS on GitHub Pages with OpenFreeMap’s Liberty style. The base map is familiar and free, attribution is handled automatically by MapLibre, and the overlay remains plain GeoJSON you can inspect and replace.

North America air corridor sample

Six metro areas, one corridor, and a restrained analytical overlay

Circle area encodes illustrative annual passengers. The ochre line isolates the Boston-Washington corridor so the basemap stays contextual instead of argumentative.

  • Northeast megaregion
  • Interior hubs
  • Highlighted corridor
Basemap Liberty
Overlay GeoJSON
Host model No key
Recommended public example stack: MapLibre GL JS plus OpenFreeMap’s hosted Liberty style. This avoids leaning on the community-operated raw OSM raster tile servers while still keeping the demo free and inspectable.

Basemap: OpenFreeMap Liberty, derived from OpenStreetMap/OpenMapTiles. Overlay data is illustrative only and intended to demonstrate symbol hierarchy, labeling, and annotation.

Why this stack

OpenFreeMap publishes a no-key hosted style URL for MapLibre, including labels and attribution. That makes it a better docs default than pointing a public demo at tile.openstreetmap.org, whose usage policy explicitly says the standard tile service is limited and should be avoided if you cannot meet its requirements or need another OSM-derived service.

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/maplibre-gl.css">
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>

<div class="gp-map-frame">
  <div id="slippyMap" class="gp-map"></div>
  <div class="gp-map-panel">...legend and narrative...</div>
</div>

<script>
const map = new maplibregl.Map({
  container: 'slippyMap',
  style: 'https://tiles.openfreemap.org/styles/liberty',
  center: [-77.2, 39.3],
  zoom: 3.55
});

map.on('load', () => {
  map.addSource('metros', { type: 'geojson', data: myGeoJson });
  map.addLayer({ id: 'metro-circles', type: 'circle', source: 'metros' });
});
</script>

Strategy Brief

Review note from a live client: the typography is already doing good work, but the KPI row, tab strip, analytic cards, and timeline bars want their own system instead of bespoke app CSS.

Multi-wallet merge arbitrage · 4 trading wallets · 2025-07-31 to 2026-03-24

Merge Machine Alpha

A compact briefing shell for strategy dashboards: terse provenance up top, one dominant number, then a disciplined sequence of metrics, cards, and timelines beneath.

Combined volume $93.52M 17,358,089 fills
Wallets4
Live days235
Markets / day480
Mean PnL / hr$18.07
Spread / trade0.88¢
← Back to Cash Cows
Primary wallet

Main (0x6031...f96d)

$71.58M 107 days live · 853k maker fills
Sibling A

sb_a (0xa45f...)

$11.79M 81 days live · 846k maker fills
Sibling B

sb_b (0xddda...)

$3.33M 55 days live · 435k maker fills
Sibling C

sb_c (0x5b49...)

$6.81M 206 days live · 922k maker fills
Wallet rotation timeline Sequence matters more than exact timestamps
main
107d
sb_a
81d
sb_b
55d
sb_c
206d

Algorithm sketch

every hour:
  scan active prediction markets
  filter to paired YES/NO liquidity
  select the 20 newest markets
  post makers on both sides
  fill for 12 minutes
  roll to next market pair

Illustrative strategy-brief numbers modeled after a live Recall Network client layout. Values here are synthetic and intended to demonstrate typographic structure rather than document a real trading strategy.

<div class="brief-header">...</div>
<div class="metric-strip">...</div>
<ul class="tab-nav">...</ul>
<div class="analytic-grid">
  <article class="analytic-card green">...</article>
</div>
<div class="activity-timeline">...</div>
<div class="algorithm-panel"><pre><code>...</code></pre></div>

Analytics App UI

Some client work needs more than prose blocks: filters, search, state messaging, KPI tiles, and dense tables. The app layer keeps those pieces readable without dropping into generic SaaS styling.

Universe 500 Updated 5m ago
Total PnL $14.2M USDC-verified aggregate
Maker Share 68.4% Fill-count basis
Open Markets 93 With active inventory
State block

Readable by default

Metadata, controls, and headers sit closer to 12.5 to 14px equivalents. That is the floor, not the exception.

Progress

Scanner pass: 61%

Use for scans, ingestion status, or background jobs.

Representative Positions

Dense tables should still read without zooming
Wallet Status PnL Maker % Volume
0x6031...f96d Live $71.6M 91% $44.8M
0xa45f...39b1 Kalshi $11.8M 87% $21.3M
0x6916...0e11 Derived $7.3M 74% $9.5M
<div class="gp-toolbar">...</div>
<div class="gp-kpi-grid">...</div>
<div class="gp-card-grid">...</div>
<section class="gp-panel">
  <div class="gp-panel__header">...</div>
  <div class="gp-data-table__wrap">
    <table class="gp-data-table">...</table>
  </div>
</section>

Use .gp-meter for quantitative table cells like maker share, completion rate, balance ratio, or other compact progress-style values.

Workspace Shell

Top-level analytics routes usually want a wider frame and a different header balance than dossier/detail pages. Keep that behavior in the library instead of app-local layout CSS.

Analytics workspace

Prediction Markets Explorer [Beta]

Market universe 8,214 27 categories
Markets 8,214 Categories 27
<div class="gp-app-frame gp-workspace">
  <ul class="tab-nav tab-nav--filled gp-workspace__tabs">...</ul>
  <div class="brief-header gp-workspace__header">...</div>
  <div class="gp-toolbar gp-workspace__toolbar">...</div>
</div>

Chart Interop

Use semantic chart variables instead of app-local hex palettes. The same tokens can drive D3, Recharts, ECharts, or plain SVG without diverging from the typographic system.

Positive / Negative

Use semantic outcomes first

Positive Negative Neutral

Profit, loss, unresolved, baseline, and other semantic roles should not be re-decided per chart.

Accent Set

Shared hues for library and app charts

Accent 1 Accent 2 Accent 3 Accent 4

Use these when a chart needs multiple peers instead of winner/loser semantics.

const styles = getComputedStyle(document.documentElement);
const positive = styles.getPropertyValue('--gp-chart-positive').trim();
const negative = styles.getPropertyValue('--gp-chart-negative').trim();
const axis = styles.getPropertyValue('--gp-chart-axis').trim();
const grid = styles.getPropertyValue('--gp-chart-grid').trim();

Editorial Spread

Investigative pages often need narrow paired columns for prose and metadata, followed by a chart or timeline that spans both. The spread layout makes that explicit instead of forcing every app to hand-roll breakpoint math.

Left Column

Use for the main interpretation, a sequence explanation, or notes about what the chart is proving.

Keep prose narrow and aligned. This is the reading column for operational narrative.

Right Column

Use for counter-evidence, derived metrics, or compact tables that should align against the main reading.

This column can carry the side-analysis without collapsing into card spam.

Wide Breakout

Timelines, maps, and dense charts can intentionally span both columns when they need the width.

Week 1 Week 2 Week 3 Week 4 Week 5
<div class="gp-spread">
  <div class="gp-spread__col">...</div>
  <div class="gp-spread__col">...</div>
  <div class="gp-spread__wide">
    <div class="gp-chart-frame">...</div>
  </div>
</div>

Treemap Framing

Treemaps in research apps usually need more than bare rectangles. Make the hierarchy loud and the decoration quiet: strong category frames, explicit legend and crumbs, muted micro-cells, and only enough venue color to separate meaning without shimmer.

Hierarchy First

Use category blocks and header bands to orient the reader before asking them to parse any individual tile.

Quiet Dense Leaves

Tiny rectangles should recede. Heavy borders and loud color on every cell create shimmer and visual fatigue.

Meaningful Color

Reserve stronger color for actual semantic distinctions such as venue or status. Do not let every stroke compete equally.

Polymarket Kalshi Limitless
Let category blocks carry the hierarchy. Dense leaf fields should stay quiet; venue tint only needs to separate meaning, not light up every border.

Politics — 482 markets

Category frames first, venue tint second, dense leaves kept quiet
US Elections 188 Electoral College Nominee odds Senate control Geopolitics 174 Ukraine ceasefire Taiwan Iran Trade Policy 120 Tariffs Sanctions Energy exports China policy

US Elections

Will the Democratic nominee win Arizona?

Polymarket

Do

Let category structure do most of the work. Use restrained box borders, a clearer top band, and a short legend.

Don’t

Don’t round or saturate everything. Rounded section blocks and bright leaf borders make a treemap feel like generic app chrome instead of analytical evidence.

Do

Mute micro-cells. Dense fields should read as texture until the user hovers or zooms.

Don’t

Don’t give tiny tiles equal visual weight. If every small mark has a strong outline, the whole figure starts vibrating.

<div class="gp-treemap__legend">
  <div class="gp-treemap__legend-group">...</div>
  <span class="gp-treemap__legend-note">...</span>
</div>
<div class="gp-treemap__crumbs">...</div>
<div class="gp-treemap">
  <div class="gp-treemap__header">...</div>
  <div class="gp-treemap__body">
    <svg>...</svg>
  </div>
</div>
<div class="gp-treemap__tooltip">...</div>

Analytics Application Shell

When using tufte2 for a real dashboard or research tool, keep the workspace wide and the interpretation narrow. The top frame should handle tabs, KPIs, and filters; individual figures should still read like evidence, not like toy widgets.

Workspace Header

Wide app frame for tabs, filters, and overall counts. Do not force the top-level shell into the narrow reading column.

Market Universe 2,798 8 categories
Active Venues 3 Polymarket, Kalshi, Limitless
Updated Live Sync every 5 minutes

The app shell can be wide. The actual argument still lives inside specific figures, tables, and briefings. Use the shell to orient, not to decorate.

PnL Over Time Wide figure, calm chrome
Jan Mar May Jul
Table Guidance In-cell bars and aligned numerics
WalletSharePnL
High Conviction 1 78% +$184k
Macro Rotation 44% +$71k

Use .gp-meter for compact quantitative cells and keep the number visible. The bar is support, not the whole message.

Do

Separate shell from evidence. Tabs, KPIs, and filters belong in the workspace header; charts and tables should still read like editorial figures.

Don’t

Don’t card-ify everything. If every view becomes a rounded widget, the app loses the calm analytical rhythm that makes dense information readable.

Do

Use semantic chart tokens everywhere. Positive, negative, accent, axis, grid, and surface should all come from shared library variables.

Don’t

Don’t invent route-by-route palettes. Treemaps, bars, tables, and timelines should all speak the same color language.

<div class="gp-analytics-shell">
  <div class="gp-analytics-shell__top">...</div>
  <p class="gp-analytics-shell__note">...</p>
  <div class="gp-analytics-shell__layout">
    <div><div class="gp-chart-frame">...</div></div>
    <div><table class="gp-data-table">...</table></div>
    <div class="gp-analytics-shell__wide">...</div>
  </div>
</div>

Ranked Bars

Category tallies, market-share ladders, and top-N bar views should read like ranked evidence, not like a stack of random cards. Use a shared ranked-bar shell so bar tabs in analytics apps stay aligned with the rest of the system.

Markets by Category Top-level distribution with nested subcategories
Politics
34.7%
US Elections18.8%
Congress9.4%
Geopolitics
26.4%
<div class="gp-rank-list">
  <article class="gp-rank-card">
    <div class="gp-rank-card__header">...</div>
    <div class="gp-stack-bar">...</div>
    <div class="gp-rank-list gp-rank-list--nested">
      <div class="gp-rank-row">...</div>
    </div>
  </article>
</div>

CSS Custom Properties Reference

All design tokens are CSS custom properties. Override any at :root to retheme without forking the library.

Layout

PropertyDefaultDescription
--gp-text-width640pxReading column width
--gp-margin-width260pxSidenote/margin column
--gp-gutter48pxGap between text and margin
--gp-outer-marginclamp(3rem, 8vw, 6rem)Fluid outer margin

Typefaces

PropertyDefault
--gp-serifET Book, Palatino Linotype, Palatino, Georgia, serif
--gp-sansGill Sans, Gill Sans MT, Calibri, Optima, sans-serif
--gp-monoLucida Console, Andale Mono, Monaco, Consolas, monospace

Type Scale

PropertyDefault
--gp-font-sizeclamp(14px, 1vw + 10px, 16px)
--gp-line-heightclamp(1.58, 1.5 + 0.4vw, 1.68)
--gp-small0.8em
--gp-caption0.77em
--gp-micro0.69em

Spacing

PropertyDefault
--gp-space-xs0.25rem
--gp-space-sm0.5rem
--gp-space-md1rem
--gp-space-lg2rem
--gp-space-xl4rem

Surface & Ink

PropertyLightRole
--gp-paperoklch(99.4% 0.008 98)Background
--gp-inkoklch(17% 0.004 60)Body text
--gp-margin-inkoklch(43% 0.003 60)Secondary text
--gp-ghost-inkoklch(69% 0.003 60)Tertiary text
--gp-ruleoklch(62% 0.002 60)Rules, borders
--gp-light-ruleoklch(84% 0.005 95)Light separators
--gp-code-bgoklch(95% 0.006 95)Code background

Categorical Palette (equal perceptual lightness)

PropertyValueSwatch
--gp-redoklch(44% 0.14 27)
--gp-blueoklch(44% 0.09 251)
--gp-greenoklch(50% 0.08 149)
--gp-ochreoklch(49% 0.10 72)
--gp-brownoklch(42% 0.09 52)
--gp-gray-dataoklch(51% 0.002 60)

Sequential & Diverging Ramps

PropertyValue
--gp-seq-1 through --gp-seq-5Ochre hue, L=95% to L=42%
--gp-div-lo-2 through --gp-div-hi-2Blue to red through neutral midpoint

Animation

PropertyValue
--gp-easecubic-bezier(0.4, 0, 0.2, 1)
--gp-ease-outcubic-bezier(0, 0, 0.2, 1)

Graphics Press CSS v4.5.0  ·  MIT License  ·  ET Book typeface: Edward Tufte, Dmitry Krasny, Bonnie Scranton  ·  Color: Eduard Imhof, Cartographic Relief Presentation