empowered.house ↗

sections/stats.ex

Stats

A stats section with three or four key numbers, a label under each and hairlines between them.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.sections_stats animated />
</div>
default
isometric
three numbers · custom copy
hover · no glow · no notes
light surface
isometric · fade out · still

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
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
stats.css
/* sections/stats */
.ehv-sections-stats { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }