Recently I tried mastering the d3js library (which is great) and I am trying to do the easiest stuff but for some reason it doesn't work correctly at all. I did look at related issues but none could help me to find my problem.
The setup is simple, I have a force layout with only one node and no link. When the user clicks on that node, I would like to add a new node that gets linked to the node that was clicked. Here is my code so far. The first added node has a very random position and after that I keep having a message "Uncaught TypeError: Cannot read property '__data__' of null".
Thank you for your help !
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 test</title>
</head>
<body>
<div id="viz">
</div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript">
var w = 800;
var h = 800;
var padding = 100;
var svg = d3.select("#viz")
.append("svg")
.attr("width", w)
.attr("height", h);
var nodes = [{name:"n0"}];
var links = [];
var force = d3.layout.force()
.size([w, h])
.linkDistance([100])
.charge([-30])
.start();
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
update();
function update() {
force.nodes(nodes)
.links(links)
.start();
link = link.data(force.links(), function(d) {return d.source.name + "-" + d.target.name;})
.enter()
.insert("line", ".node")
.attr("class", "link")
.style("stroke", "red")
.style("stroke-width", 2);
node = node.data(force.nodes(), function(d) {return d.name;})
.enter()
.append("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", "black")
.on("click", function(d) {
var n = {name:"n"+nodes.length};
nodes.push(n);
links.push({source:d, target:n});
update();
})
.call(force.drag);
}
force.on("tick", function() {
link.attr("x1", function(d) {return d.source.x;})
.attr("y1", function(d) {return d.source.y;})
.attr("x2", function(d) {return d.target.x;})
.attr("y2", function(d) {return d.target.y;});
node.attr("cx", function(d) {return d.x})
.attr("cy", function(d) {return d.y});
});
</script>
</body>
For a reason that I can't explain, it works perfectly when I change this bit
node = node.data(force.nodes(), function(d) {return d.name;}).enter()...
to this
node = node.data(force.nodes(), function(d) {return d.name;})
node.enter()...
And do the same to the "link" selection
I guess it has to do with enter selections and co.
Related
I am very new to d3js v3 and I was trying out a new program where there are lines and the according to the data, circles get embedded into them.
This is what I have so far.
var width = 500,
height = 500;
var animals = ['dog', 'cat', 'bat'];
var fruits = ['apple', 'banana'];
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var line1 = svg.append("line")
.attr("x1", 350)
.attr("y1", 5)
.attr("x2", 350)
.attr("y2", 350)
.attr("stroke-width", 2)
.attr("stroke", "black");
var line2 = svg.append("line")
.attr("x1", 80)
.attr("y1", 5)
.attr("x2", 100)
.attr("y2", 350)
.attr("stroke-width", 2)
.attr("stroke", "black");
var animal_scale = d3.scale.ordinal()
.domain(animals)
.rangePoints([5, 350],.2);
var fruit_scale = d3.scale.ordinal()
.domain(fruits)
.rangePoints([5, 350],.2);
var animal_circles = svg.selectAll('circle')
.data(animals)
.enter()
.append('circle')
.attr('cx', function(d) {
// is there a way to calc it automatically according to line 1
})
.attr('cy', function(d) {
return animal_scale(d);
})
.attr('id', function(d) {
return d;
})
.attr('r', 20);
var fruits_circles = svg.selectAll('circle')
.data(fruits)
.enter()
.append('circle')
.attr('cx', function(d) {
// is there a way to calc it automatically according to line 2
})
.attr('cy', function(d) {
return fruit_scale(d);
})
.attr('id', function(d) {
return d;
})
.attr('r', 20);
<!DOCTYPE html>
<html>
<meta charset=utf-8>
<head>
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
</body>
</html>
I looked at some sources and being new, its kinda hard to understand most of it. I eventually want to be able to move and drag the circles between lines at the end of the project.There are some issues with the current code, as it does not display the second set of circles too.
Could someone please help me understand further how to do this. It would be a great way for me to learn.
You can select objects by class name and set data. Here is my fast solution for drag-n-drop: jsFiddle. You can modify drag function to add limits to cx position
I have implemented a simple network visualization app in D3.js by adopting ideas from http://jsbin.com/omokap/8/edit?html,css,js,output.
This application reads node names (separated by line breaks) from a textarea in an html page and then constructs a network in which all nodes are connected to each other.
All my codes are included in the end of this message.
My problem is that I cannot set labels to nodes.
More specifically I get the following error message when loading the D3jNetVis.html on a web browser.
Uncaught TypeError: D3jNetVis.js:64
undefined is not a function
This error happens when I try to set labels to nodes in the following code snippet:
dataSet.nodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This type of node label setter was suggested in D3.js, force-graph, cannot display text/label of nodes.
Any ideas why I get this error and how to fix it?
Google gave a hint that this could be related the importing order of the js-files in the html page but have been trying various combinations without success.
Thanks,
Erno Lindfors
D3jNetVis.html:
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Network Visualization Example - d3js</title>
<link rel="stylesheet" type="text/css" href="D3jNetVis.css">
</head>
<body>
<table>
<tr><td><div id="svgContent"></div></td></tr>
<tr><th align="left">Give node ids</th></tr>
<tr><td><textarea id="nodeIds" cols=5 rows=20></textarea></td></tr>
<tr><td><button type="button" onclick="constNet()">Construct Network</button></td></tr>
</table>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="D3jNetVis.js" charset="utf-8"></script>
</body>
</html>
D3jNetVis.js:
function constNet() {
var textArea = document.getElementById("nodeIds");
var nodeIdsArray = document.getElementById("nodeIds").value.split("\n");
var w = 500,
h = 500;
var svg = d3.select("#svgContent")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr('preserveAspectRatio', 'xMinYMin slice')
.append('g');
var nodesArray = [];
for (var i = 0; i < nodeIdsArray.length; i++) {
var nodeId = nodeIdsArray[i];
var newNode = {name: "Node" + nodeId, id:nodeId, fixed:false};
nodesArray[nodesArray.length] = newNode;
}
var edgesArray = [];
for (var i = 0; i < nodeIdsArray.length-1; i++) {
var sNodeId = nodeIdsArray[i];
for (var j = i+1; j < nodeIdsArray.length; j++) {
var tNodeId = nodeIdsArray[j];
edgesArray[edgesArray.length] = {source:sNodeId-1, target:tNodeId-1};
}
}
var dataSet = {
nodes: nodesArray,
edges: edgesArray
};
var force = self.force = d3.layout.force()
.nodes(dataSet.nodes)
.links(dataSet.edges)
.gravity(0.05)
.distance(100)
.charge(-100)
.size([w,h])
.start();
var link = svg.selectAll(".link")
.data(dataSet.edges)
.enter().append("line")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var node = svg.selectAll("circle")
.data(dataSet.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 4.5)
.call(node_drag);
/*
The "Uncaught TypeError" happens in the next line.
*/
dataSet.nodes.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
function dragstart(d, i) {
force.stop(); // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick();
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
}
force.on("tick", tick);
function tick() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
}
D3jNetVis.css:
line{
stroke: #cccccc;
stroke-width: 1;
}
circle{
fill: blue;
}
You can just append html elements to a selection of html elements (so the browser knows where to append in the DOM).
dataSet.nodes is not a selection of html elements, that's why you get the error message.
Write instead:
node.append("text").....
I'm creating a webpage that will make a d3 force-layout graph using data from a JSON file, and when a button is pushed the user will be prompted for a name, and a new node will be created with the provided name. The page loads successfully and when the button is pushed an alert asks for a name, but when a name is entered there is an error that says 'Uncaught TypeError: undefined is not a function' and a new node isn't added.
What can I do to fix this error? Using Chrome I know the error is happening in the line where I try to append newNode. Here's my code:
<button onclick="addNode()">Click to add Node</button>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
function addNode() {
var nodeName = prompt("Name of new node:");
var newGroup = Math.floor(Math.random() * 6);
if (nodeName != null) {
var newNode = {"node":{"name":nodeName,"group":newGroup}};
force.nodes.append(newNode);
force.start();
}
}
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("user_interactions(1).json", function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
force.nodes is a function that returns the array of nodes. It's not a property. So force.nodes.append doesn't make any sense. You probably want to add the new node to the data,
force.nodes(force.nodes().push(newNode));
I'm not certain, however, that D3 will gracefully handle changing the nodes array dynamically.
In any case you'll have to add SVG elements for the new node and specify their attributes in a manner similar to how you're setting the initial properties (r, fill, etc.)
I understand that it's generally bad practice to inline javascript in html, but I have to work around Squarespace's content managing system, and this seems to be the best way forward. In short, I have a .js file and .json file that I need to write into my .html file. I wanted to take this one step at a time, so I tried to inline just the .js file, but I can't seem to get the inline .js to work (the code is meant to implement an interactive network visualization in d3.js):
<!DOCTYPE html>
<html>
<head>
<title>Force-Directed Layout</title>
<script type="text/javascript">
var w = 960,
h = 500,
fill = d3.scale.category20();
var vis = d3.select(".sqs-block-content")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
d3.json("cites.json", function(json) {
var force = d3.layout.force()
.charge(-125)
.linkDistance(50)
.nodes(json.nodes)
.links(json.links)
.size([w, h])
.start();
var link = vis.selectAll("line.link")
.data(json.links)
.enter().append("svg:line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); })
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
var node = vis.selectAll("g.node")
.data(json.nodes)
.enter().append("svg:g")
.attr("class", "node")
node.append("svg:circle")
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); })
.call(force.drag);
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 10)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
node.append("svg:title")
.text(function(d) { return d.name; });
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
});
</script>
<script type="text/javascript" src="d3/d3.geom.js"></script>
<script type="text/javascript" src="d3/d3.layout.js"></script>
<link type="text/css" rel="stylesheet" href="d3/force.css"/>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript" src="cites.js"></script>
</body>
</html>
I'm sorry to ask such a benighted question, but I've been tinkering and reading around for a few hours and don't seem to be getting anywhere. Does anyone see what I'm missing? Here is the directory containing the other files (including the cites.html file which renders properly, and which I'm trying to replicate using the inline page test.html, the code from which is posted above).
I'm not familiar with d3, but it looks like you are trying to both (a) Use the d3 library before you have loaded it, and (b) access DOM elements before they have been loaded. I recommend changing the order of your script elements:
<script type="text/javascript" src="d3/d3.geom.js"></script>
<script type="text/javascript" src="d3/d3.layout.js"></script>
<script type="text/javascript">
var w = 960,
...
</script>
And put your actions in an onload event:
window.onload = function() {
var vis = d3.select(".sqs-block-content")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
...
};
I think that will at least get you started in the right direction.
I am completely stuck adding labels to the force directed tree graph found here http://bl.ocks.org/mbostock/1138500
I have attempted to synthesize the force directed tree with other examples that include labels as well as following the answer to Add text label to d3 node in Force directed Graph and resize on hover but the graph always seems to break.
This code works for the force directed graph with labels and pictures
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("graph.json", function(error, json) {
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
});
</script>
However when I attempt to modify this to form a tree structure from Mike's example my code looks like this but does not work.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("test.json", function(error, json) {
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
function tick(e) {
// Push sources up and targets down to form a weak tree.
var k = 6 * e.alpha;
json.links.forEach(function(d, i) {
d.source.y -= k;
d.target.y += k;
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
});
</script>
I have tried and tried to resolve this but cannot combine the labels with the force directed graph, any assistance would be greatly appreciated, I've been beating my head against the wall on this for some time now...
Thanks!
The part missing from your sample code is the addition to the tick callback that decreases the y value of the source, and increases that of the target by a small amount each time.
Here's a jsfiddle example which I think does what you're after.
The key portion is the addition of a parameter, called e here, to the tick function, along with the lines
var k = 6 * e.alpha;
json.links.forEach(function(d, i) {
d.source.y -= k;
d.target.y += k;
});
The result looks like this, once you also increase the magnitude of the charge to push the nodes a little further away from each other: