charts/gauge.ex
Gauge
A semicircular gauge whose arc sweeps to its value, with a target tick and an optional warning zone.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.charts_gauge animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Warehouse capacity in use" |
|
caption |
string | "Today" |
|
value |
any | 72 |
A number between 0 and `max`. |
max |
any | 100 |
The right end of the scale. The left end is 0. |
unit |
string | "%" |
Written right after the value and the target. |
target |
any | 85 |
A number on the scale, drawn as a tick across the arc. `nil` hides it. |
zone |
any | ~c"Zd" |
The warning zone as `[from, to]` on the scale. A value inside it turns the arc coral. `nil` hides it. |
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/gauge.ex
defmodule EHVisuals.Charts.Gauge do
@moduledoc """
A semicircular gauge: the arc sweeps to the value, a tick marks the target
and a thin outer arc marks the warning zone.
"""
use EHVisuals.Composition,
name: "charts/gauge",
title: "Gauge",
description:
"A semicircular gauge whose arc sweeps to its value, with a target tick and an optional warning zone."
# The dial: centre of the half circle and the radius of the middle of the arc.
@cx 160
@cy 170
@r 92
attr(:title, :string, default: "Warehouse capacity in use")
attr(:caption, :string, default: "Today")
attr(:value, :any, default: 72, doc: "A number between 0 and `max`.")
attr(:max, :any, default: 100, doc: "The right end of the scale. The left end is 0.")
attr(:unit, :string, default: "%", doc: "Written right after the value and the target.")
attr(:target, :any,
default: 85,
doc: "A number on the scale, drawn as a tick across the arc. `nil` hides it."
)
attr(:zone, :any,
default: [90, 100],
doc:
"The warning zone as `[from, to]` on the scale. A value inside it turns the arc coral. `nil` hides it."
)
attr(:label, :string,
default: "A gauge at 72 percent, under its target of 85 and clear of the warning zone"
)
frame_attrs()
@spec charts_gauge(map()) :: Phoenix.LiveView.Rendered.t()
def charts_gauge(assigns) do
top = if is_number(assigns.max) and assigns.max > 0, do: assigns.max, else: 100
share = fn number -> number |> max(0) |> min(top) |> Kernel./(top) end
value = if is_number(assigns.value), do: assigns.value, else: 0
zone =
case assigns.zone do
[from, to] when is_number(from) and is_number(to) and to > from -> {from, to}
_other -> nil
end
assigns =
assigns
|> assign(:track, arc(0, 1, @r))
|> assign(:sweep, if(share.(value) > 0, do: arc(0, share.(value), @r)))
|> assign(:tip, point(share.(value), @r))
|> assign(:warn, zone != nil and value >= elem(zone, 0) and value <= elem(zone, 1))
|> assign(:zone_arc, zone && arc(share.(elem(zone, 0)), share.(elem(zone, 1)), @r + 11))
|> assign(
:tick,
is_number(assigns.target) &&
{point(share.(assigns.target), @r - 10), point(share.(assigns.target), @r + 10)}
)
|> assign(
:marks,
for(part <- [0.25, 0.5, 0.75], do: {point(part, @r - 12), point(part, @r - 17)})
)
|> assign(:shown, "#{number(value)}#{assigns.unit}")
|> assign(
:goal,
is_number(assigns.target) && "Target #{number(assigns.target)}#{assigns.unit}"
)
|> assign(:ends, {number(0), number(top)})
|> assign(:cx, @cx)
|> assign(:cy, @cy)
|> assign(:r, @r)
~H"""
<.frame name="charts/gauge" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 320 208">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="319" height="207" rx="12" />
<text class="ehv-t-2 ehv-a-fade ehv-i-1" x="20" y="34" font-size="12">{@title}</text>
<text class="ehv-mono ehv-a-fade ehv-i-1" x="300" y="34" font-size="9" text-anchor="end">
{String.upcase(@caption)}
</text>
<path class="ehv-charts-gauge__track ehv-a-fade ehv-i-2" d={@track} />
<g class="ehv-a-fade ehv-i-2">
<line
:for={{{x1, y1}, {x2, y2}} <- @marks}
class="ehv-line"
x1={x1}
y1={y1}
x2={x2}
y2={y2}
/>
</g>
<path
:if={@zone_arc}
class="ehv-charts-gauge__zone ehv-a-fade ehv-i-3"
d={@zone_arc}
/>
<path
:if={@sweep}
class={[
"ehv-charts-gauge__value ehv-a-draw ehv-i-3",
@warn && "ehv-charts-gauge__value--warn"
]}
d={@sweep}
pathLength="1"
/>
<circle
:if={@sweep}
class="ehv-fill-canvas ehv-a-pop ehv-i-10"
cx={elem(@tip, 0)}
cy={elem(@tip, 1)}
r="2.5"
/>
<line
:if={@tick}
class="ehv-charts-gauge__target ehv-a-fade ehv-i-6"
x1={@tick |> elem(0) |> elem(0)}
y1={@tick |> elem(0) |> elem(1)}
x2={@tick |> elem(1) |> elem(0)}
y2={@tick |> elem(1) |> elem(1)}
/>
<text
class={["ehv-t-display ehv-a-fade ehv-i-4", @warn && "ehv-t-coral"]}
x={@cx}
y={@cy - 22}
font-size="32"
text-anchor="middle"
>
{@shown}
</text>
<text
:if={@goal}
class="ehv-mono ehv-t-2 ehv-a-fade ehv-i-6"
x={@cx}
y={@cy - 3}
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@goal)}
</text>
<g class="ehv-a-fade ehv-i-2">
<text class="ehv-mono" x={@cx - @r} y={@cy + 22} font-size="8" text-anchor="middle">
{elem(@ends, 0)}
</text>
<text class="ehv-mono" x={@cx + @r} y={@cy + 22} font-size="8" text-anchor="middle">
{elem(@ends, 1)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# An arc of the dial between two shares of the scale, 0 at the left end and
# 1 at the right end, drawn over the top.
defp arc(from, to, radius) do
{x1, y1} = point(from, radius)
{x2, y2} = point(to, radius)
"M#{x1} #{y1} A#{radius} #{radius} 0 0 1 #{x2} #{y2}"
end
defp point(share, radius) do
angle = :math.pi() * (1 - share)
{Float.round(@cx + radius * :math.cos(angle), 2),
Float.round(@cy - radius * :math.sin(angle), 2)}
end
defp number(value) when is_float(value) do
if value == Float.round(value), do: Integer.to_string(round(value)), else: "#{value}"
end
defp number(value), do: "#{value}"
end
assets/css/charts/gauge.css
/* charts/gauge */
.ehv-charts-gauge { --ehv-max-w: 350px; }
.ehv-charts-gauge__track {
fill: none;
stroke: var(--ehv-border-soft);
stroke-width: 12;
stroke-linecap: round;
}
.ehv-charts-gauge__value {
fill: none;
stroke: var(--ehv-lime-fill);
stroke-width: 12;
stroke-linecap: round;
}
.ehv-charts-gauge__value--warn { stroke: var(--ehv-coral); }
.ehv-charts-gauge__zone { fill: none; stroke: var(--ehv-coral); stroke-width: 2; stroke-linecap: round; }
.ehv-charts-gauge__target { fill: none; stroke: var(--ehv-ink); stroke-width: 1.5; stroke-linecap: round; }
.ehv--hover .ehv-charts-gauge__value { transition: stroke-width 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-charts-gauge__value { stroke-width: 15; }