UNPKG

15.5 kBJavaScriptView Raw
1import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray';
2import _defineProperty from '@babel/runtime/helpers/defineProperty';
3import d3 from 'd3';
4import { is, join, replace, pipe, isEmpty } from 'ramda';
5import { map2tree } from 'map2tree';
6import deepmerge from 'deepmerge';
7import _typeof from '@babel/runtime/helpers/typeof';
8import { tooltip } from 'd3tooltip';
9
10function sortObject(obj, strict) {
11 if (obj instanceof Array) {
12 var ary;
13
14 if (strict) {
15 ary = obj.sort();
16 } else {
17 ary = obj;
18 }
19
20 return ary;
21 }
22
23 if (obj && _typeof(obj) === 'object') {
24 var tObj = {};
25 Object.keys(obj).sort().forEach(function (key) {
26 return tObj[key] = sortObject(obj[key]);
27 });
28 return tObj;
29 }
30
31 return obj;
32}
33
34function sortAndSerialize(obj) {
35 return JSON.stringify(sortObject(obj, true), undefined, 2);
36}
37
38function toggleChildren(node) {
39 if (node.children) {
40 node._children = node.children;
41 node.children = null;
42 } else if (node._children) {
43 node.children = node._children;
44 node._children = null;
45 }
46
47 return node;
48}
49function visit(parent, visitFn, childrenFn) {
50 if (!parent) {
51 return;
52 }
53
54 visitFn(parent);
55 var children = childrenFn(parent);
56
57 if (children) {
58 var count = children.length;
59
60 for (var i = 0; i < count; i++) {
61 visit(children[i], visitFn, childrenFn);
62 }
63 }
64}
65function getNodeGroupByDepthCount(rootNode) {
66 var nodeGroupByDepthCount = [1];
67
68 var traverseFrom = function traverseFrom(node) {
69 var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
70
71 if (!node.children || node.children.length === 0) {
72 return 0;
73 }
74
75 if (nodeGroupByDepthCount.length <= depth + 1) {
76 nodeGroupByDepthCount.push(0);
77 }
78
79 nodeGroupByDepthCount[depth + 1] += node.children.length;
80 node.children.forEach(function (childNode) {
81 traverseFrom(childNode, depth + 1);
82 });
83 };
84
85 traverseFrom(rootNode);
86 return nodeGroupByDepthCount;
87}
88function getTooltipString(node, i, _ref) {
89 var _ref$indentationSize = _ref.indentationSize,
90 indentationSize = _ref$indentationSize === void 0 ? 4 : _ref$indentationSize;
91 if (!is(Object, node)) return '';
92 var spacer = join('&nbsp;&nbsp;');
93 var cr2br = replace(/\n/g, '<br/>');
94 var spaces2nbsp = replace(/\s{2}/g, spacer(new Array(indentationSize)));
95 var json2html = pipe(sortAndSerialize, cr2br, spaces2nbsp);
96 var children = node.children || node._children;
97 if (typeof node.value !== 'undefined') return json2html(node.value);
98 if (typeof node.object !== 'undefined') return json2html(node.object);
99 if (children && children.length) return "childrenCount: ".concat(children.length);
100 return 'empty';
101}
102
103function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
104
105function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
106var defaultOptions = {
107 state: undefined,
108 rootKeyName: 'state',
109 pushMethod: 'push',
110 tree: undefined,
111 id: 'd3svg',
112 style: {
113 node: {
114 colors: {
115 default: '#ccc',
116 collapsed: 'lightsteelblue',
117 parent: 'white'
118 },
119 radius: 7
120 },
121 text: {
122 colors: {
123 default: 'black',
124 hover: 'skyblue'
125 }
126 },
127 link: {
128 stroke: '#000',
129 fill: 'none'
130 }
131 },
132 size: 500,
133 aspectRatio: 1.0,
134 initialZoom: 1,
135 margin: {
136 top: 10,
137 right: 10,
138 bottom: 10,
139 left: 50
140 },
141 isSorted: false,
142 heightBetweenNodesCoeff: 2,
143 widthBetweenNodesCoeff: 1,
144 transitionDuration: 750,
145 blinkDuration: 100,
146 onClickText: function onClickText() {// noop
147 },
148 tooltipOptions: {
149 disabled: false,
150 left: undefined,
151 top: undefined,
152 offset: {
153 left: 0,
154 top: 0
155 },
156 style: undefined
157 }
158};
159function tree (DOMNode) {
160 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
161
162 var _deepmerge = deepmerge(defaultOptions, options),
163 id = _deepmerge.id,
164 style = _deepmerge.style,
165 size = _deepmerge.size,
166 aspectRatio = _deepmerge.aspectRatio,
167 initialZoom = _deepmerge.initialZoom,
168 margin = _deepmerge.margin,
169 isSorted = _deepmerge.isSorted,
170 widthBetweenNodesCoeff = _deepmerge.widthBetweenNodesCoeff,
171 heightBetweenNodesCoeff = _deepmerge.heightBetweenNodesCoeff,
172 transitionDuration = _deepmerge.transitionDuration,
173 blinkDuration = _deepmerge.blinkDuration,
174 state = _deepmerge.state,
175 rootKeyName = _deepmerge.rootKeyName,
176 pushMethod = _deepmerge.pushMethod,
177 tree = _deepmerge.tree,
178 tooltipOptions = _deepmerge.tooltipOptions,
179 onClickText = _deepmerge.onClickText;
180
181 var width = size - margin.left - margin.right;
182 var height = size * aspectRatio - margin.top - margin.bottom;
183 var fullWidth = size;
184 var fullHeight = size * aspectRatio;
185 var attr = {
186 id: id,
187 preserveAspectRatio: 'xMinYMin slice'
188 };
189
190 if (!style.width) {
191 attr.width = fullWidth;
192 }
193
194 if (!style.width || !style.height) {
195 attr.viewBox = "0 0 ".concat(fullWidth, " ").concat(fullHeight);
196 }
197
198 var root = d3.select(DOMNode);
199 var zoom = d3.behavior.zoom().scaleExtent([0.1, 3]).scale(initialZoom);
200 var vis = root.append('svg').attr(attr).style(_objectSpread({
201 cursor: '-webkit-grab'
202 }, style)).call(zoom.on('zoom', function () {
203 var _d3$event = d3.event,
204 translate = _d3$event.translate,
205 scale = _d3$event.scale;
206 vis.attr('transform', "translate(".concat(translate.toString(), ")scale(").concat(scale, ")"));
207 })).append('g').attr({
208 transform: "translate(".concat(margin.left + style.node.radius, ", ").concat(margin.top, ") scale(").concat(initialZoom, ")")
209 });
210 var layout = d3.layout.tree().size([width, height]);
211 var data;
212
213 if (isSorted) {
214 layout.sort(function (a, b) {
215 return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
216 });
217 } // previousNodePositionsById stores node x and y
218 // as well as hierarchy (id / parentId);
219 // helps animating transitions
220
221
222 var previousNodePositionsById = {
223 root: {
224 id: 'root',
225 parentId: null,
226 x: height / 2,
227 y: 0
228 }
229 }; // traverses a map with node positions by going through the chain
230 // of parent ids; once a parent that matches the given filter is found,
231 // the parent position gets returned
232
233 function findParentNodePosition(nodePositionsById, nodeId, filter) {
234 var currentPosition = nodePositionsById[nodeId];
235
236 while (currentPosition) {
237 currentPosition = nodePositionsById[currentPosition.parentId];
238
239 if (!currentPosition) {
240 return null;
241 }
242
243 if (!filter || filter(currentPosition)) {
244 return currentPosition;
245 }
246 }
247 }
248
249 return function renderChart() {
250 var nextState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : tree || state;
251 data = !tree ? // eslint-disable-next-line @typescript-eslint/ban-types
252 map2tree(nextState, {
253 key: rootKeyName,
254 pushMethod: pushMethod
255 }) : nextState;
256
257 if (isEmpty(data) || !data.name) {
258 data = {
259 name: 'error',
260 message: 'Please provide a state map or a tree structure'
261 };
262 }
263
264 var nodeIndex = 0;
265 var maxLabelLength = 0; // nodes are assigned with string ids, which reflect their location
266 // within the hierarcy; e.g. "root|branch|subBranch|subBranch[0]|property"
267 // top-level elemnt always has id "root"
268
269 visit(data, function (node) {
270 maxLabelLength = Math.max(node.name.length, maxLabelLength);
271 node.id = node.id || 'root';
272 }, function (node) {
273 return node.children && node.children.length > 0 ? node.children.map(function (c) {
274 c.id = "".concat(node.id || '', "|").concat(c.name);
275 return c;
276 }) : null;
277 });
278 update();
279
280 function update() {
281 // path generator for links
282 var diagonal = d3.svg.diagonal().projection(function (d) {
283 return [d.y, d.x];
284 }); // set tree dimensions and spacing between branches and nodes
285
286 var maxNodeCountByLevel = Math.max.apply(Math, _toConsumableArray(getNodeGroupByDepthCount(data)));
287 layout = layout.size([maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, width]);
288 var nodes = layout.nodes(data);
289 var links = layout.links(nodes);
290 nodes.forEach(function (node) {
291 return node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff);
292 });
293 var nodePositions = nodes.map(function (n) {
294 return {
295 parentId: n.parent && n.parent.id,
296 id: n.id,
297 x: n.x,
298 y: n.y
299 };
300 });
301 var nodePositionsById = {};
302 nodePositions.forEach(function (node) {
303 return nodePositionsById[node.id] = node;
304 }); // process the node selection
305
306 var node = vis.selectAll('g.node').property('__oldData__', function (d) {
307 return d;
308 }).data(nodes, function (d) {
309 return d.id || (d.id = ++nodeIndex);
310 });
311 var nodeEnter = node.enter().append('g').attr({
312 class: 'node',
313 transform: function transform(d) {
314 var position = findParentNodePosition(nodePositionsById, d.id, function (n) {
315 return !!previousNodePositionsById[n.id];
316 });
317 var previousPosition = position && previousNodePositionsById[position.id] || previousNodePositionsById.root;
318 return "translate(".concat(previousPosition.y, ",").concat(previousPosition.x, ")");
319 }
320 }).style({
321 fill: style.text.colors.default,
322 cursor: 'pointer'
323 }).on('mouseover', function mouseover() {
324 d3.select(this).style({
325 fill: style.text.colors.hover
326 });
327 }).on('mouseout', function mouseout() {
328 d3.select(this).style({
329 fill: style.text.colors.default
330 });
331 });
332
333 if (!tooltipOptions.disabled) {
334 nodeEnter.call(tooltip(d3, 'tooltip', _objectSpread(_objectSpread({}, tooltipOptions), {}, {
335 root: root
336 })).text(function (d, i) {
337 return getTooltipString(d, i, tooltipOptions);
338 }).style(tooltipOptions.style));
339 } // g inside node contains circle and text
340 // this extra wrapper helps run d3 transitions in parallel
341
342
343 var nodeEnterInnerGroup = nodeEnter.append('g');
344 nodeEnterInnerGroup.append('circle').attr({
345 class: 'nodeCircle',
346 r: 0
347 }).on('click', function (clickedNode) {
348 if (d3.event.defaultPrevented) return;
349 toggleChildren(clickedNode);
350 update();
351 });
352 nodeEnterInnerGroup.append('text').attr({
353 class: 'nodeText',
354 'text-anchor': 'middle',
355 transform: 'translate(0,0)',
356 dy: '.35em'
357 }).style({
358 'fill-opacity': 0
359 }).text(function (d) {
360 return d.name;
361 }).on('click', onClickText); // update the text to reflect whether node has children or not
362
363 node.select('text').text(function (d) {
364 return d.name;
365 }); // change the circle fill depending on whether it has children and is collapsed
366
367 node.select('circle').style({
368 stroke: 'black',
369 'stroke-width': '1.5px',
370 fill: function fill(d) {
371 return d._children ? style.node.colors.collapsed : d.children ? style.node.colors.parent : style.node.colors.default;
372 }
373 }); // transition nodes to their new position
374
375 var nodeUpdate = node.transition().duration(transitionDuration).attr({
376 transform: function transform(d) {
377 return "translate(".concat(d.y, ",").concat(d.x, ")");
378 }
379 }); // ensure circle radius is correct
380
381 nodeUpdate.select('circle').attr('r', style.node.radius); // fade the text in and align it
382
383 nodeUpdate.select('text').style('fill-opacity', 1).attr({
384 transform: function transform(d) {
385 var x = (d.children || d._children ? -1 : 1) * (this.getBBox().width / 2 + style.node.radius + 5);
386 return "translate(".concat(x, ",0)");
387 }
388 }); // blink updated nodes
389
390 node.filter(function flick(d) {
391 // test whether the relevant properties of d match
392 // the equivalent property of the oldData
393 // also test whether the old data exists,
394 // to catch the entering elements!
395 return this.__oldData__ && d.value !== this.__oldData__.value;
396 }).select('g').style('opacity', '0.3').transition().duration(blinkDuration).style('opacity', '1'); // transition exiting nodes to the parent's new position
397
398 var nodeExit = node.exit().transition().duration(transitionDuration).attr({
399 transform: function transform(d) {
400 var position = findParentNodePosition(previousNodePositionsById, d.id, function (n) {
401 return !!nodePositionsById[n.id];
402 });
403 var futurePosition = position && nodePositionsById[position.id] || nodePositionsById.root;
404 return "translate(".concat(futurePosition.y, ",").concat(futurePosition.x, ")");
405 }
406 }).remove();
407 nodeExit.select('circle').attr('r', 0);
408 nodeExit.select('text').style('fill-opacity', 0); // update the links
409
410 var link = vis.selectAll('path.link').data(links, function (d) {
411 return d.target.id;
412 }); // enter any new links at the parent's previous position
413
414 link.enter().insert('path', 'g').attr({
415 class: 'link',
416 d: function d(_d) {
417 var position = findParentNodePosition(nodePositionsById, _d.target.id, function (n) {
418 return !!previousNodePositionsById[n.id];
419 });
420 var previousPosition = position && previousNodePositionsById[position.id] || previousNodePositionsById.root;
421 return diagonal({
422 source: previousPosition,
423 target: previousPosition
424 });
425 }
426 }).style(style.link); // transition links to their new position
427
428 link.transition().duration(transitionDuration).attr({
429 d: diagonal
430 }); // transition exiting nodes to the parent's new position
431
432 link.exit().transition().duration(transitionDuration).attr({
433 d: function d(_d2) {
434 var position = findParentNodePosition(previousNodePositionsById, _d2.target.id, function (n) {
435 return !!nodePositionsById[n.id];
436 });
437 var futurePosition = position && nodePositionsById[position.id] || nodePositionsById.root;
438 return diagonal({
439 source: futurePosition,
440 target: futurePosition
441 });
442 }
443 }).remove(); // delete the old data once it's no longer needed
444
445 node.property('__oldData__', null); // stash the old positions for transition
446
447 previousNodePositionsById = nodePositionsById;
448 }
449 };
450}
451
452export { tree };