empowered.house ↗

intelligence/knowledge-graph.ex

Knowledge graph

A goal in the middle with the facts, rules, owners and sources the company connects to it.

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

<div class="h-96 rounded-xl border border-border">
  <.intelligence_knowledge_graph animated />
</div>
default
isometric
four nodes · custom copy
hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
center map %{label: "Ship in 48 hours", type: "Goal"} `%{type, label}` of the middle card.
nodes list [%{label: "Cut-off is 14:00", type: "Fact"}, %{label: "Stock check first", type: "Rule"}, ...] Up to six `%{type, label}` around it. Keep labels under 18 characters.
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/intelligence/knowledge-graph.ex
knowledge-graph.ex
defmodule EHVisuals.Intelligence.KnowledgeGraph do
  @moduledoc """
  What the company knows about one thing: a goal in the middle, and the
  facts, rules, owners and sources connected to it.
  """

  use EHVisuals.Composition,
    name: "intelligence/knowledge-graph",
    title: "Knowledge graph",
    description:
      "A goal in the middle with the facts, rules, owners and sources the company connects to it."

  @nodes [
    %{type: "Fact", label: "Cut-off is 14:00"},
    %{type: "Rule", label: "Stock check first"},
    %{type: "Owner", label: "Marta, Delivery"},
    %{type: "Metric", label: "On time: 94%"},
    %{type: "Process", label: "Order to cash"},
    %{type: "Source", label: "Ops sync, 12 Mar"}
  ]

  # Centre points of the six slots around the middle card.
  @slots [{84, 52}, {316, 52}, {66, 150}, {334, 150}, {84, 248}, {316, 248}]

  attr(:center, :map,
    default: %{type: "Goal", label: "Ship in 48 hours"},
    doc: "`%{type, label}` of the middle card."
  )

  attr(:nodes, :list,
    default: @nodes,
    doc: "Up to six `%{type, label}` around it. Keep labels under 18 characters."
  )

  attr(:label, :string,
    default: "A goal connected to the facts, rules, owners and sources behind it"
  )

  frame_attrs()

  @spec intelligence_knowledge_graph(map()) :: Phoenix.LiveView.Rendered.t()
  def intelligence_knowledge_graph(assigns) do
    items =
      for {{node, {cx, cy}}, index} <- assigns.nodes |> Enum.zip(@slots) |> Enum.with_index() do
        node |> Map.put(:cx, cx) |> Map.put(:cy, cy) |> Map.put(:index, index)
      end

    assigns = assign(assigns, :items, items)

    ~H"""
    <.frame name="intelligence/knowledge-graph" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 400 300">
        <g class="ehv-h-lift">
          <line
            :for={item <- @items}
            class={"ehv-line ehv-a-draw ehv-i-#{item.index + 2}"}
            x1="200"
            y1="150"
            x2={item.cx}
            y2={item.cy}
            pathLength="1"
          />
          <g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-12">
            <circle :for={item <- @items} class="ehv-fill-lime" r="2.5">
              <animate
                attributeName="cx"
                from={item.cx}
                to="200"
                dur="3s"
                begin={"#{item.index * 0.5}s"}
                repeatCount="indefinite"
              />
              <animate
                attributeName="cy"
                from={item.cy}
                to="150"
                dur="3s"
                begin={"#{item.index * 0.5}s"}
                repeatCount="indefinite"
              />
              <animate
                attributeName="opacity"
                values="0;1;1;0"
                keyTimes="0;0.2;0.7;1"
                dur="3s"
                begin={"#{item.index * 0.5}s"}
                repeatCount="indefinite"
              />
            </circle>
          </g>
          <g :for={item <- @items} class={"ehv-a-rise ehv-i-#{item.index + 3}"}>
            <rect
              class="ehv-card ehv-intelligence-knowledge-graph__node"
              x={item.cx - 60}
              y={item.cy - 20}
              width="120"
              height="40"
              rx="8"
            />
            <text class="ehv-mono" x={item.cx - 49} y={item.cy - 4} font-size="7.5">
              {String.upcase(item.type)}
            </text>
            <text x={item.cx - 49} y={item.cy + 11} font-size="11.5">{item.label}</text>
          </g>
          <g class="ehv-a-pop">
            <rect class="ehv-card-lime" x="132.5" y="126.5" width="135" height="47" rx="10" />
            <text class="ehv-mono ehv-t-lime" x="146" y="144" font-size="7.5">
              {String.upcase(@center.type)}
            </text>
            <text class="ehv-t-strong" x="146" y="161" font-size="12.5">{@center.label}</text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/intelligence/knowledge-graph.css
knowledge-graph.css
/* intelligence/knowledge-graph */
.ehv-intelligence-knowledge-graph { --ehv-max-w: 440px; }

.ehv--hover .ehv-intelligence-knowledge-graph__node { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover .ehv-intelligence-knowledge-graph__node:hover { stroke: var(--ehv-lime); }