πŸ“ˆ

Textplot

ASCII and Unicode plots inline in DuckDB query results β€” bars, sparklines, and density plots that fit in a single VARCHAR column. Useful for eyeballing distributions and trends in the DuckDB CLI without leaving the terminal.

Install

-- Install the extension
INSTALL textplot FROM community;

-- Load it into your session
LOAD textplot;

-- See the shape of a column without leaving the CLI
SELECT tp_density(array_agg(value)) AS distribution
FROM measurements;

-- One sparkline per row in a status table
SELECT host, tp_sparkline(cpu_history, width := 24) AS cpu
FROM hosts_status;

-- Inline progress bar
SELECT tp_bar(0.75) AS progress;

-- QR code (ASCII / Unicode)
SELECT tp_qr('https://query.farm');

Technical Overview

Why Use Textplot?

Textplot renders ASCII and Unicode charts as VARCHAR values right inside a SELECT. The point: when you're poking at data in the DuckDB CLI, you can see the shape of a distribution, the trend of a series, or the level of a metric without copying numbers into a notebook or a BI tool. Quick eyeballing β€” not a replacement for real visualization when you actually need axes, hover, or pixels.

πŸ“• What this extension is for

Inline visuals for terminal-first SQL workflows. The output is plain text, so it travels through .mode line, log files, status emails, and READMEs the same way any other column does.

  • β€’ Eyeballing distributions: Wrap an aggregate in tp_density to see whether a column is roughly normal, bimodal, or has a long tail β€” in one row of CLI output, no matplotlib round-trip.
  • β€’ Per-row trend cells: Drop a tp_sparkline into a SELECT list against a table where each row carries an array of values; you get one sparkline per entity in the result table.
  • β€’ Status indicators: tp_bar with threshold-based coloring is the natural shape for SLA / disk / health columns in a fleet table.
  • β€’ Scannable links: tp_qr renders a QR code as Unicode blocks for embedding in terminal output, dashboards, or docs.

βš™οΈ How it works

Every function returns a VARCHAR β€” there's no client-side renderer, no asset pipeline, no extra dependency in the query plan. Each function takes a positional value (or array) plus a small set of named parameters for width, style, and characters.

  • β€’ Charts as strings: tp_bar, tp_sparkline, and tp_density all return a single VARCHAR cell. Width is parameterized so columns line up across rows.
  • β€’ Named-parameter styling: shape, style, theme, width, on_color, off_color, and thresholds are all passed via DuckDB's named-parameter syntax. on and off collide with SQL keywords β€” quote them as "on" and "off".
  • β€’ Three sparkline modes: tp_sparkline picks a vocabulary based on mode: absolute (height ∝ value), delta (direction of change), or trend (signed magnitude). Each mode has multiple themes β€” blocks, arrows, faces, slopes.
  • β€’ Density plot styles: tp_density ships several character-set styles β€” shaded, ascii, dots, height, circles, rainbow_circle β€” so you can pick one that fits the output medium (terminal, email, README).

πŸ›‘οΈ What it isn't

Textplot is a CLI ergonomics tool. It earns its place when the alternative is squinting at numbers β€” not when you actually need a real chart.

  • β€’ Not a visualization library: Character-grid resolution caps detail. For interactive dashboards, publication figures, or anything where pixels matter, use a real charting tool β€” Matplotlib, Vega-Lite, Observable Plot, Apache Superset, etc.
  • β€’ Not a statistical summary: tp_density shows the shape of a distribution; it doesn't compute moments, fit kernels, or give you confidence intervals. Pair it with quantile_cont, stddev_pop, or datasketches when you need numbers.
  • β€’ Unicode rendering varies: Emoji and block characters depend on the terminal font. For maximum portability use style := 'ascii' (tp_density), theme := 'ascii_basic' (tp_sparkline), or custom "on" / "off" characters with tp_bar and tp_qr.

🎯 Common Use Cases

See the shape of a column inline

SELECT tp_density(array_agg(value)) FROM measurements; β€” one row of CLI output that tells you whether the data is roughly normal, bimodal, or skewed, without leaving SQL.

Sparklines per row in a status table

Pre-aggregate a series per entity into an array column, then render tp_sparkline in the SELECT. You get a fleet view that fits in a terminal window.

