Choreo.C4 (Choreo v0.8.0)

Copy Markdown View Source

C4 Model architecture diagram builder on top of Yog.

Choreo.C4 lets you document software architecture using the C4 model — a simple hierarchical approach for visualising software architecture at different levels of abstraction.

The four levels

  • L1 System Context — people and software systems
  • L2 Containers — applications and data stores inside a system
  • L3 Components — building blocks inside a container
  • L4 Code — classes / interfaces (delegate to Choreo.UML)

When to use

Use Choreo.C4 when you need to communicate architecture to both technical and non-technical stakeholders. The zoom levels map directly to the C4 abstraction levels, so you can generate Context, Container, and Component diagrams from a single model.

Quick Start

c4 =
  Choreo.C4.new()
  |> Choreo.C4.add_person(:customer, label: "Customer",
       description: "A user of the bank")
  |> Choreo.C4.add_software_system(:banking, label: "Internet Banking",
       description: "Allows customers to check balances", scope: :in)
  |> Choreo.C4.add_software_system(:mainframe, label: "Mainframe",
       description: "Stores core banking info", scope: :out)
  |> Choreo.C4.add_relationship(:customer, :banking,
       label: "Views balances using")
  |> Choreo.C4.add_relationship(:banking, :mainframe,
       label: "Gets account info from")
  # L2 — Containers inside the in-scope system
  |> Choreo.C4.add_container(:web_app, label: "Web App",
       technology: "JavaScript, React", parent: :banking)
  |> Choreo.C4.add_container(:api, label: "API",
       technology: "Elixir, Phoenix", parent: :banking)
  |> Choreo.C4.add_container(:db, label: "Database",
       technology: "Postgres", parent: :banking)
  |> Choreo.C4.add_relationship(:customer, :web_app, label: "Uses")
  |> Choreo.C4.add_relationship(:web_app, :api, label: "Makes API calls to")
  |> Choreo.C4.add_relationship(:api, :db, label: "Reads from and writes to")
  |> Choreo.C4.add_relationship(:api, :mainframe, label: "Makes RPC calls to")
  # L3 — Components inside a container
  |> Choreo.C4.add_component(:signin, label: "Sign In Controller",
       technology: "Phoenix Controller", parent: :api)
  |> Choreo.C4.add_component(:accounts, label: "Accounts Context",
       technology: "Domain logic", parent: :api)
  |> Choreo.C4.add_relationship(:signin, :accounts, label: "Uses")

# Generate diagrams at different zoom levels
context   = Choreo.View.zoom(c4, level: 0)
container = Choreo.View.zoom(c4, level: 1)
component = Choreo.View.zoom(c4, level: 2)

dot = Choreo.C4.to_dot(c4)
mermaid = Choreo.C4.to_mermaid(c4)

Analysis

# Find containers with no relationships
Choreo.C4.Analysis.orphan_nodes(c4)

# Validate structural soundness
Choreo.C4.Analysis.validate(c4)

# Find missing descriptions
Choreo.C4.Analysis.missing_descriptions(c4)

# Find missing technology labels
Choreo.C4.Analysis.missing_technology(c4)

Summary

Functions

Defines a cluster (subgraph) for grouping nodes visually.

Adds a component (building block inside a container).

Adds a container (application or data store within a software system).

Adds a person (user, actor, or role).

Adds a relationship (directed edge) between two C4 elements.

Adds a software system.

Returns all children of a given parent node.

Clears the current scope.

Returns all edges as {from, to, weight} tuples.

Returns all edges with their metadata as {from, to, weight, meta} tuples.

Creates a new empty C4 model.

Returns all node IDs in the C4 model.

Returns all nodes of a given C4 type.

Returns the parent ID of a node, or nil.

Returns the scope node ID, or nil.

Sets the in-scope node for drill-down diagrams.

Returns a theme for Choreo.C4 diagrams.

Renders the C4 model to DOT format.

Returns the raw Yog.Multi.Graph struct underpinning the model.

Renders the C4 model to Mermaid.js flowchart syntax.

Types

t()

@type t() :: %Choreo.C4{
  clusters: %{required(String.t()) => map()},
  edge_meta: %{optional(Yog.Multi.Graph.edge_id()) => map()},
  graph: Yog.Multi.Graph.t(),
  scope: Yog.node_id() | nil
}

