metrics/health-grid.ex
Health grid
The health of each company area, week by week, as a grid of cells.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.metrics_health_grid animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Area health" |
|
caption |
string | "Last 8 weeks" |
|
areas |
list | [%{name: "Sales", weeks: [2, 2, 3, ...]}, ...] |
Up to five `%{name, weeks}`. Each week is a level: 3 healthy, 2 fine, 1 watch, 0 off track. Up to eight weeks. |
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/health-grid.ex
defmodule EHVisuals.Metrics.HealthGrid do
@moduledoc """
The health of each company area, week by week.
"""
use EHVisuals.Composition,
name: "metrics/health-grid",
title: "Health grid",
description: "The health of each company area, week by week, as a grid of cells."
@areas [
%{name: "Sales", weeks: [2, 2, 3, 3, 2, 3, 3, 3]},
%{name: "Delivery", weeks: [1, 2, 2, 0, 1, 2, 3, 3]},
%{name: "Finance", weeks: [3, 3, 3, 2, 3, 3, 3, 3]},
%{name: "People", weeks: [2, 1, 1, 2, 2, 2, 3, 2]},
%{name: "Support", weeks: [0, 0, 1, 2, 2, 3, 3, 3]}
]
attr(:title, :string, default: "Area health")
attr(:caption, :string, default: "Last 8 weeks")
attr(:areas, :list,
default: @areas,
doc:
"Up to five `%{name, weeks}`. Each week is a level: 3 healthy, 2 fine, 1 watch, 0 off track. Up to eight weeks."
)
attr(:label, :string, default: "A grid of area health by week, mostly healthy")
frame_attrs()
@spec metrics_health_grid(map()) :: Phoenix.LiveView.Rendered.t()
def metrics_health_grid(assigns) do
rows =
for {area, row} <- assigns.areas |> Enum.take(5) |> Enum.with_index() do
cells =
for {level, column} <- area.weeks |> Enum.take(8) |> Enum.with_index() do
%{x: 114 + column * 26, level: level, step: min(row + column + 2, 24)}
end
%{name: area.name, y: 72 + row * 30, cells: cells, step: row + 1}
end
assigns =
assigns
|> assign(:rows, rows)
|> assign(:height, 72 + length(rows) * 30 + 12)
~H"""
<.frame name="metrics/health-grid" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 340 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
<text class="ehv-t-strong ehv-a-fade ehv-i-1" x="20" y="35" font-size="14">{@title}</text>
<text class="ehv-mono ehv-a-fade ehv-i-1" x="320" y="35" font-size="9" text-anchor="end">
{String.upcase(@caption)}
</text>
<line class="ehv-line-faint" x1="20" x2="320" y1="52.5" y2="52.5" />
<g :for={row <- @rows}>
<text
class={"ehv-t-2 ehv-a-fade ehv-i-#{row.step}"}
x="20"
y={row.y + 13.5}
font-size="12"
>
{row.name}
</text>
<rect
:for={cell <- row.cells}
class={"ehv-metrics-health-grid__cell ehv-metrics-health-grid__cell--#{cell.level} ehv-a-pop ehv-i-#{cell.step}"}
x={cell.x}
y={row.y}
width="18"
height="18"
rx="5"
/>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/metrics/health-grid.css
/* metrics/health-grid */
.ehv-metrics-health-grid { --ehv-max-w: 360px; --ehv-step: 45ms; --ehv-dur: 500ms; }
.ehv-metrics-health-grid__cell { stroke-width: 1; }
.ehv-metrics-health-grid__cell--3 { fill: var(--ehv-lime-fill); stroke: none; }
.ehv-metrics-health-grid__cell--2 { fill: var(--ehv-lime-soft); stroke: var(--ehv-lime); stroke-opacity: 0.35; }
.ehv-metrics-health-grid__cell--1 { fill: var(--ehv-surface-3); stroke: var(--ehv-border-strong); }
.ehv-metrics-health-grid__cell--0 { fill: var(--ehv-coral-soft); stroke: var(--ehv-coral); }
.ehv--hover .ehv-metrics-health-grid__cell { transition: opacity 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-metrics-health-grid__cell:not(:hover) { opacity: 0.55; }