status/uptime-bar.ex
Uptime bar
An uptime bar of one cell per day with the incident days marked and the uptime percentage.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.status_uptime_bar animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "ERP connector" |
|
uptime |
string | "99.94%" |
|
status |
string | "Operational" |
Up to 14 characters. |
days |
integer | 90 |
How many days the bar covers, 30 to 90. The last cell is today. |
incidents |
list | [%{kind: "down", ago: 71}, %{kind: "degraded", ago: 38}, ...] |
`%{ago, kind}` per incident day. `ago` counts days back from today (0), `kind` is `down` or `degraded`. |
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/status/uptime-bar.ex
defmodule EHVisuals.Status.UptimeBar do
@moduledoc """
The uptime of one service: a bar of one cell per day with the incident days
marked, and the uptime percentage above it.
"""
use EHVisuals.Composition,
name: "status/uptime-bar",
title: "Uptime bar",
description:
"An uptime bar of one cell per day with the incident days marked and the uptime percentage."
@incidents [
%{ago: 71, kind: "down"},
%{ago: 38, kind: "degraded"},
%{ago: 37, kind: "degraded"},
%{ago: 12, kind: "down"}
]
attr(:title, :string, default: "ERP connector")
attr(:uptime, :string, default: "99.94%")
attr(:status, :string, default: "Operational", doc: "Up to 14 characters.")
attr(:days, :integer,
default: 90,
doc: "How many days the bar covers, 30 to 90. The last cell is today."
)
attr(:incidents, :list,
default: @incidents,
doc:
"`%{ago, kind}` per incident day. `ago` counts days back from today (0), `kind` is `down` or `degraded`."
)
attr(:label, :string,
default: "An uptime bar of ninety days with four incident days and 99.94 percent uptime"
)
frame_attrs()
@spec status_uptime_bar(map()) :: Phoenix.LiveView.Rendered.t()
def status_uptime_bar(assigns) do
days = assigns.days |> max(30) |> min(90)
kinds = Map.new(assigns.incidents, &{&1.ago, &1.kind})
pitch = 340 / days
width = Float.round(pitch * 0.7, 2)
cells =
for index <- 0..(days - 1) do
%{
x: Float.round(20 + index * pitch + (pitch - width) / 2, 2),
kind: Map.get(kinds, days - 1 - index, "up"),
step: div(index * 24, days)
}
end
marked = Enum.count(cells, &(&1.kind != "up"))
today = Map.get(kinds, 0, "up")
assigns =
assigns
|> assign(:cells, cells)
|> assign(:cell_w, width)
|> assign(:cell_rx, Float.round(min(1.5, width / 2), 2))
|> assign(:days, days)
|> assign(:off, today != "up")
|> assign(:status_w, Float.round(String.length(assigns.status) * 5.9, 1))
|> assign(:note, note(marked))
~H"""
<.frame name="status/uptime-bar" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 380 160">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="379" height="159" rx="12" />
<text class="ehv-t-strong ehv-a-fade ehv-i-1" x="20" y="35" font-size="14">{@title}</text>
<g class="ehv-a-fade ehv-i-1">
<circle
class={["ehv-a-pulse", if(@off, do: "ehv-fill-coral", else: "ehv-fill-lime")]}
cx={360 - @status_w - 9}
cy="30.5"
r="3"
/>
<text
class={["ehv-mono", if(@off, do: "ehv-t-coral", else: "ehv-t-lime")]}
x="360"
y="33.5"
font-size="8.5"
text-anchor="end"
>
{String.upcase(@status)}
</text>
</g>
<text class="ehv-t-display ehv-a-fade ehv-i-2" x="20" y="78" font-size="28">{@uptime}</text>
<text class="ehv-mono ehv-a-fade ehv-i-2" x="360" y="77" font-size="8.5" text-anchor="end">
UPTIME · {@days} DAYS
</text>
<rect
:for={cell <- @cells}
class={[
"ehv-status-uptime-bar__day ehv-a-grow-y ehv-i-#{cell.step}",
"ehv-status-uptime-bar__day--#{cell.kind}"
]}
x={cell.x}
y="94"
width={@cell_w}
height="30"
rx={@cell_rx}
/>
<g class="ehv-a-fade ehv-i-6">
<text class="ehv-mono" x="20" y="143" font-size="8">{@days} DAYS AGO</text>
<text class="ehv-mono ehv-t-2" x="190" y="143" font-size="8" text-anchor="middle">
{String.upcase(@note)}
</text>
<text class="ehv-mono" x="360" y="143" font-size="8" text-anchor="end">TODAY</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp note(0), do: "No incidents"
defp note(1), do: "1 incident day"
defp note(count), do: "#{count} incident days"
end
assets/css/status/uptime-bar.css
/* status/uptime-bar */
.ehv-status-uptime-bar { --ehv-max-w: 410px; --ehv-step: 28ms; --ehv-dur: 450ms; }
/* A day is lime when the service was up. Any other kind of day is coral. */
.ehv-status-uptime-bar__day { fill: var(--ehv-coral); }
.ehv-status-uptime-bar__day--up { fill: var(--ehv-lime-fill); }
.ehv-status-uptime-bar__day--degraded { fill-opacity: 0.5; }
.ehv-status-uptime-bar__day--down { fill-opacity: 1; }
.ehv--hover .ehv-status-uptime-bar__day { transition: opacity 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-status-uptime-bar__day:not(:hover) { opacity: 0.55; }