empowered.house ↗

hero/process-map.ex

Process map

A process mapped across swimlanes, with the step to fix first picked out in lime.

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

<div class="h-96 rounded-xl border border-border">
  <.hero_process_map animated />
</div>
default
isometric
nothing marked
custom process
light surface

Props

Prop Type Default What it does
caption string "Your process · the map"
lanes list ["Customer", "Your team", "Systems"] Three lane names, top to bottom.
steps list [%{system: "Mail", title: "Order comes in", lane: 0}, %{system: "Spreadsheet", ...}, ...] Up to five `%{lane, title, system}` in order. `lane` is 0, 1 or 2. Keep titles under 16 characters.
highlight integer 1 Index of the marked step. A negative index marks none.
highlight_label string "Fix first"
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/hero/process-map.ex
process-map.ex
defmodule EHVisuals.Hero.ProcessMap do
  @moduledoc """
  A process mapped across swimlanes, with the step to fix first marked.
  """

  use EHVisuals.Composition,
    name: "hero/process-map",
    title: "Process map",
    description:
      "A process mapped across swimlanes, with the step to fix first picked out in lime."

  @steps [
    %{lane: 0, title: "Order comes in", system: "Mail"},
    %{lane: 1, title: "Check stock", system: "Spreadsheet"},
    %{lane: 2, title: "Reserve stock", system: "ERP"},
    %{lane: 1, title: "Confirm date", system: "CRM"},
    %{lane: 0, title: "Order confirmed", system: "Mail"}
  ]

  attr(:caption, :string, default: "Your process · the map")

  attr(:lanes, :list,
    default: ["Customer", "Your team", "Systems"],
    doc: "Three lane names, top to bottom."
  )

  attr(:steps, :list,
    default: @steps,
    doc:
      "Up to five `%{lane, title, system}` in order. `lane` is 0, 1 or 2. Keep titles under 16 characters."
  )

  attr(:highlight, :integer,
    default: 1,
    doc: "Index of the marked step. A negative index marks none."
  )

  attr(:highlight_label, :string, default: "Fix first")

  attr(:label, :string,
    default: "A process map over three lanes with one manual step marked to fix first"
  )

  frame_attrs()

  @spec hero_process_map(map()) :: Phoenix.LiveView.Rendered.t()
  def hero_process_map(assigns) do
    items =
      for {step, index} <- assigns.steps |> Enum.take(5) |> Enum.with_index() do
        step
        |> Map.put(:index, index)
        |> Map.put(:x, 96 + index * 120)
        |> Map.put(:y, 73 + step.lane * 104)
        |> Map.put(:on, index == assigns.highlight)
      end

    assigns =
      assigns
      |> assign(:items, items)
      |> assign(:links, items |> Enum.chunk_every(2, 1, :discard) |> Enum.map(&connector/1))
      |> assign(:tag_w, 14 + String.length(assigns.highlight_label) * 5.6)

    ~H"""
    <.frame name="hero/process-map" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 684 372">
        <text class="ehv-mono ehv-a-fade" x="684" y="24" font-size="9.5" text-anchor="end">
          {String.upcase(@caption)}
        </text>
        <g :for={{lane, index} <- Enum.with_index(@lanes)} class={"ehv-a-fade ehv-i-#{index}"}>
          <line class="ehv-line-soft" x1="0" x2="684" y1={44.5 + index * 104} y2={44.5 + index * 104} />
          <text class="ehv-t-3" x="0" y={70 + index * 104} font-size="12">{lane}</text>
        </g>
        <line class="ehv-line-soft ehv-a-fade ehv-i-3" x1="0" x2="684" y1="356.5" y2="356.5" />

        <g :for={link <- @links} class={"ehv-a-fade ehv-i-#{link.step}"}>
          <path
            class={"ehv-line-ink ehv-round ehv-a-draw ehv-i-#{link.step}"}
            d={link.d}
            pathLength="1"
          />
          <path class="ehv-fill-2" d={link.head} />
        </g>

        <g :for={item <- @items}>
          <rect
            :if={item.on}
            class="ehv-hero-process-map__halo ehv-a-pulse ehv-i-12"
            x={item.x - 5}
            y={item.y - 5}
            width="114"
            height="56"
            rx="11"
          />
          <g class={"ehv-a-rise ehv-i-#{item.index * 2 + 2}"}>
            <rect
              class={if item.on, do: "ehv-card-lime", else: "ehv-card"}
              x={item.x + 0.5}
              y={item.y + 0.5}
              width="103"
              height="45"
              rx="7"
            />
            <text class="ehv-t-strong" x={item.x + 10} y={item.y + 20} font-size="12">
              {item.title}
            </text>
            <text
              class={["ehv-mono", item.on && "ehv-t-lime"]}
              x={item.x + 10}
              y={item.y + 35}
              font-size="8"
            >
              {String.upcase(item.system)}
            </text>
            <g :if={item.on}>
              <rect
                class="ehv-fill-lime"
                x={item.x}
                y={item.y - 24}
                width={@tag_w}
                height="16"
                rx="4"
              />
              <text class="ehv-mono ehv-t-on-lime" x={item.x + 7} y={item.y - 13} font-size="7.5">
                {String.upcase(@highlight_label)}
              </text>
            </g>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  # An elbow from the right edge of one step to the left edge of the next.
  defp connector([from, to]) do
    x1 = from.x + 104
    y1 = from.y + 23
    x2 = to.x
    y2 = to.y + 23

    %{
      d: "M#{x1} #{y1} H#{x1 + 8} V#{y2} H#{x2 - 5}",
      head: "M#{x2} #{y2} l-6 -3.5 v7 z",
      step: from.index * 2 + 3
    }
  end
end
assets/css/hero/process-map.css
process-map.css
/* hero/process-map */
.ehv-hero-process-map { --ehv-max-w: 760px; --ehv-step: 110ms; --ehv-iso-scale: 0.74; }

.ehv-hero-process-map__halo {
  fill: var(--ehv-lime-soft);
  stroke: none;
}