PrettyGraphs
PrettyGraphs is a tiny Elixir library for generating good‑looking SVG/HTML charts suitable for Phoenix LiveView and other HTML renderers.
The initial release provides a horizontal bar chart with:
- Slightly rounded corners
- Titles shown by default
- Values rendered just past the end of each bar
-
Simple inputs and a self-contained
<svg>string output
Installation
Add pretty_graphs to your dependencies in mix.exs:
def deps do
[
{:pretty_graphs, "~> 0.1.0"}
]
endThen fetch your dependencies:
mix deps.getQuick start
iex> svg = PrettyGraphs.bar_chart(
...> [{"Apples", 10}, {"Bananas", 25}, {"Cherries", 18}],
...> title: "Fruit Sales"
...> )
iex> String.starts_with?(svg, "<svg")
true
The returned value is a standalone <svg> element as a string. You can embed it anywhere HTML is accepted.
Phoenix LiveView usage
Render the returned SVG string with Phoenix.HTML.raw/1 so it isn’t escaped. Assign it in mount/3 (or a handler) and render in your template:
defmodule MyAppWeb.DemoLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
data = [{"Apples", 10}, {"Bananas", 25}, {"Cherries", 18}]
svg =
PrettyGraphs.bar_chart(data,
title: "Fruit Sales",
gradient: [from: "#4f46e5", to: "#a78bfa", direction: :right]
)
{:ok, assign(socket, bar_svg: svg)}
end
@impl true
def render(assigns) do
~H"""
<div class="w-full max-w-3xl">
<%= Phoenix.HTML.raw(@bar_svg) %>
</div>
"""
end
endAdding LiveView events (phx-click, phx-value-*, hooks)
You can attach Phoenix attributes and classes to either:
-
the entire SVG root (via
:svg_attrsand:svg_class), and/or -
each bar
<rect>(via:bar_attrsand:bar_class).
Per-bar attributes/classes can also be set at the data point level, so you can, for example, attach a global phx-click to every bar and a specific phx-value-* to each item.
Global SVG and bar attributes/classes:
data = [{"Apples", 10}, {"Bananas", 25}, {"Cherries", 18}]
svg =
PrettyGraphs.bar_chart(
data,
title: "Interactive Bars",
# Attach a hook to the root SVG:
svg_attrs: %{"phx-hook" => "Chart"},
svg_class: ["w-full", "h-auto"],
# Attach a click handler to each bar rect and add classes:
bar_attrs: [data_role: "bar", %{"phx-click" => "bar_clicked"}],
bar_class: ["cursor-pointer", "hover:opacity-80"]
)Per-data-point attributes and classes using a 3-tuple:
data = [
{"Apples", 10, [attrs: %{"phx-value-fruit" => "apples"}]},
{"Bananas", 25, [attrs: %{"phx-value-fruit" => "bananas"}]},
{"Cherries", 18, [attrs: %{"phx-value-fruit" => "cherries"}]}
]
svg =
PrettyGraphs.bar_chart(
data,
title: "Per-bar values + global click",
# Global click for all bars:
bar_attrs: [%{"phx-click" => "bar_clicked"}],
bar_class: "cursor-pointer hover:opacity-80"
)Per-data-point attributes using the map shape:
data = %{
"Apples" => {10, [attrs: %{"phx-value-fruit" => "apples"}, class: "fill-lime-600"]},
"Bananas" => {25, [attrs: %{"phx-value-fruit" => "bananas"}]},
"Cherries" => {18, [attrs: %{"phx-value-fruit" => "cherries"}]}
}
svg =
PrettyGraphs.bar_chart(
data,
title: "Map shape with per-point attrs/classes",
bar_attrs: [%{"phx-click" => "bar_clicked"}],
bar_class: "cursor-pointer hover:opacity-80"
)In your LiveView, handle the event as usual:
@impl true
def handle_event("bar_clicked", %{"fruit" => fruit}, socket) do
# Do something with fruit e.g., "apples"
{:noreply, socket}
endNotes:
:svg_attrs/:bar_attrsaccept keyword lists or maps. Use string keys for hyphenated attributes (e.g.,%{"phx-click" => "...", "phx-value-id" => "123"}).- Per-point attrs/classes override global ones on conflicts.
-
Always render with
Phoenix.HTML.raw/1to avoid escaping the SVG.
Data shapes
You can pass data in any of the following forms:
List of
{label, value}tuples:[{"A", 10}, {"B", 20}]List of numbers (labels default to
"1","2", ...):[10, 20, 30]Map of
label => value:%{"A" => 10, "B" => 20}
Values can be integers, floats, or numeric strings (e.g., "42", "3.14").
Bar chart API
PrettyGraphs.bar_chart(data, opts \\ [])data: one of the shapes described aboveopts: keyword list to customize the chart
Options and defaults
:title— Chart title string (default:nil):width— Overall chart width in px (default:640):bar_height— Height of each bar in px (default:28):bar_gap— Gap between bars in px (default:2):padding— Keyword list of{left, right, top, bottom}padding in px-
default:
[left: 120, right: 48, top: 32, bottom: 24]
-
default:
:bar_radius— Corner radius for bars (rx/ry) in px (default:6):bar_color— Fill color for bars (default:"#4f46e5"):label_color— Color for bar labels (default:"#111827"):value_color— Color for value labels (default:"#111827"):title_color— Color for title text (default:"#111827"):background— Background color for the SVG (default:nil, no background rect):show_values— Whether to render value labels (default:true):value_formatter—fun(value :: number) :: String.t()for custom formatting- default: integers without decimals; floats up to 2 decimals (trimmed)
:font_family— Font family used for text (default: system UI stack):font_size— Base font size in px (default:12):gradient— Enable a linear gradient fill for bars (default:nil, disabled). Example:[from: "#4f46e5", to: "#a78bfa", direction: :right]for left-to-right[from: "#4f46e5", to: "#a78bfa", direction: :down]for top-to-bottom
Example: Theming and formatting
data = %{"One" => 12, "Two" => 7.5, "Three" => 19.2}
svg =
PrettyGraphs.bar_chart(data,
title: "Custom Theme",
width: 720,
bar_height: 24,
bar_gap: 10,
bar_radius: 8,
bar_color: "#0ea5e9",
label_color: "#374151",
value_color: "#111827",
title_color: "#111827",
background: "#ffffff",
font_family: "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
value_formatter: fn v -> :erlang.float_to_binary(v * 1.0, [:compact, {:decimals, 1}]) <> "%" end
)Example: Gradient fill
data = [{"Apples", 10}, {"Bananas", 25}, {"Cherries", 18}]
svg =
PrettyGraphs.bar_chart(data,
title: "Gradient Bars",
width: 640,
gradient: [from: "#4f46e5", to: "#a78bfa", direction: :right]
)
You can change the direction to :down for a vertical gradient:
PrettyGraphs.bar_chart(data, gradient: [from: "#4f46e5", to: "#a78bfa", direction: :down])
Diagonal directions are also supported: :down_right, :down_left, :up_right, and :up_left.
# Top-left -> bottom-right
PrettyGraphs.bar_chart(data, gradient: [from: "#0ea5e9", to: "#a78bfa", direction: :down_right])
# Top-right -> bottom-left
PrettyGraphs.bar_chart(data, gradient: [from: "#0ea5e9", to: "#a78bfa", direction: :down_left])
# Bottom-left -> top-right
PrettyGraphs.bar_chart(data, gradient: [from: "#0ea5e9", to: "#a78bfa", direction: :up_right])
# Bottom-right -> top-left
PrettyGraphs.bar_chart(data, gradient: [from: "#0ea5e9", to: "#a78bfa", direction: :up_left])IEx-ready snippet you can paste:
iex> data = [{"Apples", 10}, {"Bananas", 25}, {"Cherries", 18}]
iex> svg = PrettyGraphs.bar_chart(data, title: "Diagonal Gradient", gradient: [from: "#0ea5e9", to: "#a78bfa", direction: :down_right])
iex> String.starts_with?(svg, "<svg")
trueDefaults tuned for LiveView
- Horizontal layout
- Slightly rounded bars
-
Title shown (when provided via
:title) - Value labels near the end of the bar
- Sensible spacing for labels and values
-
Standalone
<svg>output (no external CSS required)
Accessibility
-
The root
<svg>includesrole="img"and an accessible label. - Text labels are rendered as real text nodes for better screen reader support.
-
Consider providing descriptive titles via
:titlefor additional context.
Testing
Run the test suite with:
mix testexample
Roadmap
- Vertical bar charts
- Stacked/grouped bars
- Axes and gridlines
- Line/area charts
- Pie/donut charts
- Legend and color scales
- Animation and transitions (where appropriate)
Contributions and suggestions are welcome!
License
MIT