It is not the man who has too little that is poor, but the one who hankers after more.
— Seneca, “Letters from a Stoic”
Warren Buffett
???
(if you think from the perspective of “degrees of freedom”…)
A minimal reimplementation of the R Markdown ecosystem:
$$\mathrm{litedown} = \min{\{R\}} + \{D_i\} - \{D_e\} + \{J\}$$
\(R\) = knitr + evaluate + rmarkdown + bookdown + blogdown + pagedown + pkgdown + xaringan + tufte + distill + htmlwidgets
\(D_i\) = (internal dependencies) commonmark + xfun. That’s it.
\(D_e\) = (removed) Pandoc + Bootstrap + jQuery + GitBook + Hugo + paged.js + remark.js + …
\(J\) = Super lightweight vanilla JS/CSS (a few hundred lines total)
install.packages('litedown')
litedown::roam() # get started
Do you know how awesome web browsers are?…
(now cast the spell “go up”, “go down”, “dark mode”, “mirror my slides”, “edit my slides”, “hi *anyone…” (voice input), or “reset my slides”)
Reports (HTML and LaTeX/PDF) e.g., easybio
Slides (like this presentation!)
Books (multi-page websites with cross-references) e.g., litedown’s documentation
Package documentation (a lightweight alternative to pkgdown) e.g., gglite’s package site
Websites (blog-style or project sites)
Articles (with margin notes, like Tufte style)
All from one package. Two dependencies. No Pandoc.
I re-implemented 12 years of work from scratch in a couple of months in 2024, with fewer features and dependencies—not because I’m smart, but because I had a decade to think about what really matters to me.
An example to demo most features (source)
The online playground (you don’t even need to install R): https://pkg.yihui.org/litedown/playground/
Minimalism has costs:
No Word/PowerPoint output. If your collaborator insists on .docx, you
need rmarkdown (or Quarto, or a new collaborator).
Smaller community. With AI tools, that’s probably no longer a big deal.
Some features are intentionally missing (e.g., only pipe tables are supported). This is by design, not by accident.
ggplot2 is brilliant. The Grammar of Graphics changed how we think about data visualization. I have nothing but respect for this masterpiece of software engineering.
But…
ggplot2 has 16 recursive dependencies (rlang, vctrs, cli, scales, cpp11, …)
Its output is static (PNG/PDF, etc). Want interactivity? Add plotly (63 dependencies!!).
File sizes add up. A simple scatter plot in plotly can be 3Mb of HTML.
What if we could have Grammar of Graphics and interactivity and keep it light?
gglite = a lightweight R interface to G2, a JavaScript visualization library built on the Grammar of Graphics.
One R package dependency: xfun (authored by me). That’s it.
Output: interactive HTML/JavaScript (tooltips, brushing, filtering—for free!); static image is also possible (right-click and copy).
API: feels like ggplot2, and accepts both the pipe |> and + (no way
you can make mistakes!).
Rendering: client-side JavaScript (the browser does the work, not R)
install.packages('gglite', repos = 'https://yihui.r-universe.dev')
| ggplot2 | gglite | Notes |
|---|---|---|
ggplot(data, aes(x, y)) |
g2(data, x = 'x', y = 'y') |
Column names are character strings |
g2(data, y ~ x) |
Formula interface | |
+ operator |
|> pipe or + |
|
geom_point() |
mark_point() |
“geom” → “mark” |
geom_line() |
mark_line() |
|
theme_minimal() |
theme_light() |
|
labs(title = ...) |
titles(...) |
|
| Static PNG/PDF | Interactive HTML/JS | Built-in tooltips, brushing |
No aes(), no non-standard evaluation, no .data[[]] gymnastics.
iris!)library(gglite)
g2(iris, x = 'Sepal.Width', y = 'Sepal.Length', color = 'Species')
Try: hover over points, click legend entries to filter species.
g2(iris, x = 'Species', y = 'Petal.Width') |>
mark_boxplot()
Box plots with hover tooltips showing quartiles—no extra packages needed.
# Simulated clinical trial enrollment by site and treatment arm
enrollment = data.frame(
site = rep(c('UNMC', 'Johns Hopkins', 'Mayo Clinic', 'UCSF'), each = 2),
arm = rep(c('Treatment', 'Placebo'), 4),
count = c(45, 42, 38, 35, 52, 48, 30, 33)
)
g2(enrollment, x = 'site', y = 'count', color = 'arm') |>
mark_interval() |>
transform('dodgeX') |>
titles('Trial Enrollment by Site and Arm') |>
interact('elementHighlightByX')
# Causes of missing data (we've all been there)
missing = data.frame(
reason = c('Patient withdrew', 'Lab error', 'Lost to follow-up',
'Data entry mistake', 'The dog ate it'),
count = c(25, 15, 30, 20, 10)
)
g2(missing, y = 'count', color = 'reason') |>
mark_interval() |>
transform('stackY') |>
coord_theta(innerRadius = 0.4) |>
titles('Reasons for Missing Data') |>
labels(text = 'reason')
# Simulated daily case counts
set.seed(42)
n = 365
dates = as.character(seq(as.Date('2025-01-01'), by = 'day', length.out = n))
cases = data.frame(
date = dates,
count = cumsum(rpois(n, lambda = 5)) + round(50 * sin(seq(0, 4*pi, length.out = n)))
)
g2(cases, x = 'date', y = 'count') |>
mark_area(style = list(fill = 'steelblue', fillOpacity = 0.4)) |>
mark_line(style = list(stroke = 'steelblue')) |>
titles('Daily Case Count (2025)', subtitle = 'Drag the slider to zoom') |>
slider_x()
scores = data.frame(
criterion = rep(c('Significance', 'Innovation', 'Approach',
'Investigators', 'Environment'), 2),
score = c(8, 7, 9, 8, 6, 6, 9, 5, 7, 8),
reviewer = rep(c('Reviewer 1', 'Reviewer 2'), each = 5)
)
g2(scores, x = 'criterion', y = 'score', color = 'reviewer') |>
mark_area(style = list(fillOpacity = 0.3)) |>
mark_line(style = list(lineWidth = 2)) |>
mark_point() |>
coord_polar() |>
scale_x(padding = 0.5, align = 0) |>
scale_y(domainMin = 0, domainMax = 10) |>
axis_y(grid = TRUE, title = FALSE) |>
titles('NIH Grant Review Scores')
feedback = data.frame(
text = c('revise', 'resubmit', 'interesting', 'methods', 'typo',
'references', 'sample size', 'p-value', 'bias', 'limitation',
'future work', 'well-written', 'novel', 'unclear', 'Table 1'),
value = c(50, 45, 30, 35, 40, 25, 38, 42, 28, 33, 20, 15, 18, 35, 22)
)
g2(feedback) |>
mark_word_cloud() |>
encode(text = 'text', value = 'value', color = 'text') |>
titles('Dissertation Committee Feedback')
flow = data.frame(
source = c('Screened', 'Screened', 'Eligible', 'Eligible',
'Randomized', 'Randomized', 'Treatment', 'Placebo'),
target = c('Eligible', 'Excluded', 'Randomized', 'Declined',
'Treatment', 'Placebo', 'Completed', 'Completed'),
value = c(200, 50, 150, 30, 60, 60, 55, 52)
)
g2(flow) |>
mark_sankey(layout = list(nodeAlign = 'center', nodePadding = 0.03)) |>
encode(source = 'source', target = 'target', value = 'value') |>
titles('Clinical Trial Patient Flow (CONSORT-ish)')
No ggplot2 extensions. No ggridges, no ggforest, no survminer geoms.
The ggplot2 extension ecosystem is massive. I’m not sure how much of it can
be re-implemented in gglite.
Requires a browser. If you need a static PDF figure for a journal submission, that’s possible with gglite (give me 5 minutes and I can probably implement it), but still requires a browser.
Early stage. This package barely existed before Monday this week! The API is likely to change (I’d love to hear your ideas). Documentation is growing.
For exploratory analysis, dashboards, HTML reports, and presentations? gglite shines. For The New England Journal of Medicine Figure 2? Probably stick with ggplot2 for now.
Reproducibility: fewer dependencies = fewer things that can break between now and when your reviewer asks for “minor revisions” 18 months from now
Installation: install.packages('litedown') takes seconds, not minutes.
No “please install Pandoc separately” emails to collaborators.
Portability: works in WebAssembly (R in the browser!), Docker containers, restricted computing environments
Debuggability: when something goes wrong, there are fewer layers to dig through.
Speed: less overhead = faster compilation. Your weekly report compiles in seconds, not minutes.
Complex documents: if you need .docx output, Pandoc citation processing,
or cross-format publishing, rmarkdown/Quarto is the right choice
Team conventions: if your lab already uses Quarto and everyone is happy, switching has a cost
Specialized visualizations: if you need a forest plot with ggforest or
survival curves with survminer, ggplot2’s extension ecosystem is unmatched
The right tool depends on the job. I built litedown and gglite not to replace everything, but to offer a lighter path when the heavy machinery isn’t needed.
litedown:
install.packages('litedown')
litedown::roam() # live preview (like a mini RStudio viewer)
gglite:
install.packages('gglite', repos = 'https://yihui.r-universe.dev')
Slides made with litedown (of course). Plots made with gglite (of course).