d3 dynamically updating nodes based on user selection - javascript

I'm trying to enable a user to click nodes in a force layout and reduce the data set to just those nodes and their associated links.
I've gotten as far as this; http://jsfiddle.net/hiwilson1/4gemo0xe/1/
The section starting at 179;
d3.select("#filterResults")
.on("click", function() {
//feed the indexes of the selected nodes into array
var nodeArray = []
d3.selectAll("circle[fill=\"Red\"]")
.each(function(d, i) {
nodeArray.push(d3.select(this).attr("node-index"))
})
//step2: loop through nodes array and return only those nodes that share
//index with nodeArray into newNodes
var newNodes = []
nodes.forEach(function(nde, idx, list) {
nodeArray.forEach(function(nde2, idx2, list2) {
if (idx == nde2) {
newNodes.push(nde)
}
})
})
//step3: loop through links array and return only those links that
//have source or target as index of nodes in nodeArray
var newLinks = []
links.forEach(function(link, lidx, llist) {
nodeArray.forEach(function(nde, idx, list) {
if (link.source == nde || link.target == nde) {
newLinks.push(link)
}
})
})
alert(newLinks.length)
})
is where I'm stuck, specifically step 3. I can't seem to get at the links from within the onclick function, the alert returns zero where it should return however many links are associated with the selected nodes.
Ultimately I want to identify nodes and links associated with the selected and then update the data set to reflect this new data set, if that's even possible! Only one way to find out..

Once force is started, source and target values got replaced with corresponding node objects. So instead of comparing link sources with node index, compare it with the node.
if (link.source == nodes[nde] || link.target == nodes[nde]) {
newLinks.push(link)
}
Or
if (link.source.index == nde || link.target.index ==nde) {
newLinks.push(link)
}
Updated JSFiddle
Note: the values of the source and target attributes may be initially
specified as indexes into the nodes array; these will be replaced by
references after the call to start.
This is the sample structure of a node.
{
fixed: 0,
index: 12,
label: "I am an information label",
px: 287.64956227452404,
py: 83.71383910494417,
weight: 7,
x: 287.7214898272057,
y: 83.59069881862021
}
d3.select(".node").data() will return you the data associated with that node, where node is the class name.
You can refer more about d3 force layout from here.

Related

Traverse and Replace Items in TreeData

I am stuck at the task of traversing a tree and replacing the nodes if they match the key. This is necessary as I load children lazy via REST, insert them into the parent node and then replace the parent node (The node I clicked onto) with the parent with loaded children.
The treeData interface is:
export interface TreeData {
keyNum:number,
icon:string,
name:string,
length:number,
type: string,
links: string,
children?: TreeData[]
}
_nodes = Temp array that replaces the current nodes
nodes = All elements
event = Current parent
test = Temporary array for pushing items when doing it recursively
In a first attempt I created this function:
let _nodes = nodes.map(node => {
if (node.keyNum=== event.keyNum) {
node = event;
}
return node;
})
setNodes(_nodes)
Unfortunately this doesn't do this recursively. Only when I open up the root item. In a next attempt I created this function:
const test = [] as TreeData[]
traverseTree(event,nodes, test)
dispatch(setNodes(test))
const traverseTree = (event: TreeData, currentNodes: TreeData[], temp: TreeData[])=>{
currentNodes.forEach(node=>{
if (node.keyNum === event.keyNum) {
node = event;
temp.push(node)
}
else{
if(node.children && node.children.length>0){
return node.children.forEach(nodeInLoop=>{
return traverseTree(event,[nodeInLoop], temp)
})
}
}
})
}
I can finally traverse the tree but the problem is that when I click element1
-root
- element 1
- element 2
this gets transformed to:
- element 1
- subelement 1
- subelement 2
- ...
So when I click a child everything above is replaced. How can I preserve also the parent when clicking element 1. So that the expected output is
- root
- element 1
- subelement 1
- subelement 2
- ...
- element 2
I would not use a third parameter like in your recursive code. Instead make the function return the new tree.
It is indeed a problem that in your attempt the only thing that happens to temp is that nodes are pushed unto it. But you'll also need to repopulate children arrays in the hierarchy. You can better use map for that, and spread syntax to clone nodes with newly created children arrays.
Your recursive attempt could be corrected like this:
const traverseTree = (event: TreeData, nodes: TreeData[]): TreeData[] =>
nodes.map(node =>
node.keyNum === event.keyNum ? event
: !node.children?.length ? node
: { ...node, children: traverseTree(event, node.children) };
);
Call like:
dispatch(setNodes(traverseTree(event,nodes)))

d3 force graph using node names for links

