The pipeline
Compose, apply, render
import Visualize.Chart.Build
rows = [%{t: 1, v: 3}, %{t: 2, v: 9}, %{t: 3, v: 4}]
{:ok, design} =
Visualize.Chart.compose([
chart(meta: %{name: "Load"}),
source(:load, [:t, :v]),
cartesian(),
linear_scale(:x),
linear_scale(:y, domain: [0, :auto]),
axis(:x, :bottom),
axis(:y, :left),
line(:load, %{x: :t, y: :v})
])
{:ok, applied} = Visualize.Chart.apply(design, sources: %{load: rows})
svg = Visualize.Chart.render(applied, root: true)A design is a map. Every Build function returns a fragment, and compose/1 folds them.
apply/2 validates, binds rows and realises the frames. render/2 draws.
Store and reload
chart = Visualize.Chart.from_map!(design)
map = Visualize.Chart.to_map(chart)
{:ok, ^chart} = Visualize.Chart.from_map(map)to_json/1 and from_json/1 do the same through JSON (needs Jason).
Apply options
| Option | Meaning |
|---|---|
sources: | the pool: slot name to rows, columns, Explorer or Nx |
vars: | variable name to value |
size: | {width, height}, default {600, 400} |
theme: | a %Visualize.Theme{} |
now: | the instant window/3 measures back from |
Compose or stack
compose/1 | stack/1 | |
|---|---|---|
| a name declared twice | :conflict | the higher layer wins |
| marks, labels, axes | append | replace by id: / {scale, side} |
| returns | {:ok, map} | map |
explain/1 shows which layer set each value. free_vars/1 lists open variables.
Build functions
Design
| Function | Sets |
|---|---|
chart(opts) | the seed: version, meta: |
source(name, fields, opts) | a slot; types:, default: |
var(name, default) | a variable's declaration |
style(name, keys) | a named style |
gradient(name, kind, stops: …) | a paint; use with paint(name) |
theme(:dark) | the theme, by name or inline |
layout(columns:, rows:, gap:) | the grid frames sit on |
interaction(sync: "group") | shared hover and brush |
Frames
| Function | Frame |
|---|---|
cartesian(opts) | x and y |
polar(opts) | angle and r; marks bend |
geo(projection: %{type: …}) | a map projection |
facet(by: field, columns: n) | small multiples |
in_frame(name, fragments) | puts fragments in a named frame |
adopt(:x, frame) | shares another frame's scale |
Frame options: margin:, box: [x, y, w, h] (fractions), cell: [c, r], span:, z:,
background:.
Scales
linear_scale log_scale power_scale sqrt_scale symlog_scale time_scale
band_scale ordinal_scale quantize_scale quantile_scale threshold_scale
sequential_scale diverging_scale radial_scale, each (name, opts).
Options: domain: (:auto, [0, :auto], a list), range:, nice:, clamp:,
padding: (band), start:/sweep: (angle), zone: (time), transition:.
Names with a meaning: :x, :y, :angle, :r, :color, :x_offset, :y_offset.
Furniture
| Function | Draws |
|---|---|
axis(scale, side, opts) | ticks:, format:, unit:, grid: |
legend(scale, opts) | position:, inside:, title: |
label(anchor, text, opts) | a text in the frame |
title(text) | the :title label |
Anchors: :title, :subtitle, :caption, {:axis, s}, {:frame, :top_left} (any corner,
or :center), {:data, [x, y]}.
Marks
| Function | Channels |
|---|---|
line area | x, y (y0, y1) |
rect | x0, x1, y0, y1, value |
circle symbol | x, y, value |
rule | x, or y in polar |
band x_band | x0, x1 |
percentile_band | x, median, inner_lo… |
arc | value (or start, end) |
rose | angle, value |
needle | x (polar only) |
path | path |
text | x, y, value |
tiles | none (a basemap) |
Every one takes (data, channels, opts). series works on all but percentile_band and
tiles. Common opts: style:, options:, label:, id:, offset:, scales:.
Transforms
from(source) starts a pipeline and each step appends:
filter(field) bin(field) stack(fields) fold(fields) sum(field) sort(field)
take(n) window(field) lttb(fields, n) m4(fields, width) spectrum(field)
treemap() pack() partition() tree() cluster() chord(fields, field)
sankey(fields, field) force(fields) projection(fields) contour(field, size)
density(fields) delaunay(fields) voronoi(fields) hexbin(fields)
Each takes the data first: from(:s) |> bin(:v, thresholds: 20).
Style keys
fill fill_opacity stroke stroke_width stroke_dasharray stroke_style opacity
font_family font_size font_weight text_anchor curve symbol symbol_size
blend effect extends class
Values: a literal, a theme slot (:series_1, :axis, :text, :background),
{:field, f}, paint(name), :contrast, :none.
Recipes
Line, several series
import Visualize.Chart.Build
load = [
%{t: 1, v: 3, host: "a"}, %{t: 2, v: 5, host: "a"},
%{t: 1, v: 2, host: "b"}, %{t: 2, v: 6, host: "b"}
]
{:ok, line_chart} =
Visualize.Chart.compose([
chart(),
source(:load, [:t, :v, :host]),
style(:series, stroke_width: 2, curve: :monotone_x),
cartesian(),
linear_scale(:x),
linear_scale(:y, domain: [0, :auto], nice: true),
ordinal_scale(:color, domain: ["a", "b"]),
axis(:x, :bottom),
axis(:y, :left, grid: true),
legend(:color),
line(:load, %{x: :t, y: :v, series: :host}, style: :series)
])
{:ok, _} = Visualize.Chart.apply(line_chart, sources: %{load: load})Bar
import Visualize.Chart.Build
letters = [%{letter: "A", n: 8}, %{letter: "B", n: 2}, %{letter: "C", n: 5}]
{:ok, bar_chart} =
Visualize.Chart.compose([
chart(),
source(:letters, [:letter, :n]),
cartesian(),
band_scale(:x, padding: 0.1),
linear_scale(:y, domain: [0, :auto], nice: true),
axis(:x, :bottom),
axis(:y, :left),
# x0 and x1 on one field are the band's two edges.
rect(:letters, %{x0: :letter, x1: :letter, y0: 0, y1: :n})
])
{:ok, _} = Visualize.Chart.apply(bar_chart, sources: %{letters: letters})Grouped bars: fold/3 the columns into rows, add band_scale(:x_offset) and pass
offset: %{x: :series} to the rect.
Stacked area
import Visualize.Chart.Build
fruit = [
%{x: 1, apples: 10, pears: 4},
%{x: 2, apples: 12, pears: 7},
%{x: 3, apples: 9, pears: 11}
]
{:ok, stacked_area} =
Visualize.Chart.compose([
chart(),
source(:fruit, [:x, :apples, :pears]),
cartesian(margin: %{top: 20, right: 80, bottom: 30, left: 40}),
linear_scale(:x),
linear_scale(:y, domain: [0, :auto]),
ordinal_scale(:color, domain: [:apples, :pears]),
axis(:x, :bottom),
axis(:y, :left),
# In the right margin, clear of the layers.
legend(:color, position: :right, inside: false),
area(stack(from(:fruit), [:apples, :pears]), %{x: :x, y: :y1, y0: :y0, series: :key})
])
{:ok, _} = Visualize.Chart.apply(stacked_area, sources: %{fruit: fruit})Scatter, coloured by category
import Visualize.Chart.Build
points = [
%{x: 10, y: 20, size: 4, kind: "A"},
%{x: 40, y: 70, size: 8, kind: "B"},
%{x: 80, y: 30, size: 6, kind: "A"}
]
{:ok, scatter} =
Visualize.Chart.compose([
chart(),
source(:points, [:x, :y, :size, :kind]),
style(:point, fill: {:field, :kind}, opacity: 0.7),
cartesian(),
linear_scale(:x, domain: [0, 100]),
linear_scale(:y, domain: [0, 100]),
ordinal_scale(:color, domain: ["A", "B"]),
axis(:x, :bottom),
axis(:y, :left),
legend(:color, title: ["Kind"]),
# value on a circle is its radius in pixels.
circle(:points, %{x: :x, y: :y, value: :size}, style: :point)
])
{:ok, _} = Visualize.Chart.apply(scatter, sources: %{points: points})Pie and donut
import Visualize.Chart.Build
slices = [%{name: "A", share: 50}, %{name: "B", share: 30}, %{name: "C", share: 20}]
{:ok, donut} =
Visualize.Chart.compose([
chart(),
source(:slices, [:name, :share]),
style(:slice, fill: {:field, :name}, stroke: :background),
polar(),
ordinal_scale(:color, domain: ["A", "B", "C"]),
legend(:color),
# Leave out inner_radius for a pie.
arc(:slices, %{value: :share},
options: %{inner_radius: 90, outer_radius: 150, pad_angle: 0.02},
style: :slice,
label: %{text: [{:field, :name}], style: [:label, %{fill: :contrast}]}
),
label({:frame, :center}, ["100%"])
])
{:ok, _} = Visualize.Chart.apply(donut, sources: %{slices: slices})Map
import Visualize.Chart.Build
globe = [%{shape: %{"type" => "Sphere"}}]
cities = [%{name: "Oslo", lon: 10.75, lat: 59.91}, %{name: "Lima", lon: -77.04, lat: -12.05}]
{:ok, map_chart} =
Visualize.Chart.compose([
chart(),
source(:globe, [:shape]),
source(:cities, [:name, :lon, :lat]),
style(:ocean, fill: :surface, stroke: :axis),
# Natural Earth is about 5.5 scales wide: 100 fills a 600-wide chart.
geo(projection: %{type: :natural_earth, scale: 100}),
# field: a GeoJSON column, projected to a path.
path(projection(from(:globe), nil, field: :shape), %{path: :path}, style: :ocean),
# fields: [lon, lat], projected to x and y pixels.
circle(projection(from(:cities), [:lon, :lat]), %{x: :x, y: :y},
label: %{text: [{:field, :name}], anchor: :right}
)
])
{:ok, _} = Visualize.Chart.apply(map_chart, sources: %{globe: globe, cities: cities})Land and countries are GeoJSON features under a column, drawn as the globe is.
Rendering
SVG
{:ok, applied} = Visualize.Chart.apply(scatter, sources: %{points: points}, size: {480, 320})
# A bare <g>, for a template.
group = Visualize.Chart.render(applied)
# A whole accessible <svg> document.
document = Visualize.Chart.render(applied, root: true)Theme colours are CSS custom properties, so a stylesheet can re-theme the page.
Canvas and binary
# Canvas 2D commands as JSON, for the canvas hook.
commands = Visualize.Chart.render(applied, backend: :canvas)
# A compact binary stream, for the binary canvas hook (needs Nx).
literal = Visualize.Chart.generate(applied, resolve: :literal)
{:ok, stream} = Visualize.Backend.CanvasBinary.encode_element(literal)Above about 1,000 points, prefer a canvas backend.
Hybrid and live data
{:ok, %{main: compiled}} = Visualize.Chart.compile(applied)
# Static furniture as SVG, dense marks on a canvas layer.
hybrid = Visualize.Chart.Compiled.render(compiled, %{points: points})
# One tick of live data: a payload to push and the next state.
{payload, compiled} = Visualize.Chart.Compiled.tick(compiled, %{points: points}, [])The LiveView guide covers the components and hooks that draw these.
PNG
document = Visualize.Chart.generate(applied, root: true, resolve: :literal)
case Visualize.Render.to_png(document, scale: 2, background: "#ffffff") do
{:ok, png, _warnings} -> byte_size(png)
{:error, :no_rasterizer} -> :install_resvg
endPNG needs the resvg binary (0.45 or later) on the PATH, or
config :visualize, :resvg, path. Use literal colours: resvg cannot read CSS variables.