The Chart component brings uPlot into PUI with a LiveView-friendly API. The server sends serializable config, while the hook builds and updates the client-side chart instance during mount, patch, resize, and teardown.
Import
use PUI
# or
import PUI.Chart
Low-level Chart
Use the low-level chart/1 component when you want full control over the payload and hook pairing.
<.chart
id="deploys"
phx-hook="PUI.BarChart"
height={280}
config=%{
preset: "bar",
categories: ["Jan", "Feb", "Mar", "Apr"],
data: [
[0, 1, 2, 3],
[4.2, 6.0, 5.1, 7.3]
],
series: [
%{label: "Deploys", suffix: "x"}
]
}
/>
Low-level Chart + Hook
This demo uses the generic <.chart> component with the
prebuilt PUI.BarChart hook. The LiveView component only sends
serializable config; the hook builds the uPlot instance.
<div class="space-y-4">
<p class="max-w-2xl text-sm text-muted-foreground">
This demo uses the generic <code><.chart></code> component with the
prebuilt <code>PUI.BarChart</code> hook. The LiveView component only sends
serializable config; the hook builds the uPlot instance.
</p>
<.chart
id="docs-chart-base"
phx-hook="PUI.BarChart"
height={280}
config={chart_base_config()}
/>
</div>
Preconfigured Bar Chart
Use bar_chart/1 for categorical comparisons. It generates the aligned x-axis data for you and defaults phx-hook to PUI.BarChart.
<.bar_chart
id="monthly-revenue"
categories={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[
%{label: "Revenue", data: [12.4, 18.7, 15.2, 22.1, 19.8, 25.3], suffix: " jt"},
%{label: "Target", data: [10.0, 14.0, 16.0, 18.0, 20.0, 24.0], suffix: " jt"}
]}
/>
Preconfigured Bar Chart
Monthly revenue
Styled with the default PUI chart palette, axes, and tooltip.
<div class="space-y-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-foreground">Monthly revenue</p>
<p class="text-sm text-muted-foreground">
Styled with the default PUI chart palette, axes, and tooltip.
</p>
</div>
<.badge variant="outline">Hook: PUI.BarChart</.badge>
</div>
<.bar_chart
id="docs-bar-chart"
categories={chart_categories()}
height={300}
series={[
%{label: "Revenue", data: [12.4, 18.7, 15.2, 22.1, 19.8, 25.3], suffix: " jt"},
%{label: "Target", data: [10.0, 14.0, 16.0, 18.0, 20.0, 24.0], suffix: " jt"}
]}
/>
</div>
Preconfigured Line Chart
Use line_chart/1 for trend data. Switch between linear, stepped, and spline path renderers through the component API while LiveView patches the existing hook payload. Each series also accepts color as a convenient alias for the line color, plus optional fill, width, and formatting keys.
<.line_chart
id="server-temps"
curve={@chart_curve}
area={@chart_show_area}
grid={@chart_show_grid}
labels={["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"]}
tooltip={%{title: "Page Views"}}
series={[
%{label: "Desktop", color: @chart_color, data: [186, 202, 194, 228, 246, 268]}
]}
/>
Interactive Chart Playground
Curve
Series color
<div class="space-y-5">
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_auto]">
<div class="space-y-4">
<div class="space-y-2">
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
Curve
</p>
<div class="flex flex-wrap gap-2">
<button
:for={curve <- ~w(linear stepped spline)}
type="button"
phx-click="chart_set_curve"
phx-value-curve={curve}
class={[
"rounded-full px-3 py-1 text-xs font-medium transition-all",
curve == @chart_curve && "bg-primary text-primary-foreground shadow-sm",
curve != @chart_curve &&
"bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
]}
>
{curve}
</button>
</div>
</div>
<div class="space-y-2">
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
Series color
</p>
<div class="flex flex-wrap gap-2">
<button
:for={option <- chart_color_options()}
type="button"
phx-click="chart_set_color"
phx-value-color={option.value}
class={[
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-all",
option.value == @chart_color &&
"border-primary bg-primary/10 text-foreground shadow-sm",
option.value != @chart_color &&
"border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground"
]}
>
<span
class="size-2.5 rounded-full"
style={"background-color: #{option.value}"}
/>
{option.label}
</button>
</div>
</div>
</div>
<div class="flex flex-wrap items-start justify-start gap-2 lg:justify-end">
<button
id="chart-toggle-area"
type="button"
phx-click="chart_toggle_area"
class={[
"rounded-full px-3 py-1 text-xs font-medium transition-all",
@chart_show_area && "bg-primary text-primary-foreground shadow-sm",
!@chart_show_area &&
"bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
]}
>
Area {if @chart_show_area, do: "on", else: "off"}
</button>
<button
id="chart-toggle-grid"
type="button"
phx-click="chart_toggle_grid"
class={[
"rounded-full px-3 py-1 text-xs font-medium transition-all",
@chart_show_grid && "bg-primary text-primary-foreground shadow-sm",
!@chart_show_grid &&
"bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
]}
>
Grid {if @chart_show_grid, do: "on", else: "off"}
</button>
<.button
id="chart-reseed"
type="button"
variant="outline"
size="sm"
phx-click="chart_advance_revision"
>
Refresh dataset
</.button>
</div>
</div>
<div class="rounded-xl border border-dashed border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
Curve: <span class="font-medium text-foreground">{@chart_curve}</span>
· Color: <span class="font-medium text-foreground">{@chart_color}</span>
· Area:
<span class="font-medium text-foreground">
{if @chart_show_area, do: "on", else: "off"}
</span>
· Grid:
<span class="font-medium text-foreground">
{if @chart_show_grid, do: "on", else: "off"}
</span>
· Revision: <span class="font-medium text-foreground">{@chart_revision}</span>
</div>
<.line_chart
id="docs-line-chart"
curve={@chart_curve}
area={@chart_show_area}
grid={@chart_show_grid}
labels={chart_line_labels()}
height={320}
tooltip={%{title: "Page Views"}}
series={chart_playground_series(@chart_revision, @chart_color)}
/>
</div>
Extending the Hook
The base hook exports this.uPlot, this.chart, this.data, and this.opts. That means derivative hooks can stay very small:
import { ChartHook } from "pui";
export default class BarChart extends ChartHook {}
For custom behavior, subclass ChartHook, keep the component payload serializable, and override the relevant methods such as buildSeries(payload), buildAxes(payload), or buildHooks(payload).
Colocated hook example
When a chart should only live next to one LiveView or demo, use a colocated hook directly in the HEEx template. This avoids separate files and manual LiveSocket registration — colocated hooks are automatically bundled.
Create a hook that hides axes, grid, and cursor for a minimal look:
<.line_chart
id="mini-temps"
phx-hook=".MiniChart"
labels={["00:00", "04:00", "08:00", "12:00"]}
series={[
%{label: "CPU", data: [42, 45, 43, 46], suffix: "°C"}
]}
/>
<script :type={Phoenix.LiveView.ColocatedHook} name=".MiniChart">
import { LineChart } from "pui";
export default class MiniChart extends LineChart {
buildAxes(payload) {
return [{ show: false }, { show: false }];
}
}
</script>
The hook name must start with a . prefix and the script must use :type={Phoenix.LiveView.ColocatedHook} — never raw <script> tags. That keeps the component payload serializable while letting the colocated hook hide grid lines, cursor chrome, and axis labels for just that one chart.
API Reference
chart/1
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
generated | Unique chart DOM id |
phx-hook |
global attr | "PUI.Chart" |
Hook that renders the chart |
height |
integer |
320 |
Reserved chart height in pixels |
class |
string |
"" |
Additional wrapper classes |
card |
boolean |
true |
Wraps the chart in a bordered card container |
config |
map |
required | Serializable chart payload consumed by the hook |
bar_chart/1
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
generated | Unique chart DOM id |
categories |
list |
required | Labels used on the x-axis |
series |
list |
required | Series maps containing label and data |
height |
integer |
320 |
Reserved chart height in pixels |
class |
string |
"" |
Additional wrapper classes |
card |
boolean |
true |
Wraps the chart in a bordered card container |
bar_width |
float |
0.72 |
Relative bar width passed to uPlot.paths.bars/1 |
max_bar_width |
integer |
64 |
Maximum rendered bar width in pixels |
bar_radius |
integer |
2 |
Top corner radius in pixels for rendered bars |
grid |
boolean |
true |
Toggles the y-axis grid |
legend |
boolean |
false |
Enables the built-in uPlot legend |
tooltip |
map |
%{} |
Tooltip configuration |
options |
map |
%{} |
Additional serialized chart options |
line_chart/1
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
generated | Unique chart DOM id |
x |
list |
generated | Explicit x-axis values |
labels |
list |
[] |
Optional categorical x-axis labels |
series |
list |
required | Series maps containing label and data |
curve |
string |
"linear" |
"linear", "stepped", or "spline" |
time |
boolean |
false |
Enables a temporal x-axis |
area |
boolean |
false |
Adds a fill beneath each line |
sparkline |
boolean |
false |
Switches to the compact PUI.SparklineChart hook |
height |
integer |
320 |
Reserved chart height in pixels |
class |
string |
"" |
Additional wrapper classes |
card |
boolean |
true |
Wraps the chart in a bordered card container |
grid |
boolean |
true |
Toggles the y-axis grid |
legend |
boolean |
false |
Enables the built-in uPlot legend |
tooltip |
map |
%{} |
Tooltip configuration |
options |
map |
%{} |
Additional serialized chart options |