Bland (Elixir Technical Drawing v0.3.0)

Copy Markdown View Source

BLAND — Elixir Technical Drawing.

Damped oscillation — hero figure with title block

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

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

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)

bode(omegas, mag_or_tf, phase_or_opts \\ [], opts \\ [])

@spec bode([number()], [number()] | function(), [number()] | keyword(), keyword()) ::
  String.t()

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} end returning the complex value of H(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)

boxplot(fig, categories_and_samples, opts \\ [])

@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 (default 0.6)
  • :stroke_width

Example

Bland.boxplot(fig, [
  {"control", control_samples},
  {"treated", treated_samples}
], label: "distribution")

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

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

@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 (default 0..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])

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

@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; set nil to 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)

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()

grid(figures, opts \\ [])

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

grid_to_kino(figures, opts \\ [])

@spec grid_to_kino(
  [Bland.Figure.t()],
  keyword()
) :: any()

Like grid/2, but returns a Kino.Image for Livebook inline display.

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)

polar_figure(opts \\ [])

@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 (default 1.0)
  • :size — canvas size; defaults to :square for equal aspect
  • Any other figure option (:title, :theme, etc.) is forwarded.

polar_grid(fig, opts \\ [])

@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 at rmax)
  • :theta_step — angle between radial lines, in degrees (default 30)
  • :stroke — dash preset for the grid lines (default :dotted)
  • :stroke_width — grid line weight (default 0.4)
  • :labelstrue (default) to annotate each angular direction at the perimeter
  • :r_labelstrue to annotate each radius on the 0° ray (default false)
  • :samples — points per circle (default 120)

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

qq_plot(fig, samples, opts \\ [])

@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.
  • :referencetrue (default) to draw the y=x reference line
  • :marker — default :circle_open
  • :marker_size, :label

Example

Bland.qq_plot(fig, samples, label: "residuals")

quiver(fig, xs, ys, us, vs, opts \\ [])

@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 (default 1.0)
  • :head_size — arrow-head pixel length (default 6)
  • :stroke, :stroke_width, :label

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.

smith_figure(opts \\ [])

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

smith_grid(fig, opts \\ [])

@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 (default Bland.Smith.default_r_values/0)
  • :x_values — list of normalized reactance magnitudes; each also draws its negative counterpart (default Bland.Smith.default_x_values/0)
  • :stroke — grid dash preset (default :dotted)
  • :stroke_width — (default 0.4)
  • :boundary_stroke_width — weight of the unit circle and real axis (default 0.8)
  • :labelstrue to annotate each R circle and X arc (default true)
  • :samples — points per circle (default 120)

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

@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

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.