charts/sparkline.ex
Sparkline
A stat card with an inline sparkline, for tight spaces and metric rows.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.charts_sparkline animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Orders handled" |
|
value |
string | "1,284" |
|
change |
string | "+12.5%" |
A leading minus turns the badge coral. |
values |
list | [12, 14, 13, 17, 16, 21, ...] |
The series, oldest first. |
Frame props, shared by every composition
| Prop | Type | Default | What it does |
|---|---|---|---|
animated |
boolean | false |
Turns motion on. Without it the composition renders in its final, still state. |
trigger |
"mount" | "in-view" | "in-view-repeat" | "in-view" |
When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view. |
hover |
boolean | false |
Enables the hover micro-interactions the composition supports. |
glow |
boolean | true |
The warm incandescent halo behind the composition. |
fade_out |
boolean | false |
Fades the bottom edge into the surface underneath. |
isometric |
boolean | false |
Tilts the composition into the isometric plane of the platform stack. |
theme |
"dark" | "light" | "dark" |
`dark` for the site canvas, `light` for the cream reversed surface. |
class |
string | nil |
Extra classes for the frame. |
rest |
global | — |
Any other HTML attribute, set on the frame. |
Source
Two files. Copy them into your app if you would rather own the code than depend on the library.
lib/eh_visuals/charts/sparkline.ex
defmodule EHVisuals.Charts.Sparkline do
@moduledoc """
A stat card with an inline sparkline, for tight spaces and metric rows.
"""
use EHVisuals.Composition,
name: "charts/sparkline",
title: "Sparkline",
description: "A stat card with an inline sparkline, for tight spaces and metric rows."
@default_values [12, 14, 13, 17, 16, 21, 19, 24, 23, 28, 27, 33]
attr(:title, :string, default: "Orders handled")
attr(:value, :string, default: "1,284")
attr(:change, :string, default: "+12.5%", doc: "A leading minus turns the badge coral.")
attr(:values, :list, default: @default_values, doc: "The series, oldest first.")
attr(:label, :string, default: "A stat card with a rising sparkline")
frame_attrs()
@spec charts_sparkline(map()) :: Phoenix.LiveView.Rendered.t()
def charts_sparkline(assigns) do
points = points(assigns.values, 172, 40, 124, 52)
assigns =
assigns
|> assign(:down, String.starts_with?(assigns.change, "-"))
|> assign(:line, Enum.map_join(points, " ", fn {x, y} -> "#{x},#{y}" end))
|> assign(:last, List.last(points))
|> assign(:badge_w, 30 + String.length(assigns.change) * 6.3)
~H"""
<.frame name="charts/sparkline" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 320 132">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="319" height="131" rx="12" />
<text class="ehv-t-2 ehv-a-fade ehv-i-2" x="20" y="34" font-size="12">{@title}</text>
<text class="ehv-t-display ehv-a-fade ehv-i-3" x="20" y="72" font-size="30">{@value}</text>
<g class="ehv-a-fade ehv-i-5">
<rect
class={if @down, do: "ehv-fill-coral-soft", else: "ehv-fill-lime-soft"}
x="20"
y="90"
width={@badge_w}
height="22"
rx="6"
/>
<path
class={if @down, do: "ehv-fill-coral", else: "ehv-fill-lime"}
d={if @down, do: "M29 98 h8 l-4 6 z", else: "M29 104 h8 l-4 -6 z"}
/>
<text
class={["ehv-mono", if(@down, do: "ehv-t-coral", else: "ehv-t-lime")]}
x="43"
y="105"
font-size="10"
>
{@change}
</text>
</g>
<line class="ehv-line-faint ehv-a-fade ehv-i-4" x1="172" y1="92.5" x2="296" y2="92.5" />
<polyline
class="ehv-line-ink ehv-round ehv-a-draw ehv-i-4"
points={@line}
pathLength="1"
/>
<circle
class={[
"ehv-charts-sparkline__dot ehv-a-pop ehv-i-12",
if(@down, do: "ehv-fill-coral", else: "ehv-fill-lime")
]}
cx={elem(@last, 0)}
cy={elem(@last, 1)}
r="3.5"
/>
</g>
</svg>
</.frame>
"""
end
# Scales the series into the box at {x, y} of size {w, h}, y axis flipped.
defp points(values, x, y, w, h) do
{min, max} = Enum.min_max(values)
range = if max == min, do: 1, else: max - min
step = w / max(length(values) - 1, 1)
values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
{Float.round(x + index * step, 1), Float.round(y + h - (value - min) / range * h, 1)}
end)
end
end
assets/css/charts/sparkline.css
/* charts/sparkline */
.ehv-charts-sparkline { --ehv-max-w: 340px; }
.ehv--hover .ehv-charts-sparkline__dot { transition: r 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-charts-sparkline__dot { r: 5; }