malevich

Reference

kaz, the command line

Pipe data to an honest plot. One subcommand per chart, the plot on stderr, the data still flowing.

Pipe data to an honest terminal plot. A stdin-first CLI over malevich. The first look at any data, drawn in the shell. Every chart, explained and illustrated, is at shergin.github.io/malevich.

cat loss.tsv | kaz line -t training
awk '{print $5}' access.log | kaz hist
cut -f2 species.tsv | kaz count

The plot goes to stderr by default, so stdout stays the data. -O echoes the input through, so the plot can sit in the middle of a pipeline without breaking it:

cat data.tsv | kaz line -O | next-tool     # plot on stderr, data flows on
                        sine
20 ┤      ⡠⠤⠒⠉⠉⠉⠉⠉⠒⠤⣀
   │   ⡠⠔⠊           ⠉⠢⡀
   │ ⠔⠊                ⠈⠢⢄                       ⢀⡠⠊
10 ┤                      ⠑⠤⡀                  ⢀⠔⠁
   │                        ⠈⠢⢄⡀            ⢀⠤⠒⠁
   │                           ⠈⠢⣀⡀     ⢀⡠⠔⠊⠁
 0 ┤                              ⠈⠉⠉⠒⠉⠉⠁
   └┬───────────┬───────────┬──────────┬───────────┬
    0          10          20         30          40

count tallies bare labels, so sort | uniq -c is unnecessary:

awk '{print $9}' access.log | kaz count -t 'status codes'
              status codes
5 ┤  ███████
  │  ███████  ▁▁▁▁▁▁
  │  ███████  ██████
  │  ███████  ██████  ▂▂▂▂▂▂▂  ▂▂▂▂▂▂
0 ┤  ███████  ██████  ███████  ██████
  └─────────────────────────────────────
       200      404      301     500

Install#

brew install shergin/tap/kaz     # Homebrew (builds from source)
cargo install malevich-cli       # or via cargo

The Homebrew formula (homebrew/kaz.rb) also installs the completions and the man page. Shell completions (bash, zsh, fish) live in completions/, and the man page is man/kaz.1. To wire them up by hand:

cp completions/kaz.fish ~/.config/fish/completions/   # fish
source completions/kaz.bash                            # bash
man ./man/kaz.1

Charts#

CommandAliasWhatInput shape
linelline chart, one line per seriesy | xy | xyy | xyxy | yx
scattersscatter plotxy | xyy
barbone bar per label (--horizontal, --stack, --group)label value | label v1 v2 …
hist—histogram (--bins N to fix the count; --normalize, --cumulative)columns of numbers
countcvalue frequencies as barsone column of labels
densitydkernel density estimatecolumns of numbers
ecdf—empirical cumulative distributioncolumns of numbers
box—a box plot per columncolumns are groups
violin—a violin plot per columncolumns are groups
hist2d—2D histogram (density grid)xy
heatmap—shade a row-major matrixrows of numbers
spark—sparkline: bars from zero, no axes, one row tallcolumns of numbers
describe—summary statistics per columncolumns are groups
table—the numbers as an aligned tablerows of numbers
spec—render a serialized malevich documentJSON
caps—what detection sees for this terminal—

No other CLI plotter ships ecdf, violin, or hist2d.

Input#

Fields split on any run of whitespace by default, so bare numbers, TSV, and column-style output all come through. -d CHAR sets one explicit separator (-d, for CSV-shaped data). -H reads a header row and uses its names to label the series.

--fmt says how the columns sit on the axes:

  • y — each column is a y-series over its row index (default: one column)
  • xy — first column x, second column y
  • xyy — first column x, every remaining column a series (default: 2+ columns)
  • xyxy — columns pair up: (x0,y0) (x1,y1) …
  • yx — first column y, second column x (YouPlot compatibility)

A field that will not parse becomes an honest gap in the plot. A one-line tally (3 values could not be parsed) goes to stderr afterward, and -q silences it. This parses fields, not CSV. For quotes and embedded delimiters, shape the data upstream (xsv select …, mlr --c2t …) and pipe the result in.

