Sankey Chart
A sankey chart: what flows from where to where, and how much. Nodes stand in columns, and between them lie the flows as bands, each as wide as what flows: where the visitors of a site come from and what they go on to, what a budget is made of and what it is spent on, where the energy of a plant ends up.
import com.pantrixui.components.chart.sankey.PantrixSankeyChart
- Nodes and links. The chart takes its nodes (
SankeyNode) and the links between them (SankeyLink); a link names its two nodes by their keys. Links between the same two nodes are one flow. - Columns. The columns come from the links: a node nothing flows into stands in the first column, the others a column past the last of those that flow into them, and a node nothing flows out of in the last.
alignchooses where a node that could stand in more than one column stands:Justify,Left,Right,Center. The nodes of a column are put in the order that keeps their flows from crossing as far as it can;order = SankeyOrder.Givenleaves them in the order they were given. - Size. A node is as tall as the greater of what flows into it and what flows out of it; a band is as wide as what flows. A node's width and the room between two nodes of a column are
ChartStyle'snodeWidthandnodeGap. - The node pointed at. The node under the pointer or a finger is ringed, its flows stand out and the others fade. The tooltip writes the node and each of its flows, with the name of the node at its other end. On the keyboard the arrows go to the nearest node the way they point.
- The flow pointed at. Where the pointer or a finger is on a flow, the flow stands out, the others fade, and the tooltip writes its two nodes the way it flows ("Search → Signed up"). The keyboard stays on the nodes.
- What is refused. A link from a node to itself, links that lead back to where they started, and a link that names a node the chart does not have are refused.
- Export.
exportputs a button over the chart: the chart is made, as it is on the screen and without the part that is pointed at or its tooltip, into a PNG, an SVG or a PDF file and handed to the app. - Automation. The chart is tagged
Sankey_<tagKey>, a nodeSankey_<tagKey>.Node_<number>. A press on a node is reported as aselectevent with the node's key, and a press on a flow with the keys of its two nodes (source>target).
Examples
Where visitors come from and where they go
Three columns: the sources visitors come from, what they do on the site, and how they end up. The chart works the columns out from the links itself, and puts the nodes of a column in an order that keeps their flows from crossing.
Move the pointer over a node, or touch the node: the node's flows grow stronger and the tooltip writes each of them with the node at its other end. On a flow, that flow alone grows stronger and the tooltip writes it with its two nodes.
PantrixSankeyChart(
nodes = VISITOR_NODES,
links = VISITOR_LINKS,
modifier = Modifier.height(280.dp),
description = "The sources visitors come from, what they do on the site, and how they end up",
tagKey = "plain"
)
private val VISITOR_NODES = listOf(
SankeyNode("search", "Search"),
SankeyNode("social", "Social"),
SankeyNode("direct", "Direct"),
SankeyNode("signup", "Signed up"),
SankeyNode("browse", "Browsed"),
SankeyNode("paid", "Paid"),
SankeyNode("free", "Stayed free"),
SankeyNode("left", "Left")
)
private val VISITOR_LINKS = listOf(
SankeyLink("search", "signup", 300),
SankeyLink("search", "browse", 200),
SankeyLink("social", "signup", 100),
SankeyLink("social", "browse", 200),
SankeyLink("direct", "signup", 120),
SankeyLink("direct", "browse", 80),
SankeyLink("signup", "paid", 220),
SankeyLink("signup", "free", 300),
SankeyLink("browse", "free", 180),
SankeyLink("browse", "left", 300)
)
Income and spending
Two columns: a month's income and where it is spent. The tooltip's values are written short (the format of ChartTooltip). Turn the switch: the nodes' names go away (names = false) and the names stay in the tooltip alone.
var named by remember { mutableStateOf(true) }
val strings = PantrixTheme.strings
PantrixSwitchButton(
checked = named,
onCheckedChange = { named = it },
label = "Node names",
tagKey = "sankey_names"
)
PantrixSankeyChart(
nodes = BUDGET_NODES,
links = BUDGET_LINKS,
names = named,
tooltip = ChartTooltip(format = { strings.compact(it) + " TL" }),
description = "A month's income and where it is spent",
tagKey = "budget"
)
private val BUDGET_NODES = listOf(
SankeyNode("salary", "Salary"),
SankeyNode("extra", "Extra income"),
SankeyNode("rent", "Rent"),
SankeyNode("food", "Food"),
SankeyNode("bills", "Bills"),
SankeyNode("saved", "Savings")
)
private val BUDGET_LINKS = listOf(
SankeyLink("salary", "rent", 28_000),
SankeyLink("salary", "food", 16_000),
SankeyLink("salary", "bills", 9_000),
SankeyLink("salary", "saved", 12_000),
SankeyLink("extra", "saved", 8_000),
SankeyLink("extra", "food", 4_000)
)
Pressing a node
A press hands over the node: its key in series, its name in name, what it counts for in value. A press on a flow hands over the flow: the key of the node it leaves in series, that of the node it reaches in x. Press a node, or a flow between two columns: the line under the chart writes what was pressed.
var pressed by remember { mutableStateOf<String?>(null) }
PantrixSankeyChart(
nodes = VISITOR_NODES,
links = VISITOR_LINKS,
modifier = Modifier.height(280.dp),
onSelect = { pressed = "${it.name}: ${it.text} visitors" },
description = "The sources visitors come from, what they do on the site, and how they end up",
tagKey = "press"
)
PantrixText(text = pressed ?: "Press a node")
private val VISITOR_NODES = listOf(
SankeyNode("search", "Search"),
SankeyNode("social", "Social"),
SankeyNode("direct", "Direct"),
SankeyNode("signup", "Signed up"),
SankeyNode("browse", "Browsed"),
SankeyNode("paid", "Paid"),
SankeyNode("free", "Stayed free"),
SankeyNode("left", "Left")
)
private val VISITOR_LINKS = listOf(
SankeyLink("search", "signup", 300),
SankeyLink("search", "browse", 200),
SankeyLink("social", "signup", 100),
SankeyLink("social", "browse", 200),
SankeyLink("direct", "signup", 120),
SankeyLink("direct", "browse", 80),
SankeyLink("signup", "paid", 220),
SankeyLink("signup", "free", 300),
SankeyLink("browse", "free", 180),
SankeyLink("browse", "left", 300)
)
Column alignment and order
Column alignment (align). Those from ads flow straight to a payment, and some of those from search leave at once: these two nodes could stand in more than one column. "Justify" puts a node nothing flows out of in the last column, "Left" as early as it can be, "Right" a node nothing flows into as late as it can be, and "Center" a column before the first node it flows into. Turn the switch: the nodes stand in a column in the order they were given (order = SankeyOrder.Given).
var align by remember { mutableStateOf(SankeyAlign.Justify) }
var given by remember { mutableStateOf(false) }
PantrixSegmentButton(
options = SANKEY_ALIGNS,
selected = align,
onSelect = { align = it },
fullWidth = false,
tagKey = "sankey_align"
)
PantrixSwitchButton(
checked = given,
onCheckedChange = { given = it },
label = "In the given order",
tagKey = "sankey_given"
)
PantrixSankeyChart(
nodes = ALIGN_NODES,
links = ALIGN_LINKS,
modifier = Modifier.height(260.dp),
align = align,
order = if (given) SankeyOrder.Given else SankeyOrder.Untangled,
description = "Where visitors come from and where they end up",
tagKey = "align"
)
private val ALIGN_NODES = listOf(
SankeyNode("search", "Search"),
SankeyNode("ads", "Ads"),
SankeyNode("signup", "Signed up"),
SankeyNode("browse", "Browsed"),
SankeyNode("left", "Left"),
SankeyNode("paid", "Paid"),
SankeyNode("bounce", "Left at once")
)
private val ALIGN_LINKS = listOf(
SankeyLink("search", "signup", 300),
SankeyLink("search", "browse", 200),
SankeyLink("search", "bounce", 100),
SankeyLink("ads", "paid", 60),
SankeyLink("signup", "paid", 120),
SankeyLink("signup", "left", 180),
SankeyLink("browse", "left", 200)
)
private val SANKEY_ALIGNS = listOf(
SegmentOption(value = SankeyAlign.Justify, label = "Justify", tagKey = "justify"),
SegmentOption(value = SankeyAlign.Left, label = "Left", tagKey = "left"),
SegmentOption(value = SankeyAlign.Right, label = "Right", tagKey = "right"),
SegmentOption(value = SankeyAlign.Center, label = "Center", tagKey = "center")
)
Your own sizes
The nodes' own sizes: wider nodes (nodeWidth) and more room between the nodes of a column (nodeGap). A field that is not given stays the theme's.
PantrixSankeyChart(
nodes = BUDGET_NODES,
links = BUDGET_LINKS,
description = "A month's income and where it is spent",
style = ChartStyle(nodeWidth = 24.dp, nodeGap = 16.dp),
tagKey = "own_sizes"
)
private val BUDGET_NODES = listOf(
SankeyNode("salary", "Salary"),
SankeyNode("extra", "Extra income"),
SankeyNode("rent", "Rent"),
SankeyNode("food", "Food"),
SankeyNode("bills", "Bills"),
SankeyNode("saved", "Savings")
)
private val BUDGET_LINKS = listOf(
SankeyLink("salary", "rent", 28_000),
SankeyLink("salary", "food", 16_000),
SankeyLink("salary", "bills", 9_000),
SankeyLink("salary", "saved", 12_000),
SankeyLink("extra", "saved", 8_000),
SankeyLink("extra", "food", 4_000)
)
Export
export = ChartExport(...) puts an export button over the chart: press it and choose the kind (PNG, SVG or PDF). The chart makes the file and hands it to onExport; below, its name and size show, and a PNG's picture, and "Save the file" hands it over in the platform's way.
The file has the nodes, the flows and the names; a flow or a node that is pointed at does not stand out in it.
var file by remember { mutableStateOf<ChartExportFile?>(null) }
PantrixSankeyChart(
nodes = VISITOR_NODES,
links = VISITOR_LINKS,
modifier = Modifier.height(280.dp),
description = "The sources visitors come from, what they do on the site, and how they end up",
tagKey = "exported",
export = ChartExport(fileName = "visitors") { file = it }
)
ExportedFile(file, tagKey = "exported")
private val VISITOR_NODES = listOf(
SankeyNode("search", "Search"),
SankeyNode("social", "Social"),
SankeyNode("direct", "Direct"),
SankeyNode("signup", "Signed up"),
SankeyNode("browse", "Browsed"),
SankeyNode("paid", "Paid"),
SankeyNode("free", "Stayed free"),
SankeyNode("left", "Left")
)
private val VISITOR_LINKS = listOf(
SankeyLink("search", "signup", 300),
SankeyLink("search", "browse", 200),
SankeyLink("social", "signup", 100),
SankeyLink("social", "browse", 200),
SankeyLink("direct", "signup", 120),
SankeyLink("direct", "browse", 80),
SankeyLink("signup", "paid", 220),
SankeyLink("signup", "free", 300),
SankeyLink("browse", "free", 180),
SankeyLink("browse", "left", 300)
)
API
nodes
A sankey chart: what flows from where to where, and how much. Nodes stand in columns, and between them lie the flows as bands, each as wide as what flows: where the visitors of a site come from and what they go on to, what a budget is made of and what it is spent on, where the energy of a plant ends up.
| Parameter | Type | Default | What it is |
|---|---|---|---|
nodes | List<SankeyNode> | the nodes. Their keys tell them apart, and two with one key are refused. | |
links | List<SankeyLink> | what flows between the nodes: each from one node to another, by their keys. | |
modifier | Modifier | Modifier | the chart's modifier. A height here is the chart's. |
names | Boolean | true | whether each node's name is written beside it, where it has room. |
tooltip | ChartTooltip? | ChartTooltip() | what is shown for the node that is pointed at: its name and what it counts for, and under them each flow into it and out of it, with the node at its other end. Its format writes the values; shared and crosshair are not read. null shows none. |
empty | (@Composable () -> Unit)? | null | what the chart shows while it has no value to draw, in the place of the theme's words for it ("No data"), which a screen reader is then not told either. null writes the words. |
state | ChartState | rememberChartState() | what the reader points at: the chart's own where none is given. |
description | String? | null | what the chart shows, in a few words, for a screen reader: the first thing it says of the chart. Left out, the chart is announced by the names of its nodes. |
onSelect | ((ChartPoint) -> Unit)? | null | called with the node that is pressed: its key is its series and its x, its name its name and its label, and its value what it counts for; and with the flow that is pressed: the key of the node it leaves is its series and of the node it reaches its x, its name the two nodes' names, and its value how much flows. |
style | ChartStyle? | null | this chart's own geometry: the fields set here, with the theme's for the rest. A node is nodeWidth wide, two nodes of a column are a nodeGap apart, and a node's corners are as round as barRadius says. |
colors | ChartColors? | null | this chart's own colours, where they are not the theme's. |
tagKey | String? | null | the chart's automation key: the chart is Sankey_<tagKey>, its nodes and flows together Sankey_<tagKey>.Plot, a node Sankey_<tagKey>.Node_<number>, counted from one, column by column and in each from the top down, and the tooltip Sankey_<tagKey>.Tooltip. A press on a node is reported as a Sankey select event with the node's key, and a press on a flow with the keys of its two nodes, from>to. Nothing of the values is reported. |
align | SankeyAlign | SankeyAlign.Justify | which column each node stands in, of those it could stand in. |
order | SankeyOrder | SankeyOrder.Untangled | the order the nodes of a column stand in: the one that keeps the flows from crossing, or as given. |
export | ChartExport? | null | what lets the reader take the chart out of the app as a PNG, an SVG or a PDF: a button over the chart makes the file of it as it is and hands it over (see ChartExport). null, the chart has no such button. |
ChartStyle
A chart's geometry: how tall it is where nothing says, how thick its lines and how large its marks are, and the room between its parts; and whether its fills have textures besides their colours.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | How tall a chart's plot is, with its axes, where the chart's modifier gives it no height. A legend above or under the plot adds its own. |
lineWidth | Dp | Dp.Unspecified | The width of a series' line. |
markerSize | Dp | Dp.Unspecified | The width and height of the mark on a point. |
selectedMarkerSize | Dp | Dp.Unspecified | The same of the mark on the point that is pointed at. |
gridWidth | Dp | Dp.Unspecified | The width of the grid's lines, of the lines along the axes and of the line that marks the place pointed at. Zero draws none. |
labelGap | Dp | Dp.Unspecified | Between the plot and what is written beside it and under it, and between that and an axis' title. |
tickSpacing | Dp | Dp.Unspecified | The least room between two values marked along the axis of values: what decides how many are marked where the axis does not say. |
legendGap | Dp | Dp.Unspecified | Between the legend and the chart it stands beside. |
legendItemGap | Dp | Dp.Unspecified | Between one entry of the legend and the next. |
swatchSize | Dp | Dp.Unspecified | How wide the sample of a series is in the legend and in the tooltip: a short run of its line, or a patch of its fill. |
swatchGap | Dp | Dp.Unspecified | Between that sample and the name after it. |
tooltipShape | ShapeSpec | ShapeSpec.Unspecified | The tooltip's corners: a step of the brand's shape scale, or a fixed shape. |
tooltipHorizontalPadding | Dp | Dp.Unspecified | Between the tooltip's sides and what it holds. |
tooltipVerticalPadding | Dp | Dp.Unspecified | Between its top and bottom and what it holds, and between its heading and rows. |
tooltipGap | Dp | Dp.Unspecified | Between the place pointed at and the tooltip beside it. |
tooltipElevation | Dp | Dp.Unspecified | The tooltip's shadow. |
barGap | Dp | Dp.Unspecified | Between two bars that stand side by side at a place. Bars too thin for it stand closer. |
barMaxWidth | Dp | Dp.Unspecified | The widest a bar is, however much room its place has: a chart of three places is not three slabs. |
barRadius | Dp | Dp.Unspecified | How round the corners of a bar's end are, the end away from nought. |
sliceGap | Dp | Dp.Unspecified | Between two slices of a pie that lie side by side, the same from the pie's middle to its rim. |
selectedSliceOffset | Dp | Dp.Unspecified | How far the slice of a pie that is pointed at stands out past the pie's rim. The pie keeps this much room about itself, so that the slice has where to stand. |
gaugeWidth | Dp | Dp.Unspecified | How thick the arc of a gauge is. An arc is no thicker than half its radius. |
meterHeight | Dp | Dp.Unspecified | How tall the bar of a meter group is. |
trackerHeight | Dp | Dp.Unspecified | How tall the blocks of a status tracker are. |
bubbleMinSize | Dp | Dp.Unspecified | How far across the smallest bubble of a scatter chart is: a point whose size is nothing, or too little to be seen as an area, is still a point. |
bubbleMaxSize | Dp | Dp.Unspecified | How far across its largest bubble is: the one whose size is the most of the chart's. |
navigatorHeight | Dp | Dp.Unspecified | How tall the strip under a chart's plot is, on which the part of the chart that is in view is marked and moved, in a chart whose zoom has one: how wide, beside a plot whose places go down its side. |
barBandShare | Float | Float.NaN | How much of a place's band its bars take, more than nought and one at the most: the rest is the room between one place's bars and the next's. Bars are no wider than barMaxWidth all the same. |
markLineWidth | Dp | Dp.Unspecified | The width of a mark's line: a target, a limit, a day. Zero draws none, and the mark's words are written all the same. |
markBandAlpha | Float | Float.NaN | How much of its colour a mark's band is drawn in, from nought to one: a band lies behind the series, and is a tint. |
patterns | Boolean? | null | Whether the fills of a chart's series, slices and tiles are drawn with textures besides their colours, and its lines broken in turn (ChartPattern): what tells them apart for a reader who cannot tell the colours apart. Left out, as the system is set: on where it is set to tell things apart without colour, iOS's "Differentiate Without Color" and Android's colour correction, and off otherwise. |
nodeWidth | Dp | Dp.Unspecified | How wide a node of a sankey is. |
nodeGap | Dp | Dp.Unspecified | Between two nodes of a column of a sankey. |
ChartColors
What a chart is painted with, for a chart that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.
| Parameter | Type | Default | What it is |
|---|---|---|---|
series | List<Color> | emptyList() | The colours the series are drawn in, in the order they are handed out, for the series that name none of their own. Empty, they are the theme's chart colours. |
grid | Color | Color.Unspecified | The lines drawn across the chart at the marks of an axis. The theme's divider. |
axis | Color | Color.Unspecified | The line along an axis. The theme's border. |
label | Color | Color.Unspecified | What is written beside the axes: the values, the places and the axes' titles, and the names in the legend. The theme's textSecondary. |
crosshair | Color | Color.Unspecified | The line that marks the place pointed at. The theme's borderHover: stronger than the grid it crosses, and quieter than the series it stands behind. |
tooltipContainer | Color | Color.Unspecified | The tooltip's fill. The theme's surface. |
tooltipBorder | Color | Color.Unspecified | The tooltip's edge. The theme's border. |
tooltipText | Color | Color.Unspecified | The tooltip's heading and its values. The theme's textPrimary. |
tooltipLabel | Color | Color.Unspecified | The names of the series in the tooltip. The theme's textSecondary. |
rising | Color | Color.Unspecified | What a value that goes up is drawn in: a step of a waterfall that adds. The theme's success. |
falling | Color | Color.Unspecified | What a value that goes down is drawn in: a step of a waterfall that takes away. The theme's danger. |
Accessibility
- Screen reader. First the chart's description, then each node, column by column: what it counts for, and each of its flows with where it comes from or goes to.