1 | import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray';
|
2 | import _defineProperty from '@babel/runtime/helpers/defineProperty';
|
3 | import d3 from 'd3';
|
4 | import { is, join, replace, pipe, isEmpty } from 'ramda';
|
5 | import { map2tree } from 'map2tree';
|
6 | import deepmerge from 'deepmerge';
|
7 | import _typeof from '@babel/runtime/helpers/typeof';
|
8 | import { tooltip } from 'd3tooltip';
|
9 |
|
10 | function 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 |
|
34 | function sortAndSerialize(obj) {
|
35 | return JSON.stringify(sortObject(obj, true), undefined, 2);
|
36 | }
|
37 |
|
38 | function 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 | }
|
49 | function 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 | }
|
65 | function 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 | }
|
88 | function 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(' ');
|
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 |
|
103 | function 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 |
|
105 | function _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; }
|
106 | var 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() {
|
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 | };
|
159 | function 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 | }
|
218 |
|
219 |
|
220 |
|
221 |
|
222 | var previousNodePositionsById = {
|
223 | root: {
|
224 | id: 'root',
|
225 | parentId: null,
|
226 | x: height / 2,
|
227 | y: 0
|
228 | }
|
229 | };
|
230 |
|
231 |
|
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 ?
|
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;
|
266 |
|
267 |
|
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 |
|
282 | var diagonal = d3.svg.diagonal().projection(function (d) {
|
283 | return [d.y, d.x];
|
284 | });
|
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 | });
|
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 | }
|
340 |
|
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);
|
362 |
|
363 | node.select('text').text(function (d) {
|
364 | return d.name;
|
365 | });
|
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 | });
|
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 | });
|
380 |
|
381 | nodeUpdate.select('circle').attr('r', style.node.radius);
|
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 | });
|
389 |
|
390 | node.filter(function flick(d) {
|
391 |
|
392 |
|
393 |
|
394 |
|
395 | return this.__oldData__ && d.value !== this.__oldData__.value;
|
396 | }).select('g').style('opacity', '0.3').transition().duration(blinkDuration).style('opacity', '1');
|
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);
|
409 |
|
410 | var link = vis.selectAll('path.link').data(links, function (d) {
|
411 | return d.target.id;
|
412 | });
|
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);
|
427 |
|
428 | link.transition().duration(transitionDuration).attr({
|
429 | d: diagonal
|
430 | });
|
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();
|
444 |
|
445 | node.property('__oldData__', null);
|
446 |
|
447 | previousNodePositionsById = nodePositionsById;
|
448 | }
|
449 | };
|
450 | }
|
451 |
|
452 | export { tree };
|