Functions

add_cluster(c4, name, opts \\ [])

@spec add_cluster(t(), String.t() | atom(), keyword()) :: t()

Defines a cluster (subgraph) for grouping nodes visually.

In C4 diagrams clusters are often used to group containers by their parent software system or components by their parent container.

Options

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_cluster("banking", label: "Internet Banking")
iex> c4.clusters["cluster_banking"].label
"Internet Banking"

add_component(c4, id, opts \\ [])

@spec add_component(t(), Yog.node_id(), keyword()) :: t()

Adds a component (building block inside a container).

Options

  • :parent - Parent container node ID.

  • :label (String.t/0) - Display label for the node.

  • :description (String.t/0) - Longer description shown as tooltip.

  • :technology (String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').

  • :shape (atom/0) - Shape override for DOT output.

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override.

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_software_system(:banking)
...>   |> Choreo.C4.add_container(:api, parent: :banking)
...>   |> Choreo.C4.add_component(:auth, label: "Auth Controller", technology: "Phoenix", parent: :api)
iex> :auth in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :auth).node_type
:component
iex> Map.get(c4.graph.nodes, :auth).parent
:api

add_container(c4, id, opts \\ [])

@spec add_container(t(), Yog.node_id(), keyword()) :: t()

Adds a container (application or data store within a software system).

Options

  • :parent - Parent software system node ID.

  • :label (String.t/0) - Display label for the node.

  • :description (String.t/0) - Longer description shown as tooltip.

  • :technology (String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').

  • :shape (atom/0) - Shape override for DOT output.

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override.

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_software_system(:banking)
...>   |> Choreo.C4.add_container(:api, label: "API", technology: "Phoenix", parent: :banking)
iex> :api in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :api).node_type
:container
iex> Map.get(c4.graph.nodes, :api).parent
:banking

add_person(c4, id, opts \\ [])

@spec add_person(t(), Yog.node_id(), keyword()) :: t()

Adds a person (user, actor, or role).

Options

  • :label (String.t/0) - Display label for the node.

  • :description (String.t/0) - Longer description shown as tooltip.

  • :technology (String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').

  • :shape (atom/0) - Shape override for DOT output.

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override.

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:customer, label: "Customer")
iex> :customer in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :customer).node_type
:person

add_relationship(c4, from, to, opts \\ [])

@spec add_relationship(t(), Yog.node_id(), Yog.node_id(), keyword()) :: t()

Adds a relationship (directed edge) between two C4 elements.

Options

  • :label (String.t/0) - Description of the relationship.

  • :technology (String.t/0) - Technology used for the relationship (e.g. 'HTTPS', 'gRPC').

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_person(:customer)
...>   |> Choreo.C4.add_software_system(:banking)
...>   |> Choreo.C4.add_relationship(:customer, :banking, label: "Uses")
iex> Choreo.C4.edges(c4)
[{:customer, :banking, 1}]

add_software_system(c4, id, opts \\ [])

@spec add_software_system(t(), Yog.node_id(), keyword()) :: t()

Adds a software system.

Options

  • :scope - Whether this system is in scope (:in) or external (:out) of the diagram being produced. The default value is :out.

  • :label (String.t/0) - Display label for the node.

  • :description (String.t/0) - Longer description shown as tooltip.

  • :technology (String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').

  • :shape (atom/0) - Shape override for DOT output.

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override.

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking, label: "Banking", scope: :in)
iex> :banking in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :banking).node_type
:software_system
iex> Map.get(c4.graph.nodes, :banking).scope
:in

children(c4, parent_id)

@spec children(t(), Yog.node_id()) :: [Yog.node_id()]

Returns all children of a given parent node.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_software_system(:banking)
...>   |> Choreo.C4.add_container(:api, parent: :banking)
...>   |> Choreo.C4.add_container(:web, parent: :banking)
iex> Enum.sort(Choreo.C4.children(c4, :banking))
[:api, :web]

clear_scope(c4)

@spec clear_scope(t()) :: t()

Clears the current scope.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> c4 = Choreo.C4.clear_scope(c4)
iex> c4.scope
nil

edges(c4)

@spec edges(t()) :: [{Yog.node_id(), Yog.node_id(), number()}]

