empowered.house ↗

status/uptime-bar.ex

Uptime bar

An uptime bar of one cell per day with the incident days marked and the uptime percentage.

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

<div class="h-96 rounded-xl border border-border">
  <.status_uptime_bar animated />
</div>
default
isometric
sixty days · no incidents · custom copy
an incident today · hover · no glow
light surface
isometric · fade out · still

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
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
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; }