BLAND — Elixir Technical Drawing.
A pure-Elixir library for producing monochrome, paper-ready plots in the visual tradition of 1960s–1980s engineering reports: thin black rules, serif type, crisp frames, hatched fills, and optional drafting title blocks.
BLAND emits SVG. SVG is the right format for paper output — resolution- independent, prints clean on any printer, and embeds into Livebook, PDF pipelines, and LaTeX figures without conversion.
See the gallery for every plot type at a glance.
Design philosophy
BLAND deliberately avoids color. The library leans on the legibility vocabulary of technical drafting — stroke weight, dash patterns, hatch density, and marker shape — so plots stay readable in photocopies, grayscale prints, and for readers with color vision deficiency.
Quick start
xs = Enum.map(0..100, &(&1 / 10.0))
fig =
Bland.figure(size: :a5_landscape, title: "Damped oscillation")
|> Bland.axes(xlabel: "t [s]", ylabel: "x(t)")
|> Bland.line(xs, Enum.map(xs, &(:math.exp(-&1 / 4) * :math.cos(&1))),
label: "response", stroke: :solid)
|> Bland.line(xs, Enum.map(xs, &(:math.exp(-&1 / 4))),
label: "envelope", stroke: :dashed)
|> Bland.legend(position: :top_right)
Bland.to_svg(fig) |> File.write!("plot.svg")API overview
figure/1— begin a new plot with canvas + themeaxes/2— labels, limits, scale type, gridline/4— connected polyline (time series, curves)scatter/4— discrete marked pointsbar/4— categorical bars with hatched fillshistogram/3— binned observations on a numeric axisheatmap/3— 2D grid shaded with a hatch ramparea/4— filled region with hatched fillcolorbar/2— ramp legend for a heatmapgraticule/2— lat/lon grid overlay (for geo figures)basemap/3— add a built-in geographic base layer (coastlines, borders, lunar maria)hline/3,vline/3— reference linesannotate/2— text / arrow overlayslegend/2— add a legendtitle_block/2— drafting title block in the cornerto_svg/1— render
All builder functions return the updated %Bland.Figure{} so you can pipe
freely.
Guides
Summary
Functions
Adds an annotation overlay.
Adds a filled area series.
Sets axis options on a figure.
Adds a categorical bar series. categories and values must line up.
Adds a built-in geographic base layer to the figure.
Renders a Bode plot — magnitude (dB, log-linear) on top, phase (degrees, log-linear) on the bottom — as a two-panel SVG.
Adds a box-and-whisker summary. categories_and_samples pairs each
category label with a list of raw observations; BLAND computes the
quartiles, Tukey-fence whiskers, and outliers for you via
Bland.Stats.boxplot_stats/2.
Attaches a colorbar (ramp legend) to the figure.
Adds contour (iso-level) curves over a 2D scalar grid.
Adds an error-bar series — data points with X and/or Y uncertainty whiskers.
Creates a new figure.
Adds a latitude/longitude graticule as a series of dotted reference
lines. Only meaningful on figures with projection: :mercator or
:equirect.
Composes a list of figures into a single SVG with a grid layout.
Like grid/2, but returns a Kino.Image for Livebook inline
display.
Adds a heatmap series.
Adds a histogram series.
Adds a horizontal reference line at y-value y.
Attaches (or replaces) a legend on the figure.
Adds a line series.
Creates a polar figure.
Adds a polar reference grid to a figure built via polar_figure/1.
Adds a closed polygon series. xs and ys give the vertices; the
renderer connects the last point back to the first.
Adds a Q-Q (quantile-quantile) plot — sample quantiles vs theoretical
quantiles of a named distribution, with a y = x reference line.
Adds a vector-field (quiver) series. Each (xs[i], ys[i]) gets an
arrow with components (us[i], vs[i]).
Adds a scatter series.
Creates a Smith chart figure — a unit-disk canvas for plotting
reflection coefficients Γ in RF / microwave work.
Adds the classical Smith chart grid to a figure: constant-resistance
circles (r = 0.2, 0.5, 1, 2, 5 by default), constant-reactance arcs
(at ±0.2, ±0.5, ±1, ±2, ±5), plus the unit circle boundary and the
real axis.
Adds a stem-plot series — the discrete-signal staple from DSP.
Attaches (or replaces) a drafting title block. See Bland.TitleBlock for
the full field list.
Shortcut for rendering + Livebook inline display.
Renders a figure to an SVG string.
Adds a vertical reference line at x-value x.
Renders a figure and writes it to path.
Functions
@spec annotate( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Adds an annotation overlay.
Supported shapes
text: "…", at: {x, y}— text at data-space coordinates. Accepts:font_sizeand:anchor("start"/"middle"/"end").arrow: {from_xy, to_xy}— straight arrow between two data points.
Example
fig
|> Bland.annotate(text: "peak", at: {3.7, 0.92})
|> Bland.annotate(arrow: {{3.5, 0.85}, {3.7, 0.92}})
@spec area(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a filled area series.
Options
:label:hatch— fill pattern (default cycle):baseline— baseline y-value (default0):stroke— outline dash preset (default:solid):stroke_width
@spec axes( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Sets axis options on a figure.
Options
:xlabel,:ylabel— axis titles:xlim,:ylim— explicit{min, max}or:auto:xscale,:yscale—:linear(default) or:log:grid—:none,:major(default),:both
@spec bar(Bland.Figure.t(), [String.t()], [number()], keyword()) :: Bland.Figure.t()
Adds a categorical bar series. categories and values must line up.
Options
:label— legend text:hatch— pattern preset (seeBland.Patterns). Defaults cycle.:group— any term used to bucket bars for side-by-side grouping. Multiple bar series with distinct:groupvalues render as a grouped bar chart; series sharing a group stack in the same slot.:stroke_width
Example
cats = ["A", "B", "C"]
Bland.bar(fig, cats, [3, 7, 2], label: "trial 1", hatch: :diagonal, group: 1)
|> Bland.bar(cats, [5, 4, 6], label: "trial 2", hatch: :crosshatch, group: 2)
@spec basemap(Bland.Figure.t(), Bland.Basemaps.layer(), keyword()) :: Bland.Figure.t()
Adds a built-in geographic base layer to the figure.
See Bland.Basemaps for the available layers. Typical usage:
Bland.figure(size: :a4_landscape, projection: :mercator,
xlim: {-180, 180}, ylim: {-70, 75})
|> Bland.basemap(:earth_coastlines)
|> Bland.basemap(:earth_borders, stroke: :dashed)
|> Bland.basemap(:earth_tropics, stroke: :dotted)
# Lunar plate
Bland.figure(size: :square, projection: :equirect,
xlim: {-90, 90}, ylim: {-60, 60})
|> Bland.basemap(:moon_maria, hatch: :dots_sparse)Options
:resolution— for:earth_coastlinesand:earth_borders, selects which vendored Natural Earth dataset to load::low(1:110m, default),:high(1:50m), or:schematic(the hand-drawn outlines shipped with BLAND 0.1).:stroke— line-dash preset for open features / outlines (default:solid):stroke_width— override stroke weight:hatch— for closed features, fill with this pattern instead of leaving the interior transparent. Ignored on open features like:earth_tropics.:only— list of feature names to include (filters the layer's built-in feature set):except— list of feature names to exclude
Examples
# High-res countries, filled with a light hatch
Bland.basemap(fig, :earth_borders, resolution: :high,
stroke: :solid, stroke_width: 0.4)
# Only draw a few named countries
Bland.basemap(fig, :earth_borders,
only: ["United States of America", "Canada", "Mexico"])
# Fill lunar maria with a hatched pattern
Bland.basemap(fig, :moon_maria, hatch: :diagonal)
Renders a Bode plot — magnitude (dB, log-linear) on top, phase (degrees, log-linear) on the bottom — as a two-panel SVG.
omegas is a list of angular frequencies (or just frequencies — they
just become x-coordinates on a log axis). Pass either:
- Pre-computed
{mag_db, phase_deg}lists, OR - A transfer-function callback
fn ω -> {real, imag} endreturning the complex value ofH(jω)at each frequency.
Returns an SVG binary ready to write or embed.
Options
:cell_width,:cell_height— per-panel size:title— outer title:xlabel— frequency axis label (default"ω"):mag_label— magnitude y-axis label (default"|H| [dB]"):phase_label— phase y-axis label (default"∠H [°]"):theme— passed through to both panels
Examples
# From precomputed magnitude and phase
Bland.bode(omegas, mag_db, phase_deg)
# From a transfer-function callback: H(s) = 1 / (1 + s/10) evaluated
# at s = jω
Bland.bode(omegas, fn omega ->
{1 / (1 + omega * omega / 100), -omega / (10 + omega * omega / 10)}
end)
@spec boxplot(Bland.Figure.t(), [{String.t(), [number()]}], keyword()) :: Bland.Figure.t()
Adds a box-and-whisker summary. categories_and_samples pairs each
category label with a list of raw observations; BLAND computes the
quartiles, Tukey-fence whiskers, and outliers for you via
Bland.Stats.boxplot_stats/2.
Options
:label— legend text:hatch— IQR box fill (default cycles):box_width— width fraction of the category slot (default0.6):stroke_width
Example
Bland.boxplot(fig, [
{"control", control_samples},
{"treated", treated_samples}
], label: "distribution")
@spec colorbar( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Attaches a colorbar (ramp legend) to the figure.
By default the colorbar describes the last heatmap added. You may
also pass an explicit series: index or a ramp: and range:
directly for standalone ramps.
Options
:position—:right(default),:left,:bottom, or a{px, py}tuple.:label— axis label for the ramp; defaults to the heatmap's:label.:ramp— override the ramp (otherwise inherited from the heatmap series).:range— override the{lo, hi}bounds shown on the ramp.:levels— number of tick marks on the ramp (default 5).
@spec contour(Bland.Figure.t(), [[number()]], keyword()) :: Bland.Figure.t()
Adds contour (iso-level) curves over a 2D scalar grid.
Options
:levels— list of scalar values at which to draw contours (default: 7 evenly-spaced levels across the data range):x_edges,:y_edges— cell boundaries (default0..cols,0..rows):origin—:bottom_left(default) or:top_left:stroke— dash preset (default:solid). Negative levels render dashed automatically to convey sign.:stroke_width,:label
Example
grid =
for j <- -20..20, do: (for i <- -20..20, do: :math.sin(i * 0.2) * :math.cos(j * 0.2))
Bland.contour(fig, grid,
x_edges: Enum.map(-20..21, &(&1 * 0.1)),
y_edges: Enum.map(-20..21, &(&1 * 0.1)),
levels: [-0.8, -0.4, 0, 0.4, 0.8])
@spec errorbar(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds an error-bar series — data points with X and/or Y uncertainty whiskers.
yerr/xerr accept either a list of symmetric half-widths or a list
of {lower, upper} tuples for asymmetric error.
Options
:yerr— symmetric or asymmetric y-error per point:xerr— same for x:marker— marker at each point (default:circle_filled; setnilto suppress):marker_size,:cap_width,:stroke_width,:label
Examples
Bland.errorbar(fig, xs, ys, yerr: sigmas, label: "±1σ")
Bland.errorbar(fig, xs, ys, yerr: Enum.zip(lower, upper))
Bland.errorbar(fig, xs, ys, yerr: yerr, xerr: xerr)
@spec figure(keyword()) :: Bland.Figure.t()
Creates a new figure.
Options
:size— paper preset atom or{width, height}tuple. Default:letter_landscape. SeeBland.Figurefor the full list.:theme— theme preset atom or theme map. SeeBland.Theme.:title,:subtitle— figure-level text:margins—{top, right, bottom, left}in px- All other
Bland.Figurestruct fields are also accepted.
Examples
Bland.figure(size: :a4, title: "Figure 3.2")
Bland.figure(size: {800, 600}, theme: :blueprint)
@spec graticule( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Adds a latitude/longitude graticule as a series of dotted reference
lines. Only meaningful on figures with projection: :mercator or
:equirect.
Options
:lon_step(default30) — meridian spacing in degrees:lat_step(default30) — parallel spacing in degrees:lon_range(default{-180, 180}):lat_range(default{-80, 80}):stroke(default:dotted):labels(defaulttrue) — annotate each line with its lat/lon value:label_position—:lon_edge | :plot_edge | :none(default:lon_edge: meridians label at the equator, parallels label at the western lon bound)
Example
Bland.figure(size: :a4_landscape, projection: :mercator)
|> Bland.graticule(lon_step: 30, lat_step: 20)
|> Bland.line(coast_lons, coast_lats, stroke: :solid)
|> Bland.to_svg()
@spec grid( [Bland.Figure.t()], keyword() ) :: String.t()
Composes a list of figures into a single SVG with a grid layout.
This is how you build multi-panel figures — Bode plots, dashboards, before/after comparisons — while getting a single printable SVG at the end. Each panel renders independently: its own ticks, labels, legend, ornaments.
See Bland.Grid for the full option list. Common options:
:columns,:rows— grid shape:cell_width,:cell_height— pixel size of each cell:gap,:padding— spacing:title— outer title across all panels
Example
a = Bland.figure(title: "Before") |> Bland.line(xs, ys1)
b = Bland.figure(title: "After") |> Bland.line(xs, ys2)
svg = Bland.grid([a, b], columns: 2, title: "Comparison")
@spec grid_to_kino( [Bland.Figure.t()], keyword() ) :: any()
Like grid/2, but returns a Kino.Image for Livebook inline
display.
@spec heatmap(Bland.Figure.t(), [[number()]], keyword()) :: Bland.Figure.t()
Adds a heatmap series.
grid is a 2D list (rows x cols) of numeric values. Each cell is
quantized to one of N levels and filled with the corresponding
pattern from the ramp.
Options
:x_edges— list of lengthcols + 1giving column boundaries in data space. Defaults to0..cols.:y_edges— list of lengthrows + 1giving row boundaries. Defaults to0..rows.:ramp— list of pattern preset atoms, light → dark. Defaults toBland.Heatmap.default_ramp/0(7 levels).:range—{lo, hi}for quantization. Defaults to:auto(min/max of the data).:origin—:bottom_left(default, Cartesian) or:top_left(matrix-style; row 0 renders at the top).:label— label for the colorbar entry.
Examples
# 20 × 20 grid of a 2D Gaussian
grid =
for j <- -10..9, into: [] do
for i <- -10..9, into: [] do
:math.exp(-(i * i + j * j) / 40)
end
end
Bland.figure(size: :square, title: "2D Gaussian")
|> Bland.axes(xlabel: "x", ylabel: "y")
|> Bland.heatmap(grid,
x_edges: Enum.map(-10..10, &(&1 * 1.0)),
y_edges: Enum.map(-10..10, &(&1 * 1.0)),
label: "density")
|> Bland.colorbar()
|> Bland.to_svg()See Bland.Heatmap for the underlying ramp/quantize helpers.
@spec histogram(Bland.Figure.t(), [number()], keyword()) :: Bland.Figure.t()
Adds a histogram series.
observations is a raw list of numeric samples — BLAND bins them for
you. Unlike bar/4, histograms render on a numeric x-axis with bars
flush against each other at bin boundaries.
Options
:bins— bin-count strategy. IntegerNfor exactlyNequal-width bins, or one of:sturges(default),:sqrt,:scott,:freedman_diaconis.:bin_edges— explicit edge list; overrides:bins.:normalize— how values are normalized::count(default) — raw counts per bin:pmf— probability mass:count / total,Σ = 1:density— density:count / (total · width), ∫ = 1:cmf— cumulative mass: rendered as a staircase step line from0at the leftmost edge to1at the rightmost
:density— shorthand fornormalize: :density. Kept for backwards compatibility.:label:hatch— fill pattern preset (default cycles). Ignored for:cmf, which renders as a line.:stroke— for:cmfonly; dash preset for the step line (default:solid):stroke_width
Examples
# 20 equal-width bins, count on the y-axis
Bland.histogram(fig, samples, bins: 20, label: "trial 1")
# PMF — probability mass per bin
Bland.histogram(fig, samples, bins: 30, normalize: :pmf,
label: "Pr{X ∈ bin}")
# Density with Scott's rule
Bland.histogram(fig, samples, bins: :scott, normalize: :density)
# Empirical CDF — renders as a staircase line, not bars
Bland.histogram(fig, samples, bins: 50, normalize: :cmf,
label: "F(x)")
# Explicit edges — useful for apples-to-apples comparison across
# two datasets
Bland.histogram(fig, samples_a,
bin_edges: Enum.map(0..10, &(&1 * 1.0)), label: "A")See Bland.Histogram for the underlying binning helpers.
@spec hline(Bland.Figure.t(), number(), keyword()) :: Bland.Figure.t()
Adds a horizontal reference line at y-value y.
Options: :label, :stroke (default :dashed), :stroke_width.
@spec legend( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Attaches (or replaces) a legend on the figure.
Options
:position—:top_right(default),:top_left,:bottom_right,:bottom_left, or a{px, py}tuple for manual placement.:title— optional bold heading above the entries.
@spec line(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a line series.
Options
:label— legend text. When omitted, the series is unlabeled.:stroke— dash preset::solid,:dashed,:dotted,:dash_dot,:long_dash,:fine. Defaults cycle viaBland.Strokes.:stroke_width— px override:markers—trueto draw markers at each data point:marker— marker preset (seeBland.Markers). Defaults cycle.:marker_size— px override
Example
Bland.line(fig, xs, ys, label: "velocity", stroke: :dashed, markers: true)
@spec polar_figure(keyword()) :: Bland.Figure.t()
Creates a polar figure.
Series data on the returned figure is interpreted as {θ, r} pairs
(θ in radians). The renderer projects every point through
(θ, r) → (r·cos θ, r·sin θ), clips to the disk of radius rmax,
and suppresses the default x/y axes.
Pair with polar_grid/2 to add the concentric / radial reference
grid:
Bland.polar_figure(rmax: 1.0, title: "Antenna gain")
|> Bland.polar_grid(r_ticks: [0.25, 0.5, 0.75, 1.0])
|> Bland.line(thetas, gains)Options
:rmax— radius of the plotting disk in data units (default1.0):size— canvas size; defaults to:squarefor equal aspect- Any other figure option (
:title,:theme, etc.) is forwarded.
@spec polar_grid( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Adds a polar reference grid to a figure built via polar_figure/1.
Produces concentric circles at the requested radii and radial lines at the requested angles, plus perimeter labels for the angles.
Options
:r_ticks— list of radii (default: four evenly-spaced steps ending atrmax):theta_step— angle between radial lines, in degrees (default30):stroke— dash preset for the grid lines (default:dotted):stroke_width— grid line weight (default0.4):labels—true(default) to annotate each angular direction at the perimeter:r_labels—trueto annotate each radius on the 0° ray (defaultfalse):samples— points per circle (default120)
@spec polygon(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a closed polygon series. xs and ys give the vertices; the
renderer connects the last point back to the first.
Unlike area/4, which fills down to a baseline, polygon/4 fills
whatever arbitrary shape the vertices describe — suitable for
country outlines, mare boundaries, inset markers.
Options
:label:hatch— fill pattern;nil(default) leaves the polygon stroke-only (transparent interior):stroke— outline dash preset (default:solid):stroke_width
@spec qq_plot(Bland.Figure.t(), [number()], keyword()) :: Bland.Figure.t()
Adds a Q-Q (quantile-quantile) plot — sample quantiles vs theoretical
quantiles of a named distribution, with a y = x reference line.
Options
:distribution—:normal(default). Other distributions can be added later.:reference—true(default) to draw the y=x reference line:marker— default:circle_open:marker_size,:label
Example
Bland.qq_plot(fig, samples, label: "residuals")
@spec quiver( Bland.Figure.t(), [number()], [number()], [number()], [number()], keyword() ) :: Bland.Figure.t()
Adds a vector-field (quiver) series. Each (xs[i], ys[i]) gets an
arrow with components (us[i], vs[i]).
Options
:scale— multiply each vector before drawing (default1.0):head_size— arrow-head pixel length (default6):stroke,:stroke_width,:label
@spec scatter(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a scatter series.
Accepts :label, :marker, :marker_size, :stroke_width.
@spec smith_figure(keyword()) :: Bland.Figure.t()
Creates a Smith chart figure — a unit-disk canvas for plotting
reflection coefficients Γ in RF / microwave work.
The returned figure has no standard axes, a circular clip to |Γ| ≤ 1, and a square canvas. Pair with smith_grid/2 for the
classical grid of constant-resistance circles and constant-reactance
arcs.
Bland.smith_figure(title: "S₁₁")
|> Bland.smith_grid()
|> Bland.line(gamma_real, gamma_imag, label: "sweep")Convert impedance values to Γ via Bland.Smith.gamma_from_z/1.
Options
:size— canvas size; defaults to:square- All other figure options are forwarded.
@spec smith_grid( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Adds the classical Smith chart grid to a figure: constant-resistance
circles (r = 0.2, 0.5, 1, 2, 5 by default), constant-reactance arcs
(at ±0.2, ±0.5, ±1, ±2, ±5), plus the unit circle boundary and the
real axis.
Options
:r_values— list of normalized resistances for the R-circles (defaultBland.Smith.default_r_values/0):x_values— list of normalized reactance magnitudes; each also draws its negative counterpart (defaultBland.Smith.default_x_values/0):stroke— grid dash preset (default:dotted):stroke_width— (default0.4):boundary_stroke_width— weight of the unit circle and real axis (default0.8):labels—trueto annotate each R circle and X arc (defaulttrue):samples— points per circle (default120)
@spec stem(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a stem-plot series — the discrete-signal staple from DSP.
Each point renders as a vertical line from :baseline (default 0)
up to (x, y), with a marker at the tip.
Options
:baseline,:marker(default:circle_filled),:marker_size,:stroke,:stroke_width,:label
@spec title_block( Bland.Figure.t(), keyword() ) :: Bland.Figure.t()
Attaches (or replaces) a drafting title block. See Bland.TitleBlock for
the full field list.
@spec to_kino(Bland.Figure.t()) :: any()
Shortcut for rendering + Livebook inline display.
In Livebook, Kino.Image.new/2 expects a binary and a MIME type. If
:kino is not installed, call to_svg/1 and wrap the result yourself.
@spec to_svg(Bland.Figure.t()) :: String.t()
Renders a figure to an SVG string.
@spec vline(Bland.Figure.t(), number(), keyword()) :: Bland.Figure.t()
Adds a vertical reference line at x-value x.
Options: :label, :stroke (default :dashed), :stroke_width.
@spec write!(Bland.Figure.t(), Path.t()) :: :ok
Renders a figure and writes it to path.