I've been at this for a few days now, and I've seen the questions on stackoverflow and other places, but I am missing something.
Lets say we have the following JSON:
{
"nodes":[
{"name":"node1"},
{"name":"node2"},
{"name":"node3"},
{"name":"node4"}
],
"links":[
{"source":"node1","target":"node2"},
{"source":"node1","target":"node3"},
{"source":"node1","target":"node4"}
]
}
Why do the following two pieces of code yield the same output in the console, but the second gives me an error (Uncaught TypeError: Cannot read property 'push' of undefined)?
links = links.map(function(l) {
var sourceNode = nodes.filter(function(n) { return n.name === l.source; })[0];
var targetNode = nodes.filter(function(n) { return n.name === l.target; })[0];
return {
source: sourceNode,
target: targetNode
};
});
_
links.forEach(function(link) {
link.source = {name: link.source};
link.target = {name: link.target};
});
Console output:
[{"source":{"name":"node1"},"target":{"name":"node2"}},
{"source":{"name":"node1"},"target":{"name":"node3"}},
{"source":{"name":"node1"},"target":{"name":"node4"}}]
There is one significant difference between results of the 2 code snippets.
The first one links source and target nodes by reference. They point to the objects in the nodes array.
The second one creates new objects. They have the same names as those in nodes but they are different objects in memory.
D3 force layout requires that links point to nodes by reference or by index in the array. If you specify indices they will by transformed to references anyway:
Note: the values of the source and target attributes may be initially specified as indexes into the nodes array; these will be replaced by references after the call to start.

$firebaseArray child records don't have IDs on their child nodes

I'm making a modded version of the angular-fire todo list. This modification includes making sublists and sublists of sublists.
My problem is that when I add my first level of sublists, the sub-objects don't have the $id that I need to affix a next level of sublist.
They don't appear to have the usual rigamarole of firebase properties, just "title" and "completed" status.
I can't figure out why the ng-repeat I have doesn't give me more information, and especially why it works for the top level objects but not further below.
The original addition:
$scope.addTodo = function(theTodo) {
var newTodo = theTodo.trim();
if (!newTodo.length) {
return;
}
$scope.todos.$add({
title: newTodo,
completed: false
});
$scope.newTodo = '';
$scope.subtodo = false;
};
The sublist addition:
$scope.addSubList = function(parent, toDo) {
console.log(parent, toDo)
var subRef = newRef.child(parent.$id)
var newArray = $firebaseArray(subRef)
var newTodo = toDo.trim();
if (!newTodo.length) {
return;
}
newArray.$add({title: newTodo,
completed: false
})
$scope.sublistExists = true;
$scope.newTodo = '';
}
The $firebaseArray object only works its magic on the first-level children under the location that you initialize it with. So the behavior you are seeing is "working as designed".
If you want to handle multi-level collections, you have two options (as far as I can see):
handle the lower levels yourself
store all lists on a single level and then build the multi-level manually by keeping a parentId in each list
I'd recommend going with option 2, since it closer aligns with the Firebase recommendation to prevent unnecessary nesting. See https://www.firebase.com/docs/web/guide/structuring-data.html

ExtJS -- highlight matching nodes for tree typeAhead

I'm using a tree filter plugin created by one of the members at Sencha. Here is his fiddle for the plugin: http://jsfiddle.net/slemmon/fSJwF/2/
As you can see in the fiddle when a typeAhead result is a parent node, all of its children are expanded. So I want to highlight all matching results because I often get numerous parent nodes that contain the matching typeAhead search term and they have hundreds of children, making it hard to find the matching parent node(s).
copy pasting a snippet from the fiddle since SO is throwing "links to jsfiddle must be accompanied by code" error...
filter: function (value, property, re) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, re = re || new RegExp(value, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, viewNode;
if (Ext.isEmpty(value)) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
// iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
if (node.get(property).match(re)) { // if the node matches the search criteria and is a leaf (could be modified to searh non-leaf nodes)
matches.push(node) // add the node to the matches array
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (!match.isLeaf()) { Ext.Array.remove(matches, match); }
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node) // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node)
});
}
visibleNodes.push(item) // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
Thanks.
One of the methods to solve that problem is to append additional class to matched elements and style it the way you want.
In the source below, I add class 'matched' to every matched item. Fiddle link is http://jsfiddle.net/0o0wtr7j/
Ext.define('TreeFilter', {
extend: 'Ext.AbstractPlugin'
, alias: 'plugin.treefilter'
, collapseOnClear: true // collapse all nodes when clearing/resetting the filter
, allowParentFolders: false // allow nodes not designated as 'leaf' (and their child items) to be matched by the filter
, init: function (tree) {
var me = this;
me.tree = tree;
tree.filter = Ext.Function.bind(me.filter, me);
tree.clearFilter = Ext.Function.bind(me.clearFilter, me);
}
, filter: function (value, property, re) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, re = re || new RegExp(value, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, matchedClass = 'matched'
, viewNode;
if (Ext.isEmpty(value)) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
// iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
if (node.get(property).match(re)) { // if the node matches the search criteria and is a leaf (could be modified to searh non-leaf nodes)
node.set('cls', matchedClass);
matches.push(node) // add the node to the matches array
} else {
node.set('cls', '');
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (!match.isLeaf()) { Ext.Array.remove(matches, match); }
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node) // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node)
});
}
visibleNodes.push(item) // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
, clearFilter: function () {
var me = this
, tree = this.tree
, root = tree.getRootNode();
if (me.collapseOnClear) { tree.collapseAll(); } // collapse the tree nodes
root.cascadeBy(function (node) { // final loop to hide/show each node
node.set('cls', '');
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional and show all nodes
viewNode.show();
}
});
}});

Ember store adding attributes incorrectly

I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error

Categories

Resources