I built simple-ascii-chart for a very unglamorous reason: I wanted faster answers in the terminal.
When I am already in a shell, I usually do not want a dashboard. I want to know whether CPU usage is spiking, whether a script is slowing down, whether a counter is drifting upward, or whether a process looks stable over time. In that moment, opening a browser feels heavy. Reading raw numbers works, but it is slower than seeing the shape of the signal.
Terminal tools are good for testing opinions because they expose trade-offs quickly. In a browser, you can hide behind pixels. In a terminal, every character is a product decision.
This post is a walkthrough of the decisions that mattered most: the defaults that kept charts readable, the escape hatches that didn’t bloat the API, and the CLI choices that made it compose well in real pipelines.
What I wanted was surprisingly hard to find in one place:
- a small TypeScript library with a simple input model
- output that stays readable in a real terminal
- a CLI that feels natural in pipes and shell one-liners
- a project small enough to stay opinionated
That is why this became two tools instead of one.
Try it in 60 seconds
If you just want to see what it does:
# Example: space-delimited points from stdin (static)
printf '1 12\n2 18\n3 10\n4 20\n5 16\n' \
| simple-ascii-chart --format space --title "Build time (ms)" --height 8
And for a live stream:
# Example: stream numbers, keep a rolling window
some_command_that_prints_numbers \
| simple-ascii-chart --stream --window 60 --height 10
(You can customize axis ranges, output target, and parsing, but the goal is to stay useful in one-liners.)
Why this became both a library and a CLI
simple-ascii-chart is the library layer. It renders charts as plain terminal text and is meant for scripts, internal tools, logs, quick diagnostics, and any case where a browser chart would be unnecessary overhead.
The core data model is intentionally small. A single series is an array of [x, y] points. Multiple series are just an array of those series. That shape is easy to remember, easy to generate, and easy to move between actual code and quick examples.
The library supports enough range to stay useful, including colors, legends, thresholds, overlay points, custom formatters, symbol overrides, and several modes such as line, point, bar, and horizontalBar.
simple-ascii-chart-cli solves a different problem. The moment you are already at the command line, you do not want to write code just to see a trend—you want a command that works in a pipeline and gets out of the way.
That is why the CLI depends on the library instead of duplicating the renderer. The plotting logic stays reusable, while the CLI focuses on shell workflow: reading from --input, --input-file, static stdin, or streaming stdin, and turning that input into a chart quickly. The main command is simple-ascii-chart, and the package also keeps simple-ascii-chart-cli as a compatibility alias.
That split still feels right to me:
- use the library when you are writing code
- use the CLI when you are already in the shell
Around both packages, I also built a small docs site with examples, documentation, a playground, and an API endpoint. It is not the main subject of this post, but it made the project easier to explore, test, and explain.
The terminal forces more discipline than the browser
Terminal rendering is unforgiving.
A browser chart can hide a lot behind pixels, spacing, hover states, color variation, and extra visual layers. A terminal chart has none of that. Every label competes with the plot. Every symbol takes up meaningful space. Every default becomes visible immediately.
A few constraints ended up shaping almost every design decision:
- the available width and height are usually small
- the visual vocabulary is tiny
- labels compete directly with the chart area
- scaling choices can make a chart readable or misleading
- every extra feature adds cognitive noise
That last point mattered more than I expected. In terminal tools, a feature is never just a feature. If it does not earn its place, it becomes clutter. It makes the chart harder to read, the API harder to remember, or both.
That pushed me toward restraint much more than expansion.
The API I wanted to reach for
The most interesting part of the project was not the renderer itself. It was deciding what kind of tool this should be once real constraints showed up.
First, I kept the data model small so the jump from “I have values” to “I can see the trend” stays short. An array of [x, y] points is not especially novel, but it is easy to hold in your head, easy to serialize, and easy to generate from real code or ad hoc scripts.
Second, I treated defaults as part of the product. Early versions leaned too far toward flexibility because every option looked reasonable from inside the implementation. From the outside, too many options create friction immediately. I ended up using a stricter rule: the first chart should be easy to make, and customization should exist only where it clearly improves a real use case.
Third, I left escape hatches without turning the whole library into a wall of configuration. That is why the library supports custom symbols, axis formatting, legends, thresholds, points, and lower-level hooks like lineFormatter, but still tries to keep the main interface small.
Fourth, packaging is part of the experience. Import ergonomics matter. Installation matters. First-run experience matters. A small utility should feel easy to reach for.
// ESM
import { plot } from 'simple-ascii-chart';
// CommonJS
const { plot } = require('simple-ascii-chart');
This is the kind of library usage I wanted to optimize for:
import { plot } from 'simple-ascii-chart';
const chart = plot(
[
[1, 12],
[2, 18],
[3, 10],
[4, 20],
[5, 16],
],
{
title: 'Build time (ms)',
width: 24,
height: 8,
color: 'ansiCyan',
},
);
console.log(chart);
The exact output will vary with your terminal width and settings, but the goal is consistent: a tiny input shape, sensible defaults, and an immediate “shape of the signal”.
The CLI is where the project became practical
A library is useful when you already know you want to write code. A CLI is useful when you are already looking at a terminal and want an answer now.
I ended up designing the CLI around two workflows:
- “Plot this once” — parse a small payload from stdin or a file and print a chart.
- “Keep plotting” — stream numbers, keep a rolling window, and redraw at a controlled rate.
From there, most flags fall into four buckets:
- input parsing (
--format,--input,--input-file) - streaming control (
--stream,--window,--refresh-ms) - composability (
--passthrough,--plot-output) - small helpers (
--rate,--series)
One detail that mattered more than I expected is stdout vs stderr. In passthrough mode, the raw stream can stay visible on stdout while the live chart renders to stderr. That is a small detail, but it is exactly the kind of detail that determines whether a CLI feels composable in real shell workflows.
Error handling follows the same “one-liner friendly” logic: missing input, invalid dimensions, and broken JSON fail fast with concise messages; malformed add-ons like thresholds or points can warn and still render.
This tiny stdin example captures the experience I wanted:
printf '1 12\n2 18\n3 10\n4 20\n5 16\n' \
| simple-ascii-chart --format space --title "Build time (ms)" --height 8
If a tool is annoying in a case that small, I am not going to trust it in a more complicated pipeline.
The CPU example that clarified the design
The best test for a terminal charting CLI is whether it solves a real terminal problem. For me, one of the best examples is visualizing CPU usage without opening a GUI monitor.
Sometimes I do not need a profiler yet. I just want a quick answer to a simpler question: is the machine calm, spiky, trending upward, or pinned high?
On macOS:
while true; do
top -l 1 | awk -F'[, %]+' '/^CPU usage:/ {print $3+$6}'
sleep 1
done | simple-ascii-chart --stream --window 60 --height 10 --yRange 0 100 --title "CPU usage %"
On Linux:
vmstat 1 | awk 'NR>2 {print 100-$15}' \
| simple-ascii-chart --stream --window 60 --height 10 --yRange 0 100 --title "CPU usage %"
I like this example because it makes the purpose of the CLI obvious. It is not about producing a beautiful chart. It is about reducing friction. Instead of watching numbers scroll by, I can see the shape of the signal. That is often enough to answer the first question and decide whether deeper investigation is worth it.
This example also forced the design in useful ways:
- streamed stdin input
- a sliding window of recent values
- redraw throttling
- stable y-axis limits for percentage data
- simple numeric parsing with very little ceremony
Once a tool has to survive a real one-liner, decorative complexity becomes obvious quickly.
What terminal charts are good at, and what they are not
I think simple-ascii-chart is strongest in situations where speed and proximity matter more than presentation:
- quick diagnostics
- script or CI output
- SSH sessions
- lightweight monitoring
- developer tooling
- demos and educational examples
It is not a replacement for presentation graphics or dense analytical plots. Terminal charts have hard limits: small widths impose trade-offs, dense data can become messy, and some visual ideas do not translate well to plain text.
I do not see those limits as failures. I see them as part of respecting the medium.
What I learned
Building this project made a few things much clearer to me:
- terminal tools reward restraint
- defaults are product decisions, not implementation details
- CLI UX matters just as much as library API design
- text-only rendering forces clearer thinking
- small projects are a good place to test strong opinions against real trade-offs
Small projects give you enough surface area to face real engineering decisions without disappearing into complexity.
Closing
simple-ascii-chart started as a small library experiment, but it became a useful reminder that good tooling is often about reducing friction rather than maximizing features.
I built it for workflows where the terminal is already the right place to be: scripts, diagnostics, SSH sessions, quick monitoring, and small internal tools. That is also where I think it fits best.
The library taught me a lot about defaults, API surface area, and rendering constraints. The CLI taught me even more about practical UX. The docs site made the whole thing easier to explore and share.
That is my favorite kind of project: small enough to stay opinionated, useful enough to keep using.
You can explore the project here: