Skip to content

Latest commit

 

History

History
881 lines (711 loc) · 24.8 KB

File metadata and controls

881 lines (711 loc) · 24.8 KB

Matplotex

A comprehensive charting library for Elixir. All coordinate math runs through Nx tensor operations. Charts compile to SVG strings.

🚀 Release v0.5.0

This release is a ground-up rewrite: every coordinate calculation now runs on Nx tensors, and charts are built through a new composable Chart / Axis / Canvas / Dataset API that compiles straight to SVG strings.

11 new chart types (up from 6 in v0.4.8):

  • Area — filled line charts
  • Step — stepped line charts
  • Bubble — scatter with sized points
  • Heatmap — color-mapped grids
  • Box Plot — quartile/whisker distributions
  • Violin — density distributions
  • Radar — multi-axis polar charts
  • Donut — ring charts
  • Funnel — stage/conversion charts
  • Candlestick — OHLC financial charts
  • Waterfall — cumulative running-total charts

Carried over and rebuilt on the new engine: Line, Bar, Pie, Spline, Histogram, Scatter.

Other features added in this release:

  • Inclined and vertical tick labels for dense axes
  • Configurable axis limits (x/y min & max) to pan and clip plots
  • Grid styling controls (dashed grids, grid color, axis line color & width)
  • Optional axis labels and legends

Gallery

Line Bar Scatter
Line Bar Scatter
Area Step Spline
Area Step Spline
Histogram Bubble Heatmap
Histogram Bubble Heatmap
Box Violin Radar
Box Plot Violin Radar
Pie Donut Funnel
Pie Donut Funnel
Candlestick Waterfall
Candlestick Waterfall
Inclined Labels Vertical Labels
Inclined Labels Vertical Labels

Installation

Add matplotex to your dependencies in mix.exs:

def deps do
  [
    {:matplotex, "~> 0.1.0"}
  ]
end

Quick Start

alias Matplotex.{Chart, Axis, Canvas, Dataset}
alias Matplotex.Render.SVG

Chart.new(:line,
  canvas: %Canvas{
    grid_style:      :dashed,
    grid_color:      "#c8d8e8",
    axis_line_color: "#4a6fa5",
    axis_line_width: 2.0
  }
)
|> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Month"})
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Revenue"})
|> Chart.add_data(Enum.zip(1..12, [3,5,4,8,7,9,6,10,8,11,9,12]), name: "2024")
|> Chart.set_title("Monthly Revenue")
|> Chart.compile()
|> SVG.to_svg()
# => "<svg>...</svg>"

Core Concepts

Building a Chart

Every chart follows the same pipeline:

Chart.new/2  →  put_axis/3  →  put_dataset/2  →  compile/1  →  SVG.to_svg/1

All builder functions return an updated chart struct — the API is fully immutable and pipe-friendly.

Adding Data

From {x, y} pairs — the simplest way:

Chart.add_data(chart, [{1, 10}, {2, 20}, {3, 15}], name: "Series A", color: "#4e79a7")

From columns — when you have separate lists or need z values:

ds = Dataset.from_columns(
  x: [1.0, 2.0, 3.0],
  y: [10.0, 20.0, 15.0],
  z: [5.0, 8.0, 12.0],   # optional third dimension (size, intensity, etc.)
  labels: ["A", "B", "C"],
  name: "Series",
  color: "#4e79a7"
)

Chart.put_dataset(chart, ds)

Axes

%Axis{
  dimension:          :x | :y | :z,                    # which data column maps to this axis
  position:           :bottom | :top | :left | :right,  # where the axis is drawn
  label:              "Axis label",                     # optional title
  tick_count:         5,                                # approximate number of ticks
  tick_format:        &Float.to_string/1,               # optional custom formatter
  tick_label_rotation: :horizontal                      # see "Tick Label Rotation" below
}

Tick Label Rotation

Use tick_label_rotation on any x-axis to rotate tick labels when they are long or tightly packed. The option only applies to x-axes (:bottom and :top positions); y-axis labels are already vertical.

Value Angle Anchor When to use
:horizontal 0° (default) center Short labels with plenty of space
:inclined −45° end Medium-length labels, the most common fix for overlapping ticks
:vertical −90° end Very long labels or many ticks
any number custom (degrees) end if negative, start if positive Fine-grained control

Inclined (−45°) — most common fix for crowded labels:

Inclined Labels

alias Matplotex.{Chart, Axis, Canvas}
alias Matplotex.Render.SVG

months = ~w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]

Chart.new(:bar, canvas: %Canvas{grid_style: :dashed, grid_color: "#c8d8e8"})
|> Chart.put_axis(:x1, %Axis{
     dimension: :x, position: :bottom, label: "Month",
     tick_count: 12,
     tick_format: fn x -> Enum.at(months, round(x) - 1) end,
     tick_label_rotation: :inclined
   })
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left, label: "Sales"})
|> Chart.add_data(Enum.zip(1..12, [42,38,55,61,49,70,65,80,74,68,77,90]),
     name: "2024", color: "#4e79a7")
|> Chart.set_title("Monthly Sales — Inclined Labels")
|> Chart.set_axis_limits(:x1, 1.0, 12.0)
|> Chart.compile()
|> SVG.to_svg()

Vertical (−90°) — for very long category names:

Vertical Labels

products = ["Product Alpha", "Product Beta", "Product Gamma",
            "Product Delta", "Product Epsilon"]

Chart.new(:bar, canvas: %Canvas{grid_style: :solid, grid_color: "#e0e0e0"})
|> Chart.put_axis(:x1, %Axis{
     dimension: :x, position: :bottom,
     tick_count: 5,
     tick_format: fn x -> Enum.at(products, round(x) - 1) end,
     tick_label_rotation: :vertical
   })
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left, label: "Units Sold"})
|> Chart.add_data(Enum.zip(1..5, [120, 95, 142, 88, 110]),
     name: "Q3", color: "#59a14f")
|> Chart.set_title("Sales by Product — Vertical Labels")
|> Chart.set_axis_limits(:x1, 1.0, 5.0)
|> Chart.compile()
|> SVG.to_svg()

Custom angle — fine-grained control:

Chart.new(:line)
|> Chart.put_axis(:x1, %Axis{
     dimension: :x, position: :bottom,
     tick_label_rotation: -30   # degrees; negative = counter-clockwise
   })
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left})
|> Chart.add_data(Enum.zip(1..8, [3,5,4,8,7,9,6,10]), name: "Series")
|> Chart.compile()
|> SVG.to_svg()

Canvas

Control physical size, fonts, grid, and axis appearance:

canvas = %Matplotex.Canvas{
  figsize:          {10, 6},      # width × height in inches
  dpi:              96,
  margin:           0.05,         # fraction of canvas on each side
  background:       "#ffffff",    # SVG background fill (nil = transparent)
  title_font_size:  18,
  tick_font_size:   12,
  label_font_size:  14,
  grid_color:       "#e0e0e0",
  grid_style:       :dashed,      # :solid | :dashed | :dotted | :none
  grid_width:       1,
  axis_line_color:  "#555555",
  axis_line_width:  1.5,
  axis_line_style:  :solid,       # :solid | :dashed | :none
  origin_at_zero:   true          # force axis min to 0 when all data is positive
}

Chart.new(:line, canvas: canvas)

Axis Limits (xmin / xmax / ymin / ymax)

By default, axis domains are inferred from the data. Use set_axis_limits/4 to pin them explicitly — useful for zooming into a region, keeping consistent scales across charts, or padding beyond the data range.

Chart.new(:line)
|> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Month"})
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Revenue"})
|> Chart.add_data(Enum.zip(1..12, [3,5,4,8,7,9,6,10,8,11,9,12]), name: "2024")
|> Chart.set_axis_limits(:x1, 0.0, 15.0)   # x from 0 to 15 regardless of data
|> Chart.set_axis_limits(:y1, 0.0, 20.0)   # y from 0 to 20 regardless of data
|> Chart.compile()
|> SVG.to_svg()
  • Limits apply per axis — you can fix one axis while leaving the other auto-scaled.
  • Tick labels and tick pixel positions are computed from the explicit limits.
  • The guard min < max is enforced at call time; swapped values raise FunctionClauseError.