Threshold-colored health bars

tp_bar with a thresholds list is the classic SLA / disk-utilization / risk indicator. Sort by the underlying number; let the bar carry the visual.

Embed scannable links

tp_qr renders Unicode-block QR codes for terminal output, status emails, and README embeds. ecc := 'high' for tolerant rendering, custom characters for styling.

Deep Dive

Technical Details

What you can do with one query

The shortest path from β€œa column of numbers” to β€œI can see what’s going on” β€” without leaving the DuckDB CLI:

SELECT tp_density(array_agg(value)) AS distribution
FROM measurements;

tp_density renders the array as a one-line Unicode density plot β€” enough to tell whether the data is roughly normal, bimodal, or has a long tail. No notebook, no matplotlib, no BI tool round-trip. The same shape works for sparklines and threshold-colored bars in any result table:

SELECT host,
       tp_sparkline(cpu_history, theme := 'utf8_blocks', width := 24) AS cpu,
       tp_bar(disk_pct, min := 0, max := 100, width := 12,
              thresholds := [(90, 'red'), (75, 'yellow'), (0, 'green')]) AS disk
FROM hosts_status
ORDER BY disk_pct DESC;

One sparkline per row, one threshold-colored bar per row, all inline in the result table.

Quick eyeballing, not real visualization

Textplot is a CLI ergonomics tool. Character-grid resolution caps the detail you can see β€” these plots are for quick visual checks during interactive SQL, not a replacement for Matplotlib, Vega-Lite, or a proper BI dashboard. When you need axes, hover, drilldown, or pixels, use a real charting tool. When you just want to know the shape of a column without leaving the terminal, use this.

How it fits the CLI workflow

Every function returns a single VARCHAR. That’s the whole API: any chart you produce is just another column in the result set. It composes with .mode line, .mode markdown, log redirection, tee to a file, or paste into a status email β€” same as any other string column.

The companion piece for the CLI is fuzzycomplete, which fixes tab-completion for table and column names. Together they’re the two extensions worth installing for β€œI live in the DuckDB REPL” workflows: fuzzycomplete makes typing queries painless, textplot makes reading their output painless.

Function map

FunctionShapeUse for
tp_barSingle barProgress, percentages, SLA indicators
tp_sparklineOne-line trendTime series, KPIs in tables
tp_densityDistribution plotSpotting outliers, shape of a column
tp_qrQR codeScannable links in terminal/docs

Styling vocabulary

The named-parameter surface is shared in spirit across functions:

  • Shapes / styles / themes β€” shape := 'circle' | 'square' | 'heart' on tp_bar; named character-set styles on tp_density (shaded, ascii, dots, height, circles, rainbow_circle, …); per-mode themes on tp_sparkline (utf8_blocks, arrows, faces, slopes, …).
  • Colors β€” on_color / off_color on tp_bar and tp_density. thresholds := [(threshold, color), ...] gives value-conditional coloring; each block uses the highest threshold it satisfies.
  • Custom characters β€” every function with on / off accepts arbitrary strings (single chars, emoji, multi-char sequences). They’re SQL reserved words β€” quote them as "on" and "off".
  • Width β€” fixed character width keeps charts aligned in tables.

Portability: ASCII vs Unicode

Defaults use Unicode block characters and emoji, which look great in modern terminals but vary by font. For maximum portability:

-- ASCII-only density plot
SELECT tp_density([1, 5, 3, 8, 2], style := 'ascii');

-- ASCII sparkline
SELECT tp_sparkline([1, 3, 2, 5, 4, 6], theme := 'ascii_basic');

-- ASCII bar with custom characters
SELECT tp_bar(0.7, "on" := '#', "off" := '-', width := 15);

-- ASCII QR code
SELECT tp_qr('https://query.farm', "on" := '##', "off" := '  ');

For QR codes specifically, .mode ascii in the CLI gives the cleanest rendering β€” the default table borders interact poorly with multi-line block output.

When to reach for it (and when not to)

Reach for Textplot when:

  • You’re in the DuckDB CLI doing ad-hoc analysis and want to see a distribution or trend without copying values out.
  • You need per-row visual cells in a result table β€” sparklines per host, bars per SLA, etc.
  • The output goes to a terminal, log file, status email, or README.

