# `Expresso.Element.Chart.Sankey`
[🔗](https://github.com/rellen/expresso/blob/main/lib/expresso/element/chart/sankey.ex#L1)

Draws a sankey chart: the flows between the nodes of a graph

A category of a sankey is a path of two nodes or more, such as
`["Coal", "Electricity"]` or `"Gas/Electricity/Homes"`. Its value goes
along each link of the path. `links/4` adds the values of the same link.
Thus a CSV file can hold one row for each flow, or one row for each record
of a path with the `levels` option.

The layout has these rules:

1. A node with no flow into it is in the first column. Each other node is
   one column after its last source. Thus a link can go across a column.
   The `column` of a node entity sets the column of its node.
2. The height of a node is the larger of the flow into it and the flow out
   of it. One scale sets the height of each node, and the fullest column
   fills the height of the chart. The nodes of each column are in the
   vertical middle of the chart.
3. The nodes of a column keep the order of the data, or the `order` of
   their node entity. At each node, the links have the order of their
   other ends, from the top, so few links cross.
4. A link is a ribbon between two cubic Bézier curves. The control points
   are at the middle between the two columns, so a link leaves and enters
   each node horizontally. The ribbon is a filled shape, and not a thick
   stroke, so its height is its value at each point.

An alluvial chart has one node for each name in each column. The column of
a node is its place in the path, so the same name in two columns becomes
two nodes. Its node is the tuple `{column, name}`, as `links/4` returns it. The
nodes of one name have one color and one place in each column.

A graph with a loop has no first column. `back_links/1` finds the links
that close a loop. The layout finds the columns from the other links, and
a back link goes in a lane under the nodes. The frames of a chart share the
columns, the order and the scale of the nodes, as `layout_frames/3`
returns them. Each frame is a layer, and the opacity of the layers changes
from one frame to the next.

`render/1` draws each series of the chart as a group. The group holds the
links of the series and the nodes that show first with it. The node labels
of each series are in a group after the links, with the same steps.
`table/1` writes a row for each link for a screen reader.

# `key`

```elixir
@type key() :: String.t() | {non_neg_integer(), String.t()}
```

A node: its name, or its column from 0 and its name in an alluvial chart

# `link`

```elixir
@type link() :: %{from: key(), to: key(), value: number()}
```

A link: two nodes and a value

# `back_links`

```elixir
@spec back_links([%{from: key(), to: key()}]) :: MapSet.t({key(), key()})
```

Return the links that close a loop, as pairs of a source and a target

A depth-first search from each node, in the order of the data, follows
the links in their order. A link to a node on the path of the search
closes a loop. The graph without these links has no loop, so `columns/2`
uses it to find the columns.

    iex> links = [%{from: "A", to: "B"}, %{from: "B", to: "C"}, %{from: "C", to: "A"}]
    iex> Expresso.Element.Chart.Sankey.back_links(links)
    MapSet.new([{"C", "A"}])

# `cell`

```elixir
@spec cell(map(), key()) :: String.t()
```

Return the text of a node in the table

A node of an alluvial chart names its column, because one name can be in
each column.

# `check`

```elixir
@spec check([String.t() | [String.t()]], [number()], String.t(), boolean()) ::
  :ok | {:error, String.t()}
```

Make sure that the categories and the values make a graph

Each category names two nodes or more, each node has a name, and no link
goes from a node to the same node. Each value is zero or more, and their
sum is more than zero. A loop of two nodes or more is correct:
`back_links/1` finds the links that close it. With `alluvial` true, each
column has its own nodes, so a name can follow the same name.

    iex> Expresso.Element.Chart.Sankey.check([["A", "B"], "B/C"], [1, 2], "/")
    :ok
    iex> Expresso.Element.Chart.Sankey.check([["A", "B"], ["B", "A"]], [1, 2], "/")
    :ok
    iex> Expresso.Element.Chart.Sankey.check([["A", "A"]], [1], "/")
    {:error, "a flow of a sankey goes from \"A\" to \"A\". A flow goes to another node."}
    iex> Expresso.Element.Chart.Sankey.check([["A", "A"], ["A", "B"]], [1, 2], "/", true)
    :ok

# `columns`

```elixir
@spec columns([%{from: key(), to: key()}], %{
  required(String.t()) =&gt; non_neg_integer()
}) :: %{
  required(key()) =&gt; non_neg_integer()
}
```

Return the column of each node, from 0

A node with no link into it is in column 0. Each other node is one column
after its last source. A link that closes a loop, as `back_links/1` finds
it, does not count. `pinned` holds the columns of some nodes, from 0, and
the nodes after them move to follow them. The pin of a node that no link
touches does not count. A node of an alluvial chart holds its column.

    iex> links = [%{from: "A", to: "B"}, %{from: "B", to: "C"}, %{from: "A", to: "C"}]
    iex> Expresso.Element.Chart.Sankey.columns(links)
    %{"A" => 0, "B" => 1, "C" => 2}
    iex> Expresso.Element.Chart.Sankey.columns(links, %{"C" => 4})
    %{"A" => 0, "B" => 1, "C" => 4}
    iex> Expresso.Element.Chart.Sankey.columns(links, %{"D" => 4})
    %{"A" => 0, "B" => 1, "C" => 2}
    iex> Expresso.Element.Chart.Sankey.columns([%{from: "A", to: "B"}, %{from: "B", to: "A"}])
    %{"A" => 0, "B" => 1}
    iex> Expresso.Element.Chart.Sankey.columns([%{from: {0, "A"}, to: {1, "A"}}])
    %{{0, "A"} => 0, {1, "A"} => 1}

# `inner_block`
*macro* 

# `label_place`

```elixir
@spec label_place(map()) :: keyword()
```

Return the attributes that place the label of a node: at the right of the
node, and in the middle of its height

    iex> Expresso.Element.Chart.Sankey.label_place(%{x: 4, y: 10, height: 20})
    [x: "28", y: "27", text_anchor: "start"]

# `label_text`

```elixir
@spec label_text(map(), boolean()) :: String.t()
```

Return the text of the label of a node: its label, and its value when the
chart shows the values

    iex> Expresso.Element.Chart.Sankey.label_text(%{label: "Gas", value: 95}, true)
    "Gas 95"

# `labeled`

```elixir
@spec labeled([map()]) :: [map()]
```

Mark each node that shows its label with the key `labeled`

A node as high as a label shows its label. A shorter node shows its label
when the middle of the label is one line or more from the middle of each
other label in its column, from the top down, so two labels do not cover
each other.

    iex> nodes = [
    ...>   %{name: "A", column: 0, y: 0, height: 40},
    ...>   %{name: "B", column: 0, y: 54, height: 6},
    ...>   %{name: "C", column: 0, y: 64, height: 4}
    ...> ]
    iex> nodes |> Expresso.Element.Chart.Sankey.labeled() |> Enum.map(& &1.labeled)
    [true, true, false]

# `layout`

```elixir
@spec layout(
  [%{from: String.t(), to: String.t(), value: number(), series: pos_integer()}],
  number(),
  %{
    required(String.t()) =&gt; Expresso.Element.Node.t()
  }
) :: %{
  nodes: [map()],
  links: [map()],
  node_width: pos_integer(),
  xs: [number()]
}
```

Place the nodes and the links of a sankey in the box of the SVG

`links` holds each link with its value and the number of its series, from
1. The function returns the nodes, with their boxes, colors and the number
of their first series, and the links, with their paths. `top` is the top of
the box, and `settings` holds the `Expresso.Element.Node` of each node with
a `node` entity.

# `layout_frames`

```elixir
@spec layout_frames([[map()]], number(), %{
  required(String.t()) =&gt; Expresso.Element.Node.t()
}) :: [
  %{nodes: [map()], links: [map()], node_width: pos_integer(), xs: [number()]}
]
```

Place the nodes and the links of each frame of a sankey

`frames` holds the links of each frame, as `layout/2` takes them. The
frames share the columns, the order and the colors of the nodes, from the
links of each frame together, and one scale, so a height means the same
value in each frame. A node or a link with no value in a frame does not
show in that frame.

# `links`

```elixir
@spec links([String.t() | [String.t()]], [number()], String.t(), boolean()) :: [
  link()
]
```

Return the links of the paths, with the sum of the values of each link

The links keep the order of their first path, and each pair of neighbors
in a path is a link. With `alluvial` true, each node is the tuple of its
place in the path and its name.

    iex> Expresso.Element.Chart.Sankey.links([["A", "B", "C"], ["A", "B"]], [2, 3], "/")
    [%{from: "A", to: "B", value: 5}, %{from: "B", to: "C", value: 2}]
    iex> Expresso.Element.Chart.Sankey.links([["A", "A"]], [2], "/", true)
    [%{from: {0, "A"}, to: {1, "A"}, value: 2}]

# `name`

```elixir
@spec name(key()) :: String.t()
```

Return the name of a node

    iex> Expresso.Element.Chart.Sankey.name("Gas")
    "Gas"
    iex> Expresso.Element.Chart.Sankey.name({2, "Gas"})
    "Gas"

# `node_width`

```elixir
@spec node_width() :: pos_integer()
```

Return the width of a node in the units of the SVG

    iex> Expresso.Element.Chart.Sankey.node_width()
    16

# `path`

```elixir
@spec path(String.t() | [String.t()] | [key()], String.t()) :: [key()]
```

Return the nodes of a category of a sankey, as a list of names

A category is a list of names, or a string with the separator between two
names. The spaces next to a name do not count. A path of an alluvial chart
after the check holds its nodes, and the function returns them.

    iex> Expresso.Element.Chart.Sankey.path("Gas / Electricity", "/")
    ["Gas", "Electricity"]
    iex> Expresso.Element.Chart.Sankey.path(["I/O", "Disks"], "/")
    ["I/O", "Disks"]

# `pinned`

```elixir
@spec pinned(%{required(String.t()) =&gt; Expresso.Element.Node.t()}) :: %{
  required(String.t()) =&gt; non_neg_integer()
}
```

Return the columns that the `node` entities give, from 0

    iex> Expresso.Element.Chart.Sankey.pinned(%{"A" => %Expresso.Element.Node{name: "A", column: 3}})
    %{"A" => 2}

# `render`

```elixir
@spec render(map()) :: Phoenix.HTML.safe()
```

Draw the links, the nodes and the labels of a sankey in its SVG

The assigns are the assigns of `Expresso.Element.Chart`. Each category is a
link, and the value of a link is in one series. A chart with frames has one
layer for each frame. The layer of the frame that shows has the opacity 1.
The channel `--k<n>` of `Expresso.Element.Chart.Motion` sets the opacity,
so the opacity of the layers changes from one frame to the next.

# `render_slot`
*macro* 

# `ribbon`

```elixir
@spec ribbon(number(), number(), number(), number(), number()) :: String.t()
```

Return the path of a ribbon from `(x0, y0)` to `(x1, y1)`, with a height

The top edge and the bottom edge are cubic Bézier curves. Their control
points are at the middle, so the ribbon leaves and enters horizontally.

    iex> Expresso.Element.Chart.Sankey.ribbon(0, 0, 100, 50, 10)
    "M0,0 C50,0 50,50 100,50 L100,60 C50,60 50,10 0,10 Z"

# `table`

```elixir
@spec table(map()) :: %{headers: [String.t()], rows: [[String.t()]]}
```

Return the headers and the rows of the table of a sankey: one row for each
link, with its source, its target and its value in each frame

# `titles`

```elixir
@spec titles(map()) :: Phoenix.HTML.safe()
```

Draw the title of each column of an alluvial chart or a parallel sets
chart, in the row above the nodes

The first title is at the left edge of its column, and the last title is
at the right edge of its column. Each other title is at the middle of its
column. The key `xs` holds the x of each column, as the layout returns it.

# `titles?`

```elixir
@spec titles?(map()) :: boolean()
```

Tell whether a chart shows a title over each column: an alluvial chart or
a parallel sets chart with the `levels` option

    iex> Expresso.Element.Chart.Sankey.titles?(%{kind: :alluvial, levels: ["2020", "2026"]})
    true
    iex> Expresso.Element.Chart.Sankey.titles?(%{kind: :sankey, levels: ["from", "to"]})
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