Viewport (Zoom & Pan)

Chart.set_viewport(chart, %Matplotex.Viewport{
  zoom_x: 1.5,   # horizontal zoom factor
  zoom_y: 1.0,
  pan_x:  0.1,   # normalized offset
  pan_y:  0.0
})

Chart Types

Line

Continuous path connecting {x, y} points. Points are auto-sorted by x. Supports multiple series.

Line Chart

alias Matplotex.{Chart, Axis, Canvas}
alias Matplotex.Render.SVG

svg =
  Chart.new(:line,
    canvas: %Canvas{
      grid_style:      :dashed,
      grid_color:      "#c8d8e8",
      grid_width:      1,
      axis_line_color: "#4a6fa5",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Month"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Sales"})
  |> Chart.add_data(Enum.zip(1..12, [3,5,4,8,7,9,6,10,8,11,9,12]),
       name: "2023", color: "#4e79a7")
  |> Chart.add_data(Enum.zip(1..12, [2,4,6,5,9,8,7,11,9,12,10,13]),
       name: "2024", color: "#e15759")
  |> Chart.set_title("Monthly Sales")
  |> Chart.set_axis_limits(:x1, 0.0, 13.0)
  |> Chart.set_axis_limits(:y1, 0.0, 15.0)
  |> Chart.compile()
  |> SVG.to_svg()

Bar

Vertical bars from a baseline of y=0. x values are treated as category indices.

Bar Chart

alias Matplotex.Canvas

svg =
  Chart.new(:bar,
    canvas: %Canvas{
      grid_style:      :solid,
      grid_color:      "#d4d4d4",
      grid_width:      1,
      axis_line_color: "#333333",
      axis_line_width: 1.5
    }
  )
  |> Chart.put_axis(:x1, %Axis{
       dimension: :x, position: :bottom, label: "Quarter",
       tick_label_rotation: :inclined
     })
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Revenue ($k)"})
  |> Chart.add_data([{1, 42}, {2, 58}, {3, 51}, {4, 73}], name: "Revenue", color: "#59a14f")
  |> Chart.set_title("Quarterly Revenue")
  |> Chart.set_axis_limits(:x1, 0.5, 4.5)
  |> Chart.compile()
  |> SVG.to_svg()

Scatter

One circle per {x, y} point. Use color per dataset to distinguish groups.

Scatter Chart

svg =
  Chart.new(:scatter,
    canvas: %Canvas{
      grid_style:      :dotted,
      grid_color:      "#cccccc",
      grid_width:      1,
      axis_line_color: "#e15759",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Height (cm)"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Weight (kg)"})
  |> Chart.add_data(
       [{160, 55}, {170, 65}, {175, 72}, {180, 80}, {185, 85}],
       name: "Group A", color: "#4e79a7")
  |> Chart.add_data(
       [{155, 50}, {165, 60}, {172, 68}, {178, 75}],
       name: "Group B", color: "#f28e2b")
  |> Chart.set_title("Height vs Weight")
  |> Chart.set_axis_limits(:x1, 150.0, 190.0)
  |> Chart.compile()
  |> SVG.to_svg()

Area

Filled region between a baseline and the y values. Ideal for showing volume over time.

Area Chart

svg =
  Chart.new(:area,
    canvas: %Canvas{
      grid_style:      :dashed,
      grid_color:      "#b2dfdb",
      grid_width:      1.5,
      axis_line_color: "#00796b",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Day"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Visitors"})
  |> Chart.add_data(
       Enum.zip(1..7, [120, 180, 150, 210, 195, 240, 230]),
       name: "Unique Visitors", color: "#76b7b2")
  |> Chart.set_title("Weekly Traffic")
  |> Chart.set_axis_limits(:x1, 1.0, 7.0)
  |> Chart.compile()
  |> SVG.to_svg()

Step

Staircase line — horizontal segment first, then vertical. Good for discrete state changes.

Step Chart

