# cinderplot
# Agent-facing reference for the cinderplot plotting tool.
# Canonical: https://zhou-lab.github.io/cinderplot/llms.txt
# Source of truth: skills/cinderplot/SKILL.md in the repository.
# Human docs: https://zhou-lab.github.io/cinderplot/
# cinderplot — ggplot2-style figures from a small C binary
`cinderplot` turns a CSV/TSV into a publication-ready PDF using Cairo. One
binary, no language runtime; the only link-time dependencies are Cairo and zlib.
Because the whole figure is a single command-line argument, it composes with a
shell pipeline, a Makefile rule or a tool call without an intermediate script.
Docs and gallery:
Source:
## Install and invoke
```sh
conda install -c zhou-lab cinderplot # or: make && make install (needs cairo, zlib)
cinderplot '' out.pdf [--size WxH] [--dpi N]
cat data.tsv | cinderplot 'heatmap(cluster=both)' out.png # data on stdin
cinderplot data.csv -x hp -y mpg -c cyl -f gear out.pdf # shortcut flags
```
- **The output file is required** — a trailing filename or `-o FILE`. The format
comes from the extension (`.pdf`, `.svg`, `.png`); `--dpi` affects PNG only.
- Quote the whole expression in **single** quotes; string literals inside it use
double quotes. Newlines inside the expression are fine.
- `--size WxH` in inches. A partial `9x` or `x7` auto-fits the other axis.
Grammar mode defaults to 6x4; heatmap and track modes size themselves.
- `--dump-spec` prints the desugared expression without rendering.
- The delimiter is **sniffed from the first line** — a tab anywhere means TSV,
otherwise CSV. The extension is ignored, except that `.gz` (gzip or bgzip) is
decompressed.
- Rows dropped for NA, or for non-positive values on a log axis, are reported on
stderr, ggplot2-style.
## Three modes, chosen by which verbs appear
Mixing them is an error. The distinction is the data model, not the appearance.
### 1. Grammar — `aes()` + `geom_*()`
```sh
cinderplot 'sweep.csv + aes(coverage, mae, colour=model) + geom_line() + geom_point()
+ facet_wrap(~panel, levels=c("continuous","binary")) + scale_x_log10()
+ scale_colour_manual(values=c("#2a78d6","#eb6834"))
+ labs(title="MAE vs coverage", x="CpGs per cell", y="MAE") + theme_bw()' fig.pdf
```
R's own spelling is accepted too, so a call written from ggplot2 memory runs:
```sh
cinderplot 'ggplot("mtcars.csv", aes(hp, mpg, colour=factor(cyl))) + geom_point()' f.pdf
```
`data=` and `mapping=` keywords work, and a trailing `ggsave(...)` is accepted
and ignored (the output path is a command-line argument here).
- **aes**: `x`, `y` positionally, then named — `colour`/`color`/`fill`, `size`,
`label`, `xend`/`xmax`, `yend`/`ymax`, `chrom`. A value is a bare column name
or `factor(col[, levels=c(...)])`.
- **geoms**: `point line col histogram boxplot bar segment rect density tile
raster hline vline abline text label`, plus `text_repel`/`label_repel`.
Up to 8 layers.
- **layer options**: `size=`, `alpha=`, `linetype="solid"|"dashed"|"dotted"`,
`bins=` (histogram), `nudge_x=`/`nudge_y=` (text), and
`geom_point(raster=TRUE)` — see trap 9.
- **scales**: `scale_x_log10()` `scale_y_log10()` `scale_*_continuous(labels=percent,
limits=)` `xlim()` `ylim()` `scale_*_manual(values=c(...))`
`scale_(fill|colour)_(viridis|jet|parula|bwr|gradient|gradient2)()`,
`scale_x_genome("seqinfo.tsv.gz")`, `ideogram("cytoband.tsv.gz")`.
- **also**: `facet_wrap(~v[, levels=c(...)])`, `coord_flip()`, `guides(colour="none")`
(or `--no-legend`), `labs()`/`xlab()`/`ylab()`/`ggtitle()`, and
`theme_{gray,bw,minimal,classic,void,linedraw,light,dark,few}()`.
### 2. Heatmap — a matrix with clustering, anchor-placed objects
```sh
cinderplot 'm.tsv + heatmap(name="m", cluster=both, rownames=right)
+ dendrogram(top_of("m")) + dendrogram(left_of("m"))
+ annotation("groups.tsv", column="diagnosis", left_of("m"))
+ legend(right_of("m"))
+ scale_fill_gradient2(low="#2166ac", mid="#f7f7f7", high="#b2182b")' out.pdf
```
Input is a **wide matrix** (first column may be row names). Placements are
`top_of` / `beneath` / `left_of` / `right_of`, each taking an anchor name plus
`pad=`/`width=`/`height=`. The first placed object must be a `heatmap()`.
Clustering is hclust ward.D2, matching R. `annotation()` takes `column=` to pick
which column colours the bar (default: the last). An annotation may be anchored
by its data column name, e.g. `legend(right_of("diagnosis"))`.
The heatmap's own fill is **continuous only** — for a categorical grid use
`geom_tile()` in grammar mode, which takes a discrete fill and a keyed legend.
### 3. Genome locus browser — stacked tracks over one or more windows
```sh
cinderplot 'region("chr20:44616522-44655233")
+ cytoband("cytoband.tsv.gz", height=0.5) + genes("genes.bed.gz", height=1.5)
+ matrix("betas_long.tsv", name="b", cluster=samples, rownames=off,
colnames=off, height=10)' locus.pdf
```
Track verbs: `coverage() interval() genes() arcs() matrix() cytoband()`, each
taking a file plus `name= height= max= color= data= cluster= rownames=
colnames= transcripts=`. Inputs are BED/bedGraph/BEDPE/BED12/matrix TSV, tabix
`.gz` allowed. `region()` with no argument infers the window from `matrix()`.
**Several windows on one axis** — `regions("windows.bed")` lays each window in
its own panel with a real gap between them, sharing the sample rows, colour
scale and legend:
```sh
cinderplot 'regions("windows.bed") + genes("genes.bed.gz", height=1.4)
+ matrix("betas_long.tsv", name="b", cluster=samples, colnames=off, height=9)' multi.pdf
```
`windows.bed` is `chrom start end [name]`; the optional name titles the panel.
A header row is tolerated. Panel widths follow the matrix column count when a
matrix track is present, so a cell is about the same width in every window.
Supported inside `regions()` so far: `matrix()`, `genes()`, `interval()`.
## When unsure, try it
Every unimplemented verb, aesthetic or option fails with a message that
**enumerates the supported subset**, so a wrong guess costs one fast run and
returns a menu rather than a syntax error.
Known absent: `geom_smooth()`, `facet_grid()`, `facet_wrap(scales=)`, an
arbitrary `theme()` (presets only), stacked/dodged bars, and statistical
transformations beyond binning and density.
## Traps
1. **`y` must be numeric except with `geom_tile()`.** A category-vs-value bar
chart is `aes(x=factor(cat), y=value) + geom_col() + coord_flip()`, not
`aes(x=value, y=cat)`.
2. **`geom_boxplot()` and `geom_bar()` need a discrete x** — wrap it:
`aes(x=factor(g), ...)`. A text column is discrete already.
3. **Colour on bars must be constant per panel.** `geom_col()`/`geom_histogram()`
accept `fill=` only when every bar in a panel shares a value — the common
faceted case. Genuinely mixed fills need stacking or dodging, which is not
implemented.
4. **`geom_histogram()`/`geom_bar()`/`geom_density()` compute y themselves** —
do not map `y`, and do not combine them with other data geoms.
5. **Factor level order is sorted, R-style.** Impose an order with `levels=`:
`facet_wrap(~panel, levels=c("b","a"))` or `aes(colour=factor(m, levels=c("B","A")))`.
Unlike R, `levels=` must name **every** value present — an unlisted value is
an error, not a silent drop to NA that would delete rows from the figure.
6. **A long default axis or legend title can squeeze the panel.** The default
title is the verbatim `aes()` text; set it with `labs()` if it is long.
7. **Silent and hard caps.** `scale_*_manual(values=)` keeps the first 16
colours. Hard errors above 40 discrete x categories, 15 legend levels,
8 layers, 12 tracks, 16 heatmap objects.
8. **`coord_flip()` is grammar-mode only** — it errors with `scale_x_genome()`
and `ideogram()`, and is ignored in heatmap and track modes.
9. **A dense scatter is the one slow case — use `geom_point(raster=TRUE)`.**
A vector PDF spends ~136 bytes and one fill per point, so 10^6 points takes
~18 s and a 31 MB file. `raster=TRUE` draws that layer into an embedded
300-dpi image (as R's ggrastr does), leaving axes, ticks and legend as vector
text: ~9 s and 0.35 MB. Aggregating geoms do not need it.
10. **The default font is Arial.** If it is missing, Cairo substitutes and the
figure will not match the gallery.