Jsplumb - Connectors - jsplumb

Am trying to draw a flowchart. I create divs dynamically and have set a unique 'id' property for each div and connect them using Jsplumb connectors.
I get the source and destination id from database(note that 'id' property for div dynamically created is its ID from database) and store in 'connectors' json. Its format is
Eg:
{[from:A,to:B], [from:A,to:C], [from:B,to:C]}
angular.forEach(connectors, function (connect) {
$scope.connection(connect.from, connect.to);
})
The jsplumb code is as follows
$scope.connection = function (s, t) {
var stateMachineConnector1 = {
connector: ["Flowchart", { stub: 25, midpoint: 0.001 }],
maxConnections: -1,
paintStyle: { lineWidth: 3, stroke: "#421111" },
endpoint: "Blank",
anchor: "Continuous",
anchors: [strt, end],
overlays: [["PlainArrow", { location: 1, width: 15, length: 12 }]]
};
var firstInstance = jsPlumb.getInstance();
firstInstance.connect({ source: s.toString(), target: t.toString() }, stateMachineConnector1);
}
THE PROBLEM:
What i have now is
Here the connector B to C overlaps existing A to C connector.
What i need is to separate the two connections like below
I could not find a solution for this anywhere. Any help? Thanks!

Using anchor perimeter calculates the appropriate position for endpoints.
jsfiddle demo for perimeter
jsPlumb.connect({
source:$('#item1'),
target:$("#item2"),
endpoint:"Dot",
connector: ["Flowchart", { stub: 25, midpoint: 0.001 }],
anchors:[
[ "Perimeter", { shape:"Square" } ],
[ "Perimeter", { shape:"Square" } ]
]
});
Jsplumb anchors

What I suggest you to do, to exactly replicate your schema, would be to set 2 endpoints on on box on A, B and C
A Endpoints should be [0.25, 1, 0, 0, 0, 0] and [0.75, 1, 0, 0, 0, 0]
B and C Endpoints should be [0.25, 0, 0, 0, 0, 0] and [0.75, 0, 0, 0, 0, 0]
It basically works like this (I might be wrong for the 4 last one its been a while but you only need to worry about the x and y)
[x,y,offsetx, offsety, angle, angle]
For the x 0 is the extreme left and 1 extreme right
Same goes for y (0 is top and 1 is bottom).
Take care

Related

RethinkDB branch / do + "Argument 1 to deleteAt may not be `undefined`."

I am trying to write some somewhat complicated ReQL that deletes a single value from an array in a field, or deletes the record entirely if that value is the only one in the array.
I thought I had worked it out using branch/do/offsetAt, but then I ran into this "Argument 1 to deleteAt may not be undefined." error and I'm at a loss as to what the fix is.
r.branch(
r.db('db').table('table').get('uuid').eq(null),
{"deleted": 0, "errors": 0, "inserted": 0, "replaced": 0, "skipped": 0, "unchanged": 1},
r.db('db').table('table').get('uuid').getField('array_field').eq(['value']),
r.db('db').table('table').get('uuid').delete(),
r.do(
r.db('db').table('table').get('uuid').getField('array_field').offsetsOf('value'),
function(index) {
return r.branch(
index.eq([]),
{"deleted": 0, "errors": 0, "inserted": 0, "replaced": 0, "skipped": 0, "unchanged": 1},
r.db('db').table('table').get('uuid').update({
"array_field": r.row('array_field').deleteAt(index[0])
})
)
}
)
)
Also, as a side question, is there a more efficient way to do this without constantly fetching the record over and over calling .get()
So it turns out the issue here was in thinking I could use [] on the index.
Switching to .deleteAt(index.nth(0)) at line 16 solved the problem, or at least changed the error to "Cannot use r.row in nested queries"
So I rewrote the code to also solve my secondary issue, of constantly fetching the data using .get()
My new code is now
r.do(
r.db('db').table('table').get('uuid'),
function(record) {
return r.branch(
record.eq(null),
{"deleted": 0, "errors": 0, "inserted": 0, "replaced": 0, "skipped": 0, "unchanged": 1, "result": "no record"},
record.getField('array_field').eq(['array_value']),
r.db('db').table('table').get('uuid').delete(),
r.do(
record.getField('array_field').offsetsOf('array_value'),
function(index) {
return r.branch(
index.eq([ ]),
{"deleted": 0, "errors": 0, "inserted": 0, "replaced": 0, "skipped": 0, "unchanged": 1, "result": "no index"},
r.db('db').table('table').get('uuid').update({
"array_field": record.getField('array_field').deleteAt(index.nth(0))
})
)
}
)
)
}
)