Options#

-o TARGET      plot destination: stderr (default), - for stdout, or a FILE
-O             pass input through to stdout (mid-pipeline mode)
-d CHAR        field separator (default: any run of whitespace)
-H             first row is a header; its names label the series
--fmt FMT      column mapping: y | xy | xyy | xyxy | yx
-w N, -h N     frame width and height in cells (0..4096; max 4194304 cells)
-t TITLE       plot title
--xlabel TEXT  --ylabel TEXT
--xlim A,B     --ylim A,B         fix an axis range
--log-x  --log-y
--time-x       read the x column as time (unix seconds or ISO 8601)
--bins N       histogram bin count (hist; 1..1000000; default: automatic)
--binwidth W   histogram bin width (exclusive with --bins)
--horizontal   bar: sideways
--stack        bar: stack the value columns of `label v1 v2 …` rows
--group        bar: group them side by side within each band
--unit U       label the value axis: an SI unit, bytes, or a suffix such as %
--hline V      horizontal reference line (repeatable)
--vline V      vertical reference line (repeatable)
--normalize N  histogram heights: count (default) | probability | percent | density
--cumulative   accumulate histogram bins left to right
--cols LIST    select/reorder columns: header names (with -H) or 0-based indices
--by COL       scatter: color points by this column's categories
--colormap M   heatmap/hist2d: viridis | magma | cividis | greys | red-blue | purple-orange
--midpoint V   center the colormap on a value (signed data)
--log-color    logarithmic colormap (decades share equal color steps)
--labels-x A,B band labels across heatmap columns
--labels-y A,B band labels down heatmap rows, top to bottom
--reduce R     dense-heatmap bucket summary: mean | max | min | median
--emit-code    print the equivalent malevich Rust program, data inlined
--color WHEN   auto | always | never
--charset SET  auto | ascii | half | quad | sextant | braille | octant
--pixels WHEN  auto | always | never   — sixel/kitty/iTerm2 image panel from a pipe
-q             suppress the unparsed-values tally
--version      --help

Color follows the destination stream. The glyph tier defaults to quadrants in UTF-8, and to ASCII for a non-UTF-8 locale. Use --charset or MALEVICH_CHARSET to opt into a denser tier your font supports. Where the terminal speaks a pixel protocol, the plot panel becomes a real image, even mid-pipeline. MALEVICH_GRAPHICS=kitty|sixel|iterm2|none names the protocol when the sniff cannot. -h is height. Help is --help only.

--emit-code is the way out of the shell. Once the piped chart looks right, it prints the equivalent malevich Rust program: the same calls, your parsed data inlined as literals, ready to paste into a project:

kaz scatter penguins.tsv -H --by species --emit-code > plot.rs

Live#

--live reads stdin forever, one value per line, and repaints a sliding line in place. There is no alt-screen, so the final frame stays in your scrollback, and Ctrl-C restores the cursor. Each repaint is one synchronized-output frame. When the plot's destination is not a terminal (2>log), the frames append as plain text and no escape byte is written. Line only.

ping -i.2 host | grep -oE 'time=[0-9.]+' | tr -d 'time=' | kaz line --live -t ping
vmstat 1 | awk 'NR>2{print $1}' | kaz line --live -t runnable

--window N sets the window length (1..1000000). --fps N sets the repaint rate (1..1000; default 10). --rate plots the per-interval delta of a monotonic counter.

If a live plot looks frozen, the producer is buffering. Pipes hold output until a block fills. Unbuffer at the source: stdbuf -oL producer, grep --line-buffered, or awk '{print; fflush()}'.

Design#

kaz contains zero rendering logic. It parses arguments, frames stdin, and calls the public malevich API. Every flag names an existing library concept: a frame field, a preset argument, a scale option, or plot furniture. It is the proof of the library's central claim, that a pure string-renderer is enough.

License#

MIT or Apache-2.0, matching malevich.