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.
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.
Attaches a colorbar (ramp legend) to the figure.
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.
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.
Adds a closed polygon series. xs and ys give the vertices; the
renderer connects the last point back to the first.
Adds a scatter series.
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)
@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 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 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 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 scatter(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()
Adds a scatter series.
Accepts :label, :marker, :marker_size, :stroke_width.
@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.