empowered.house ↗

charts/bar.ex

Bar chart

A bar chart card with one bar picked out in lime.

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

<div class="h-96 rounded-xl border border-border">
  <.charts_bar animated />
</div>
default
isometric
highlight a middle bar · custom copy
hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "Orders shipped on time"
value string "94%"
caption string "Last 8 weeks"
values list ~c">D@JOMX^"
labels list ["W1", "W2", "W3", "W4", "W5", "W6", ...] One label per bar.
highlight integer -1 Index of the lime bar. A negative index counts from the end.
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/bar.ex
bar.ex
defmodule EHVisuals.Charts.Bar do
  @moduledoc """
  A bar chart card with one bar picked out in lime.
  """

  use EHVisuals.Composition,
    name: "charts/bar",
    title: "Bar chart",
    description: "A bar chart card with one bar picked out in lime."

  attr(:title, :string, default: "Orders shipped on time")
  attr(:value, :string, default: "94%")
  attr(:caption, :string, default: "Last 8 weeks")
  attr(:values, :list, default: [62, 68, 64, 74, 79, 77, 88, 94])
  attr(:labels, :list, default: ~w(W1 W2 W3 W4 W5 W6 W7 W8), doc: "One label per bar.")

  attr(:highlight, :integer,
    default: -1,
    doc: "Index of the lime bar. A negative index counts from the end."
  )

  attr(:label, :string, default: "A bar chart rising week over week")
  frame_attrs()

  @spec charts_bar(map()) :: Phoenix.LiveView.Rendered.t()
  def charts_bar(assigns) do
    assigns = assign(assigns, :bars, bars(assigns.values, assigns.labels, assigns.highlight))

    ~H"""
    <.frame name="charts/bar" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 360 240">
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="359" height="239" 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-t-display ehv-a-fade ehv-i-2" x="20" y="64" font-size="24">{@value}</text>
          <text class="ehv-mono ehv-a-fade ehv-i-2" x="340" y="34" font-size="9" text-anchor="end">
            {String.upcase(@caption)}
          </text>
          <line
            :for={y <- [88.5, 124.5, 160.5]}
            class="ehv-line-faint"
            x1="20"
            x2="340"
            y1={y}
            y2={y}
          />
          <line class="ehv-line-soft" x1="20" x2="340" y1="196.5" y2="196.5" />
          <g :for={bar <- @bars}>
            <rect
              class={[
                "ehv-charts-bar__bar ehv-a-grow-y ehv-i-#{bar.step}",
                bar.on && "ehv-charts-bar__bar--on"
              ]}
              x={bar.x}
              y={bar.y}
              width={bar.w}
              height={bar.h}
              rx="3"
            />
            <text
              class={["ehv-mono ehv-a-fade ehv-i-#{bar.step}", bar.on && "ehv-t-ink"]}
              x={bar.cx}
              y="216"
              font-size="8"
              text-anchor="middle"
            >
              {bar.label}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  # The plot is 320 wide from x = 20 and 108 tall above the baseline at y = 196.
  defp bars(values, labels, highlight) do
    count = max(length(values), 1)
    top = max(Enum.max(values, fn -> 1 end), 1)
    slot = 320 / count
    width = Float.round(slot * 0.56, 1)
    on = Integer.mod(highlight, count)

    for {value, index} <- Enum.with_index(values) do
      height = Float.round(value / top * 108, 1)

      %{
        x: Float.round(20 + index * slot + (slot - width) / 2, 1),
        y: Float.round(196 - height, 1),
        w: width,
        h: height,
        cx: Float.round(20 + index * slot + slot / 2, 1),
        on: index == on,
        step: min(index + 2, 24),
        label: Enum.at(labels, index)
      }
    end
  end
end
assets/css/charts/bar.css
bar.css
/* charts/bar */
.ehv-charts-bar { --ehv-max-w: 380px; --ehv-step: 60ms; }

.ehv-charts-bar__bar {
  fill: var(--ehv-surface-3);
  stroke: var(--ehv-border-strong);
  stroke-width: 1;
}
.ehv-charts-bar__bar--on { fill: var(--ehv-lime-fill); stroke: none; }

.ehv--hover .ehv-charts-bar__bar { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-charts-bar__bar:not(.ehv-charts-bar__bar--on):hover { fill: var(--ehv-ink-4); }