sections/stats.ex
Stats
A stats section with three or four key numbers, a label under each and hairlines between them.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_stats animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "This year" |
|
title |
string | "The year in operations" |
Up to 44 characters. |
stats |
list | [%{label: "Orders handled", value: "1.2M", note: "+18% on last year"}, %{label: "Saved per week", ...}, ...] |
Two to four `%{value, label}` with an optional `note`. With four: a value of up to 6 characters, a label of 17, a note of 18. |
highlight |
integer | 0 |
Index of the number set in lime. A negative index sets none. |
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/sections/stats.ex
defmodule EHVisuals.Sections.Stats do
@moduledoc """
A stats section: three or four key numbers with a label each, separated by
hairlines, under a short heading.
"""
use EHVisuals.Composition,
name: "sections/stats",
title: "Stats",
description:
"A stats section with three or four key numbers, a label under each and hairlines between them."
@stats [
%{value: "1.2M", label: "Orders handled", note: "+18% on last year"},
%{value: "38 h", label: "Saved per week", note: "per team"},
%{value: "99.4%", label: "Posted untouched", note: "no manual edits"},
%{value: "12", label: "Agents at work", note: "across 5 teams"}
]
attr(:eyebrow, :string, default: "This year")
attr(:title, :string, default: "The year in operations", doc: "Up to 44 characters.")
attr(:stats, :list,
default: @stats,
doc:
"Two to four `%{value, label}` with an optional `note`. With four: a value of up to 6 characters, a label of 17, a note of 18."
)
attr(:highlight, :integer,
default: 0,
doc: "Index of the number set in lime. A negative index sets none."
)
attr(:label, :string, default: "Four key numbers in a row with a label under each")
frame_attrs()
@spec sections_stats(map()) :: Phoenix.LiveView.Rendered.t()
def sections_stats(assigns) do
stats = Enum.take(assigns.stats, 4)
count = max(length(stats), 1)
width = 432 / count
noted = Enum.any?(stats, &Map.get(&1, :note))
bottom = if noted, do: 180, else: 164
cells =
stats
|> Enum.with_index()
|> Enum.map(fn {stat, index} ->
%{
value: stat.value,
label: stat.label,
note: Map.get(stat, :note),
x: Float.round(24 + (index + 0.5) * width, 1),
post: index > 0 && Float.round(24.5 + index * width, 1),
index: index
}
end)
assigns =
assigns
|> assign(:cells, cells)
|> assign(:bottom, bottom)
|> assign(:height, bottom + 24)
~H"""
<.frame name="sections/stats" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 480 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="479" height={@height - 1} rx="12" />
<text
class="ehv-mono ehv-a-fade ehv-i-1"
x="240"
y="34"
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@eyebrow)}
</text>
<text
class="ehv-t-strong ehv-a-fade ehv-i-2"
x="240"
y="58"
font-size="16"
text-anchor="middle"
>
{@title}
</text>
<g class="ehv-a-fade ehv-i-3">
<line class="ehv-line-soft" x1="24" x2="456" y1="78.5" y2="78.5" />
<line class="ehv-line-soft" x1="24" x2="456" y1={@bottom + 0.5} y2={@bottom + 0.5} />
<line
:for={cell <- @cells}
:if={cell.post}
class="ehv-line-faint"
x1={cell.post}
x2={cell.post}
y1="92"
y2={@bottom - 13}
/>
</g>
<g :for={cell <- @cells} class={"ehv-a-rise ehv-i-#{cell.index + 4}"}>
<text
class={["ehv-t-display", cell.index == @highlight && "ehv-t-lime"]}
x={cell.x}
y="124"
font-size="28"
text-anchor="middle"
>
{cell.value}
</text>
<text class="ehv-t-2" x={cell.x} y="144" font-size="10.5" text-anchor="middle">
{cell.label}
</text>
<text
:if={cell.note}
class="ehv-mono"
x={cell.x}
y="161"
font-size="7"
text-anchor="middle"
>
{String.upcase(cell.note)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/sections/stats.css
/* sections/stats */
.ehv-sections-stats { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }