-
Notifications
You must be signed in to change notification settings - Fork 0
/
Sankey.js
338 lines (304 loc) · 10 KB
/
Sankey.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
/**
@external Viz
@see https://github.com/d3plus/d3plus-viz#Viz
*/
import {nest} from "d3-collection";
import {
sankey,
sankeyCenter,
sankeyJustify,
sankeyLeft,
sankeyLinkHorizontal,
sankeyRight
} from "d3-sankey";
const sankeyAligns = {
center: sankeyCenter,
justify: sankeyJustify,
left: sankeyLeft,
right: sankeyRight
};
import {accessor, assign, configPrep, constant, elem} from "d3plus-common";
import {Path} from "d3plus-shape";
import * as shapes from "d3plus-shape";
import {addToQueue, Viz} from "d3plus-viz";
/**
@class Sankey
@extends external:Viz
@desc Creates a sankey visualization based on a defined set of nodes and links. [Click here](http://d3plus.org/examples/d3plus-network/sankey-diagram/) for help getting started using the Sankey class.
*/
export default class Sankey extends Viz {
/**
@memberof Sankey
@desc Invoked when creating a new class instance, and sets any default parameters.
@private
*/
constructor() {
super();
this._nodeId = accessor("id");
this._links = accessor("links");
this._linksSource = "source";
this._linksTarget = "target";
this._noDataMessage = false;
this._nodes = accessor("nodes");
this._nodeAlign = sankeyAligns.justify;
this._nodePadding = 8;
this._nodeWidth = 30;
this._on.mouseenter = () => {};
this._on["mouseleave.shape"] = () => {
this.hover(false);
};
const defaultMouseMove = this._on["mousemove.shape"];
this._on["mousemove.shape"] = (d, i, x, event) => {
defaultMouseMove(d, i, x, event);
if (this._focus && this._focus === d.id) {
this.hover(false);
this._on.mouseenter.bind(this)(d, i, x, event);
this._focus = undefined;
}
else {
const id = this._nodeId(d, i),
node = this._nodeLookup[id],
nodeLookup = Object.keys(this._nodeLookup).reduce((all, item) => {
all[this._nodeLookup[item]] = !isNaN(item) ? parseInt(item, 10) : item;
return all;
}, {});
const links = this._linkLookup[node];
const filterIds = [id];
links.forEach(l => {
filterIds.push(nodeLookup[l]);
});
this.hover((h, x) => {
if (h.source && h.target) {
return h.source.id === id || h.target.id === id;
}
else {
return filterIds.includes(this._nodeId(h, x));
}
});
}
};
this._path = sankeyLinkHorizontal();
this._sankey = sankey();
this._shape = constant("Rect");
this._shapeConfig = assign(this._shapeConfig, {
Path: {
fill: "none",
hoverStyle: {
"stroke-width": d => Math.max(1, Math.abs(d.source.y1 - d.source.y0) * (d.value / d.source.value) - 2)
},
label: false,
stroke: "#DBDBDB",
strokeOpacity: 0.5,
strokeWidth: d => Math.max(1, Math.abs(d.source.y1 - d.source.y0) * (d.value / d.source.value) - 2)
},
Rect: {}
});
this._value = constant(1);
}
/**
Extends the draw behavior of the abstract Viz class.
@private
*/
_draw(callback) {
super._draw(callback);
const height = this._height - this._margin.top - this._margin.bottom,
width = this._width - this._margin.left - this._margin.right;
const _nodes = Array.isArray(this._nodes)
? this._nodes
: this._links.reduce((all, d) => {
if (!all.includes(d[this._linksSource])) all.push(d[this._linksSource]);
if (!all.includes(d[this._linksTarget])) all.push(d[this._linksTarget]);
return all;
}, []).map(id => ({id}));
const nodes = _nodes
.map((n, i) => ({
__d3plus__: true,
data: n,
i,
id: this._nodeId(n, i),
node: n,
shape: "Rect"
}));
const nodeLookup = this._nodeLookup = nodes.reduce((obj, d, i) => {
obj[d.id] = i;
return obj;
}, {});
const links = this._links.map((link, i) => {
const check = [this._linksSource, this._linksTarget];
const linkLookup = check.reduce((result, item) => {
result[item] = nodeLookup[link[item]];
return result;
}, {});
return {
source: linkLookup[this._linksSource],
target: linkLookup[this._linksTarget],
value: this._value(link, i)
};
});
this._linkLookup = links.reduce((obj, d) => {
if (!obj[d.source]) obj[d.source] = [];
obj[d.source].push(d.target);
if (!obj[d.target]) obj[d.target] = [];
obj[d.target].push(d.source);
return obj;
}, {});
const transform = `translate(${this._margin.left}, ${this._margin.top})`;
this._sankey
.nodeAlign(this._nodeAlign)
.nodePadding(this._nodePadding)
.nodeWidth(this._nodeWidth)
.nodes(nodes)
.links(links)
.size([width, height])();
this._shapes.push(
new Path()
.config(this._shapeConfig.Path)
.data(links)
.d(this._path)
.select(
elem("g.d3plus-Links", {
parent: this._select,
enter: {transform},
update: {transform}
}).node()
)
.render()
);
nest()
.key(d => d.shape)
.entries(nodes)
.forEach(d => {
this._shapes.push(
new shapes[d.key]()
.data(d.values)
.height(d => d.y1 - d.y0)
.width(d => d.x1 - d.x0)
.x(d => (d.x1 + d.x0) / 2)
.y(d => (d.y1 + d.y0) / 2)
.select(
elem("g.d3plus-sankey-nodes", {
parent: this._select,
enter: {transform},
update: {transform}
}).node()
)
.config(configPrep.bind(this)(this._shapeConfig, "shape", d.key))
.render()
);
});
return this;
}
/**
@memberof Sankey
@desc If *value* is specified, sets the hover method to the specified function and returns the current class instance.
@param {Function} [*value*]
@chainable
*/
hover(_) {
this._hover = _;
this._shapes.forEach(s => s.hover(_));
if (this._legend) this._legendClass.hover(_);
return this;
}
/**
@memberof Sankey
@desc A predefined *Array* of edges that connect each object passed to the [node](#Sankey.node) method. The `source` and `target` keys in each link need to map to the nodes in one of one way:
1. A *String* value matching the `id` of the node.
The value passed should be an *Array* of data. An optional formatting function can be passed as a second argument to this method. This custom function will be passed the data that has been loaded, as long as there are no errors. This function should return the final links *Array*.
@param {Array} *links* = []
@chainable
*/
links(_, f) {
if (arguments.length) {
addToQueue.bind(this)(_, f, "links");
return this;
}
return this._links;
}
/**
@memberof Sankey
@desc The key inside of each link Object that references the source node.
@param {String} [*value* = "source"]
@chainable
*/
linksSource(_) {
return arguments.length ? (this._linksSource = _, this) : this._linksSource;
}
/**
@memberof Sankey
@desc The key inside of each link Object that references the target node.
@param {String} [*value* = "target"]
@chainable
*/
linksTarget(_) {
return arguments.length ? (this._linksTarget = _, this) : this._linksTarget;
}
/**
@memberof Sankey
@desc Sets the nodeAlign property of the sankey layout, which can either be "left", "right", "center", or "justify".
@param {Function|String} [*value* = "justify"]
@chainable
*/
nodeAlign(_) {
return arguments.length
? (this._nodeAlign = typeof _ === "function" ? _ : sankeyAligns[_], this)
: this._nodeAlign;
}
/**
@memberof Sankey
@desc If *value* is specified, sets the node id accessor(s) to the specified array of values and returns the current class instance. If *value* is not specified, returns the current node group accessor.
@param {String} [*value* = "id"]
@chainable
*/
nodeId(_) {
return arguments.length
? (this._nodeId = typeof _ === "function" ? _ : accessor(_), this)
: this._nodeId;
}
/**
@memberof Sankey
@desc The list of nodes to be used for drawing the network. The value passed must be an *Array* of data.
Additionally, a custom formatting function can be passed as a second argument to this method. This custom function will be passed the data that has been loaded, as long as there are no errors. This function should return the final node *Array*.
@param {Array} *nodes* = []
@chainable
*/
nodes(_, f) {
if (arguments.length) {
addToQueue.bind(this)(_, f, "nodes");
return this;
}
return this._nodes;
}
/**
@memberof Sankey
@desc If *value* is specified, sets the padding of the node and returns the current class instance. If *value* is not specified, returns the current nodePadding. By default, the nodePadding size is 8.
@param {Number} [*value* = 8]
@chainable
*/
nodePadding(_) {
return arguments.length ? (this._nodePadding = _, this) : this._nodePadding;
}
/**
@memberof Sankey
@desc If *value* is specified, sets the width of the node and returns the current class instance. If *value* is not specified, returns the current nodeWidth. By default, the nodeWidth size is 30.
@param {Number} [*value* = 30]
@chainable
*/
nodeWidth(_) {
return arguments.length ? (this._nodeWidth = _, this) : this._nodeWidth;
}
/**
@memberof Sankey
@desc If *value* is specified, sets the width of the links and returns the current class instance. If *value* is not specified, returns the current value accessor.
@param {Function|Number} *value*
@example
function value(d) {
return d.value;
}
*/
value(_) {
return arguments.length
? (this._value = typeof _ === "function" ? _ : accessor(_), this)
: this._value;
}
}