Bland (Elixir Technical Drawing v0.2.0)

Copy Markdown View Source

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

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

annotate(fig, opts)

@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_size and :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}})

area(fig, xs, ys, opts \\ [])

@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 (default 0)
  • :stroke — outline dash preset (default :solid)
  • :stroke_width

axes(fig, opts)

@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

bar(fig, categories, values, opts \\ [])

@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 (see Bland.Patterns). Defaults cycle.
  • :group — any term used to bucket bars for side-by-side grouping. Multiple bar series with distinct :group values 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)

basemap(fig, layer, opts \\ [])

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_coastlines and :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)

colorbar(fig, opts \\ [])

@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).

figure(opts \\ [])

@spec figure(keyword()) :: Bland.Figure.t()

Creates a new figure.

Options

  • :size — paper preset atom or {width, height} tuple. Default :letter_landscape. See Bland.Figure for the full list.
  • :theme — theme preset atom or theme map. See Bland.Theme.
  • :title, :subtitle — figure-level text
  • :margins{top, right, bottom, left} in px
  • All other Bland.Figure struct fields are also accepted.

Examples

Bland.figure(size: :a4, title: "Figure 3.2")
Bland.figure(size: {800, 600}, theme: :blueprint)

graticule(fig, opts \\ [])

@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 (default 30) — meridian spacing in degrees
  • :lat_step (default 30) — parallel spacing in degrees
  • :lon_range (default {-180, 180})
  • :lat_range (default {-80, 80})
  • :stroke (default :dotted)
  • :labels (default true) — 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()

heatmap(fig, grid, opts \\ [])

@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 length cols + 1 giving column boundaries in data space. Defaults to 0..cols.
  • :y_edges — list of length rows + 1 giving row boundaries. Defaults to 0..rows.
  • :ramp — list of pattern preset atoms, light → dark. Defaults to Bland.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.

histogram(fig, observations, opts \\ [])

@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. Integer N for exactly N equal-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 from 0 at the leftmost edge to 1 at the rightmost
  • :density — shorthand for normalize: :density. Kept for backwards compatibility.
  • :label
  • :hatch — fill pattern preset (default cycles). Ignored for :cmf, which renders as a line.
  • :stroke — for :cmf only; 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.

hline(fig, y, opts \\ [])

@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.

legend(fig, opts \\ [])

@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.

line(fig, xs, ys, opts \\ [])

@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 via Bland.Strokes.
  • :stroke_width — px override
  • :markerstrue to draw markers at each data point
  • :marker — marker preset (see Bland.Markers). Defaults cycle.
  • :marker_size — px override

Example

Bland.line(fig, xs, ys, label: "velocity", stroke: :dashed, markers: true)

polygon(fig, xs, ys, opts \\ [])

@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

scatter(fig, xs, ys, opts \\ [])

@spec scatter(Bland.Figure.t(), [number()], [number()], keyword()) :: Bland.Figure.t()

Adds a scatter series.

Accepts :label, :marker, :marker_size, :stroke_width.

title_block(fig, opts)

@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.

to_kino(fig)

@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.

to_svg(fig)

@spec to_svg(Bland.Figure.t()) :: String.t()

Renders a figure to an SVG string.

vline(fig, x, opts \\ [])

@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.

write!(fig, path)

@spec write!(Bland.Figure.t(), Path.t()) :: :ok

Renders a figure and writes it to path.