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")
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:
- 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.56and1.234,56and1 234,56, percent signs (kept as an axis unit),(1,240)accounting negatives,3.2M/150ksuffixes, 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. - 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.
- 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.
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
- Quickstart — install to first chart in a minute
- The chart forms — all eight, with their options
- How ingestion thinks — the parsing rules, in full
- Styling & annotations — themes, references, facets
- API reference — every public name, every error
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/2026has 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec30399bd71aaa79100c76c70483ced5e852a4ff054764abd5654b11d91e7e94
|
|
| MD5 |
2e98f1eccd6ec826d1274f69ed28b24f
|
|
| BLAKE2b-256 |
393712f5b0b2e508a8e67085fde9e9cd2a134652f6eddd4d0b76a880a5e078eb
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57d37349fb3d6a0fd711fcf1689eea1c3d450ea2c8c12eab1d412df72c72cd1c
|
|
| MD5 |
9420e00bda42cff93c2845d24e09eac0
|
|
| BLAKE2b-256 |
542250ebc8fff53cd6b2b3e2ce4529d86599e63d4bdc29e19a4e88f8a4a9ac5d
|