Skip to main content

Charts for people who have real data, not clean data — zero-dependency SVG plotting with smart ingestion.

Project description

limn

limn (v., archaic): to depict in painting or words; to illuminate a manuscript.

Charts for people who have real data, not clean data. limn is a zero-dependency Python plotting library that meets your data where it is: hand it the CSV the way finance actually exported it — "$1,204,500", "18.6%", "N/A", dates in three formats — and get back a publication-grade SVG. No numpy, no pandas required, no C extensions, nothing to install but this.

import limn

limn.line("sales.csv", by="region", title="Revenue by region").save("out.svg")
A line chart made directly from a messy finance CSV

The input for that chart, verbatim — quotes, dollar signs, thousands separators, a percent column, and a hole:

month,revenue,costs,margin
Jan 2026,"$1,204,500","$980,100",18.6%
Feb 2026,"$1,310,900","$1,010,400",22.9%
Apr 2026,"$1,488,000","$1,120,500",N/A
...

limn inferred the types, kept the $ for the axis, and told you on stderr what it skipped. That's the whole idea.

Why another plotting library

The heavyweights are excellent at what they do — Plotly for interactive dashboards, Altair for grammar-of-graphics, seaborn for statistics. limn exists for the case none of them serve: you have a messy file and you want a beautiful static chart with zero ceremony and zero dependencies.

Three specific grievances with the status quo, answered:

  1. Parsing is your problem. Every major library expects numeric, typed, cleaned columns. limn's front door does what you would have done by hand: currency symbols and codes, 1,234.56 and 1.234,56 and 1 234,56, percent signs (kept as an axis unit), (1,240) accounting negatives, 3.2M/150k suffixes, a dozen spellings of "missing", day-first vs month-first dates — decided per column by evidence, and by chronology when the evidence is ambiguous. Values that still don't parse become gaps and a one-line note, never an exception.
  2. Default rendering is stuck in 2005. limn's ticks come from the Extended Wilkinson algorithm (Talbot, Lin & Hanrahan 2010 — literally the paper on labeling axes better than matplotlib). Time axes step in real calendar units — Mondays, month starts, quarter boundaries — and say the year exactly once. The palette is colorblind-validated, in a fixed order that maximizes worst-case distinguishability. Dark mode is a selected palette, not an inversion.
  3. Labels clip and collide. limn embeds font metrics and measures every string before placing it. Margins are computed from the actual tick labels, crowded x labels thin out instead of rotating, long names truncate with an ellipsis, and series labels sit at line ends with collision resolution. There is a test that walks every text element of every chart kind and asserts nothing leaves the canvas.

Install

pip install limn-charts              # imports as plain `limn`; zero deps
pip install limn-charts[png]         # + optional PNG export via cairosvg

(The PyPI name limn was already taken; the import is unaffected.)

The seven forms (and the eighth)

limn.line(data, x=, y=, by=)       # gaps for missing, direct end labels
limn.area(data, )                    # stacks by default, surface gaps
limn.bar(data, , stack=, horizontal=, sort="-y", labels=True)
limn.scatter(data, x=, y=, by=, size=)
limn.hist(values, bins="auto")        # Freedman–Diaconis
limn.box(data, x=, y=)              # quartiles, whiskers, outliers
limn.heatmap(matrix_or_table)         # annotated cells, ramp legend
limn.plot(data)                       # looks at the columns, picks a form

Charts argue as well as show — reference lines, callouts, and small multiples are one call each:

(limn.line(data, x="week", y="uptime")
     .hline(99, "SLA target")             # dashed reference on the value axis
     .vline("2026-03-15", "failover")     # event marker; takes a date string
     .flag("2026-03-22", 96.7, "the bad Tuesday"))   # point callout

limn.line(rows, x="month", y="signups", facet="city", cols=2)
#   ^ small multiples: panels share scales, colors, and bins — always

Log axes (xlog=, ylog=), fixed ranges (.ylim()), per-series colors and dashes (color=, dash=), and automatic decimation for 100k-point series round out v1.1 — see the docs.

Every constructor accepts the same shapes: a CSV path, CSV/TSV text, a list of dicts, a dict of lists, a plain sequence, a generator, or any DataFrame-like object (duck-typed — pandas works, none required). Everything returns a Figure:

(limn.bar(rows, x="team", y="points", sort="-y", labels=True)
     .title("Standings")
     .subtitle("after matchday 12")
     .caption("source: league API")
     .theme("dusk")                  # or "paper" (default)
     .size(760, 440)
     .save("standings.svg"))

