PhiaUI ships 19 chart types rendered entirely as server-side SVG with zero JavaScript required. This tutorial covers every chart component, data formats, customisation hooks, composable patterns, and real-time LiveView integration.
What you'll learn
- How data is structured for every chart type
- All 19 chart components and their key attributes
- Composable charts with
xy_chart(mixed bar + line + area) - Responsive wrappers and aspect-ratio scaling
- Theme overrides, custom colours, animation control
- Real-time dashboard with Phoenix LiveView streams
- Advanced patterns: mark lines, data zoom, chart toolbox
Prerequisites
- Elixir 1.17+ and Phoenix 1.7+ with LiveView 1.0+
- TailwindCSS v4 configured in your project
- PhiaUI installed:
{:phia_ui, "~> 0.1.17"}inmix.exs
Step 1 — Install PhiaUI
mix deps.get
mix phia.install
Import the CSS layer in assets/css/app.css:
@import "../../deps/phia_ui/priv/static/theme.css";Import PhiaUI hooks in assets/js/app.js:
import { PhiaHooks } from "./phia_hooks"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...PhiaHooks }
})Import the Data module in any LiveView that renders charts:
import PhiaUi.Components.DataStep 2 — Data format
All PhiaUI charts use a consistent data format so you can swap chart types without changing your data pipeline.
Series-based charts (bar, line, area, radar, scatter)
# Single series
series = [%{name: "Revenue", data: [120, 200, 150, 80, 250, 190]}]
# Multi-series — grouped or stacked
series = [
%{name: "Desktop", data: [120, 200, 150, 80, 250, 190]},
%{name: "Mobile", data: [90, 140, 110, 60, 180, 150]}
]
# X-axis category labels
categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]Slice-based charts (pie, donut, polar area)
data = [
%{label: "Direct", value: 40},
%{label: "Organic", value: 30},
%{label: "Referral", value: 20},
%{label: "Social", value: 10}
]Scatter / bubble
# Scatter: {x, y} tuples
series = [%{name: "Cluster A", data: [{1.2, 3.4}, {2.1, 4.5}, {3.8, 2.9}]}]
# Bubble: {x, y, r} tuples where r is bubble radius
series = [%{name: "Segments", data: [{1, 2, 10}, {3, 4, 25}, {5, 1, 15}]}]Heatmap
# series is a list of row maps; categories is a list of column labels
series = [
%{name: "Mon", data: [12, 8, 4, 20, 15, 9, 3]},
%{name: "Tue", data: [7, 3, 18, 11, 6, 14, 10]},
%{name: "Wed", data: [5, 16, 9, 2, 18, 7, 13]}
]
categories = ~w[00:00 04:00 08:00 12:00 16:00 20:00 23:00]Waterfall
data = [
%{label: "Start", value: 100},
%{label: "New sales", value: 50},
%{label: "Refunds", value: -20},
%{label: "Upsell", value: 30},
%{label: "Churn", value: -15},
%{label: "End", value: 145}
]Treemap
data = [
%{label: "Engineering", value: 60},
%{label: "Marketing", value: 30},
%{label: "Sales", value: 25},
%{label: "Design", value: 15},
%{label: "Support", value: 10}
]Step 3 — All 19 chart types
Bar chart
Vertical bars, grouped, stacked, or horizontal. Best for categorical comparisons.
<%!-- Basic vertical bar --%>
<.bar_chart
id="bar-basic"
series={[%{name: "Sales", data: [120, 200, 150, 80, 250, 190]}]}
categories={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
/>
<%!-- Grouped (multi-series, default) --%>
<.bar_chart
id="bar-grouped"
series={[
%{name: "Desktop", data: [120, 200, 150, 80, 250, 190]},
%{name: "Mobile", data: [90, 140, 110, 60, 180, 150]}
]}
categories={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
/>
<%!-- Stacked --%>
<.bar_chart
id="bar-stacked"
series={@series}
categories={@categories}
stacked={true}
show_totals={true}
/>
<%!-- Horizontal --%>
<.bar_chart
id="bar-horizontal"
series={@series}
categories={@categories}
orientation={:horizontal}
border_radius={4}
/>Key attributes: stacked, orientation (:vertical / :horizontal), border_radius, show_totals, animate, animation_duration, colors, theme.
Line chart
Continuous line with optional dots. Supports 6 curve modes.
<%!-- Default linear --%>
<.line_chart id="line" series={@series} categories={@categories} />
<%!-- Smooth Catmull-Rom spline --%>
<.line_chart id="line-smooth" series={@series} categories={@categories} curve={:smooth} />
<%!-- Monotone cubic (no overshoot, recommended for time series) --%>
<.line_chart id="line-mono" series={@series} categories={@categories} curve={:monotone} />
<%!-- Step variants --%>
<.line_chart id="line-step" series={@series} categories={@categories} curve={:step_after} />
<.line_chart id="line-step-before" series={@series} categories={@categories} curve={:step_before} />
<.line_chart id="line-step-mid" series={@series} categories={@categories} curve={:step_middle} />
<%!-- With point labels --%>
<.line_chart id="line-labels" series={@series} categories={@categories} show_point_labels={true} />Key attributes: curve (:linear / :smooth / :monotone / :step_before / :step_after / :step_middle), show_point_labels, animate, animation_duration, colors, theme.
Area chart
Filled line chart. Stacking is correct — areas use cumulative baselines.
<%!-- Basic filled area --%>
<.area_chart id="area" series={@series} categories={@categories} />
<%!-- Stacked areas --%>
<.area_chart id="area-stacked" series={@series} categories={@categories} stacked={true} />
<%!-- Smooth curve + point labels --%>
<.area_chart
id="area-smooth"
series={@series}
categories={@categories}
curve={:smooth}
show_point_labels={true}
/>Key attributes: same as line chart, plus stacked.
Pie chart
360° slice chart with optional leader-line labels.
<.pie_chart
id="pie"
data={[
%{label: "Direct", value: 40},
%{label: "Organic", value: 30},
%{label: "Referral", value: 20},
%{label: "Social", value: 10}
]}
/>
<%!-- With arc link labels (Nivo-style leader lines) --%>
<.pie_chart
id="pie-links"
data={@pie_data}
show_link_labels={true}
spacing={2}
corner_radius={4}
/>Key attributes: data, colors, spacing, corner_radius, show_link_labels, animate.
Donut chart
Pie chart with a hollow centre. Accepts a center_label slot for KPI text.
<.donut_chart
id="donut"
data={[
%{label: "Chrome", value: 62},
%{label: "Safari", value: 20},
%{label: "Firefox", value: 12},
%{label: "Other", value: 6}
]}
>
<:center_label>
Browsers<br/>100%
</:center_label>
</.donut_chart>Key attributes: same as pie_chart, plus the center_label slot.
Radar chart
Spider-web polygon. Best for multi-dimensional profile comparisons.
<.radar_chart
id="radar"
series={[
%{name: "Product A", data: [80, 60, 90, 70, 85]},
%{name: "Product B", data: [60, 75, 55, 90, 65]}
]}
categories={["Performance", "Design", "Reliability", "Support", "Value"]}
/>Scatter chart
X/Y point cloud. Data is {x, y} tuples inside each series.
<.scatter_chart
id="scatter"
series={[
%{name: "Group A", data: [{1.2, 3.4}, {2.1, 4.5}, {0.8, 2.1}, {3.5, 5.0}]},
%{name: "Group B", data: [{4.0, 1.8}, {5.1, 2.9}, {3.8, 3.2}]}
]}
show_point_labels={false}
/>Bubble chart
Scatter chart where each point {x, y, r} includes a radius.
<.bubble_chart
id="bubble"
series={[
%{name: "Segments", data: [
{1, 2, 10},
{3, 4, 25},
{5, 1, 15},
{2, 5, 20}
]}
]}
/>Radial bar chart
Progress arcs arranged concentrically. Useful for multi-metric attainment.
<.radial_bar_chart
id="radial"
series={[
%{name: "Revenue", data: [78]},
%{name: "Users", data: [63]},
%{name: "Conversions", data: [45]}
]}
/>Histogram chart
Frequency distribution with configurable bin count.
<.histogram_chart
id="hist"
series={[%{name: "Response Time (ms)", data: [42,85,67,120,95,200,55,73,88,140,110,62]}]}
bins={8}
/>Key attributes: bins (default 10).
Waterfall chart
Running total with positive/negative segments. Accepts explicit %{label, value} list.
<.waterfall_chart
id="waterfall"
data={[
%{label: "Q1 Revenue", value: 500_000},
%{label: "New Deals", value: 120_000},
%{label: "Churn", value: -45_000},
%{label: "Upsell", value: 80_000},
%{label: "COGS", value: -200_000},
%{label: "Net", value: 455_000}
]}
/>Heatmap chart
Grid of coloured cells. Intensity maps from min → max value.
<.heatmap_chart
id="heat"
series={[
%{name: "Mon", data: [12, 8, 4, 20, 15, 9, 3]},
%{name: "Tue", data: [ 7, 3, 18, 11, 6, 14, 10]},
%{name: "Wed", data: [ 5,16, 9, 2, 18, 7, 13]},
%{name: "Thu", data: [19, 4, 14, 8, 11, 3, 16]},
%{name: "Fri", data: [ 2,17, 7, 13, 4, 20, 8]}
]}
categories={["00:00", "04:00", "08:00", "12:00", "16:00", "20:00", "23:00"]}
/>Bullet chart
Compact progress bar with a target marker. Useful for KPI attainment.
<.bullet_chart
id="bullet"
series={[
%{name: "Revenue", data: [78], target: 90, ranges: [60, 80, 100]},
%{name: "Users", data: [63], target: 75, ranges: [50, 70, 100]},
%{name: "Conversions", data: [45], target: 60, ranges: [30, 55, 100]}
]}
/>Slope chart
Before/after comparison across two time points.
<.slope_chart
id="slope"
series={[
%{name: "Product A", data: [42, 68]},
%{name: "Product B", data: [75, 55]},
%{name: "Product C", data: [30, 52]}
]}
categories={["2023", "2024"]}
/>Treemap chart
Nested rectangles with area proportional to value.
<.treemap_chart
id="treemap"
data={[
%{label: "Engineering", value: 60},
%{label: "Marketing", value: 30},
%{label: "Sales", value: 25},
%{label: "Design", value: 15},
%{label: "Support", value: 10}
]}
/>Timeline chart
Gantt-style horizontal bars across a time axis.
<.timeline_chart
id="timeline"
data={[
%{label: "Discovery", start: 1, finish: 3},
%{label: "Design", start: 2, finish: 5},
%{label: "Development", start: 4, finish: 10},
%{label: "QA", start: 9, finish: 11},
%{label: "Launch", start: 11, finish: 12}
]}
categories={["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}
/>Polar area chart
Equal-angle wedges with variable radius (Chart.js PolarArea pattern).
<.polar_area_chart
id="polar"
data={[
%{label: "North", value: 80},
%{label: "East", value: 55},
%{label: "South", value: 70},
%{label: "West", value: 45}
]}
/>Gauge chart
Semicircular dial. Supports coloured zones and a threshold marker.
<.gauge_chart
id="gauge"
value={73}
max={100}
zones={[{0, 40, "#ef4444"}, {40, 70, "#f59e0b"}, {70, 100, "#22c55e"}]}
animate={true}
>
<:center_label>
<tspan font-size="24" font-weight="700">73%</tspan>
<tspan x="0" dy="18" font-size="11">Health Score</tspan>
</:center_label>
</.gauge_chart>Key attributes: value, min, max, zones, threshold, animate, animation_duration, center_label slot.
Sparkline card
Inline KPI card with a tiny line chart. No axes.
<.sparkline_card
id="orders"
title="Orders"
value="1,247"
sparkline_data={[80, 95, 110, 90, 125, 140, 130]}
delta={12.5}
delta_type={:increase}
animate={true}
/>Step 4 — Composable charts with xy_chart
xy_chart lets you mix bar, line, and area series in a single viewport without writing any SVG.
<.xy_chart
id="mixed"
categories={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
>
<:series type={:bar} name="Revenue" data={[120, 200, 150, 80, 250, 190]} />
<:series type={:line} name="Trend" data={[100, 160, 140, 120, 210, 180]} />
<:series type={:area} name="Forecast" data={[140, 180, 160, 130, 230, 200]} />
</.xy_chart>xy_chart uses ChartCoord.auto_cartesian to compute domains automatically and ChartSeriesRegistry.render to dispatch to the correct renderer per series type.
You can also add scatter series:
<.xy_chart id="scatter-mixed" categories={@categories}>
<:series type={:bar} name="Volume" data={@volumes} />
<:series type={:scatter} name="Outliers" data={[{1, 150}, {3, 210}, {5, 95}]} />
</.xy_chart>Step 5 — Responsive charts
Wrap any chart in responsive_chart to apply a CSS aspect-ratio container that scales the SVG automatically on all screen sizes.
<%!-- 16:9 — good for area/line --%>
<.responsive_chart ratio="16/9">
<.line_chart id="resp-line" series={@series} categories={@categories} />
</.responsive_chart>
<%!-- 4:3 — good for bar charts --%>
<.responsive_chart ratio="4/3">
<.bar_chart id="resp-bar" series={@series} categories={@categories} />
</.responsive_chart>
<%!-- Square — good for pie/donut --%>
<.responsive_chart ratio="1/1">
<.donut_chart id="resp-donut" data={@pie_data} />
</.responsive_chart>Step 6 — Customisation
Custom colours
Pass a list of hex colours. PhiaUI cycles through them when there are more series than colours.
<.bar_chart
id="custom-colors"
series={@series}
categories={@categories}
colors={["#6366f1", "#f59e0b", "#10b981", "#ef4444"]}
/>Theme overrides
The theme attribute deep-merges onto the default ChartTheme. Useful for adjusting axis font size, grid colour, or legend positioning.
<.bar_chart
id="themed"
series={@series}
categories={@categories}
theme={%{
axis: %{font_size: 14, color: "#6b7280"},
grid: %{stroke: "#e5e7eb", stroke_dasharray: "4 2"},
legend: %{position: :bottom}
}}
/>Animation control
All charts with animate support an animation_duration attribute (milliseconds).
<.line_chart
id="animated"
series={@series}
categories={@categories}
animate={true}
animation_duration={1200}
/>
<%!-- Disable for reduced-motion environments --%>
<.bar_chart id="no-anim" series={@series} categories={@categories} animate={false} />Note: PhiaUI automatically respects
prefers-reduced-motionvia a CSS rule intheme.css. Settinganimate={true}is safe for all users.
Border radius (bar charts)
<.bar_chart
id="rounded"
series={@series}
categories={@categories}
border_radius={8}
/>Step 7 — Advanced: mark lines and mark points
Add reference lines (average, median, custom threshold) or marker symbols to any SVG chart.
<%!-- Dashed average line --%>
<.bar_chart id="bar-mark" series={@series} categories={@categories}>
<.chart_mark_line type={:average} label="Avg" />
</.bar_chart>
<%!-- Custom threshold --%>
<.bar_chart id="bar-threshold" series={@series} categories={@categories}>
<.chart_mark_line type={:custom} value={150} label="Target" />
</.bar_chart>
<%!-- Max point marker --%>
<.line_chart id="line-mark" series={@series} categories={@categories}>
<.chart_mark_point type={:max} symbol={:diamond} />
<.chart_mark_point type={:min} symbol={:circle} />
</.line_chart>Step 8 — Advanced: data zoom
data_zoom adds a range slider beneath the chart. Drag the handles to zoom into a region. The PhiaDataZoom hook handles pointer interaction.
<div class="space-y-2">
<.line_chart id="zoom-chart" series={@series} categories={@categories} />
<.data_zoom id="zoom-bar" target="zoom-chart" initial_start={0} initial_end={100} />
</div>Step 9 — Advanced: chart toolbox
Download, reset, and toggle series visibility with chart_toolbox.
<div class="space-y-2">
<.chart_toolbox id="toolbox" target="my-chart" />
<.bar_chart id="my-chart" series={@series} categories={@categories} />
</div>Step 10 — Real-time dashboard with LiveView
Charts update instantly when you push new data using assign or streams. No JavaScript coordination needed — LiveView diffs the SVG in the DOM.
defmodule MyAppWeb.RealtimeDashboardLive do
use MyAppWeb, :live_view
import PhiaUi.Components.Data
import PhiaUi.Components.Cards
@update_interval_ms 5_000
@impl true
def mount(_params, _session, socket) do
if connected?(socket), do: schedule_update()
{:ok,
socket
|> assign(:categories, last_12_months())
|> assign(:revenue_series, revenue_series())
|> assign(:pie_data, traffic_sources())}
end
@impl true
def handle_info(:update_charts, socket) do
schedule_update()
{:noreply,
socket
|> assign(:revenue_series, revenue_series())
|> assign(:pie_data, traffic_sources())}
end
defp schedule_update do
Process.send_after(self(), :update_charts, @update_interval_ms)
end
defp last_12_months do
today = Date.utc_today()
for i <- 11..0//-1 do
today
|> Date.add(-i * 30)
|> Calendar.strftime("%b")
end
end
defp revenue_series do
# Replace with real DB queries
[%{name: "Revenue", data: Enum.map(1..12, fn _ -> :rand.uniform(500) + 100 end)}]
end
defp traffic_sources do
[
%{label: "Direct", value: :rand.uniform(50) + 20},
%{label: "Organic", value: :rand.uniform(40) + 15},
%{label: "Referral", value: :rand.uniform(30) + 10},
%{label: "Social", value: :rand.uniform(20) + 5}
]
end
end<%!-- lib/my_app_web/live/realtime_dashboard_live.html.heex --%>
<div class="p-6 space-y-6">
<h1 class="text-2xl font-bold tracking-tight">Real-time Dashboard</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<%!-- Revenue line chart updates every 5 seconds --%>
<div class="rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium text-muted-foreground">Revenue (12 months)</p>
<.responsive_chart ratio="16/9">
<.line_chart
id="rt-revenue"
series={@revenue_series}
categories={@categories}
curve={:monotone}
animate={false}
/>
</.responsive_chart>
</div>
<%!-- Traffic source donut --%>
<div class="rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium text-muted-foreground">Traffic Sources</p>
<.responsive_chart ratio="1/1">
<.donut_chart id="rt-traffic" data={@pie_data} show_link_labels={true}>
<:center_label>Sources</:center_label>
</.donut_chart>
</.responsive_chart>
</div>
</div>
</div>Tip: Set
animate={false}for real-time charts to avoid re-triggering CSS animations on every update.
Step 11 — Complete analytics page example
A full-page analytics view with KPI cards, mixed chart, heatmap, and sparklines:
defmodule MyAppWeb.AnalyticsLive do
use MyAppWeb, :live_view
import PhiaUi.Components.Data
import PhiaUi.Components.Cards
import PhiaUi.Components.Display
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:categories, ~w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec])
|> assign(:revenue_series, [
%{name: "Desktop", data: [120, 200, 150, 80, 250, 190, 210, 180, 230, 270, 300, 320]},
%{name: "Mobile", data: [ 90, 140, 110, 60, 180, 150, 170, 140, 190, 220, 250, 280]}
])
|> assign(:pie_data, [
%{label: "Direct", value: 40},
%{label: "Organic", value: 30},
%{label: "Referral", value: 20},
%{label: "Social", value: 10}
])
|> assign(:heatmap_series, build_heatmap())
|> assign(:heatmap_categories, ~w[Mon Tue Wed Thu Fri Sat Sun])}
end
defp build_heatmap do
hours = ~w[00 04 08 12 16 20]
for h <- hours do
%{name: "#{h}:00", data: Enum.map(1..7, fn _ -> :rand.uniform(20) end)}
end
end
end<div class="p-6 space-y-8">
<%!-- KPI row --%>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<.sparkline_card id="sp-revenue" title="Revenue" value="$142k"
sparkline_data={[80, 95, 110, 90, 125, 140, 130]} delta={12.5} delta_type={:increase} />
<.sparkline_card id="sp-users" title="Users" value="8,420"
sparkline_data={[60, 70, 65, 80, 75, 90, 85]} delta={5.3} delta_type={:increase} />
<.sparkline_card id="sp-orders" title="Orders" value="1,247"
sparkline_data={[40, 55, 50, 65, 60, 70, 68]} delta={-2.1} delta_type={:decrease} />
<.sparkline_card id="sp-churn" title="Churn Rate" value="2.3%"
sparkline_data={[3.1, 2.8, 2.5, 2.6, 2.4, 2.3, 2.3]} delta={-0.8} delta_type={:decrease} />
</div>
<%!-- Revenue: stacked area + donut side by side --%>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium">Revenue by channel (stacked)</p>
<.area_chart
id="revenue-area"
series={@revenue_series}
categories={@categories}
stacked={true}
curve={:monotone}
animate={true}
/>
</div>
<div class="rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium">Traffic sources</p>
<.donut_chart id="traffic-donut" data={@pie_data} show_link_labels={true}>
<:center_label>Traffic</:center_label>
</.donut_chart>
</div>
</div>
<%!-- Composable mixed chart --%>
<div class="rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium">Volume vs. Trend</p>
<.xy_chart id="mixed" categories={@categories}>
<:series type={:bar} name="Volume" data={[120, 200, 150, 80, 250, 190, 210, 180, 230, 270, 300, 320]} />
<:series type={:line} name="Trend" data={[100, 150, 130, 90, 200, 170, 190, 160, 210, 245, 275, 300]} />
</.xy_chart>
</div>
<%!-- Activity heatmap --%>
<div class="rounded-lg border bg-card p-4 space-y-2">
<p class="text-sm font-medium">Activity heatmap (hour × weekday)</p>
<.heatmap_chart
id="activity-heat"
series={@heatmap_series}
categories={@heatmap_categories}
/>
</div>
</div>Chart component quick reference
| Component | Data input | Best for |
|---|---|---|
bar_chart | series + categories | Categorical comparison |
line_chart | series + categories | Trends over time |
area_chart | series + categories | Volume + trend |
pie_chart | data list | Part-to-whole |
donut_chart | data list | Part-to-whole + KPI centre |
radar_chart | series + categories | Multi-axis profiles |
scatter_chart | series with {x,y} tuples | Correlation |
bubble_chart | series with {x,y,r} tuples | Weighted correlation |
radial_bar_chart | series | Multi-metric attainment |
histogram_chart | series + bins | Distribution |
waterfall_chart | data list | Running totals |
heatmap_chart | series + categories | Matrix intensity |
bullet_chart | series with target/ranges | KPI vs. target |
slope_chart | series + 2 categories | Before/after |
treemap_chart | data list | Hierarchical size |
timeline_chart | data with start/finish | Gantt / project schedule |
polar_area_chart | data list | Directional / polar categories |
gauge_chart | value scalar | Single KPI dial |
sparkline_card | sparkline_data list | Inline mini-trend |
xy_chart | series slots (bar/line/area/scatter) | Composable mixed chart |
Related guides
- Analytics Dashboard tutorial — full shell + data grid + chart
- Charts & Data components reference
- Design System — colour tokens and dark mode