Skip to content

Commit

Permalink
ENH: categories can be an array. Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
sanand0 committed Oct 28, 2024
1 parent d8a8398 commit a8b250f
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 118 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
TODO*
docs/api.md
node_modules/
dist/
147 changes: 144 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Given this [table of sales](docs/sales.json ":ignore"):

[![Sales dataset screenshot](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/sales-data.webp)](docs/sales.json ":ignore")

... we can render the following Sankey diagram:
... we can render the following clickable Sankey diagram:

[![Sales Sankey diagram](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/sales.webp)](docs/sales.html ":include")
[![Sales Sankey diagram](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/sales.webp)](docs/sales.html ":include height=320px")

[Here is the source code for the network above](docs/sales.html ":include :type=code")
[Here is the source code for the diagram above](docs/sales.html ":include :type=code")

## Installation

Expand Down Expand Up @@ -56,6 +56,145 @@ Use via CDN as a script:
</script>
```

## Use a DataFrame

The `sankey()` function accepts an array of objects, like this:

```json
[
{ "channel": "Online", "city": "Tokyo", "product": "Biscuit", "sales": 866.1, "prev": 1186.4 },
{ "channel": "Online", "city": "Tokyo", "product": "Cake", "sales": 26.4, "prev": 34.8 },
{ "channel": "Online", "city": "Tokyo", "product": "Cream", "sales": 38.3, "prev": 54.0 }
// ...
]
```

Pick any _categorical_ columns and use them as categories, like this:

```js
const graph = sankey("#sankey", { data, categories: ["channel", "city", "product"], d3 });
```

You can modify the labels with a `categories` object, as `{ "label": "key" }`:

```js
const graph = sankey("#sankey", { data, categories: { Channel: "channel", City: "city", Product: "product" }, d3 });
```

The `categories` object can also have values as functions that return each box's label:

```js
const graph = sankey("#sankey", {
data,
categories: { Channel: (d) => d.channel, City: "city", Product: (d) => `${d.product} (${d.subProduct})` },
d3,
});
```

[![Example](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/simple.png)](docs/simple.html ":include height=320px")

[See how to use sankey()](docs/simple.html ":include :type=code")

## Style the graph

Sankey accepts the following inputs options:

- `labelWidth`: Width of the category labels on the left
- `gap`: Vertical gap between boxes as a % of the box height. 0.5 (default) means half the space is used by the gaps.
- `size`: Box width key or function. Defaults to `d => 1`, i.e. each row counts as 1. Use `"sales"` or `d => d.sales` to use a `sales` column as the box size.
- `text`: Box label key or function. Defaults to `"key"`. Use `d => d.key.toUpperCase()` to display the category key in uppercase.

The returned `graph` object has these D3 joins:

- `nodes`: The `<rect>` boxes across all rows. The joined data object has these keys:
- `cat`: The category name. From keys of `categories`
- `key`: The box label. The value in the category
- `group`: Data rows for this box
- `size`: Box size. Sum of the `size` accessor on `group`
- `range`: Box X position as [start, end] with values between 0 and 1
- `width`: Box width in pixels when rendered
- `links`: The `<path>` links connecting the boxes. The joined data object has these keys:
- `source`: Source node object.
- `target`: Target node object.
- `group`: Data rows for this link, for this source AND target combination
- `size`: Link size. Sum of the `size` accessor on `group`
- `sourceRange`: Link top X position as [start, end] with values between 0 and 1
- `targetRange`: Link bottom Xposition as [start, end] with values between 0 and 1
- `texts`: The `<text>` labels on top of the boxes
- `text`: The text label
- `labels`: The `<text>` category names on the left
- `label`: The category name
- `nodeData`: Array of node data (entries have the same values as the `nodes` join)
- `linkData`: Array of link data (entries have the same values as the `links` join)

You can apply the D3 [`.attr`](https://github.com/d3/d3-selection#selection_attr),
[`.classed`](https://github.com/d3/d3-selection#selection_classed),
[`.style`](https://github.com/d3/d3-selection#selection_style),
[`.text`](https://github.com/d3/d3-selection#selection_text),
and any other [selection methods](https://github.com/d3/d3-selection) to style the elements.

You can use any node/link keys in the styles. For example:

```js
const graph = sankey("#sankey", data, { categories: ["channel", "city", "product"] });
graph.nodes.attr("fill", d3.scaleOrdinal(d3.schemeCategory10));
graph.links.attr("fill", "rgba(0,0,0,0.2)");
```

The generated SVG has the following structure:

```html
<svg>
<g class="labels">
<text class="label"></text>
</g>
<g class="links">
<path class="link"></path>
<path class="link"></path>
</g>
<g class="nodes">
<rect class="node"></rect>
<rect class="node"></rect>
<text class="text"></text>
<text class="text"></text>
</g>
</svg>
```

[![Example](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/style.png)](docs/style.html ":include height=320px")

[See how to style nodes and links](docs/style.html ":include :type=code")

## Add tooltips

You can use [Bootstrap tooltips](https://getbootstrap.com/docs/5.3/components/tooltips/).

1. Add a `data-bs-toggle="tooltip" title="..."` attribute to each feature using `update`
2. Call `new bootstrap.Tooltip(element, {selector: '[data-bs-toggle="tooltip"]'})` to initialize tooltips

[![Example](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/tooltip.png)](docs/tooltip.html ":include height=320px")

[See how to add tooltips](docs/tooltip.html ":include :type=code")

## Interactions

- Clicking on a node highlights all links connected to it by adding the `.show` class to the links.
- If a node _already_ has all links highlighted, clicking on it hides them by removing the `.show` class from the links.

You can style the `.show` class to make the links more or less visible. For example:

```css
.link {
opacity: 0.05;
}
.link.show {
opacity: 1;
}
```

[![Sales Sankey diagram](https://raw.githubusercontent.com/gramener/gramex-sankey/main/docs/sales.webp)](docs/sales.html ":include height=320px")

[Here is the source code for the diagram above](docs/sales.html ":include :type=code")

## API

Expand All @@ -65,6 +204,8 @@ Use via CDN as a script:

- 1.1.0: 28 Oct 2024.
- Clicking a node first shows all nodes if any are missing. (Earlier it would HIDE if any were present.)
- Allow categories to be an array of keys or functions
- Refactor to remove margins
- 1.0.0: 28 Oct 2024. Initial release

## Authors
Expand Down
60 changes: 0 additions & 60 deletions docs/api.md

This file was deleted.

18 changes: 15 additions & 3 deletions docs/sales.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
<svg id="sankey" width="800" height="400"></svg>
<svg id="sankey" width="800" height="300"></svg>

<style>
#sankey .node, #sankey .link {
stroke: white;
stroke-width: 1px;
}
#sankey .link {
opacity: 0.05;
}
#sankey .link.show {
opacity: 1;
}
</style>

<script type="module">
import { sankey } from "../dist/sankey.js";
Expand All @@ -9,8 +22,7 @@

// Create the sankey
const graph = sankey("#sankey", {
data: data,
labelWidth: 60,
data,
categories: { Channel: "channel", City: "city", Product: "product" },
size: "sales",
d3,
Expand Down
Binary file modified docs/sales.webp
Binary file not shown.
16 changes: 16 additions & 0 deletions docs/simple.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<svg id="sankey" width="800" height="300"></svg>

<script type="module">
import { sankey } from "../dist/sankey.js";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

// Load the data
const data = await fetch("sales.json").then(r => r.json());

// Create the sankey
const graph = sankey("#sankey", { data, categories: ["channel", "city", "product"], d3 });

// Color nodes and links
graph.nodes.attr("fill", d3.scaleOrdinal(d3.schemeCategory10))
graph.links.attr("fill", "rgba(0,0,0,0.2)");
</script>
Binary file added docs/simple.webp
Binary file not shown.
15 changes: 15 additions & 0 deletions docs/style.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<svg id="sankey" width="800" height="300"></svg>

<script type="module">
import { sankey } from "../dist/sankey.js";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

const data = await fetch("sales.json").then(r => r.json());
const graph = sankey("#sankey", { data, categories: ["channel", "city", "product"], d3 });

// Color nodes and links
const nodeColor = d3.scaleOrdinal([...d3.schemePaired]);
const linkColor = d3.scaleSequential(d3.interpolateBlues).domain(d3.extent(graph.linkData, d => d.size));
graph.nodes.attr("fill", d => nodeColor(d.key));
graph.links.attr("fill", d => linkColor(d.size));
</script>
Binary file added docs/style.webp
Binary file not shown.
23 changes: 23 additions & 0 deletions docs/tooltip.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>

<svg id="sankey" width="800" height="300"></svg>

<script type="module">
import { sankey } from "../dist/sankey.js";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

const data = await fetch("sales.json").then(r => r.json());
const graph = sankey("#sankey", { data, categories: ["channel", "city", "product"], d3 });
graph.nodes.attr("fill", d3.scaleOrdinal(d3.schemeCategory10))
graph.links.attr("fill", "rgba(0,0,0,0.2)");

// Add tooltip text
graph.nodes.attr("data-bs-toggle", "tooltip")
.attr("title", (d, i) => `${d.key}: ${d.size.toFixed(2)}`);
graph.links.attr("data-bs-toggle", "tooltip")
.attr("title", (d, i) => `${d.source.key} - ${d.target.key}: ${d.size.toFixed(2)}`);

// Initialize tooltips
new bootstrap.Tooltip("#sankey", { selector: '[data-bs-toggle="tooltip"]' });
</script>
Binary file added docs/tooltip.webp
Binary file not shown.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta charset="UTF-8" />
<title>Sankey</title>
<link rel="icon" href="https://raw.githubusercontent.com/gramener/assets/main/straive-favicon.svg" />
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/themes/vue.css" />
</head>

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gramex/sankey",
"version": "1.0.0",
"version": "1.1.0",
"description": "A Sankey (or flow) diagram for graph visualization.",
"module": "dist/sankey.js",
"main": "dist/sankey.min.js",
Expand Down
Loading

0 comments on commit a8b250f

Please sign in to comment.