Skip to content

Commit 8775975

Browse files
committed
Layout v0.1
1 parent 59b09b0 commit 8775975

File tree

13 files changed

+267
-168
lines changed

13 files changed

+267
-168
lines changed

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,14 @@ You first need to install node dependencies (requires [Node.js](https://nodejs.
5252
The following commands will then be available from the repository root:
5353

5454
```bash
55-
> gulp build // build dist files
56-
> gulp build --watch // build and watch for changes
57-
> gulp test // run all tests
58-
> gulp test --watch // run all tests and watch for changes
59-
> gulp test --coverage // run all tests and generate code coverage
60-
> gulp lint // perform code linting
61-
> gulp package // create an archive with dist files and samples
55+
> npm run build // build dist files
56+
> npm run autobuild // build and watch for changes
57+
> npm test // run all tests
58+
> npm autotest // run all tests and watch for changes
59+
> npm lint // perform code linting
60+
> npm package // create an archive with dist files and samples
6261
```
6362

6463
## License
6564

66-
chartjs-chart-sankey is available under the [MIT license](https://opensource.org/licenses/MIT).
65+
chartjs-chart-sankey is available under the [MIT license](https://opensource.org/licenses/MIT).

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chartjs-chart-sankey",
3-
"version": "0.1.0-alpha",
3+
"version": "0.1.1-alpha",
44
"description": "Chart.js module for creating sankey diagrams",
55
"main": "dist/chartjs-chart-sankey.js",
66
"module": "dist/chartjs-chart-sankey.esm.js",

samples/basic.html

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<title>Sankey diagram</title>
6+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.js"></script>
7+
<script src="utils.js"></script>
8+
<link rel="stylesheet" type="text/css" href="style.css">
9+
</link>
10+
</head>
11+
12+
<body>
13+
<div class="canvas-holder">
14+
<canvas id="chart-area"></canvas>
15+
</div>
16+
17+
<script>
18+
/* global Utils, Chart */
19+
20+
Utils.load(() => {
21+
Chart.defaults.fontSize = 9;
22+
const ctx = document.getElementById('chart-area').getContext('2d');
23+
window.chart = new Chart(ctx, {
24+
type: 'sankey',
25+
data: {
26+
datasets: [{
27+
data: [
28+
{from: 'a', to: 'b', flow: 20},
29+
{from: 'c', to: 'd', flow: 10},
30+
{from: 'c', to: 'e', flow: 5},
31+
],
32+
colorFrom: 'red',
33+
colorTo: 'green'
34+
}],
35+
},
36+
options: {
37+
animation: false,
38+
responsive: false
39+
}
40+
});
41+
42+
});
43+
</script>
44+
</body>
45+
46+
</html>

samples/sankey.html

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,6 @@
9393
};
9494

9595
const data = [
96-
{from: "Agricultural 'waste'", to: 'Bio-conversion', flow: 124.729},
97-
{from: 'Bio-conversion', to: 'Losses', flow: 26.862},
98-
{from: 'Bio-conversion', to: 'Solid', flow: 280.322},
99-
{from: 'Bio-conversion', to: 'Gas', flow: 81.144},
100-
{from: 'Biomass imports', to: 'Solid', flow: 35},
10196
{from: 'District heating', to: 'Industry', flow: 10.639},
10297
{from: 'District heating', to: 'Heating and cooling - commercial', flow: 22.505},
10398
{from: 'District heating', to: 'Heating and cooling - homes', flow: 46.184},
@@ -112,39 +107,11 @@
112107
{from: 'Electricity grid', to: 'Rail transport', flow: 7.863},
113108
{from: 'Electricity grid', to: 'Lighting & appliances - commercial', flow: 90.008},
114109
{from: 'Electricity grid', to: 'Lighting & appliances - homes', flow: 93.494},
115-
{from: 'Gas imports', to: 'Ngas', flow: 40.719},
116-
{from: 'Gas reserves', to: 'Ngas', flow: 82.233},
117-
{from: 'Gas', to: 'Heating and cooling - commercial', flow: 0.129},
118-
{from: 'Gas', to: 'Losses', flow: 1.401},
119-
{from: 'Gas', to: 'Thermal generation', flow: 151.891},
120-
{from: 'Gas', to: 'Agriculture', flow: 2.096},
121-
{from: 'Gas', to: 'Industry', flow: 48.58},
122-
{from: 'Geothermal', to: 'Electricity grid', flow: 7.013},
123-
{from: 'H2 conversion', to: 'H2', flow: 20.897},
124-
{from: 'H2 conversion', to: 'Losses', flow: 6.242},
125-
{from: 'H2', to: 'Road transport', flow: 20.897},
126-
{from: 'Hydro', to: 'Electricity grid', flow: 6.995},
127-
{from: 'Marine algae', to: 'Bio-conversion', flow: 4.375},
128-
{from: 'Ngas', to: 'Gas', flow: 122.952},
129-
{from: 'Nuclear', to: 'Thermal generation', flow: 839.978},
130-
{from: 'Other waste', to: 'Solid', flow: 56.587},
131-
{from: 'Other waste', to: 'Bio-conversion', flow: 77.81},
132110
{from: 'Pumped heat', to: 'Heating and cooling - homes', flow: 193.026},
133111
{from: 'Pumped heat', to: 'Heating and cooling - commercial', flow: 70.672},
134-
{from: 'Solar PV', to: 'Electricity grid', flow: 59.901},
135112
{from: 'Solar Thermal', to: 'Heating and cooling - homes', flow: 19.263},
136113
{from: 'Solar', to: 'Solar Thermal', flow: 19.263},
137114
{from: 'Solar', to: 'Solar PV', flow: 59.901},
138-
{from: 'Solid', to: 'Agriculture', flow: 0.882},
139-
{from: 'Solid', to: 'Thermal generation', flow: 400.12},
140-
{from: 'Solid', to: 'Industry', flow: 46.477},
141-
{from: 'Thermal generation', to: 'Electricity grid', flow: 525.531},
142-
{from: 'Thermal generation', to: 'Losses', flow: 787.129},
143-
{from: 'Thermal generation', to: 'District heating', flow: 79.329},
144-
{from: 'Tidal', to: 'Electricity grid', flow: 9.452},
145-
{from: 'UK land based bioenergy', to: 'Bio-conversion', flow: 182.01},
146-
{from: 'Wave', to: 'Electricity grid', flow: 19.013},
147-
{from: 'Wind', to: 'Electricity grid', flow: 289.366}
148115
];
149116

150117
Utils.load(() => {

samples/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ body {
77
margin: auto;
88
}
99
div.canvas-holder {
10-
width: 800px;
10+
width: 954px;
1111
height: 600px;
1212
margin: auto;
1313
}

sankey.png

-432 KB
Loading

src/controller.js

Lines changed: 38 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Chart from 'chart.js';
44
import Flow from './flow';
5+
import {layout} from './layout';
56

67
export function buildNodesFromFlows(data) {
78
const nodes = new Map();
@@ -46,102 +47,6 @@ export function buildNodesFromFlows(data) {
4647
return nodes;
4748
}
4849

49-
export function calculateX(nodes, data) {
50-
const to = new Set(data.map(x => x.to));
51-
const from = new Set(data.map(x => x.from));
52-
const keys = new Set([...nodes.keys()]);
53-
let lvl = 0;
54-
while (keys.size) {
55-
const column = [...keys].filter(x => !to.has(x));
56-
for (let i = 0; i < column.length; i++) {
57-
nodes.get(column[i]).x = lvl;
58-
keys.delete(column[i]);
59-
}
60-
if (keys.size) {
61-
to.clear();
62-
data.filter(x => keys.has(x.from)).forEach(x => to.add(x.to));
63-
lvl++;
64-
}
65-
}
66-
[...nodes.keys()]
67-
.filter(key => !from.has(key))
68-
.forEach(key => {
69-
nodes.get(key).x = lvl;
70-
});
71-
}
72-
73-
function columnSort(nodeA, nodeB) {
74-
for (let i = 0; i < nodeA.to.length; i++) {
75-
const toA = nodeA.to[i];
76-
for (let j = 0; j < nodeB.to.length; j++) {
77-
const toB = nodeB.to[j];
78-
if (toA.key === toB.key) {
79-
return toA.flow - toB.flow;
80-
}
81-
}
82-
}
83-
84-
const toALen = nodeA.to.length;
85-
const toBLen = nodeB.to.length;
86-
return toALen === toBLen
87-
? nodeB.out - nodeA.out
88-
: toALen - toBLen;
89-
90-
}
91-
92-
function sortedNodeKeys(nodes) {
93-
return [...nodes.keys()].sort((a, b) => {
94-
const nodeA = nodes.get(a);
95-
const nodeB = nodes.get(b);
96-
return nodeA.x === nodeB.x
97-
? columnSort(nodeA, nodeB)
98-
: nodeA.x - nodeB.x;
99-
});
100-
}
101-
102-
function addPadding(nodes, padding) {
103-
let i = 0;
104-
let curX = 0;
105-
sortedNodeKeys(nodes).forEach(key => {
106-
const node = nodes.get(key);
107-
if (curX !== node.x) {
108-
i = 0;
109-
curX = node.x;
110-
}
111-
node.y += i * padding;
112-
i++;
113-
});
114-
}
115-
116-
export function calculateY(nodes) {
117-
let tmpY = 0;
118-
let curX = 0;
119-
let maxY = 0;
120-
let count = 0;
121-
let maxCount = 0;
122-
sortedNodeKeys(nodes).forEach(key => {
123-
const node = nodes.get(key);
124-
if (node.x > curX) {
125-
tmpY = 0;
126-
curX = node.x;
127-
count = 0;
128-
}
129-
if (node.x === 0) {
130-
node.y = tmpY;
131-
} else {
132-
node.y = Math.max(tmpY, node.from.reduce((acc, cur) => acc + cur.node.y, 0) / node.from.length - node.in);
133-
}
134-
tmpY = node.y + Math.max(node.in, node.out);
135-
count++;
136-
maxY = Math.max(tmpY, maxY);
137-
maxCount = Math.max(count, maxCount);
138-
});
139-
const padding = maxY / maxCount / 20;
140-
141-
addPadding(nodes, padding);
142-
143-
return maxY + maxCount * padding;
144-
}
14550

14651
function getAddY(arr, key) {
14752
for (let i = 0; i < arr.length; i++) {
@@ -160,8 +65,9 @@ export default class SankeyController extends Chart.DatasetController {
16065
const parsed = [];
16166
const nodes = me._nodes = buildNodesFromFlows(data);
16267

163-
calculateX(nodes, data);
164-
const maxY = calculateY(nodes, data);
68+
const {maxX, maxY} = layout(nodes, data);
69+
70+
xScale.options.max = maxX;
16571
yScale.options.max = maxY;
16672

16773
for (let i = 0, ilen = data.length; i < ilen; ++i) {
@@ -207,9 +113,9 @@ export default class SankeyController extends Chart.DatasetController {
207113
elems[i],
208114
index,
209115
{
210-
x: xScale.getPixelForValue(parsed.x) + 10,
116+
x: xScale.getPixelForValue(parsed.x) + 11,
211117
y,
212-
x2: xScale.getPixelForValue(custom.x),
118+
x2: xScale.getPixelForValue(custom.x) - 1,
213119
y2: yScale.getPixelForValue(custom.y),
214120
from: custom.from,
215121
to: custom.to,
@@ -223,6 +129,33 @@ export default class SankeyController extends Chart.DatasetController {
223129
me.updateSharedOptions(sharedOptions, mode);
224130
}
225131

132+
_drawLabels() {
133+
const me = this;
134+
const ctx = me._ctx;
135+
const nodes = me._nodes || new Map();
136+
const {xScale, yScale} = me._cachedMeta;
137+
138+
ctx.save();
139+
const chartArea = me.chart.chartArea;
140+
141+
for (const node of nodes.values()) {
142+
const x = xScale.getPixelForValue(node.x);
143+
const y = yScale.getPixelForValue(node.y);
144+
const max = Math.max(node.in, node.out);
145+
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
146+
ctx.fillStyle = 'black';
147+
ctx.textBaseline = 'middle';
148+
if (x < chartArea.width / 2) {
149+
ctx.textAlign = 'left';
150+
ctx.fillText(node.key, x + 15, y + height / 2);
151+
} else {
152+
ctx.textAlign = 'right';
153+
ctx.fillText(node.key, x - 5, y + height / 2);
154+
}
155+
}
156+
ctx.restore();
157+
}
158+
226159
_drawNodes() {
227160
const me = this;
228161
const ctx = me._ctx;
@@ -231,7 +164,6 @@ export default class SankeyController extends Chart.DatasetController {
231164

232165
ctx.save();
233166
ctx.strokeStyle = 'black';
234-
const chartArea = me.chart.chartArea;
235167

236168
for (const node of nodes.values()) {
237169
ctx.fillStyle = node.color;
@@ -241,17 +173,6 @@ export default class SankeyController extends Chart.DatasetController {
241173
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
242174
ctx.strokeRect(x, y, 10, height);
243175
ctx.fillRect(x, y, 10, height);
244-
if (height > 12) {
245-
ctx.fillStyle = 'black';
246-
ctx.textBaseline = 'middle';
247-
if (x < chartArea.width / 2) {
248-
ctx.textAlign = 'left';
249-
ctx.fillText(node.key, x + 15, y + height / 2);
250-
} else {
251-
ctx.textAlign = 'right';
252-
ctx.fillText(node.key, x - 5, y + height / 2);
253-
}
254-
}
255176
}
256177
ctx.restore();
257178
}
@@ -263,12 +184,16 @@ export default class SankeyController extends Chart.DatasetController {
263184

264185
for (let i = 0, ilen = data.length; i < ilen; ++i) {
265186
const flow = data[i];
266-
flow.draw(ctx);
267187
flow.from.color = flow.options.colorFrom;
268188
flow.to.color = flow.options.colorTo;
269189
}
270190

191+
me._drawLabels();
271192
me._drawNodes();
193+
194+
for (let i = 0, ilen = data.length; i < ilen; ++i) {
195+
data[i].draw(ctx);
196+
}
272197
}
273198
}
274199

src/flow.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,23 @@ export default class Flow extends Chart.Element {
4242
ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
4343
ctx.clip();
4444
}
45+
46+
const fill = ctx.createLinearGradient(x, 0, x2, 0);
47+
fill.addColorStop(0, color(options.colorFrom).alpha(0.5).rgbString());
48+
fill.addColorStop(1, color(options.colorTo).alpha(0.5).rgbString());
49+
ctx.fillStyle = fill;
50+
ctx.strokeStyle = fill;
51+
ctx.lineWidth = 0.5;
52+
4553
ctx.beginPath();
4654
ctx.moveTo(x, y);
4755
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
4856
ctx.lineTo(x2, y2 + height);
4957
ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
5058
ctx.lineTo(x, y);
59+
ctx.stroke();
5160
ctx.closePath();
5261

53-
const fill = ctx.createLinearGradient(x, 0, x2, 0);
54-
fill.addColorStop(0, color(options.colorFrom).alpha(0.5).rgbString());
55-
fill.addColorStop(1, color(options.colorTo).alpha(0.5).rgbString());
56-
ctx.fillStyle = fill;
5762
ctx.fill();
5863

5964
ctx.restore();

0 commit comments

Comments
 (0)