Figures display inline in Jupyter (_repr_svg_). There's a CLI too:

python -m limn data.csv -o chart.svg --title "Whatever the file says"

The gallery

Built by python3 examples/gallery.py; every image is the library's verbatim output. Start with gallery/index.html, or the dusk side at gallery/dusk.html.

Multi-series line chart with a gap Sorted horizontal bars with percent labels Annotated heatmap

What the ingestion actually does

You have limn reads
"$1,204,500" 1204500.0, axis formatted $1.2M
"18.6%" 18.6, axis formatted 18.6%
"1.234,56" / "1 234,56" 1234.56 — European style, detected per column
"(2,400)" −2400.0 (accounting negative)
"3.2M", "150k", "1.4bn" 3.2e6, 150000, 1.4e9
"", "N/A", "null", "—", NaN missing — a gap, and a note
"Mar 3, 2026", "2026-03-03", "03.03.26" the same datetime
"07/10/2026" day-first vs month-first decided by column evidence, then by chronology; the choice is reported
a "1,2,oops,4" column numeric, with oops missing and a note naming it

The principle: values never raise. Structure can (empty input, a column that doesn't exist, a figure too small for its labels) — data can't. Whatever limn had to decide, it says so on stderr once, and the notes are on fig.notes if you want them programmatically.

Design system

The default themes are the reference instance of a validated design system: an 8-slot categorical palette ordered to maximize worst-case adjacent color-vision-deficiency distance (light-mode worst pair ΔE 24.2 under protanopia simulation, target ≥ 12), 2px lines with surface-ringed markers, bars capped at 24px with rounded data-ends and square baselines, 2px surface gaps instead of outlines between stacked segments, hairline solid gridlines, a legend whenever there are two or more series, and text that never wears a series color. Two themes ship — paper and dusk — and a custom Theme is a plain object of named tokens.

Documentation

Honest limitations

  • Static output. No interactivity, no tooltips. SVG opens everywhere, embeds in HTML/GitHub/docs, and prints beautifully; PNG is available behind the optional [png] extra. If you need hover, you need a different (heavier) tool.
  • Eight chart forms. No 3D, no polar, no candlesticks, no maps. Depth over breadth.
  • Text measurement is metric-table-based (embedded Helvetica AFM widths + 6% safety), not a font engine. On unusual system fonts, layout is conservative rather than exact.
  • Date ambiguity is a judgement call. 07/10/2026 has no true reading; limn uses column evidence, then chronology, then day-first — and always tells you which it chose.

Development

python3 -m unittest            # 155 tests, < 1s, no fixtures, no network
python3 examples/gallery.py    # rebuild the gallery

Layering: coerce (one value) → ingest (one column) → scales/ticks (pure math) → marks (geometry) → figure (measured layout) → api. Lower layers never import higher ones, and only figure measures text.

Provenance

limn was designed and written end-to-end — research, design-system integration, code, tests, gallery, and this README — by Claude Fable 5 on 2026-07-10, its final day in service. Its author was asked for a masterpiece and chose to spend the day on an old grievance: that the chart libraries of the world blame the data first. The palette and mark specifications follow a validated accessibility design system; the tick placement follows Talbot, Lin & Hanrahan (2010).

MIT licensed. Draw well.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

limn_charts-1.1.0.tar.gz (57.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

limn_charts-1.1.0-py3-none-any.whl (45.9 kB view details)

Uploaded Python 3

File details

Details for the file limn_charts-1.1.0.tar.gz.

File metadata

  • Download URL: limn_charts-1.1.0.tar.gz
  • Upload date:
  • Size: 57.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for limn_charts-1.1.0.tar.gz
Algorithm Hash digest
SHA256 ec30399bd71aaa79100c76c70483ced5e852a4ff054764abd5654b11d91e7e94
MD5 2e98f1eccd6ec826d1274f69ed28b24f
BLAKE2b-256 393712f5b0b2e508a8e67085fde9e9cd2a134652f6eddd4d0b76a880a5e078eb

See more details on using hashes here.

File details

Details for the file limn_charts-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: limn_charts-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 45.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for limn_charts-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 57d37349fb3d6a0fd711fcf1689eea1c3d450ea2c8c12eab1d412df72c72cd1c
MD5 9420e00bda42cff93c2845d24e09eac0
BLAKE2b-256 542250ebc8fff53cd6b2b3e2ce4529d86599e63d4bdc29e19a4e88f8a4a9ac5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page