Returns all edges as {from, to, weight} tuples.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_person(:a)
...>   |> Choreo.C4.add_software_system(:b)
...>   |> Choreo.C4.add_relationship(:a, :b)
iex> Choreo.C4.edges(c4)
[{:a, :b, 1}]

edges_with_meta(c4)

@spec edges_with_meta(t()) :: [{Yog.node_id(), Yog.node_id(), number(), map()}]

Returns all edges with their metadata as {from, to, weight, meta} tuples.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_person(:a)
...>   |> Choreo.C4.add_software_system(:b)
...>   |> Choreo.C4.add_relationship(:a, :b, label: "Uses")
iex> [{_, _, _, meta}] = Choreo.C4.edges_with_meta(c4)
iex> meta.label
"Uses"

new()

@spec new() :: t()

Creates a new empty C4 model.

Examples

iex> c4 = Choreo.C4.new()
iex> Choreo.C4.nodes(c4)
[]
iex> Choreo.C4.edges(c4)
[]
iex> c4.scope
nil

nodes(c4)

@spec nodes(t()) :: [Yog.node_id()]

Returns all node IDs in the C4 model.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a) |> Choreo.C4.add_software_system(:b)
iex> Enum.sort(Choreo.C4.nodes(c4))
[:a, :b]

nodes_of_type(c4, type)

@spec nodes_of_type(t(), atom()) :: [Yog.node_id()]

Returns all nodes of a given C4 type.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_person(:a)
...>   |> Choreo.C4.add_person(:b)
...>   |> Choreo.C4.add_software_system(:c)
iex> Enum.sort(Choreo.C4.nodes_of_type(c4, :person))
[:a, :b]
iex> Choreo.C4.nodes_of_type(c4, :software_system)
[:c]

parent(c4, id)

@spec parent(t(), Yog.node_id()) :: Yog.node_id() | nil

Returns the parent ID of a node, or nil.

Examples

iex> c4 = Choreo.C4.new()
...>   |> Choreo.C4.add_software_system(:banking)
...>   |> Choreo.C4.add_container(:api, parent: :banking)
iex> Choreo.C4.parent(c4, :api)
:banking
iex> Choreo.C4.parent(c4, :banking)
nil

scope(c4)

@spec scope(t()) :: Yog.node_id() | nil

Returns the scope node ID, or nil.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> Choreo.C4.scope(c4)
:banking

set_scope(c4, scope_id)

@spec set_scope(t(), Yog.node_id()) :: t()

Sets the in-scope node for drill-down diagrams.

When generating a Container diagram, the scope is typically the software system being expanded. For a Component diagram, the scope is the container being expanded.

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking, scope: :in)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> c4.scope
:banking

theme(name \\ :default, overrides \\ [])

@spec theme(
  atom(),
  keyword()
) :: Choreo.Theme.t()

Returns a theme for Choreo.C4 diagrams.

Examples

iex> theme = Choreo.C4.theme(:default)
iex> theme.name
:c4_default

to_dot(c4, opts \\ [])

@spec to_dot(
  t(),
  keyword()
) :: String.t()

Renders the C4 model to DOT format.

Options

  • :theme:default, :dark, :warm, :forest, :ocean, or a Choreo.Theme struct
  • :syntax:default (C4-style boxes) or :nested (clusters by parent)

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a)
iex> dot = Choreo.C4.to_dot(c4)
iex> String.contains?(dot, "digraph")
true

to_graph(c4)

@spec to_graph(t()) :: Yog.Multi.Graph.t()

Returns the raw Yog.Multi.Graph struct underpinning the model.

Examples

iex> c4 = Choreo.C4.new()
iex> graph = Choreo.C4.to_graph(c4)
iex> graph.kind
:directed

to_mermaid(c4, opts \\ [])

@spec to_mermaid(
  t(),
  keyword()
) :: String.t()

Renders the C4 model to Mermaid.js flowchart syntax.

Options

  • :theme:default, :dark, :warm, :forest, :ocean, or a Choreo.Theme struct
  • :direction:lr (default), :td, :rl, :bt

Examples

iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a)
iex> mermaid = Choreo.C4.to_mermaid(c4)
iex> String.contains?(mermaid, "graph LR")
true