UNPKG

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