SVG - Convert all shapes/primitives to <path>

I'm doing a number of D3.JS operations requiring that I work with SVG paths instead of primitives/shapes (polylines, recs, etc.).
This question is general, but I'd like to know if it is possible to convert any SVG primitive to a path, either with D3 or another script/library.
For reference, here is a link which does it for polylines: https://gist.github.com/andytlr/9283541
I'd like to do this for every primitive. Any ideas? Is this possible?
JavaScript solution
You can also convert all primitives using Jarek Foksa's path-data polyfill:
It's main purpose is to parse a path's d attribute to an array of commands.
But it also provides a normalize method to convert any element's geometry to path commands.
element.getPathData({normalize: true});
This method will convert all commands and coordinates to absolute values
and reduce the set of commands to these cubic bézier commands: M,L, C, Z.
Example usage: Convert all primitives
(and convert other commands A, S, Q etc.)
const svgWrp = document.querySelector('.svgWrp');
const svg = document.querySelector('svg');
const primitives = svg.querySelectorAll('path, line, polyline, polygon, circle, rect');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = svgWrp.innerHTML;
function convertPrimitives(svg, primitives) {
primitives.forEach(function(primitive, i) {
/**
* get normalized path data:
* all coordinates are absolute;
* reduced set of commands: M, L, C, Z
*/
let pathData = primitive.getPathData({
normalize: true
});
//get all attributes
let attributes = [...primitive.attributes];
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
//exclude attributes not needed for paths
let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
'width'
];
setAttributes(path, attributes, exclude);
// set d attribute from rounded pathData
path.setPathData(roundPathData(pathData, 1));
svg.appendChild(path);
primitive.remove();
})
// optional: output new svg markup
let newSvgMarkup = svgWrp.innerHTML.
replaceAll("></path>", "/>").
replace(/([ |\n|\r|\t])/g, " ").
replace(/ +/g, ' ').trim().
replaceAll("> <", "><").
replaceAll("><", ">\n<");
svgMarkup.value = newSvgMarkup;
}
function roundPathData(pathData, decimals = 3) {
pathData.forEach(function(com, c) {
let values = com['values'];
values.forEach(function(val, v) {
pathData[c]['values'][v] = +val.toFixed(decimals);
})
})
return pathData;
}
function setAttributes(el, attributes, exclude = []) {
attributes.forEach(function(att, a) {
if (exclude.indexOf(att.nodeName) === -1) {
el.setAttribute(att.nodeName, att.nodeValue);
}
})
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.3/path-data-polyfill.min.js"></script>
<p><button type="button" onclick="convertPrimitives(svg, primitives)">Convert Primitives</button></p>
<div class="svgWrp">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 30">
<polygon id="polygon" fill="#ccc" stroke="green" points="9,22.4 4.1,14 9,5.5 18.8,5.5 23.7,14
18.8,22.4 " />
<polyline id="polyline" fill="none" stroke="red" points="43,22.4 33.3,22.4 28.4,14 33.3,5.5 43,5.5
47.9,14 " />
<rect id="rect" x="57.3" y="5.5" fill="none" stroke="orange" width="16.9" height="16.9" />
<line id="line" fill="none" stroke="purple" x1="52.6" y1="22.4" x2="52.6" y2="5.5" />
<circle class="circle" data-att="circle" id="circle" fill="none" stroke="magenta" cx="87.4" cy="14" r="8.5" />
<path transform="scale(0.9) translate(110,5)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z" fill="red" class="segment segment-1 segment-class" id="segment-01"/>
</svg>
</div>
<h3>Svg markup</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>
The above example script will also retain all attributes like class, id, fill etc.
But it will strip attributes like r, cx, rx specific to primitives.
Do we need this polyfill?
Unfortunately, the getPathData() and setPathData() methods are still a svg 2 drafts/proposals – intended to replace the deprecated pathSegList() methods.
Hopefully we will get native browser support in the near future.
Since this polyfill is still rather lightweight (~12.5 KB uncompressed) compared to more advanced svg libraries like (snap.svg, d3 etc.) it won't increase your loading times significantly.
Update: Standalone script (no polyfill dependency)
This is rather a proof of concept – you can convert svg primitives based on pretty basic value calculations – without the need of advanced frameworks/libraries – inspired by this post: Convert all shapes/primitives into path elements of SVG.
But as I fiddled around with my own clunky conversion script, I quickly realised that there were some challenges (that Jarek Foksa's normalizing implementations solves flawlessly) such as:
Relative i.e percentage based units
<circle cx="50%" cy="50%" r="25%" />
OK ... I guess we need to calculate these relative values to absolute coordinates according to the parent svg's boundaries as defined by viewBox property ... maybe no viewBox available at all ... or width/height values.
Or something like rx, ry properties to apply rounded borders to a <rect> element – for a decent conversion we'll need to add some curvy commands like a, c or s.
Paths vs. primitives
It's true that a <path> element can draw just any shape a primitive can offer via cubic or quadratic spline commands – even in a more efficient way due to it's concatenating abilities (combining multiple shapes) and furthermore its relative or shorthand commands.
But it doesn't support relative units – however the shapes you need to convert might heavily depend on relative dimensions (e.g. circular gauges pie charts etc.)
Conclusion
It's not too difficult to write your custom conversion script, but pay attention to some tricky details.
const svg = document.querySelector('svg');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = svg.outerHTML;
/**
* example script
**/
function getConvertedMarkup(svg, markupEl, decimals = 1) {
convertPrimitivesNative(svg, decimals);
//optimize output
let newSvgMarkup = svg.outerHTML.
replaceAll("></path>", "/>").
replace(/^\s+|\s+$|\s+(?=\s)/g, "").
replaceAll("> <", "><").
replaceAll("><", ">\n<");
markupEl.value = newSvgMarkup;
}
/**
* parse svg attributes and convert relative units
**/
function parseSvgAttributes(svg, atts) {
let calcW = 0;
let calcH = 0;
let calcR = 0;
//1. check viewBox
let viewBoxAtt = svg.getAttribute('viewBox');
let viewBox = viewBoxAtt ? viewBoxAtt.split(' ') : [];
[calcW, calcH] = [viewBox[2], viewBox[3]];
//2. check width attributes
if (!calcW || !calcH) {
widthAtt = svg.getAttribute('width') ? parseFloat(svg.getAttribute('width')) : '';
heightAtt = svg.getAttribute('height') ? parseFloat(svg.getAttribute('height')) : '';
[calcW, calcH] = [widthAtt, heightAtt];
}
//3. calculate by getBBox()
if (!calcW || !calcH) {
let bb = svg.getBBox();
[calcW, calcH] = [(calcW ? calcW : bb.width), (calcH ? calcH : bb.height)];
}
// calculate relative radius: needed for non square aspect ratios
calcR = Math.sqrt(Math.pow(calcW, 2) + Math.pow(calcH, 2)) / Math.sqrt(2);
let attArr = [...atts];
let attObj = {};
attArr.forEach(function(att) {
let attName = att.nodeName;
// convert percentages to absolute svg units
let val = att.nodeValue;
let percentAtts = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'r', 'rx', 'ry', 'cx', 'cy', 'width', 'height']
if (val.toString().indexOf('%') !== -1 && percentAtts.indexOf(attName) !== -1) {
// strip units
val = parseFloat(val);
switch (attName) {
case 'cx':
case 'rx':
case 'width':
case 'x':
case 'x1':
case 'x2':
val = 1 / 100 * val * calcW;
break;
case 'cy':
case 'ry':
case 'height':
case 'y':
case 'y1':
case 'y2':
val = 1 / 100 * val * calcH;
break;
case 'r':
val = 1 / 100 * val * calcR;
break;
}
}
attObj[att.nodeName] = val;
});
return attObj;
}
/**
* convert primitive attributes to relative path commands
*/
function convertPrimitivesNative(svg, decimals = 3) {
let primitives = svg.querySelectorAll('line, polyline, polygon, circle, ellipse, rect');
if (primitives.length) {
primitives.forEach(function(primitive) {
let pathData = [];
let type = primitive.nodeName;
let atts = parseSvgAttributes(svg, primitive.attributes, 2);
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
//exclude attributes not needed for paths
let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
'width'
];
switch (type) {
case 'rect':
let [rx, ry] = [atts.rx, atts.ry];
rx = !rx && ry ? ry : rx;
ry = !ry && rx ? rx : ry;
let [x, y, width, height] = [atts.x, atts.y, atts.width, atts.height];
let [widthInner, heightInner] = [width - rx * 2, height - ry * 2];
if (rx) {
pathData.push({
type: 'M',
values: [x, (y + ry)]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, -ry]
}, {
type: 'h',
values: [widthInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, ry]
}, {
type: 'v',
values: [heightInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, ry]
}, {
type: 'h',
values: [-widthInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, -ry]
}, {
type: 'z',
values: []
});
} else {
pathData.push({
type: 'M',
values: [x, y]
}, {
type: 'h',
values: [width]
}, {
type: 'v',
values: [height]
}, {
type: 'h',
values: [-width]
}, {
type: 'z',
values: []
});
}
break;
case 'line':
let [x1, y1, x2, y2] = [atts.x1, atts.y1, atts.x2, atts.y2];
pathData.push({
type: 'M',
values: [x1, y1]
}, {
type: 'l',
values: [(x2 - x1), (y2 - y1)]
});
break;
case 'circle':
case 'ellipse':
if (type == 'circle') {
let r = atts.r;
let [cX, cY] = [atts.cx, atts.cy - atts.r];
pathData.push({
type: 'M',
values: [cX, cY]
}, {
type: 'a',
values: [r, r, 0, 0, 1, r, r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, -r, r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, -r, -r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, r, -r]
}, {
type: 'z',
values: []
});
} else {
let rx = atts.rx;
let ry = atts.ry;
let [cX, cY] = [atts.cx, atts.cy - atts.ry];
pathData.push({
type: 'M',
values: [cX, cY]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, -ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, -ry]
}, {
type: 'z',
values: []
});
}
break;
case 'polygon':
case 'polyline':
let closePath = type == 'polygon' ? 'z' : '';
let points = atts.points.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
let pointArr = points.split(' ');
pathData.push({
type: 'M',
values: [+pointArr[0], +pointArr[1]]
});
for (let i = 2; i < pointArr.length; i += 2) {
let [x0, y0] = [+pointArr[i - 2], +pointArr[i - 1]];
let [x, y] = [+pointArr[i], +pointArr[i + 1]];
let com = {};
if (y == y0) {
com = {
type: 'h',
values: [x - x0]
}
} else if (x == x0) {
com = {
type: 'v',
values: [y - y0]
}
} else {
com = {
type: 'l',
values: [x - x0, y - y0]
}
}
pathData.push(com);
}
if (closePath) {
pathData.push({
type: 'z',
values: []
});
}
break;
//paths
default:
let dClean = atts.d.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
let dArr = dClean.replace(/([a-zA-Z])/g, " | $1").split(' | ');
dArr.shift();
for (let i = 0; i < dArr.length; i++) {
let command = dArr[i].trim().split(' ');
let type = command.shift();
command = command.map((x) => {
return parseFloat(x);
});
pathData.push({
type: type,
values: command
});
}
break;
}
// copy primitive's attributes to path
setAttributes(path, atts, exclude);
// round coordinates and replace primitive with path
path.setPathDataOpt(pathData, decimals);
primitive.replaceWith(path);
})
}
};
function setAttributes(el, attributes, exclude = []) {
for (key in attributes) {
if (exclude.indexOf(key) === -1) {
el.setAttribute(key, attributes[key]);
}
}
}
function getAttributes(el) {
let attArr = [...el.attributes];
let attObj = {};
attArr.forEach(function(att) {
attObj[att.nodeName] = att.nodeValue;
});
return attObj;
}
/**
* return rounded path data
* based on:
* https://github.com/jarek-foksa/path-data-polyfill/blob/master/path-data-polyfill.js
*/
if (!SVGPathElement.prototype.setPathDataOpt) {
SVGPathElement.prototype.setPathDataOpt = function(pathData, decimals = 3) {
let d = "";
if (pathData.length) {
for (let i = 0; i < pathData.length; i++) {
let seg = pathData[i];
let [type, values] = [seg.type, seg.values];
let valArr = [];
if (values.length) {
for (let v = 0; v < values.length; v++) {
val = parseFloat(values[v]);
valArr.push(+val.toFixed(decimals));
}
}
d += type;
if (valArr.length) {
d += valArr.join(" ").trim();
}
}
d = d.
replaceAll(' -', '-').
replaceAll(' 0.', ' .').
replaceAll(' z', 'z');
this.setAttribute("d", d);
}
};
}
<p><button type="button" onclick="getConvertedMarkup(svg, svgMarkup, 2)">Convert Primitives</button></p>
<svg xmlns="http://www.w3.org/2000/svg" data-width="150px" data-height="30px" viewBox="0 0 150 30">
<polygon id="polygon" fill="#CCCCCC" stroke="#E3000F" points="7.9,22.8 3,14.3 7.9,5.8 17.6,5.8 22.5,14.3
17.6,22.8 " />
<polyline id="polyline" fill="none" stroke="#E3000F" points="40.9,22.8 31.1,22.8 26.2,14.3 31.1,5.8
40.9,5.8 45.8,14.3 " />
<rect id="rect" x="37.5%" y="20%" rx="2%" ry="5%" fill="none" stroke="#E3000F" width="6%" height="56%" />
<line id="line" fill="none" stroke="#E3000F" x1="50.5" y1="22.8" x2="52.5" y2="5.8" />
<circle id="circle" fill="none" stroke="#E3000F" cx="52%" cy="49%" r="8%" />
<ellipse id="ellipse" fill="none" stroke="#E3000F" cx="68%" cy="49%" rx="7%" ry="25%" />
<path id="piechart" transform="scale(0.9) translate(130, 6)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z"
fill="red" class="segment segment-1 segment-class" id="segment-01" />
</svg>
<h3>Output</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>
Codepen converter example
I found this github site which has a set of java functions for converting shapes to paths: https://github.com/JFXtras/jfxtras-labs/blob/2.2/src/main/java/jfxtras/labs/util/ShapeConverter.java