Reach for a real charting tool when:

  • You’re building an interactive web dashboard with hover/zoom/drilldown.
  • You need precise pixel control for publication-quality figures.
  • The audience expects polished graphics and the data deserves them.

The two are complementary, not competing β€” Textplot is the β€œI’m at a SQL prompt right now” tool.

Reference

Extension Contents

Quick reference to all available functions and settings organized by category.

Name Description
Charts

Render numeric values and arrays as Unicode/ASCII charts directly in your query results β€” bars, sparklines, and density plots that fit in a single VARCHAR cell.

tp_bar() Render a horizontal bar showing a numeric value as a fraction of a min/max range
tp_density() Render an array of numeric values as a density plot β€” a compact distribution visualization where the character intensity at each x-position reflects how many input values fall in that bin
tp_sparkline() Render an array of numeric values as a sparkline β€” a compact one-line trend chart
QR Codes

Encode strings as QR codes rendered as Unicode-block art. Useful for dashboards, terminal output, and embedding scannable links in docs.

tp_qr() Encode a string as a QR code rendered with Unicode block characters

API Reference

Function Documentation

Detailed documentation for each function including signatures, parameters, and examples.

tp_bar

Scalar Function Charts
Signature
tp_bar(value: DOUBLE, min := DOUBLE = 0, max := DOUBLE = 1.0, width := INTEGER = 10, shape := VARCHAR = 'square', on_color := VARCHAR, off_color := VARCHAR, on := VARCHAR, off := VARCHAR, filled := BOOLEAN = true, thresholds := STRUCT(threshold DOUBLE, color VARCHAR)[]) β†’ VARCHAR
Parameters
Parameter Type Mode Description
value DOUBLE Positional Numeric value to visualize.
min DOUBLE Named Default: 0 Minimum of the value range.
max DOUBLE Named Default: 1.0 Maximum of the value range.
width INTEGER Named Default: 10 Bar width in characters.
shape VARCHAR Named Default: 'square' Block shape: 'square', 'circle', or 'heart'.
on_color VARCHAR Named Color name for filled portion (e.g. 'red', 'green', 'blue', 'yellow').
off_color VARCHAR Named Color name for unfilled portion.
on VARCHAR Named Custom character for filled portion. Must be quoted in SQL β€” "on" β€” because on is a reserved keyword.
off VARCHAR Named Custom character for unfilled portion. Must be quoted as "off".
filled BOOLEAN Named Default: true Fill all blocks up to the value (true) or just mark the endpoint (false).
thresholds STRUCT(threshold DOUBLE, color VARCHAR)[] Named List of (threshold, color) pairs for value-dependent coloring; each block uses the highest threshold ≀ value.
Returns
Description

Render a horizontal bar showing a numeric value as a fraction of a min/max range. Configurable width, shape (square/circle/heart), foreground/background colors, custom characters, and threshold-based coloring. Returns a VARCHAR you can drop into any SELECT.

Examples
1

Simple progress bar (50% filled)

SELECT tp_bar(0.5);

Output

tp_bar(0.5)
πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯⬜⬜⬜⬜⬜
2

Custom width and range β€” 75 out of 100

SELECT tp_bar(75, min := 0, max := 100, width := 20);

Output

tp_bar(75, min := 0, max := 100, width := 20)
πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯πŸŸ₯⬜⬜⬜⬜⬜
3

Circle shape with explicit colors

SELECT tp_bar(0.8, shape := 'circle', on_color := 'green', off_color := 'white');

Output

tp_bar(0.8, shape := 'circle', on_color := 'green', off_color := 'white')
🟒🟒🟒🟒🟒🟒🟒🟒βšͺβšͺ
4

Threshold-based coloring (85 out of 100 β†’ yellow zone)

SELECT tp_bar(85, min := 0, max := 100, thresholds := [{'threshold': 90, 'color': 'red'}, {'threshold': 70, 'color': 'yellow'}, {'threshold': 0, 'color': 'green'}]);

Output

tp_bar(85, min := 0, max := 100, thresholds := main.list_value(main.struct_pack(threshold := 90, color := 'red'), main.struct_pack(threshold := 70, color := 'yellow'), main.struct_pack(threshold := 0, color := 'green')))
🟨🟨🟨🟨🟨🟨🟨🟨🟨⬜
5