svg =
  Chart.new(:step,
    canvas: %Canvas{
      grid_style:      :none,
      axis_line_color: "#222222",
      axis_line_width: 2.5
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Time"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "State"})
  |> Chart.add_data(
       [{0, 0}, {1, 1}, {2, 1}, {3, 0}, {4, 2}, {5, 2}, {6, 0}],
       name: "Signal", color: "#e15759")
  |> Chart.set_title("Signal Over Time")
  |> Chart.set_axis_limits(:x1, -0.5, 6.5)
  |> Chart.set_axis_limits(:y1, -0.5, 3.0)
  |> Chart.compile()
  |> SVG.to_svg()

Spline

Smooth Catmull-Rom curve through {x, y} points. Same API as line.

Spline Chart

svg =
  Chart.new(:spline,
    canvas: %Canvas{
      grid_style:      :dotted,
      grid_color:      "#ddd8f0",
      grid_width:      1,
      axis_line_color: "#b07aa1",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "x"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "f(x)"})
  |> Chart.add_data(
       Enum.zip(0..9, [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]),
       name: "x²", color: "#b07aa1")
  |> Chart.set_title("Smooth Curve")
  |> Chart.set_axis_limits(:x1, 0.0, 10.0)
  |> Chart.set_axis_limits(:y1, 0.0, 90.0)
  |> Chart.compile()
  |> SVG.to_svg()

Histogram

Auto-bins a list of samples using Sturges' rule. Pass raw values to x (the y column is computed).

Histogram

alias Matplotex.{Canvas, Dataset}

samples = for _ <- 1..2000, do: :rand.normal() * 15 + 100

svg =
  Chart.new(:histogram,
    canvas: %Canvas{
      grid_style:      :solid,
      grid_color:      "#e8e8e8",
      grid_width:      1,
      axis_line_color: "#555555",
      axis_line_width: 1.5
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Value"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Count"})
  |> Chart.put_dataset(Dataset.from_columns(x: samples, y: samples, color: "#4e79a7"))
  |> Chart.set_title("Distribution of Values")
  |> Chart.compile()
  |> SVG.to_svg()

Scatter with Bubble (Bubble Chart)

Like scatter, but the z column controls each point's radius. Area is proportional to z.

Bubble Chart

ds = Dataset.from_columns(
  x:     [1, 2, 3, 4, 5],
  y:     [2, 4, 1, 5, 3],
  z:     [10, 30, 15, 25, 40],  # bubble sizes
  name:  "Products",
  color: "#76b7b2"
)

svg =
  Chart.new(:bubble,
    canvas: %Canvas{
      grid_style:      :dashed,
      grid_color:      "#b2e0dc",
      grid_width:      1,
      axis_line_color: "#76b7b2",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Market Share"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Growth Rate"})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Product Portfolio")
  |> Chart.set_axis_limits(:x1, 0.0, 6.0)
  |> Chart.set_axis_limits(:y1, 0.0, 6.0)
  |> Chart.compile()
  |> SVG.to_svg()

Pie

Circular slices sized by y values. Pass slice names in labels.

Pie Chart

ds = Dataset.from_columns(
  y:      [40, 30, 20, 10],
  labels: ["Organic", "Paid", "Social", "Direct"]
)

svg =
  Chart.new(:pie, canvas: %Canvas{grid_style: :none, axis_line_style: :none})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Traffic Sources")
  |> Chart.compile()
  |> SVG.to_svg()

Donut

Same as pie with a hollow centre. Useful when you want to annotate the centre.

Donut Chart

ds = Dataset.from_columns(
  y:      [55, 25, 20],
  labels: ["Complete", "In Progress", "Not Started"]
)

svg =
  Chart.new(:donut, canvas: %Canvas{grid_style: :none, axis_line_style: :none})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Project Status")
  |> Chart.compile()
  |> SVG.to_svg()

Heatmap

Colored grid where x = column index, y = row index, z = intensity. Colors use the viridis scale.

Heatmap

# Build a 5×5 grid of {col, row, intensity} triples
{xs, ys, zs} =
  for(row <- 0..4, col <- 0..4, do: {col * 1.0, row * 1.0, :rand.uniform() * 100})
  |> Enum.reduce({[], [], []}, fn {x, y, z}, {xs, ys, zs} ->
    {xs ++ [x], ys ++ [y], zs ++ [z]}
  end)

ds = Dataset.from_columns(x: xs, y: ys, z: zs)

svg =
  Chart.new(:heatmap,
    canvas: %Canvas{
      grid_style:      :solid,
      grid_color:      "#999999",
      grid_width:      1.5,
      axis_line_color: "#222222",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Column"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Row"})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Heatmap Example")
  |> Chart.compile()
  |> SVG.to_svg()

Box Plot

Five-number summary (min, Q1, median, Q3, max) per group. Pass raw samples with their group index.

Box Plot

# Group 1 (x=1) and Group 2 (x=2)
group_xs = List.duplicate(1, 50) ++ List.duplicate(2, 50)
group_ys =
  Enum.map(1..50, fn _ -> :rand.normal() * 5 + 20 end) ++
  Enum.map(1..50, fn _ -> :rand.normal() * 8 + 30 end)

ds = Dataset.from_columns(x: group_xs, y: group_ys)

svg =
  Chart.new(:box,
    canvas: %Canvas{
      grid_style:      :dashed,
      grid_color:      "#e0e0e0",
      grid_width:      1,
      axis_line_color: "#555555",
      axis_line_width: 1.5
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Group"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Value"})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Box Plot by Group")
  |> Chart.set_axis_limits(:x1, 0.5, 2.5)
  |> Chart.compile()
  |> SVG.to_svg()

Violin

Mirrored KDE density per group. Same data format as box plot.

Violin Plot

group_xs = List.duplicate(1, 100) ++ List.duplicate(2, 100)
group_ys =
  Enum.map(1..100, fn _ -> :rand.normal() * 3 + 10 end) ++
  Enum.map(1..100, fn _ -> :rand.normal() * 6 + 20 end)

ds = Dataset.from_columns(x: group_xs, y: group_ys, color: "#59a14f")

svg =
  Chart.new(:violin,
    canvas: %Canvas{
      grid_style:      :dotted,
      grid_color:      "#c8e6c9",
      grid_width:      1,
      axis_line_color: "#388e3c",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Group"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Samples"})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Violin Plot by Group")
  |> Chart.set_axis_limits(:x1, 0.5, 2.5)
  |> Chart.compile()
  |> SVG.to_svg()

Radar / Spider

N-dimensional data on radial axes. y = values, labels = axis names. Multiple datasets overlay as polygons.

Radar Chart

ds = Dataset.from_columns(
  y:      [0.8, 0.6, 0.9, 0.4, 0.7, 0.85],
  labels: ["Speed", "Strength", "Agility", "Defense", "Skill", "Stamina"],
  name:   "Player A",
  color:  "#4e79a7"
)

svg =
  Chart.new(:radar, canvas: %Canvas{grid_style: :none, axis_line_style: :none})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Player Stats")
  |> Chart.compile()
  |> SVG.to_svg()

Candlestick

OHLC bars for financial data. x = time, y = close price. Open, high, and low go in extra.

Candlestick Chart

times  = [1, 2, 3, 4, 5]
closes = [100.0, 103.0, 101.0, 106.0, 104.0]
opens  = [99.0,  100.0, 104.0, 102.0, 105.0]
highs  = [104.0, 105.0, 105.0, 108.0, 107.0]
lows   = [98.0,   99.0, 100.0, 101.0, 103.0]

ds =
  Dataset.from_columns(x: times, y: closes)
  |> then(fn d ->
    %{d | extra: %{
      open: Nx.tensor(opens, type: :f64),
      high: Nx.tensor(highs, type: :f64),
      low:  Nx.tensor(lows,  type: :f64)
    }}
  end)

svg =
  Chart.new(:candlestick,
    canvas: %Canvas{
      grid_style:      :solid,
      grid_color:      "#eeeeee",
      grid_width:      1,
      axis_line_color: "#111111",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Time"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Price ($)"})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("AAPL — 5-Day OHLC")
  |> Chart.set_axis_limits(:x1, 0.5, 5.5)
  |> Chart.set_axis_limits(:y1, 95.0, 110.0)
  |> Chart.compile()
  |> SVG.to_svg()

Bullish candles (close ≥ open) render green; bearish candles render red.


Waterfall

Cumulative bars where each bar starts where the previous ended. Positive increments are green, negative red.

Waterfall Chart

svg =
  Chart.new(:waterfall,
    canvas: %Canvas{
      grid_style:      :dashed,
      grid_color:      "#ffe0cc",
      grid_width:      1,
      axis_line_color: "#f28e2b",
      axis_line_width: 2.0
    }
  )
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom, label: "Category"})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left,   label: "Amount ($k)"})
  |> Chart.add_data(
       [{1, 50}, {2, -20}, {3, 30}, {4, -10}, {5, 40}],
       name: "Cash Flow")
  |> Chart.set_title("Cash Flow Waterfall")
  |> Chart.set_axis_limits(:x1, 0.5, 5.5)
  |> Chart.set_axis_limits(:y1, 0.0, 100.0)
  |> Chart.compile()
  |> SVG.to_svg()