AM Charts initial view using zoomToDates

Been stumped on this for a little bit.
I found some other help on zoomToIndexes, but I cant get the zoomToDates to work on my page.
Live page is
b2 resource urq sales
Im trying to set the initial view to show from 2000 to current.. I want to slap some original sales data from early 80's in the graph, but dont want the graph to initially show the last 30+ years..
Any help would be MUCH appreciated!
zoomToDates takes real JavaScript Date objects as parameters:
chart.zoomToDates(new Date(2005, 0, 1), new Date(2015, 11, 31));
You can use chart's rendered event to "pre-zoom" on load as well:
var chart = AmCharts.makeChart("chartdiv", {
// your chart config
// ...
});
chart.addListener("rendered", function(event) {
event.chart.zoomToDates(new Date(2005, 0, 1), new Date(2015, 11, 31));
});
Note, that months in Date() constructor parameter (second parameter) are zero-based. Meaning January is 0, February - 1, etc.
You should use valueAxis property of chart object for zoomToValues. I hope this might help you.
var chart= AmCharts.makeChart("chartdiv", {
"type": "gantt",
"theme": "black",
...
});
zoomChart();
chart.addListener("dataUpdated", zoomChart);
function zoomChart(event) {
chart.valueAxis.zoomToValues(new Date(2017, 2, 10), new Date(2017,2,12));
// or ==> event.chart.valueAxis.zoomToValues(new Date(2017, 2, 10), new Date(2017,2,12));
}
This worked for me.:
var chart = AmCharts.makeChart('chartdiv', {
type: 'serial',
...
});
chart.addListener('dataUpdated', zoomChart);
zoomChart();
function zoomChart() {
chart.zoomToDates(new Date(2018, 2, 26), new Date(2018, 2, 28));
}
Note: The months parameter in Date() constructor are zero-based. January is 0, February is 1 and etc.

