metrics/goal.ex
Goal
A company goal with its progress and the initiatives that move it.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.metrics_goal animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Company goal · Q4" |
|
title |
string | "Ship every order in 48 hours" |
|
progress |
integer | 72 |
Percent of the way there, 0 to 100. |
target |
integer | 90 |
Where the target tick sits, 0 to 100. |
caption |
string | "Target 90% by December" |
|
initiatives |
list | [%{name: "Automate order intake", status: "on track"}, %{name: "Stock check in the ERP", status: "at risk"}, ...] |
Up to three `%{name, status}`. `on track` is lime, `at risk` is coral, anything else is neutral. |
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/metrics/goal.ex
defmodule EHVisuals.Metrics.Goal do
@moduledoc """
A company goal with its progress and the initiatives that move it.
"""
use EHVisuals.Composition,
name: "metrics/goal",
title: "Goal",
description: "A company goal with its progress and the initiatives that move it."
@initiatives [
%{name: "Automate order intake", status: "on track"},
%{name: "Stock check in the ERP", status: "at risk"},
%{name: "Carrier booking agent", status: "planned"}
]
attr(:eyebrow, :string, default: "Company goal · Q4")
attr(:title, :string, default: "Ship every order in 48 hours")
attr(:progress, :integer, default: 72, doc: "Percent of the way there, 0 to 100.")
attr(:target, :integer, default: 90, doc: "Where the target tick sits, 0 to 100.")
attr(:caption, :string, default: "Target 90% by December")
attr(:initiatives, :list,
default: @initiatives,
doc:
"Up to three `%{name, status}`. `on track` is lime, `at risk` is coral, anything else is neutral."
)
attr(:label, :string, default: "A company goal at 72 percent with three initiatives")
frame_attrs()
@spec metrics_goal(map()) :: Phoenix.LiveView.Rendered.t()
def metrics_goal(assigns) do
assigns =
assigns
|> assign(:fill_w, clamp(assigns.progress) * 3)
|> assign(:target_x, 20 + clamp(assigns.target) * 3)
|> assign(:rows, assigns.initiatives |> Enum.take(3) |> Enum.with_index())
~H"""
<.frame name="metrics/goal" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 340 256">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height="255" rx="12" />
<text class="ehv-mono ehv-a-fade ehv-i-1" x="20" y="31" font-size="9">
{String.upcase(@eyebrow)}
</text>
<text class="ehv-t-strong ehv-a-fade ehv-i-2" x="20" y="56" font-size="15">{@title}</text>
<text class="ehv-t-display ehv-a-fade ehv-i-3" x="20" y="101" font-size="30">
{@progress}%
</text>
<text class="ehv-t-3 ehv-a-fade ehv-i-3" x="320" y="101" font-size="11.5" text-anchor="end">
{@caption}
</text>
<rect class="ehv-fill-soft ehv-a-fade ehv-i-3" x="20" y="114" width="300" height="6" rx="3" />
<rect
class="ehv-fill-lime ehv-a-grow-x ehv-i-5"
x="20"
y="114"
width={@fill_w}
height="6"
rx="3"
/>
<line
class="ehv-line-ink ehv-a-fade ehv-i-6"
x1={@target_x}
x2={@target_x}
y1="108"
y2="126"
/>
<line class="ehv-line-faint" x1="20" x2="320" y1="144.5" y2="144.5" />
<g :for={{row, index} <- @rows} class={"ehv-a-rise ehv-i-#{index + 6}"}>
<circle class={dot(row.status)} cx="24" cy={168 + index * 32} r="3.5" />
<text x="38" y={172 + index * 32} font-size="12.5">{row.name}</text>
<text
class={["ehv-mono", tone(row.status)]}
x="320"
y={172 + index * 32}
font-size="8.5"
text-anchor="end"
>
{String.upcase(row.status)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp clamp(percent), do: percent |> max(0) |> min(100)
defp dot("on track"), do: "ehv-fill-lime"
defp dot("at risk"), do: "ehv-fill-coral"
defp dot(_status), do: "ehv-fill-4"
defp tone("on track"), do: "ehv-t-lime"
defp tone("at risk"), do: "ehv-t-coral"
defp tone(_status), do: nil
end
assets/css/metrics/goal.css
/* metrics/goal */
.ehv-metrics-goal { --ehv-max-w: 360px; }