Funnel

Horizontal bars centred on the axis, each narrower than the one above. Pass stage values largest-first in y.

Funnel Chart

ds = Dataset.from_columns(
  y:      [1000, 750, 400, 200, 80],
  labels: ["Awareness", "Interest", "Consideration", "Intent", "Purchase"]
)

svg =
  Chart.new(:funnel, canvas: %Canvas{grid_style: :none, axis_line_style: :none})
  |> Chart.put_dataset(ds)
  |> Chart.set_title("Sales Funnel")
  |> Chart.compile()
  |> SVG.to_svg()

Supported Chart Types

Atom Chart Key columns
:line Line x, y
:bar Bar x (category), y
:scatter Scatter x, y
:area Area x, y
:step Step x, y
:spline Spline x, y
:histogram Histogram x (raw samples)
:bubble Bubble x, y, z (size)
:pie Pie y, labels
:donut Donut y, labels
:heatmap Heatmap x (col), y (row), z
:box Box Plot x (group), y (samples)
:violin Violin x (group), y (samples)
:radar Radar/Spider y (values), labels
:candlestick Candlestick x, y (close), extra OHLC
:waterfall Waterfall x, y (increments)
:funnel Funnel y, labels
Chart.supported_types()
# => [:line, :bar, :scatter, :area, :histogram, :pie, :donut, :heatmap,
#     :box, :violin, :bubble, :radar, :candlestick, :waterfall, :funnel,
#     :step, :spline]

Multiple Series

Most chart types accept multiple datasets. Add them one after another:

Chart.new(:line)
|> Chart.add_data(Enum.zip(1..6, [1,3,2,5,4,6]),  name: "Apples",  color: "#e15759")
|> Chart.add_data(Enum.zip(1..6, [2,2,4,3,5,5]),  name: "Oranges", color: "#f28e2b")
|> Chart.add_data(Enum.zip(1..6, [1,2,3,2,4,5]),  name: "Bananas", color: "#edc948")
|> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom})
|> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left})
|> Chart.compile()
|> SVG.to_svg()

When no color is given, each series gets a color from a built-in 10-color palette.


Recompilation & Dirty Tracking

Chart.compile/1 only recomputes layers that changed. Mutating data reruns scales and geometry; mutating canvas reruns layout, scales, and geometry. This makes incremental updates cheap:

compiled = Chart.compile(chart)

# Update data → only scales + geometry recompute
compiled
|> Chart.add_data(new_pairs, name: "New series")
|> Chart.compile()

Saving SVG to a File

svg_string =
  Chart.new(:bar)
  |> Chart.add_data([{1, 10}, {2, 20}, {3, 15}])
  |> Chart.put_axis(:x1, %Axis{dimension: :x, position: :bottom})
  |> Chart.put_axis(:y1, %Axis{dimension: :y, position: :left})
  |> Chart.compile()
  |> SVG.to_svg()

File.write!("chart.svg", svg_string)