Custom characters ("on" and "off" must be quoted)

SELECT tp_bar(0.7, "on" := 'β–ˆ', "off" := 'β–‘', width := 15);

Output

tp_bar(0.7, "on" := 'β–ˆ', "off" := 'β–‘', width := 15)
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘
6

Heart shape

SELECT tp_bar(0.6, shape := 'heart', on_color := 'red');

Output

tp_bar(0.6, shape := 'heart', on_color := 'red')
❀️❀️❀️❀️❀️❀️🀍🀍🀍🀍

tp_density

Scalar Function Charts
Signature
tp_density(values: DOUBLE[], width := INTEGER = 20, style := VARCHAR = 'shaded', graph_chars := VARCHAR[], marker := VARCHAR) β†’ VARCHAR
Parameters
Parameter Type Mode Description
values DOUBLE[] Positional Array of numeric values to plot the distribution of.
width INTEGER Named Default: 20 Plot width in characters.
style VARCHAR Named Default: 'shaded' Character-set style: 'shaded' (default β–‘β–’β–“β–ˆ), 'ascii' ( .:+#@), 'dots' ( .‒●), 'height' ( β–β–‚β–ƒβ–„β–…β–†β–‡β–ˆ), 'circles' (⚫βšͺπŸŸ‘πŸŸ πŸ”΄), 'safety', 'rainbow_circle' (βš«πŸŸ€πŸŸ£πŸ”΅πŸŸ’πŸŸ‘πŸŸ πŸ”΄βšͺ), 'rainbow_square', 'moon', 'sparse', or 'white'.
graph_chars VARCHAR[] Named Custom array of characters for density levels β€” overrides style.
marker VARCHAR Named Character used to highlight specific values inside the plot.
Returns
Description

Render an array of numeric values as a density plot β€” a compact distribution visualization where the character intensity at each x-position reflects how many input values fall in that bin. Multiple character-set styles, including ASCII for terminal compat and emoji for visual punch.

Examples
1

Default shaded style

SELECT tp_density([1, 2, 3, 4, 5, 4, 3, 2, 1]);

Output

tp_density(main.list_value(1, 2, 3, 4, 5, 4, 3, 2, 1))
β–ˆ β–ˆ β–ˆ β–ˆ β–’
2

Wide height-style plot

SELECT tp_density([1, 2, 3, 2, 1], width := 30, style := 'height');

Output

tp_density(main.list_value(1, 2, 3, 2, 1), width := 30, style := 'height')
β–ˆ β–ˆ β–„
3

ASCII style for terminals without Unicode

SELECT tp_density([1, 5, 3, 8, 2], style := 'ascii');

Output

tp_density(main.list_value(1, 5, 3, 8, 2), style := 'ascii')
@ @ @ @ @
4

Dot style for subtle visualization

SELECT tp_density([1, 5, 3, 8, 2], style := 'dots');

Output

tp_density(main.list_value(1, 5, 3, 8, 2), style := 'dots')
● ● ● ● ●
5

Rainbow emoji style

SELECT tp_density([1, 5, 3, 8, 2], style := 'rainbow_circle');

Output

tp_density(main.list_value(1, 5, 3, 8, 2), style := 'rainbow_circle')
βšͺ⚫βšͺ⚫⚫βšͺ⚫⚫⚫⚫⚫βšͺ⚫⚫⚫⚫⚫⚫⚫βšͺ

tp_qr

Scalar Function QR Codes
Signature
tp_qr(value: VARCHAR, ecc := VARCHAR = 'low', on := VARCHAR = '⬛', off := VARCHAR = '⬜') β†’ VARCHAR
Parameters
Parameter Type Mode Description
value VARCHAR Positional String value to encode.
ecc VARCHAR Named Default: 'low' Error correction level: 'low', 'medium', 'quartile', or 'high'. Higher levels can survive more damage at the cost of larger QR codes.
on VARCHAR Named Default: '⬛' Character for filled modules. Must be quoted as "on" (reserved keyword).
off VARCHAR Named Default: '⬜' Character for empty modules. Must be quoted as "off".
Returns
Description

Encode a string as a QR code rendered with Unicode block characters. Configurable error-correction level and custom on/off characters for styling.

Examples
1

Basic QR code

SELECT tp_qr('Hello, World!');

Output

tp_qr('Hello, World!')
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›
β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œ
β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›
β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›
β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›
β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œ
β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›
β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬›
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œ
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬œ
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬œ
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œ
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬›β¬œβ¬œ
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬›
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬œ
2

High error correction (more redundancy, larger code)

SELECT tp_qr('https://query.farm', ecc := 'high');

Output

tp_qr('https://query.farm', ecc := 'high')
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›
β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œ
β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬›
β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›
β¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬œ
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œ
β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›
β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›
β¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œ
β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œ
β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬›
β¬›β¬›β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬›
β¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬œ
β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬›
β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬›β¬œ
β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬›
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œ
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œ
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œ
β¬›β¬œβ¬›β¬›β¬›β¬œβ¬›β¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬›β¬œ
β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬œ
β¬›β¬›β¬›β¬›β¬›β¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬›β¬›β¬œβ¬œβ¬›β¬›β¬œβ¬œβ¬œβ¬œβ¬œβ¬œβ¬›β¬›β¬›β¬œβ¬œ
3

Custom emoji styling

SELECT tp_qr('https://query.farm', ecc := 'high', "on" := '🟑', "off" := '⚫');

Output

tp_qr('https://query.farm', ecc := 'high', "on" := '🟑', "off" := '⚫')
🟑🟑🟑🟑🟑🟑🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫⚫⚫🟑⚫🟑⚫🟑🟑🟑🟑🟑🟑🟑
🟑⚫⚫⚫⚫⚫🟑⚫🟑⚫⚫⚫🟑⚫🟑⚫🟑⚫⚫🟑⚫⚫🟑⚫⚫⚫⚫⚫🟑
🟑⚫🟑🟑🟑⚫🟑⚫🟑🟑🟑🟑🟑🟑⚫🟑🟑⚫⚫🟑🟑⚫🟑⚫🟑🟑🟑⚫🟑
🟑⚫🟑🟑🟑⚫🟑⚫⚫⚫⚫🟑🟑🟑⚫⚫🟑🟑⚫🟑⚫⚫🟑⚫🟑🟑🟑⚫🟑
🟑⚫🟑🟑🟑⚫🟑⚫⚫⚫🟑🟑🟑🟑🟑🟑⚫⚫⚫🟑🟑⚫🟑⚫🟑🟑🟑⚫🟑
🟑⚫⚫⚫⚫⚫🟑⚫🟑⚫⚫🟑⚫⚫🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫⚫⚫⚫⚫🟑
🟑🟑🟑🟑🟑🟑🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫🟑⚫🟑🟑🟑🟑🟑🟑🟑
⚫⚫⚫⚫⚫⚫⚫⚫🟑🟑⚫🟑⚫🟑🟑🟑🟑⚫⚫⚫🟑⚫⚫⚫⚫⚫⚫⚫⚫
⚫⚫🟑🟑🟑⚫🟑⚫🟑🟑🟑🟑⚫⚫🟑🟑🟑⚫🟑⚫🟑🟑🟑🟑⚫⚫🟑🟑🟑
🟑🟑🟑🟑⚫🟑⚫🟑⚫🟑🟑🟑⚫🟑⚫🟑⚫⚫⚫⚫⚫🟑⚫⚫🟑🟑🟑⚫🟑
🟑🟑🟑⚫⚫🟑🟑🟑🟑⚫⚫🟑⚫⚫🟑⚫🟑🟑🟑⚫🟑🟑🟑🟑🟑⚫🟑⚫⚫
🟑⚫⚫⚫⚫⚫⚫⚫🟑🟑⚫🟑⚫🟑⚫⚫⚫⚫⚫⚫⚫🟑🟑🟑⚫🟑⚫🟑⚫
⚫🟑🟑🟑🟑🟑🟑⚫⚫⚫⚫🟑🟑🟑🟑🟑⚫⚫🟑⚫🟑🟑⚫🟑⚫⚫🟑🟑🟑
🟑⚫🟑⚫🟑🟑⚫⚫⚫⚫⚫⚫🟑🟑🟑⚫🟑🟑⚫🟑🟑🟑🟑⚫🟑🟑⚫🟑🟑
⚫⚫⚫🟑⚫⚫🟑⚫🟑🟑⚫🟑🟑⚫⚫⚫🟑⚫⚫⚫🟑⚫🟑🟑⚫⚫⚫🟑⚫
⚫⚫🟑⚫⚫🟑⚫⚫🟑⚫🟑🟑⚫⚫🟑🟑🟑⚫🟑⚫⚫🟑🟑⚫🟑🟑⚫⚫⚫
⚫🟑⚫⚫🟑🟑🟑🟑⚫⚫⚫⚫🟑⚫⚫⚫⚫🟑🟑⚫⚫⚫🟑⚫⚫🟑🟑🟑🟑
🟑🟑🟑⚫⚫🟑⚫🟑🟑⚫🟑⚫⚫⚫🟑🟑⚫🟑⚫⚫🟑🟑⚫🟑🟑🟑⚫🟑🟑
🟑⚫⚫🟑⚫🟑🟑🟑⚫⚫🟑🟑⚫⚫⚫🟑🟑⚫🟑🟑⚫⚫🟑⚫⚫⚫🟑⚫⚫
🟑⚫🟑⚫⚫⚫⚫⚫🟑🟑🟑🟑🟑🟑🟑⚫⚫⚫⚫🟑🟑⚫🟑⚫🟑🟑⚫🟑🟑
🟑⚫🟑🟑⚫🟑🟑⚫⚫⚫🟑⚫🟑🟑⚫⚫⚫⚫⚫🟑🟑🟑🟑🟑🟑⚫🟑🟑⚫
⚫⚫⚫⚫⚫⚫⚫⚫🟑🟑⚫⚫🟑⚫⚫⚫🟑🟑🟑🟑🟑⚫⚫⚫🟑🟑⚫⚫🟑
🟑🟑🟑🟑🟑🟑🟑⚫⚫⚫⚫⚫⚫⚫🟑⚫⚫⚫🟑🟑🟑⚫🟑⚫🟑⚫⚫⚫⚫
🟑⚫⚫⚫⚫⚫🟑⚫⚫🟑⚫⚫🟑⚫🟑🟑🟑🟑⚫🟑🟑⚫⚫⚫🟑🟑⚫🟑🟑
🟑⚫🟑🟑🟑⚫🟑⚫🟑🟑🟑🟑⚫🟑⚫🟑⚫🟑⚫⚫🟑🟑🟑🟑🟑🟑🟑🟑⚫
🟑⚫🟑🟑🟑⚫🟑⚫🟑🟑⚫🟑⚫🟑🟑⚫🟑🟑🟑🟑🟑🟑⚫🟑⚫⚫⚫⚫⚫
🟑⚫🟑🟑🟑⚫🟑⚫🟑🟑⚫⚫⚫⚫🟑🟑⚫⚫⚫🟑🟑🟑🟑🟑🟑🟑⚫🟑⚫
🟑⚫⚫⚫⚫⚫🟑⚫⚫⚫⚫⚫🟑🟑⚫🟑🟑⚫🟑🟑⚫🟑⚫⚫🟑🟑⚫🟑⚫
🟑🟑🟑🟑🟑🟑🟑⚫⚫🟑🟑⚫🟑🟑⚫⚫🟑🟑⚫⚫⚫⚫⚫⚫🟑🟑🟑⚫⚫

tp_sparkline

Scalar Function Charts
Signature
tp_sparkline(values: DOUBLE[], mode := VARCHAR = 'absolute', theme := VARCHAR, width := INTEGER = 20) β†’ VARCHAR
Parameters
Parameter Type Mode Description
values DOUBLE[] Positional Array of numeric values, ordered (typically by time).
mode VARCHAR Named Default: 'absolute' Visualization mode: 'absolute' (height ∝ value), 'delta' (up/same/down), or 'trend' (signed magnitude).
theme VARCHAR Named Character-set theme. Options vary by mode β€” absolute: utf8_blocks, ascii_basic, hearts, faces. delta: arrows, triangles, ascii_arrows, math, faces, thumbs, trends, simple. trend: arrows, ascii, slopes, intensity, faces, chart.
width INTEGER Named Default: 20 Sparkline width in characters.
Returns
Description

Render an array of numeric values as a sparkline β€” a compact one-line trend chart. Three modes: absolute (height ∝ value), delta (direction of change), trend (direction with magnitude). Each mode has multiple themes: arrows, faces, slopes, hearts, and more.

Examples
1

Absolute mode (default)

SELECT tp_sparkline([1, 3, 2, 5, 4, 6, 2, 1]);

Output

tp_sparkline(main.list_value(1, 3, 2, 5, 4, 6, 2, 1))
β–ƒβ–ƒβ–‚β–‚β–‚β–†β–†β–…β–…β–…β–ˆβ–ˆβ–‚β–‚β–‚
2

Stock prices with explicit theme and width

SELECT tp_sparkline([45.2, 47.1, 46.8, 49.3, 52.1, 48.7], width := 20, theme := 'utf8_blocks');

Output

tp_sparkline(main.list_value(45.2, 47.1, 46.8, 49.3, 52.1, 48.7), width := 20, theme := 'utf8_blocks')
β–‚β–‚β–‚β–‚β–‚β–‚β–…β–…β–…β–…β–ˆβ–ˆβ–ˆβ–„β–„β–„
3

Delta mode with arrow theme

SELECT tp_sparkline([100, 105, 102, 108, 95], mode := 'delta');

Output

tp_sparkline(main.list_value(100, 105, 102, 108, 95), "mode" := 'delta')
↑↑↑↑↑↓↓↓↓↓↑↑↑↑↑↓↓↓↓↓
4

Delta mode with face theme

SELECT tp_sparkline([72, 75, 75, 78, 71], mode := 'delta', theme := 'faces');

Output

tp_sparkline(main.list_value(72, 75, 75, 78, 71), "mode" := 'delta', theme := 'faces')
😊😊😊😊😊😐😐😐😐😐😊😊😊😊😊😞😞😞😞😞
5

Trend mode with slope theme

SELECT tp_sparkline([10, 12, 11, 15, 8], mode := 'trend', theme := 'slopes');

Output

tp_sparkline(main.list_value(10, 12, 11, 15, 8), "mode" := 'trend', theme := 'slopes')
/////\\\\\/////\\\\\\\\\\
6

Trend mode with intensity theme

SELECT tp_sparkline([100, 110, 105, 125, 90], mode := 'trend', theme := 'intensity');

Output

tp_sparkline(main.list_value(100, 110, 105, 125, 90), "mode" := 'trend', theme := 'intensity')
+++++-----+++++----------

Practical Examples

Cookbook

Real-world recipes and patterns for common use cases.

Recipes for inline ASCII / Unicode plots in the DuckDB CLI β€” distributions, trends, bars, and QR codes that live as VARCHAR columns in your SELECT output.

See the shape of a column

The headline use β€” point an aggregate at tp_density and you’ve got a one-line distribution plot in CLI output:

SELECT tp_density(array_agg(value)) AS distribution
FROM measurements;

For a wider plot or a different character set:

SELECT tp_density(array_agg(value), width := 40, style := 'height') AS distribution
FROM measurements;

Available styles: shaded (default), ascii, dots, height, circles, safety, rainbow_circle, rainbow_square, moon, sparse, white. See tp_density for the full parameter list.

Sparkline per row in a status table

The pattern that makes tp_sparkline most useful β€” pre-aggregate a series per entity, render a sparkline cell:

SELECT host,
       tp_sparkline(cpu_history, theme := 'utf8_blocks', width := 24) AS cpu_24h
FROM hosts_status
ORDER BY host;

Three modes change what the sparkline encodes:

-- Absolute mode (default) β€” height ∝ value
SELECT tp_sparkline([45.2, 47.1, 46.8, 49.3, 52.1, 48.7]);

-- Delta mode β€” direction of change between consecutive points
SELECT tp_sparkline([100, 105, 102, 108, 95], mode := 'delta');

-- Trend mode β€” direction with magnitude
SELECT tp_sparkline([100, 110, 105, 125, 90], mode := 'trend', theme := 'intensity');

Threshold-colored health bars

tp_bar with a thresholds list is the natural shape for SLA, disk-utilization, or risk indicators. Each block adopts the highest threshold ≀ value:

SELECT host,
       disk_pct,
       tp_bar(disk_pct, min := 0, max := 100, width := 14,
              thresholds := [
                {'threshold': 90, 'color': 'red'},
                {'threshold': 75, 'color': 'yellow'},
                {'threshold':  0, 'color': 'green'}
              ]) AS disk
FROM hosts_status
ORDER BY disk_pct DESC;

Sort by the underlying number; let the bar carry the visual.

ASCII output for portable terminals

When the destination doesn’t render emoji or wide Unicode reliably (older terminals, log aggregators, plain-text email), pin every function to its ASCII vocabulary:

-- Density
SELECT tp_density([1, 5, 3, 8, 2], style := 'ascii');

-- Sparkline
SELECT tp_sparkline([1, 3, 2, 5, 4, 6], theme := 'ascii_basic');

-- Bar with custom characters ("on" / "off" are reserved β€” quote them)
SELECT tp_bar(0.7, "on" := '#', "off" := '-', width := 15);

-- QR code in ASCII blocks
SELECT tp_qr('https://query.farm', "on" := '##', "off" := '  ');

Per-row charts on a generated series

Useful when you’re prototyping or want to demo the styling vocabulary without a real dataset:

SELECT round(n, 4) AS value,
       tp_bar(n,
              width := 14,
              shape := 'circle',
              off_color := 'black',
              filled := true,
              thresholds := [
                (0.8, 'red'), (0.7, 'orange'), (0.6, 'yellow'), (0.0, 'green')
              ]) AS bar
FROM (SELECT random() AS n FROM generate_series(1, 10));

QR codes for terminal output

-- Default monochrome
SELECT tp_qr('Hello, World!');

-- High error correction (more redundancy, slightly larger code)
SELECT tp_qr('https://query.farm', ecc := 'high');

-- Custom emoji styling
SELECT tp_qr('https://query.farm', ecc := 'high', "on" := '🟑', "off" := '⚫');

In the DuckDB CLI, .mode ascii gives the cleanest rendering of QR output β€” the default table borders interact poorly with the multi-line block grid.

Pair with fuzzycomplete for a CLI-first workflow

Textplot makes CLI output readable; fuzzycomplete makes CLI input tolerable (VS-Code-style fuzzy tab-completion across schemas and databases). Install both if you live in the DuckDB REPL:

INSTALL fuzzycomplete FROM community;
LOAD fuzzycomplete;

INSTALL textplot FROM community;
LOAD textplot;

Tips

  1. Pick widths to match your output β€” width 20–30 for dashboards, 10–15 for compact reports, β‰₯40 for distribution shapes you want to actually read.
  2. Combine with raw numbers, don’t replace them β€” the visual is for shape; the number is for precision. Show both.
  3. Sparklines shine in tables β€” one cell per row is where they earn their keep.
  4. ASCII for portability, emoji for impact β€” emoji needs a Unicode-capable terminal and font.
  5. For real visualization, use a real tool β€” Textplot is for eyeballing in the CLI, not for the dashboard you ship to stakeholders.

Platform Support

Compatibility

Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.

Quick Facts

Software License MIT
Pricing Free
Written In C++
Source Available Yes
View on GitHub
Usage
23,962+
loads in last 30 days

Platforms

Linux
Linux (musl)
macOS
Windows
WASM
Supported platform architectures
Linux: x86_64, aarch64
Linux (musl): x86_64
macOS: Intel, Apple Silicon
Windows: x86_64
WASM: mvp, eh, threads
Compiled binary sizes
Platform Architecture Size
Linux aarch64 2.72 MB
Linux x86_64 3.09 MB
Linux (musl) x86_64 2.95 MB
macOS Apple Silicon 1.88 MB
macOS Intel 2.19 MB
Windows x86_64 7.54 MB
WASM eh 36.8 KB
WASM mvp 30.8 KB
WASM threads 30.8 KB

Gzipped download size from the DuckDB community-extensions registry.

DuckDB Versions

Release calendar
Supported
v1.4.4 v1.5.2

Charts in Your SELECT Statement

Install Textplot to render bars, sparklines, and density plots inline in any DuckDB query β€” no notebook, no BI tool, just SQL output.