AnimationHandler has changed for threejs, how can I get it working?

I created a webgl animation using the threejs library, some time ago, today I have been trying to get it to run on the latest version of the three.js library.
In the latest library, there is no pathcontrols.js.
I copied pathcontrols.js from my old library to the new version, but this has not solved the problem,
Below is the full function that does not work properly, but it is the .add() and the animation() calls where I think the problem is:
function initAnimationPath( parent, spline, name, duration ) {
var animationData = {
name: name,
fps: 0.6,
length: duration,
hierarchy: []
};
var i,
parentAnimation, childAnimation,
path = spline.getControlPointsArray(),
sl = spline.getLength(),
pl = path.length,
t = 0,
first = 0,
last = pl - 1;
parentAnimation = { parent: -1, keys: [] };
parentAnimation.keys[ first ] = { time: 0, pos: path[ first ], rot: [ 0, 0, 0, 1 ], scl: [ 1, 1, 1 ] };
parentAnimation.keys[ last ] = { time: duration, pos: path[ last ], rot: [ 0, 0, 0, 1 ], scl: [ 1, 1, 1 ] };
for ( i = 1; i < pl - 1; i++ ) {
t = duration * sl.chunks[ i ] / sl.total;
parentAnimation.keys[ i ] = { time: t, pos: path[ i ] };
}
animationData.hierarchy[ 0 ] = parentAnimation;
THREE.AnimationHandler.add( animationData );
return new THREE.Animation( parent, name, THREE.AnimationHandler.CATMULLROM_FORWARD, false );
};
it seems that the deprecation of animationhandler.add() seems to be the issue, but I am having difficulty working out how to replace it.
(amongst many other things) I tried replacing animationhandler line and animation line with this:
THREE.AnimationHandler.update(delta);
return new THREE.Animation( camera, animationData);
No joy. I would like to know how to get the animation started??
Thanks

