empowered.house ↗

charts/line.ex

Line chart

A line chart card with a soft area, a target line and a live last point.

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

<div class="h-96 rounded-xl border border-border">
  <.charts_line animated />
</div>
default
isometric
no target · custom copy
hover · mount
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "Requests handled without a person"
value string "52%"
caption string "12 months"
values list [18, 22, 21, 27, 25, 31, ...]
ticks list ["Jan", "Apr", "Jul", "Oct"] Labels under the plot, spread evenly.
target any 45 A value on the series scale, drawn as a dashed lime line. `nil` hides it.
target_label string "Target"
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/line.ex
line.ex
defmodule EHVisuals.Charts.Line do
  @moduledoc """
  A line chart card with a soft area, a target line and a live last point.
  """

  use EHVisuals.Composition,
    name: "charts/line",
    title: "Line chart",
    description: "A line chart card with a soft area, a target line and a live last point."

  attr(:title, :string, default: "Requests handled without a person")
  attr(:value, :string, default: "52%")
  attr(:caption, :string, default: "12 months")
  attr(:values, :list, default: [18, 22, 21, 27, 25, 31, 30, 36, 41, 39, 46, 52])
  attr(:ticks, :list, default: ~w(Jan Apr Jul Oct), doc: "Labels under the plot, spread evenly.")

  attr(:target, :any,
    default: 45,
    doc: "A value on the series scale, drawn as a dashed lime line. `nil` hides it."
  )

  attr(:target_label, :string, default: "Target")
  attr(:label, :string, default: "A line chart climbing past its target")
  frame_attrs()

  @spec charts_line(map()) :: Phoenix.LiveView.Rendered.t()
  def charts_line(assigns) do
    {points, target_y} = plot(assigns.values, assigns.target)
    line = Enum.map_join(points, " ", fn {x, y} -> "#{x},#{y}" end)

    assigns =
      assigns
      |> assign(:line, line)
      |> assign(:area, "20,196 #{line} 340,196")
      |> assign(:last, List.last(points))
      |> assign(:target_y, target_y)
      |> assign(:tick_xs, tick_xs(assigns.ticks))

    ~H"""
    <.frame name="charts/line" 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="64" 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" />
          <polygon class="ehv-charts-line__area ehv-a-fade ehv-i-8" points={@area} />
          <g :if={@target_y} class="ehv-a-fade ehv-i-6">
            <line class="ehv-line-lime ehv-dash" x1="20" x2="340" y1={@target_y} y2={@target_y} />
            <text class="ehv-mono ehv-t-lime" x="22" y={@target_y - 6} font-size="8">
              {String.upcase(@target_label)}
            </text>
          </g>
          <polyline class="ehv-line-ink ehv-round ehv-a-draw ehv-i-3" points={@line} pathLength="1" />
          <circle
            class="ehv-fill-lime ehv-a-ping ehv-i-12"
            cx={elem(@last, 0)}
            cy={elem(@last, 1)}
            r="4"
          />
          <circle
            class="ehv-charts-line__dot ehv-fill-lime ehv-a-pop ehv-i-12"
            cx={elem(@last, 0)}
            cy={elem(@last, 1)}
            r="4"
          />
          <text
            :for={{tick, x} <- Enum.zip(@ticks, @tick_xs)}
            class="ehv-mono ehv-a-fade ehv-i-4"
            x={x}
            y="216"
            font-size="8"
          >
            {String.upcase(tick)}
          </text>
        </g>
      </svg>
    </.frame>
    """
  end

  # The plot is 320 wide from x = 20, and runs from y = 92 down to y = 190,
  # leaving air above the highest point and below the lowest.
  defp plot(values, target) do
    {min, max} = Enum.min_max(values)
    {min, max} = if is_number(target), do: {min(min, target), max(max, target)}, else: {min, max}
    range = if max == min, do: 1, else: max - min
    step = 320 / max(length(values) - 1, 1)
    y = fn value -> Float.round(190 - (value - min) / range * 98, 1) end

    points =
      values
      |> Enum.with_index()
      |> Enum.map(fn {value, index} -> {Float.round(20 + index * step, 1), y.(value)} end)

    {points, if(is_number(target), do: y.(target))}
  end

  defp tick_xs(ticks) do
    step = 320 / max(length(ticks), 1)
    for index <- 0..(length(ticks) - 1)//1, do: Float.round(20 + index * step, 1)
  end
end
assets/css/charts/line.css
line.css
/* charts/line */
.ehv-charts-line { --ehv-max-w: 380px; }

.ehv-charts-line__area { fill: var(--ehv-ink); opacity: 0.05; }

.ehv--hover .ehv-charts-line__dot { transition: r 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-charts-line__dot { r: 5.5; }