.`, where the hash is the SHA of the commit being reverted.
### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
### Scope
The scope could be anything specifying place of the commit change. For example `$location`,
`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...
### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
## Recognitions
These guidelines are forked from Angular.
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Erik Gärtner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# dTree
*A library for visualizing data trees with multiple parents built on top of [D3](https://github.com/mbostock/d3).*
[](https://www.npmjs.com/package/d3-dtree) [](https://github.com/ErikGartner/dTree) [](https://www.jsdelivr.com/package/npm/d3-dtree)
**Using dTree? Send me a message with a link to your website to be listed below.**
## The Online Viewer
There exists an online viewer for dTree graphs called [Treehouse](https://treehouse.gartner.io), similar to [https://bl.ocks.org/](https://bl.ocks.org/) for D3. Treehouse allows anybody to host a dTree graph without having to create a website or interact directly with the library. It fetches data from Github's gists and displays it in a nice format. All graphs are unlisted so without your Gist ID nobody else can view them. Checkout the *demo* graph for dTree:
https://treehouse.gartner.io/ErikGartner/58e58be650453b6d49d7
The same demo is also available on [JSFiddle](https://jsfiddle.net/rha8sg79/).
## Installation
There are several ways to use dTree. One way is to simply include the compiled file ```dTree.js``` that then exposes a ```dTree``` variable. dTree is available on both NPM and Bower as *d3-dtree*.
```bash
npm install d3-dtree
bower install d3-dtree
yarn add d3-dtree
```
Lastly dTree is also available through several CDNs such as [jsDelivr](https://www.jsdelivr.com/package/npm/d3-dtree):
```
https://cdn.jsdelivr.net/npm/d3-dtree@2.4.1/dist/dTree.min.js
```
## Requirements
To use the library the follow dependencies must be loaded:
- [D3](https://github.com/mbostock/d3) v4.x
- [lodash](https://github.com/lodash/lodash) v4.x
## Usage
To create a graph from data use the following command:
```javascript
tree = dTree.init(data, options);
```
The data object should have the following structure:
```json
[
{
"name": "Father", // The name of the node
"class": "node", // The CSS class of the node
"textClass": "nodeText", // The CSS class of the text in the node
"depthOffset": 1, // Generational height offset
"marriages": [
{ // Marriages is a list of nodes
"spouse":
{ // Each marriage has one spouse
"name": "Mother",
},
"children": [
{ // List of children nodes
"name": "Child",
}]
}],
"extra":
{} // Custom data passed to renderers
}]
```
The following CSS sets some good defaults:
```css
.linage {
fill: none;
stroke: black;
}
.marriage {
fill: none;
stroke: black;
}
.node {
background-color: lightblue;
border-style: solid;
border-width: 1px;
}
.nodeText{
font: 10px sans-serif;
}
.marriageNode {
background-color: black;
border-radius: 50%;
}
```
The options object has the following default values:
```javascript
{
target: '#graph',
debug: false,
width: 600,
height: 600,
hideMarriageNodes: true,
marriageNodeSize: 10,
callbacks: {
/*
Callbacks should only be overwritten on a need to basis.
See the section about callbacks below.
*/
},
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
nodeWidth: 100,
styles: {
node: 'node',
linage: 'linage',
marriage: 'marriage',
text: 'nodeText'
}
}
```
### Zooming
The returned object, `tree = dTree.init(data, options)`, contains functions to control the viewport.
- `tree.resetZoom(duration = 500)` - Reset zoom and position to initial state
- `zoomTo(x, y, zoom = 1, duration = 500)` - Zoom to a specific position
- `zoomToNode(nodeId, zoom = 2, duration = 500)` - Zoom to a specific node
- `zoomToFit(duration = 500)` - Zoom to fit the entire tree into the viewport
### Callbacks
Below follows a short descriptions of the available callback functions that may be passed to dTree. See [dtree.js](https://github.com/ErikGartner/dTree/blob/master/src/dtree.js) for the *default implementations*. Information about e.g. mouse cursor position can retrieved by interacting with the `this` object, i.e. `d3.mouse(this)`.
#### nodeClick
```javascript
function(name, extra, id)
```
The nodeClick function is called by dTree when the node or text is clicked by the user. It shouldn't return any value.
#### nodeRightClick
```javascript
function(name, extra, id)
```
The nodeRightClick function is called by dTree when the node or text is right-clicked by the user. It shouldn't return any value.
#### nodeRenderer
```javascript
function(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer)
```
The nodeRenderer is called once for each node and is expected to return a string containing the node. By default the node is rendered using a div containing the text returned from the default textRendeder. See the JSFiddle above for an example on how to set the callback.
#### nodeHeightSeperation
```javascript
function(nodeWidth, nodeMaxHeight)
```
The nodeHeightSeperation is called during intial layout calculation. It shall return one number representing the distance between the levels in the graph.
#### nodeSize
```javascript
function(nodes, width, textRenderer)
```
This nodeSize function takes all nodes and a preferred width set by the user. It is then expected to return an array containing the width and height for all nodes (they all share the same width and height during layout though nodes may be rendered as smaller by the nodeRenderer).
#### nodeSorter
```javascript
function(aName, aExtra, bName, bExtra)
```
The nodeSorterer takes two nodes names and extra data, it then expected to return -1, 0 or 1 depending if A is less, equal or greater than B. This is used for sorting the nodes in the tree during layout.
#### textRenderer
```javascript
function(name, extra, textClass)
```
The textRenderer function returns the formatted text to the nodeRenderer. This way the user may chose to overwrite only what text is shown but may opt to keep the default nodeRenderer.
#### marriageClick
```javascript
function(extra, id)
```
Same as `nodeClick` but for the marriage nodes (connector).
#### marriageRightClick
```javascript
function(extra, id)
```
Same as `nodeRightClick` but for the marriage nodes (connector).
#### marriageRenderer
```javascript
function(x, y, height, width, extra, id, nodeClass)
```
Same as `nodeRenderer` but for the marriage nodes (connector).
#### marriageSize
```javascript
function(nodes, size)
```
Same as `nodeSize` but for the marriage nodes (connector).
## Built with dTree
- [🌳 dTree-Seed](https://github.com/JMHeartley/dTree-Seed) - Library to painlessly structure data for dTree, courtesy of [Justin Heartley](https://github.com/JMHeartley)!
## Development
dTree has the following development environment:
- node v11.x (use Docker [image](https://hub.docker.com/_/node/) `node:11`)
- gulp 3.x
- [Yarn](https://yarnpkg.com/) instead of npm.
To setup and build the library from scratch follow these steps:
1. ```yarn install```
2. ```yarn run build```
A demo is available by running:
```
yarn run demo
```
It hosts a demo on localhost:3000/ by serving [test/demo](test/demo) and using the latest compiled local version of the library.
## Contributing
Contributions are very welcomed! Checkout the [CONTRIBUTING](CONTRIBUTING.md) document for style information.
A good place to start is to make a pull request to solve an open issue. Feel free to ask questions regarding the issue since most have a sparse description.
## License
The MIT License (MIT)
Copyright (c) 2015-2024 Erik Gärtner
================================================
FILE: bower.json
================================================
{
"name": "d3-dtree",
"description": "A library for visualizing data trees built on top of D3.",
"main": "dist/dTree.js",
"authors": [
"Erik Gärtner"
],
"license": "MIT",
"keywords": [
"d3",
"graphing",
"tree",
"graph",
"genealogy",
"family",
"tree"
],
"homepage": "https://github.com/ErikGartner/dTree",
"moduleType": [
"amd",
"globals",
"node"
],
"dependencies": {
"lodash": "^4.0.0",
"d3": "^4.5.0"
},
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}
================================================
FILE: dist/dTree.js
================================================
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : global.dTree = factory();
})(this, function () {
'use strict';
var TreeBuilder = (function () {
function TreeBuilder(root, siblings, opts) {
_classCallCheck(this, TreeBuilder);
TreeBuilder.DEBUG_LEVEL = opts.debug ? 1 : 0;
this.root = root;
this.siblings = siblings;
this.opts = opts;
// flatten nodes
this.allNodes = this._flatten(this.root);
// calculate node sizes
this.nodeSize = opts.callbacks.nodeSize.call(this,
// filter hidden and marriage nodes
_.filter(this.allNodes, function (node) {
return !(node.hidden || _.get(node, 'data.isMarriage'));
}), opts.nodeWidth, opts.callbacks.textRenderer);
this.marriageSize = opts.callbacks.marriageSize.call(this,
// filter hidden and non marriage nodes
_.filter(this.allNodes, function (node) {
return !node.hidden && _.get(node, 'data.isMarriage');
}), this.opts.marriageNodeSize);
}
_createClass(TreeBuilder, [{
key: 'create',
value: function create() {
var opts = this.opts;
var allNodes = this.allNodes;
var nodeSize = this.nodeSize;
var width = opts.width + opts.margin.left + opts.margin.right;
var height = opts.height + opts.margin.top + opts.margin.bottom;
// create zoom handler
var zoom = this.zoom = d3.zoom().scaleExtent([0.1, 10]).on('zoom', function () {
g.attr('transform', d3.event.transform);
});
// make a svg
var svg = this.svg = d3.select(opts.target).append('svg').attr('viewBox', [0, 0, width, height]).call(zoom);
// create svg group that holds all nodes
var g = this.g = svg.append('g');
// set zoom identity
svg.call(zoom.transform, d3.zoomIdentity.translate(width / 2, opts.margin.top).scale(1));
// Compute the layout.
this.tree = d3.tree().nodeSize([nodeSize[0] * 2, opts.callbacks.nodeHeightSeperation.call(this, nodeSize[0], nodeSize[1])]);
this.tree.separation(function separation(a, b) {
if (a.data.hidden || b.data.hidden) {
return 0.3;
} else {
return 0.6;
}
});
this._update(this.root);
}
}, {
key: '_update',
value: function _update(source) {
var opts = this.opts;
var allNodes = this.allNodes;
var nodeSize = this.nodeSize;
var marriageSize = this.marriageSize;
var treenodes = this.tree(source);
var links = treenodes.links();
// Create the link lines.
this.g.selectAll('.link').data(links).enter()
// filter links with no parents to prevent empty nodes
.filter(function (l) {
return !l.target.data.noParent;
}).append('path').attr('class', opts.styles.linage).attr('d', this._elbow);
var nodes = this.g.selectAll('.node').data(treenodes.descendants()).enter();
this._linkSiblings();
// Draw siblings (marriage)
this.g.selectAll('.sibling').data(this.siblings).enter().append('path').attr('class', opts.styles.marriage).attr('d', _.bind(this._siblingLine, this));
// Create the node rectangles.
nodes.append('foreignObject').filter(function (d) {
return d.data.hidden ? false : true;
}).attr('x', function (d) {
return Math.round(d.x - d.cWidth / 2) + 'px';
}).attr('y', function (d) {
return Math.round(d.y - d.cHeight / 2) + 'px';
}).attr('width', function (d) {
return d.cWidth + 'px';
}).attr('height', function (d) {
return d.cHeight + 'px';
}).attr('id', function (d) {
return d.id;
}).html(function (d) {
if (d.data.isMarriage) {
return opts.callbacks.marriageRenderer.call(this, d.x, d.y, marriageSize[0], marriageSize[1], d.data.extra, d.data.id, d.data['class']);
} else {
return opts.callbacks.nodeRenderer.call(this, d.data.name, d.x, d.y, nodeSize[0], nodeSize[1], d.data.extra, d.data.id, d.data['class'], d.data.textClass, opts.callbacks.textRenderer);
}
}).on('dblclick', function () {
// do not propagate a double click on a node
// to prevent the zoom from being triggered
d3.event.stopPropagation();
}).on('click', function (d) {
// ignore double-clicks and clicks on hidden nodes
if (d3.event.detail === 2 || d.data.hidden) {
return;
}
if (d.data.isMarriage) {
opts.callbacks.marriageClick.call(this, d.data.extra, d.data.id);
} else {
opts.callbacks.nodeClick.call(this, d.data.name, d.data.extra, d.data.id);
}
}).on('contextmenu', function (d) {
if (d.data.hidden) {
return;
}
d3.event.preventDefault();
if (d.data.isMarriage) {
opts.callbacks.marriageRightClick.call(this, d.data.extra, d.data.id);
} else {
opts.callbacks.nodeRightClick.call(this, d.data.name, d.data.extra, d.data.id);
}
});
}
}, {
key: '_flatten',
value: function _flatten(root) {
var n = [];
var i = 0;
function recurse(node) {
if (node.children) {
node.children.forEach(recurse);
}
if (!node.id) {
node.id = ++i;
}
n.push(node);
}
recurse(root);
return n;
}
}, {
key: '_elbow',
value: function _elbow(d, i) {
if (d.target.data.noParent) {
return 'M0,0L0,0';
}
var ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50);
var linedata = [{
x: d.target.x,
y: d.target.y
}, {
x: d.target.x,
y: ny
}, {
x: d.source.x,
y: d.source.y
}];
var fun = d3.line().curve(d3.curveStepAfter).x(function (d) {
return d.x;
}).y(function (d) {
return d.y;
});
return fun(linedata);
}
}, {
key: '_linkSiblings',
value: function _linkSiblings() {
var allNodes = this.allNodes;
_.forEach(this.siblings, function (d) {
var start = allNodes.filter(function (v) {
return d.source.id == v.data.id;
});
var end = allNodes.filter(function (v) {
return d.target.id == v.data.id;
});
d.source.x = start[0].x;
d.source.y = start[0].y;
d.target.x = end[0].x;
d.target.y = end[0].y;
var marriageId = start[0].data.marriageNode != null ? start[0].data.marriageNode.id : end[0].data.marriageNode.id;
var marriageNode = allNodes.find(function (n) {
return n.data.id == marriageId;
});
d.source.marriageNode = marriageNode;
d.target.marriageNode = marriageNode;
});
}
}, {
key: '_siblingLine',
value: function _siblingLine(d, i) {
var ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50);
var nodeWidth = this.nodeSize[0];
var nodeHeight = this.nodeSize[1];
// Not first marriage
if (d.number > 0) {
ny -= Math.round(nodeHeight * 8 / 10);
}
var linedata = [{
x: d.source.x,
y: d.source.y
}, {
x: Math.round(d.source.x + nodeWidth * 6 / 10),
y: d.source.y
}, {
x: Math.round(d.source.x + nodeWidth * 6 / 10),
y: ny
}, {
x: d.target.marriageNode.x,
y: ny
}, {
x: d.target.marriageNode.x,
y: d.target.y
}, {
x: d.target.x,
y: d.target.y
}];
var fun = d3.line().curve(d3.curveStepAfter).x(function (d) {
return d.x;
}).y(function (d) {
return d.y;
});
return fun(linedata);
}
}], [{
key: '_nodeHeightSeperation',
value: function _nodeHeightSeperation(nodeWidth, nodeMaxHeight) {
return nodeMaxHeight + 25;
}
}, {
key: '_nodeSize',
value: function _nodeSize(nodes, width, textRenderer) {
var maxWidth = 0;
var maxHeight = 0;
var tmpSvg = document.createElement('svg');
document.body.appendChild(tmpSvg);
_.map(nodes, function (n) {
var container = document.createElement('div');
container.setAttribute('class', n.data['class']);
container.style.visibility = 'hidden';
container.style.maxWidth = width + 'px';
var text = textRenderer(n.data.name, n.data.extra, n.data.textClass);
container.innerHTML = text;
tmpSvg.appendChild(container);
var height = container.offsetHeight;
tmpSvg.removeChild(container);
maxHeight = Math.max(maxHeight, height);
n.cHeight = height;
if (n.data.hidden) {
n.cWidth = 0;
} else {
n.cWidth = width;
}
});
document.body.removeChild(tmpSvg);
return [width, maxHeight];
}
}, {
key: '_marriageSize',
value: function _marriageSize(nodes, size) {
_.map(nodes, function (n) {
if (!n.data.hidden) {
n.cHeight = size;
n.cWidth = size;
}
});
return [size, size];
}
}, {
key: '_nodeRenderer',
value: function _nodeRenderer(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer) {
var node = '';
node += '\n';
node += textRenderer(name, extra, textClass);
node += '
';
return node;
}
}, {
key: '_textRenderer',
value: function _textRenderer(name, extra, textClass) {
var node = '';
node += '\n';
node += name;
node += '
\n';
return node;
}
}, {
key: '_marriageRenderer',
value: function _marriageRenderer(x, y, height, width, extra, id, nodeClass) {
return '';
}
}, {
key: '_debug',
value: function _debug(msg) {
if (TreeBuilder.DEBUG_LEVEL > 0) {
console.log(msg);
}
}
}]);
return TreeBuilder;
})();
var dTree = {
VERSION: '2.4.1',
init: function init(data) {
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
var opts = _.defaultsDeep(options || {}, {
target: '#graph',
debug: false,
width: 600,
height: 600,
hideMarriageNodes: true,
callbacks: {
nodeClick: function nodeClick(name, extra, id) {},
nodeRightClick: function nodeRightClick(name, extra, id) {},
marriageClick: function marriageClick(extra, id) {},
marriageRightClick: function marriageRightClick(extra, id) {},
nodeHeightSeperation: function nodeHeightSeperation(nodeWidth, nodeMaxHeight) {
return TreeBuilder._nodeHeightSeperation(nodeWidth, nodeMaxHeight);
},
nodeRenderer: function nodeRenderer(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer) {
return TreeBuilder._nodeRenderer(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer);
},
nodeSize: function nodeSize(nodes, width, textRenderer) {
return TreeBuilder._nodeSize(nodes, width, textRenderer);
},
nodeSorter: function nodeSorter(aName, aExtra, bName, bExtra) {
return 0;
},
textRenderer: function textRenderer(name, extra, textClass) {
return TreeBuilder._textRenderer(name, extra, textClass);
},
marriageRenderer: function marriageRenderer(x, y, height, width, extra, id, nodeClass) {
return TreeBuilder._marriageRenderer(x, y, height, width, extra, id, nodeClass);
},
marriageSize: function marriageSize(nodes, size) {
return TreeBuilder._marriageSize(nodes, size);
}
},
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
nodeWidth: 100,
marriageNodeSize: 10,
styles: {
node: 'node',
marriageNode: 'marriageNode',
linage: 'linage',
marriage: 'marriage',
text: 'nodeText'
}
});
var data = this._preprocess(data, opts);
var treeBuilder = new TreeBuilder(data.root, data.siblings, opts);
treeBuilder.create();
function _zoomTo(x, y) {
var zoom = arguments.length <= 2 || arguments[2] === undefined ? 1 : arguments[2];
var duration = arguments.length <= 3 || arguments[3] === undefined ? 500 : arguments[3];
treeBuilder.svg.transition().duration(duration).call(treeBuilder.zoom.transform, d3.zoomIdentity.translate(opts.width / 2, opts.height / 2).scale(zoom).translate(-x, -y));
}
return {
resetZoom: function resetZoom() {
var duration = arguments.length <= 0 || arguments[0] === undefined ? 500 : arguments[0];
treeBuilder.svg.transition().duration(duration).call(treeBuilder.zoom.transform, d3.zoomIdentity.translate(opts.width / 2, opts.margin.top).scale(1));
},
zoomTo: _zoomTo,
zoomToNode: function zoomToNode(nodeId) {
var zoom = arguments.length <= 1 || arguments[1] === undefined ? 2 : arguments[1];
var duration = arguments.length <= 2 || arguments[2] === undefined ? 500 : arguments[2];
var node = _.find(treeBuilder.allNodes, { data: { id: nodeId } });
if (node) {
_zoomTo(node.x, node.y, zoom, duration);
}
},
zoomToFit: function zoomToFit() {
var duration = arguments.length <= 0 || arguments[0] === undefined ? 500 : arguments[0];
var groupBounds = treeBuilder.g.node().getBBox();
var width = groupBounds.width;
var height = groupBounds.height;
var fullWidth = treeBuilder.svg.node().clientWidth;
var fullHeight = treeBuilder.svg.node().clientHeight;
var scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
treeBuilder.svg.transition().duration(duration).call(treeBuilder.zoom.transform, d3.zoomIdentity.translate(fullWidth / 2 - scale * (groupBounds.x + width / 2), fullHeight / 2 - scale * (groupBounds.y + height / 2)).scale(scale));
}
};
},
_preprocess: function _preprocess(data, opts) {
var siblings = [];
var id = 0;
var root = {
name: '',
id: id++,
hidden: true,
children: []
};
var reconstructTree = function reconstructTree(person, parent) {
// convert to person to d3 node
var node = {
name: person.name,
id: id++,
hidden: false,
children: [],
extra: person.extra,
textClass: person.textClass ? person.textClass : opts.styles.text,
'class': person['class'] ? person['class'] : opts.styles.node
};
// hide linages to the hidden root node
if (parent == root) {
node.noParent = true;
}
// apply depth offset
for (var i = 0; i < person.depthOffset; i++) {
var pushNode = {
name: '',
id: id++,
hidden: true,
children: [],
noParent: node.noParent
};
parent.children.push(pushNode);
parent = pushNode;
}
// sort children
dTree._sortPersons(person.children, opts);
// add "direct" children
_.forEach(person.children, function (child) {
reconstructTree(child, node);
});
parent.children.push(node);
//sort marriages
dTree._sortMarriages(person.marriages, opts);
// go through marriage
_.forEach(person.marriages, function (marriage, index) {
var m = {
name: '',
id: id++,
hidden: opts.hideMarriageNodes,
noParent: true,
children: [],
isMarriage: true,
extra: marriage.extra,
'class': marriage['class'] ? marriage['class'] : opts.styles.marriageNode
};
var sp = marriage.spouse;
var spouse = {
name: sp.name,
id: id++,
hidden: false,
noParent: true,
children: [],
textClass: sp.textClass ? sp.textClass : opts.styles.text,
'class': sp['class'] ? sp['class'] : opts.styles.node,
extra: sp.extra,
marriageNode: m
};
parent.children.push(m, spouse);
dTree._sortPersons(marriage.children, opts);
_.forEach(marriage.children, function (child) {
reconstructTree(child, m);
});
siblings.push({
source: {
id: node.id
},
target: {
id: spouse.id
},
number: index
});
});
};
_.forEach(data, function (person) {
reconstructTree(person, root);
});
return {
root: d3.hierarchy(root),
siblings: siblings
};
},
_sortPersons: function _sortPersons(persons, opts) {
if (persons != undefined) {
persons.sort(function (a, b) {
return opts.callbacks.nodeSorter.call(this, a.name, a.extra, b.name, b.extra);
});
}
return persons;
},
_sortMarriages: function _sortMarriages(marriages, opts) {
if (marriages != undefined && Array.isArray(marriages)) {
marriages.sort(function (marriageA, marriageB) {
var a = marriageA.spouse;
var b = marriageB.spouse;
return opts.callbacks.nodeSorter.call(this, a.name, a.extra, b.name, b.extra);
});
}
return marriages;
}
};
return dTree;
});
//# sourceMappingURL=dTree.js.map
================================================
FILE: gulpfile.js
================================================
// Load Gulp and all of our Gulp plugins
const gulp = require('gulp');
const $ = require('gulp-load-plugins')();
// Load other npm modules
const del = require('del');
const glob = require('glob');
const path = require('path');
const isparta = require('isparta');
const babelify = require('babelify');
const watchify = require('watchify');
const buffer = require('vinyl-buffer');
const browserify = require('browserify');
const runSequence = require('run-sequence');
const source = require('vinyl-source-stream');
const rollup = require( 'rollup' );
const argv = require('minimist')(process.argv.slice(2));
const fs = require('fs');
const conventionalGithubReleaser = require('conventional-github-releaser');
// Gather the library data from `package.json`
const manifest = require('./package.json');
const config = manifest.babelBoilerplateOptions;
const mainFile = manifest.main;
const demoFolder = manifest.demo;
const destinationFolder = path.dirname(mainFile);
const exportFileName = path.basename(mainFile, path.extname(mainFile));
// Remove the built files
gulp.task('clean', function(cb) {
del([destinationFolder], cb);
});
// Remove our temporary files
gulp.task('clean-tmp', function(cb) {
del(['tmp'], cb);
});
function createLintTask(taskName, files) {
gulp.task(taskName, function() {
return gulp.src(files)
.pipe($.plumber())
.pipe($.eslint())
.pipe($.eslint.format())
.pipe($.eslint.failOnError());
});
}
// Lint our source code
createLintTask('lint-src', ['src/**/*.js']);
function getPackageJsonVersion () {
// We parse the json file instead of using require because require caches
// multiple calls so the version number won't be updated
return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
}
// Build two versions of the library
gulp.task('build', ['lint-src', 'clean'], function(done) {
var version = getPackageJsonVersion();
rollup.rollup({
entry: 'src/' + config.entryFileName,
}).then(function(bundle) {
var res = bundle.generate({
// use this instead of `toUmd`
format: 'umd',
// this is equivalent to `strict: true` -
// optional, will be auto-detected
//exports: 'named',
// `name` -> `moduleName`
moduleName: config.mainVarName,
});
$.file(exportFileName + '.js', res.code, { src: true })
.pipe($.preprocess({context: {DTREE_VERSION: version}}))
.pipe($.plumber())
.pipe($.sourcemaps.init({ loadMaps: true }))
.pipe($.babel())
.pipe($.sourcemaps.write('./'))
.pipe(gulp.dest(destinationFolder))
.pipe($.filter(['*', '!**/*.js.map']))
.pipe($.rename(exportFileName + '.min.js'))
.pipe($.sourcemaps.init({ loadMaps: true }))
.pipe($.uglify())
.pipe($.sourcemaps.write('./'))
.pipe(gulp.dest(destinationFolder))
.pipe(gulp.dest(demoFolder, {overwrite: true}))
.on('end', done);
})
.catch(done);
});
function bundle(bundler) {
return bundler.bundle()
.on('error', function(err) {
console.log(err.message);
this.emit('end');
})
.pipe($.plumber())
.pipe(source('./tmp/__spec-build.js'))
.pipe(buffer())
.pipe(gulp.dest(''))
.pipe($.livereload());
}
// These are JS files that should be watched by Gulp. When running tests in the browser,
// watchify is used instead, so these aren't included.
const jsWatchFiles = ['src/**/*'];
// These are files other than JS files which are to be watched. They are always watched.
const otherWatchFiles = ['package.json', '**/.eslintrc'];
// Run the headless unit tests as you make changes.
gulp.task('watch', function() {
const watchFiles = jsWatchFiles.concat(otherWatchFiles);
gulp.watch(watchFiles, ['build']);
});
gulp.task('bump', function() {
return gulp.src('./package.json')
.pipe($.bump({key: 'version', type: argv.bump}))
.pipe(gulp.dest('./'));
});
gulp.task('changelog', function () {
return gulp.src('./CHANGELOG.md')
.pipe($.conventionalChangelog({
preset: 'angular',
}))
.pipe(gulp.dest('./'));
});
gulp.task('update-cdn', function() {
gulp.src(['./README.md'])
.pipe($.replace(/(\d\.\d\.\d)\/dist\/dTree.min.js/g, getPackageJsonVersion() + '/dist/dTree.min.js'))
.pipe(gulp.dest('./'));
});
gulp.task('tag-release', function (cb) {
var version = getPackageJsonVersion();
$.git.tag(version, 'Created Tag for version: ' + version, cb);
});
gulp.task('commit-changes', function () {
return gulp.src(['./README.md', './CHANGELOG.md', './dist/*'])
.pipe($.git.add())
.pipe($.git.commit('chore: Bump version number'));
});
gulp.task('prepare-release', function (callback) {
runSequence(
'bump',
'changelog',
'build',
'update-cdn',
'commit-changes',
'tag-release',
function (error) {
if (error) {
console.log(error.message);
} else {
console.log('Updated workspace for release!');
}
callback(error);
});
});
gulp.task('push-changes', function (cb) {
$.git.push('origin', 'master', cb);
});
gulp.task('push-tags', function (cb) {
$.git.push('origin', 'master', {args: '--tags'}, cb);
});
gulp.task('github-release', function (done) {
conventionalGithubReleaser({
type: 'oauth',
token: process.env.GITHUB_TOKEN
}, {
preset: 'angular'
}, done);
});
gulp.task('npm-publish', $.shell.task([
'npm publish'
]))
gulp.task('push-release', function (callback) {
runSequence(
'push-changes',
'push-tags',
'npm-publish',
'github-release',
function (error) {
if (error) {
console.log(error.message);
} else {
console.log('Release Uploaded!');
}
callback(error);
});
});
gulp.task('release', function (callback) {
runSequence(
'prepare-release',
'push-release',
function (error) {
if (error) {
console.log(error.message);
} else {
console.log('Release complete!');
}
callback(error);
});
});
gulp.task('demo', ['build'], $.shell.task([
'node test/demo/demo.js'
]))
// An alias of build
gulp.task('default', ['build']);
================================================
FILE: package.json
================================================
{
"name": "d3-dtree",
"version": "2.4.1",
"description": "A library for visualizing data trees built on top of D3.",
"main": "dist/dTree.js",
"demo": "test/demo/",
"scripts": {
"build": "gulp build",
"coverage": "gulp coverage",
"demo": "gulp demo",
"release": "gulp release"
},
"repository": {
"type": "git",
"url": "https://github.com/ErikGartner/dtree.git"
},
"keywords": [
"d3",
"graphing",
"tree graph",
"genealogy",
"family tree"
],
"author": "Erik Gärtner",
"license": "MIT",
"bugs": {
"url": "https://github.com/ErikGartner/dtree/issues"
},
"homepage": "https://github.com/ErikGartner/dtree",
"dependencies": {
"d3": "^4.5.0",
"lodash": "^4.0.0"
},
"devDependencies": {
"babel-core": "^5.2.17",
"babel-eslint": "^4.0.5",
"babelify": "^6.0.0",
"browserify": "^11.0.1",
"connect": "^3.4.0",
"conventional-changelog": "^0.5.3",
"conventional-github-releaser": "^0.5.1",
"del": "^1.1.1",
"glob": "^5.0.14",
"gulp": "^3.8.10",
"gulp-babel": "^5.0.0",
"gulp-bump": "^1.0.0",
"gulp-conventional-changelog": "^0.7.0",
"gulp-eslint": "^1.0.0",
"gulp-file": "^0.2.0",
"gulp-filter": "^3.0.0",
"gulp-git": "^2.9.0",
"gulp-github-release": "^1.1.0",
"gulp-istanbul": "^0.10.0",
"gulp-livereload": "^3.4.0",
"gulp-load-plugins": "^0.10.0",
"gulp-notify": "^2.1.0",
"gulp-plumber": "^1.0.1",
"gulp-preprocess": "^2.0.0",
"gulp-rename": "^1.2.0",
"gulp-replace": "^0.5.4",
"gulp-shell": "^0.5.0",
"gulp-sourcemaps": "^1.3.0",
"gulp-uglify": "^1.2.0",
"isparta": "~3.0.3",
"jquery": "^2.1.4",
"minimist": "^1.2.0",
"rollup": "^0.41.4",
"run-sequence": "^1.0.2",
"serve-static": "^1.10.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.0.0",
"watchify": "^3.3.1"
},
"babelBoilerplateOptions": {
"entryFileName": "dtree",
"mainVarName": "dTree",
"mochaGlobals": [
"stub",
"spy",
"expect"
]
}
}
================================================
FILE: src/builder.js
================================================
class TreeBuilder {
constructor(root, siblings, opts) {
TreeBuilder.DEBUG_LEVEL = opts.debug ? 1 : 0;
this.root = root;
this.siblings = siblings;
this.opts = opts;
// flatten nodes
this.allNodes = this._flatten(this.root);
// calculate node sizes
this.nodeSize = opts.callbacks.nodeSize.call(this,
// filter hidden and marriage nodes
_.filter(
this.allNodes,
node => !(node.hidden || _.get(node, 'data.isMarriage'))
),
opts.nodeWidth,
opts.callbacks.textRenderer
)
this.marriageSize = opts.callbacks.marriageSize.call(this,
// filter hidden and non marriage nodes
_.filter(
this.allNodes,
node => !node.hidden && _.get(node, 'data.isMarriage')
),
this.opts.marriageNodeSize
)
}
create() {
let opts = this.opts;
let allNodes = this.allNodes;
let nodeSize = this.nodeSize;
let width = opts.width + opts.margin.left + opts.margin.right;
let height = opts.height + opts.margin.top + opts.margin.bottom;
// create zoom handler
const zoom = this.zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', function () {
g.attr('transform', d3.event.transform)
})
// make a svg
const svg = this.svg = d3.select(opts.target)
.append('svg')
.attr('viewBox', [0, 0, width, height])
.call(zoom)
// create svg group that holds all nodes
const g = this.g = svg.append('g')
// set zoom identity
svg.call(zoom.transform, d3.zoomIdentity.translate(width / 2, opts.margin.top).scale(1))
// Compute the layout.
this.tree = d3.tree()
.nodeSize([nodeSize[0] * 2,
opts.callbacks.nodeHeightSeperation.call(this, nodeSize[0], nodeSize[1])]);
this.tree.separation(function separation(a, b) {
if (a.data.hidden || b.data.hidden) {
return 0.3;
} else {
return 0.6;
}
});
this._update(this.root);
}
_update(source) {
let opts = this.opts;
let allNodes = this.allNodes;
let nodeSize = this.nodeSize;
let marriageSize = this.marriageSize;
let treenodes = this.tree(source);
let links = treenodes.links();
// Create the link lines.
this.g.selectAll('.link')
.data(links)
.enter()
// filter links with no parents to prevent empty nodes
.filter(function(l) {
return !l.target.data.noParent;
})
.append('path')
.attr('class', opts.styles.linage)
.attr('d', this._elbow);
let nodes = this.g.selectAll('.node')
.data(treenodes.descendants())
.enter();
this._linkSiblings();
// Draw siblings (marriage)
this.g.selectAll('.sibling')
.data(this.siblings)
.enter()
.append('path')
.attr('class', opts.styles.marriage)
.attr('d', _.bind(this._siblingLine, this));
// Create the node rectangles.
nodes.append('foreignObject')
.filter(function(d) {
return d.data.hidden ? false : true;
})
.attr('x', function(d) {
return Math.round(d.x - d.cWidth / 2) + 'px';
})
.attr('y', function(d) {
return Math.round(d.y - d.cHeight / 2) + 'px';
})
.attr('width', function(d) {
return d.cWidth + 'px';
})
.attr('height', function(d) {
return d.cHeight + 'px';
})
.attr('id', function(d) {
return d.id;
})
.html(function(d) {
if (d.data.isMarriage) {
return opts.callbacks.marriageRenderer.call(this,
d.x,
d.y,
marriageSize[0],
marriageSize[1],
d.data.extra,
d.data.id,
d.data.class
)
} else {
return opts.callbacks.nodeRenderer.call(this,
d.data.name,
d.x,
d.y,
nodeSize[0],
nodeSize[1],
d.data.extra,
d.data.id,
d.data.class,
d.data.textClass,
opts.callbacks.textRenderer
)
}
})
.on('dblclick', function () {
// do not propagate a double click on a node
// to prevent the zoom from being triggered
d3.event.stopPropagation()
})
.on('click', function(d) {
// ignore double-clicks and clicks on hidden nodes
if (d3.event.detail === 2 || d.data.hidden) {
return;
}
if (d.data.isMarriage) {
opts.callbacks.marriageClick.call(this, d.data.extra, d.data.id)
} else {
opts.callbacks.nodeClick.call(this, d.data.name, d.data.extra, d.data.id)
}
})
.on('contextmenu', function(d) {
if (d.data.hidden) {
return;
}
d3.event.preventDefault();
if (d.data.isMarriage) {
opts.callbacks.marriageRightClick.call(this, d.data.extra, d.data.id)
} else {
opts.callbacks.nodeRightClick.call(this, d.data.name, d.data.extra, d.data.id)
}
});
}
_flatten(root) {
let n = [];
let i = 0;
function recurse(node) {
if (node.children) {
node.children.forEach(recurse);
}
if (!node.id) {
node.id = ++i;
}
n.push(node);
}
recurse(root);
return n;
}
_elbow(d, i) {
if (d.target.data.noParent) {
return 'M0,0L0,0';
}
let ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50);
let linedata = [{
x: d.target.x,
y: d.target.y
}, {
x: d.target.x,
y: ny
}, {
x: d.source.x,
y: d.source.y
}];
let fun = d3.line().curve(d3.curveStepAfter)
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
return fun(linedata);
}
_linkSiblings() {
let allNodes = this.allNodes;
_.forEach(this.siblings, function(d) {
let start = allNodes.filter(function(v) {
return d.source.id == v.data.id;
});
let end = allNodes.filter(function(v) {
return d.target.id == v.data.id;
});
d.source.x = start[0].x;
d.source.y = start[0].y;
d.target.x = end[0].x;
d.target.y = end[0].y;
let marriageId = (start[0].data.marriageNode != null ?
start[0].data.marriageNode.id :
end[0].data.marriageNode.id);
let marriageNode = allNodes.find(function(n) {
return n.data.id == marriageId;
});
d.source.marriageNode = marriageNode;
d.target.marriageNode = marriageNode;
});
}
_siblingLine(d, i) {
let ny = Math.round(d.target.y + (d.source.y - d.target.y) * 0.50);
let nodeWidth = this.nodeSize[0];
let nodeHeight = this.nodeSize[1];
// Not first marriage
if (d.number > 0) {
ny -= Math.round(nodeHeight * 8 / 10);
}
let linedata = [{
x: d.source.x,
y: d.source.y
}, {
x: Math.round(d.source.x + nodeWidth * 6 / 10),
y: d.source.y
}, {
x: Math.round(d.source.x + nodeWidth * 6 / 10),
y: ny
}, {
x: d.target.marriageNode.x,
y: ny
}, {
x: d.target.marriageNode.x,
y: d.target.y
}, {
x: d.target.x,
y: d.target.y
}];
let fun = d3.line().curve(d3.curveStepAfter)
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
return fun(linedata);
}
static _nodeHeightSeperation(nodeWidth, nodeMaxHeight) {
return nodeMaxHeight + 25;
}
static _nodeSize(nodes, width, textRenderer) {
let maxWidth = 0;
let maxHeight = 0;
let tmpSvg = document.createElement('svg');
document.body.appendChild(tmpSvg);
_.map(nodes, function(n) {
let container = document.createElement('div');
container.setAttribute('class', n.data.class);
container.style.visibility = 'hidden';
container.style.maxWidth = width + 'px';
let text = textRenderer(n.data.name, n.data.extra, n.data.textClass);
container.innerHTML = text;
tmpSvg.appendChild(container);
let height = container.offsetHeight;
tmpSvg.removeChild(container);
maxHeight = Math.max(maxHeight, height);
n.cHeight = height;
if (n.data.hidden) {
n.cWidth = 0;
} else {
n.cWidth = width;
}
});
document.body.removeChild(tmpSvg);
return [width, maxHeight];
}
static _marriageSize (nodes, size) {
_.map(nodes, function (n) {
if (!n.data.hidden) {
n.cHeight = size
n.cWidth = size
}
})
return [size, size]
}
static _nodeRenderer(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer) {
let node = '';
node += '\n';
node += textRenderer(name, extra, textClass);
node += '
';
return node;
}
static _textRenderer(name, extra, textClass) {
let node = '';
node += '\n';
node += name;
node += '
\n';
return node;
}
static _marriageRenderer (x, y, height, width, extra, id, nodeClass) {
return ``
}
static _debug(msg) {
if (TreeBuilder.DEBUG_LEVEL > 0) {
console.log(msg);
}
}
}
export default TreeBuilder;
================================================
FILE: src/dtree.js
================================================
import TreeBuilder from './builder.js';
const dTree = {
VERSION: '/* @echo DTREE_VERSION */',
init: function(data, options = {}) {
var opts = _.defaultsDeep(options || {}, {
target: '#graph',
debug: false,
width: 600,
height: 600,
hideMarriageNodes: true,
callbacks: {
nodeClick: function(name, extra, id) {},
nodeRightClick: function(name, extra, id) {},
marriageClick: function(extra, id) {},
marriageRightClick: function(extra, id) {},
nodeHeightSeperation: function(nodeWidth, nodeMaxHeight) {
return TreeBuilder._nodeHeightSeperation(nodeWidth, nodeMaxHeight);
},
nodeRenderer: function(name, x, y, height, width, extra, id, nodeClass, textClass, textRenderer) {
return TreeBuilder._nodeRenderer(name, x, y, height, width, extra,
id,nodeClass, textClass, textRenderer);
},
nodeSize: function(nodes, width, textRenderer) {
return TreeBuilder._nodeSize(nodes, width, textRenderer);
},
nodeSorter: function(aName, aExtra, bName, bExtra) {return 0;},
textRenderer: function(name, extra, textClass) {
return TreeBuilder._textRenderer(name, extra, textClass);
},
marriageRenderer: function (x, y, height, width, extra, id, nodeClass) {
return TreeBuilder._marriageRenderer(x, y, height, width, extra, id, nodeClass)
},
marriageSize: function (nodes, size) {
return TreeBuilder._marriageSize(nodes, size)
},
},
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
nodeWidth: 100,
marriageNodeSize: 10,
styles: {
node: 'node',
marriageNode: 'marriageNode',
linage: 'linage',
marriage: 'marriage',
text: 'nodeText'
}
});
var data = this._preprocess(data, opts);
var treeBuilder = new TreeBuilder(data.root, data.siblings, opts);
treeBuilder.create();
function _zoomTo (x, y, zoom = 1, duration = 500) {
treeBuilder.svg
.transition()
.duration(duration)
.call(
treeBuilder.zoom.transform,
d3.zoomIdentity
.translate(opts.width / 2, opts.height / 2)
.scale(zoom)
.translate(-x, -y)
)
}
return {
resetZoom: function (duration = 500) {
treeBuilder.svg
.transition()
.duration(duration)
.call(
treeBuilder.zoom.transform,
d3.zoomIdentity.translate(opts.width / 2, opts.margin.top).scale(1)
)
},
zoomTo: _zoomTo,
zoomToNode: function (nodeId, zoom = 2, duration = 500) {
const node = _.find(treeBuilder.allNodes, {data: {id: nodeId}})
if (node) {
_zoomTo(node.x, node.y, zoom, duration)
}
},
zoomToFit: function (duration = 500) {
const groupBounds = treeBuilder.g.node().getBBox()
const width = groupBounds.width
const height = groupBounds.height
const fullWidth = treeBuilder.svg.node().clientWidth
const fullHeight = treeBuilder.svg.node().clientHeight
const scale = 0.95 / Math.max(width / fullWidth, height / fullHeight)
treeBuilder.svg
.transition()
.duration(duration)
.call(
treeBuilder.zoom.transform,
d3.zoomIdentity
.translate(
fullWidth / 2 - scale * (groupBounds.x + width / 2),
fullHeight / 2 - scale * (groupBounds.y + height / 2)
)
.scale(scale)
)
}
}
},
_preprocess: function(data, opts) {
var siblings = [];
var id = 0;
var root = {
name: '',
id: id++,
hidden: true,
children: []
};
var reconstructTree = function(person, parent) {
// convert to person to d3 node
var node = {
name: person.name,
id: id++,
hidden: false,
children: [],
extra: person.extra,
textClass: person.textClass ? person.textClass : opts.styles.text,
class: person.class ? person.class : opts.styles.node
};
// hide linages to the hidden root node
if (parent == root) {
node.noParent = true;
}
// apply depth offset
for (var i = 0; i < person.depthOffset; i++) {
var pushNode = {
name: '',
id: id++,
hidden: true,
children: [],
noParent: node.noParent
};
parent.children.push(pushNode);
parent = pushNode;
}
// sort children
dTree._sortPersons(person.children, opts);
// add "direct" children
_.forEach(person.children, function(child) {
reconstructTree(child, node);
});
parent.children.push(node);
//sort marriages
dTree._sortMarriages(person.marriages, opts);
// go through marriage
_.forEach(person.marriages, function(marriage, index) {
var m = {
name: '',
id: id++,
hidden: opts.hideMarriageNodes,
noParent: true,
children: [],
isMarriage: true,
extra: marriage.extra,
class: marriage.class ? marriage.class : opts.styles.marriageNode
}
var sp = marriage.spouse;
var spouse = {
name: sp.name,
id: id++,
hidden: false,
noParent: true,
children: [],
textClass: sp.textClass ? sp.textClass : opts.styles.text,
class: sp.class ? sp.class : opts.styles.node,
extra: sp.extra,
marriageNode: m
};
parent.children.push(m, spouse);
dTree._sortPersons(marriage.children, opts);
_.forEach(marriage.children, function(child) {
reconstructTree(child, m);
});
siblings.push({
source: {
id: node.id
},
target: {
id: spouse.id
},
number: index
});
});
};
_.forEach(data, function(person) {
reconstructTree(person, root);
});
return {
root: d3.hierarchy(root),
siblings: siblings
};
},
_sortPersons: function(persons, opts) {
if (persons != undefined) {
persons.sort(function(a, b) {
return opts.callbacks.nodeSorter.call(this, a.name, a.extra, b.name, b.extra);
});
}
return persons;
},
_sortMarriages: function(marriages, opts) {
if (marriages != undefined && Array.isArray(marriages)) {
marriages.sort(function(marriageA, marriageB) {
var a = marriageA.spouse;
var b = marriageB.spouse;
return opts.callbacks.nodeSorter.call(this, a.name, a.extra, b.name, b.extra);
});
}
return marriages;
}
};
export default dTree;
================================================
FILE: test/demo/data.json
================================================
[{
"name": "Niclas Superlongsurname",
"class": "man",
"textClass": "emphasis",
"marriages": [{
"spouse": {
"name": "Iliana",
"class": "woman",
"extra": {
"nickname": "Illi"
}
},
"children": [{
"name": "James",
"class": "man",
"marriages": [{
"spouse": {
"name": "Alexandra",
"class": "woman"
},
"children": [{
"name": "Eric",
"class": "man",
"marriages": [{
"spouse": {
"name": "Eva",
"class": "woman"
}
}]
}, {
"name": "Jane",
"class": "woman"
}, {
"name": "Jasper",
"class": "man"
}, {
"name": "Emma",
"class": "woman"
}, {
"name": "Julia",
"class": "woman"
}, {
"name": "Jessica",
"class": "woman"
}]
}]
}]
}]
}]
================================================
FILE: test/demo/demo.js
================================================
var connect = require('connect');
var serveStatic = require('serve-static');
connect().use(serveStatic(__dirname)).listen(3000);
================================================
FILE: test/demo/index.html
================================================
Demo