Complex d3.nest() manipulation

I have an array of arrays that looks like this:
var arrays = [[1,2,3,4,5],
[1,2,6,4,5],
[1,3,6,4,5],
[1,2,3,6,5],
[1,7,5],
[1,7,3,5]]
I want to use d3.nest() or even just standard javascript to convert this data into a nested data structure that I can use with d3.partition.
Specifically, I want to create this flare.json data format.
The levels of the json object I want to create with d3.nest() correspond to the index positions in the array. Notice that 1 is in the first position in all the subarrays in the example data above; therefore, it is at root of the tree. At the next positions in the arrays there are three values, 2, 3, and 7, therefore, the root value 1 has 3 children. At this point the tree looks like this:
1
/ | \
2 3 7
At the third position in the subarrays there are four values, 3, 5, and 6. These children would be places into the tree as follows:
1
____|___
/ | \
2 3 7
/ \ / / \
3 6 6 3 5
How can I produce this data structure using d3.nest()? The full data structure with the example data I showed above should look like this:
{"label": 1,
"children": [
{"label": 2, "children": [
{"label": 3, "children": [
{"label": 4, "children": [
{"label": 5}
]},
{"label": 6, "children": [
{"label": 5}
]}
]},
{"label": 6, "children": [
{"label": 4, "children": [
{"label": 5}
]}
]},
{"label": 3, "children": [
{"label": 6, "children": [
{"label": 4, "children": [
{"label": 5}
]}
]}
]},
{"label": 7, "children": [
{"label": 3, "children": [
{"label": 5}
]},
{"label": 5}
]}
]}
]}
I'm trying to convert my array data structure above using something like this (very wrong):
var data = d3.nest()
.key(function(d, i) { return d.i; })
.rollup(function(d) { return d.length; })
I've been banging my head for a week to try and understand how I can produce this hierarchical data structure from an array of arrays. I'd be very grateful if someone could help me out.
#meetamit's answer in the comments is good, but in my case my tree is too deep to repeatedly apply .keys() to the data, so I cannot manually write a function like this.
Here's a more straightforward function that just uses nested for-loops to cycle through all the path instructions in each of your set of arrays.
To make it easier to find the child element with a given label, I have implemented children as a data object/associative array instead of a numbered array. If you want to be really robust, you could use a d3.map for the reasons described at that link, but if your labels are actually integers than that's not going to be a problem. Either way, it just means that when you need to access the children as an array (e.g., for the d3 layout functions), you have to specify a function to make an array out of the values of the object -- the d3.values(object) utility function does it for you.
The key code:
var root={},
path, node, next, i,j, N, M;
for (i = 0, N=arrays.length; i<N; i++){
//for each path in the data array
path = arrays[i];
node = root; //start the path from the root
for (j=0,M=path.length; j<M; j++){
//follow the path through the tree
//creating new nodes as necessary
if (!node.children){
//undefined, so create it:
node.children = {};
//children is defined as an object
//(not array) to allow named keys
}
next = node.children[path[j]];
//find the child node whose key matches
//the label of this step in the path
if (!next) {
//undefined, so create
next = node.children[path[j]] =
{label:path[j]};
}
node = next;
// step down the tree before analyzing the
// next step in the path.
}
}
Implemented with your sample data array and a basic cluster dendogram charting method:
http://fiddle.jshell.net/KWc73/
Edited to add:
As mentioned in the comments, to get the output looking exactly as requested:
Access the data's root object from the default root object's children array.
Use a recursive function to cycle through the tree, replacing the children objects with children arrays.
Like this:
root = d3.values(root.children)[0];
//this is the root from the original data,
//assuming all paths start from one root, like in the example data
//recurse through the tree, turning the child
//objects into arrays
function childrenToArray(n){
if (n.children) {
//this node has children
n.children = d3.values(n.children);
//convert to array
n.children.forEach(childrenToArray);
//recurse down tree
}
}
childrenToArray(root);
Updated fiddle:
http://fiddle.jshell.net/KWc73/1/
If you extend the specification of Array, it's not actually that complex. The basic idea is to build up the tree level by level, taking each array element at a time and comparing to the previous one. This is the code (minus extensions):
function process(prevs, i) {
var vals = arrays.filter(function(d) { return prevs === null || d.slice(0, i).compare(prevs); })
.map(function(d) { return d[i]; }).getUnique();
return vals.map(function(d) {
var ret = { label: d }
if(i < arrays.map(function(d) { return d.length; }).max() - 1) {
tmp = process(prevs === null ? [d] : prevs.concat([d]), i+1);
if(tmp.filter(function(d) { return d.label != undefined; }).length > 0)
ret.children = tmp;
}
return ret;
});
}
No guarantees that it won't break for edge cases, but it seems to work fine with your data.
Complete jsfiddle here.
Some more detailed explanations:
First, we get the arrays that are relevant for the current path. This is done by filtering out those that are not the same as prevs, which is our current (partial) path. At the start, prevs is null and nothing is filtered.
For these arrays, we get the values that corresponds to the current level in the tree (the ith element). Duplicates are filtered. This is done by the .map() and .getUnique().
For each of the values we got this way, there will be a return value. So we iterate over them (vals.map()). For each, we set the label attribute. The rest of the code determines whether there are children and gets them through a recursive call. To do this, we first check whether there are elements left in the arrays, i.e. if we are at the deepest level of the tree. If so, we make the recursive call, passing in the new prev that includes the element we are currently processing and the next level (i+1). Finally, we check the result of this recursive call for empty elements -- if there are only empty children, we don't save them. This is necessary because not all of the arrays (i.e. not all of the paths) have the same length.
Since d3-collection has been deprecated in favor of d3.array, we can use d3.groups to achieve what used to work with d3.nest:
var input = [
[1, 2, 3, 4, 5],
[1, 2, 6, 4, 5],
[1, 3, 6, 4, 5],
[1, 2, 3, 6, 5],
[1, 7, 5],
[1, 7, 3, 5]
];
function process(arrays, depth) {
return d3.groups(arrays, d => d[depth]).map(x => {
if (
x[1].length > 1 || // if there is more than 1 child
(x[1].length == 1 && x[1][0][depth+1]) // if there is 1 child and the future depth is inferior to the child's length
)
return ({
"label": x[0],
"children": process(x[1], depth+1)
});
return ({ "label": x[0] }); // if there is no child
});
};
console.log(process(input, 0));
<script src="https://d3js.org/d3-array.v2.min.js"></script>
This:
Works as a recursion on the arrays' depths.
Each recursion step groups (d3.groups) its arrays on the array element whose index is equal to the depth.
Depending on whether there are children or not, the recursion stops.
Here is the intermediate result produced by d3.groups within a recursion step (grouping arrays on there 3rd element):
var input = [
[1, 2, 3, 4, 5],
[1, 2, 6, 4, 5],
[1, 2, 3, 6, 5]
];
console.log(d3.groups(input, d => d[2]));
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Edit - fixed
Here is my solution
Pro:It is all in one go (doesn't need objects converting to arrays like above)
Pro:It keeps the size/value count
Pro:the output is EXACTLY the same as a d3 flare with children
Con:it is uglier, and likely less efficient
Big Thanks to previous comments for helping me work it out.
var data = [[1,2,3,4,5],
[1,2,6,4,5],
[1,3,6,4,5],
[1,2,3,6,5],
[1,7,5],
[1,7,3,5]]
var root = {"name":"flare", "children":[]} // the output
var node // pointer thingy
var row
// loop through array
for(var i=0;i<data.length;i++){
row = data[i];
node = root;
// loop through each field
for(var j=0;j<row.length;j++){
// set undefined to "null"
if (typeof row[j] !== 'undefined' && row[j] !== null) {
attribute = row[j]
}else{
attribute = "null"
}
// using underscore.js, does this field exist
if(_.where(node.children, {name:attribute}) == false ){
if(j < row.length -1){
// this is not the deepest field, so create a child with children
var oobj = {"name":attribute, "children":[] }
node.children.push(oobj)
node = node.children[node.children.length-1]
}else{
// this is the deepest we go, so set a starting size/value of 1
node.children.push({"name":attribute, "size":1 })
}
}else{
// the fields exists, but we need to find where
found = false
pos = 0
for(var k=0;k< node.children.length ;k++){
if(node.children[k]['name'] == attribute){
pos = k
found = true
break
}
}
if(!node.children[pos]['children']){
// if no key called children then we are at the deepest layer, increment
node.children[pos]['size'] = parseInt(node.children[pos]['size']) + 1
}else{
// we are not at the deepest, so move the pointer "node" and allow code to continue
node = node.children[pos]
}
}
}
}
// object here
console.log(root)
// stringified version to page
document.getElementById('output').innerHTML = JSON.stringify(root, null, 1);
Working examples
https://jsfiddle.net/7qaz062u/
Output
{ "name": "flare", "children": [ { "name": 1, "children": [ { "name": 2, "children": [ { "name": 3, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] }, { "name": 6, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] } ] }, { "name": 3, "children": [ { "name": 6, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] }, { "name": 3, "children": [ { "name": 6, "children": [ { "name": 5, "size": 1 } ] } ] } ] }, { "name": 7, "children": [ { "name": 5, "size": 1 }, { "name": 3, "children": [ { "name": 5, "size": 1 } ] } ] } ] } ] }

Resources