D3 updating data on parent element wont reflect on child elements - d3.js

Let's consider I need to have a bunch of circles wrapped in <g> elements and I've bound my data to this parents.(initialNodes)
when I press the update button, the update function takes in the new data (newNodes) and I expect the x/y positions of circles to get updated, but as you can see in console, despite the fact that d.x/d.y is correctly printed, it's not taken into account in return d.x and therefore just Enter group is added to canvas.
What am I doing wrong? How can I reflect the updated data on parent <g>, on child elements?
var color = d3.schemeCategory10;
var initialNodes = [
{"id": 0, "x": 50, "y": 50},
{"id": 1, "x": 100, "y": 100},
];
var vis = d3.select("body").append("svg").attr("width", 200).attr("height", 200);
update(initialNodes);
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var circlesGroup = vis.selectAll("g.stop").data(data, function(d){return d.id});
var circlesEnter = circlesGroup.enter().append("g").attr("class", "stop");
var circlesExit = circlesGroup.exit().remove();
// ENTER
// Create new elements as needed.
circlesEnter
.append("circle")
.attr("r", 15)
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;
})
.attr("cy", function (d) {return d.y;})
.style("fill", 'red');
// UPDATE
// Update old elements as needed.
circlesGroup
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;})
.attr("cy", function (d) {return d.y;});
// EXIT
// Remove old elements as needed.
circlesExit
.remove();
}
var newNodes = [
{"id": 0, "x": 50, "y": 100},
{"id": 1, "x": 50, "y": 30},
{"id": 2, "x": 100, "y": 50}
];
var updateNodes = function() {
update(newNodes);
}
// Add the onclick callback to the button
d3.select("#updatebutton").on("click", updateNodes);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button id="updatebutton">Update</button>

circleGroup is a selection of g elements. It has no purpose to set cx and cy for a g element.
After creating new nodes select all g.stop and then the containing circle. Why? Read the d3-selection doc.
There is no reason to add the second remove().
var color = d3.schemeCategory10;
var initialNodes = [
{"id": 0, "x": 50, "y": 50},
{"id": 1, "x": 100, "y": 100},
];
var vis = d3.select("body").append("svg").attr("width", 200).attr("height", 200);
update(initialNodes);
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var circlesGroup = vis.selectAll("g.stop").data(data, function(d){return d.id});
var circlesEnter = circlesGroup.enter().append("g").attr("class", "stop");
var circlesExit = circlesGroup.exit().remove();
// ENTER
// Create new elements as needed.
circlesEnter
.append("circle")
.attr("r", 15)
.style("fill", 'red');
vis.selectAll("g.stop").select("circle")
.transition().duration(750)
.attr("cx", function (d) {
console.log('ENTERING2: id:'+d.id+' position:'+d.x+','+d.y);
return d.x;})
.attr("cy", function (d) {return d.y;});
}
var newNodes = [
{"id": 0, "x": 50, "y": 100},
{"id": 1, "x": 50, "y": 30},
{"id": 2, "x": 100, "y": 50}
];
var updateNodes = function() {
update(newNodes);
}
// Add the onclick callback to the button
d3.select("#updatebutton").on("click", updateNodes);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div><button id="updatebutton">Update</button></div>

Related

Which concepts and techniques do I need to achieve this kind of graph

I want to draw a dynamic visual of how certain functional blocks interact with each other. I'm having difficulty defining what kind of graph I need, and what (if any) d3 layout function is best suited to start with. I'm not even sure my example falls within the definition of a graph.
The general idea is to visualize a group of functions with their inputs and outputs. It starts with a set of inputs, and ends with a set of outputs. In between there are several functions who each take inputs and generate 1 or more outputs. Each output can serve as input for one or more functions. So each edge/line represents an output being transferred to a function to serve as input (and is uni-directional)
I'm not looking for answer in code, but rather an insight on which concepts I need to start with. The image probably has issues and can't be achieved literally, but I can't point out what exactly.
const nodes = [
{
id: 1,
title: 'Function A',
x: 100,
y: 25,
points: [
{
id: 11,
dx: 50,
dy: 0
},
{
id: 12,
dx: 0,
dy: 20
}
]
},
{
id: 2,
title: 'Function B',
x: 300,
y: 100,
points: [
{
id: 21,
dx: -50,
dy: 0
},
{
id: 22,
dx: 0,
dy: 20
}
]
},
{
id: 3,
title: 'Function C',
x: 170,
y: 160,
points: [
{
id: 31,
dx: 0,
dy: -20
},
{
id: 32,
dx: 50,
dy: 0
}
]
}
];
const links = [
{source: 11, target:21},
{source: 12, target:31},
{source: 22, target:32}
];
var selectedNode = null;
const renderNodes = svg => {
const allNodes = svg.selectAll('.node').data(nodes, node => node.id);
const addedNodes = allNodes.enter()
.append('g')
.style('cursor', 'pointer')
.attr('transform', d => `translate(${d.x},${d.y})`)
.on('click', d => {
selectedNode = d.id;
renderAll();
});
addedNodes.append('rect')
.attr('width', 100)
.attr('height', 40)
.attr('x', -50)
.attr('y', -20)
.attr('rx', 5);
addedNodes.append('text')
.text(d => d.title)
.style('fill', 'white')
.attr('y', 6)
.attr('text-anchor', 'middle');
addedNodes.merge(allNodes)
.select('rect')
.style('fill', d => (d.id === selectedNode) ? 'blue' : 'black');
allNodes.exit().remove();
};
const renderConnectionPoints = (svg, points) => {
const allPoints = svg.selectAll('.point').data(points, point => point.id);
const addedPoints = allPoints.enter()
.append('g')
.style('cursor', 'pointer')
.attr('transform', d => `translate(${d.x},${d.y})`);
addedPoints.append('circle')
.attr('r', 4)
.style('fill', 'white');
addedPoints.merge(allPoints)
.select('circle')
.style('stroke', d => (d.parentId === selectedNode) ? 'blue' : 'black');
allPoints.exit().remove();
}
const renderLinks = (svg, _links) => {
const linkPath = d => `M ${d.source.x}, ${d.source.y}
C ${(d.source.x + d.target.x) / 2}, ${d.source.y}
${(d.source.x + d.target.x) / 2}, ${d.target.y}
${d.target.x}, ${d.target.y}`;
const allLinks = svg.selectAll('.link').data(_links, link => link.id);
const addedLinks = allLinks.enter()
.append('path')
.style('fill', 'none')
.attr('d', linkPath);
addedLinks.merge(allLinks)
.style('stroke', d => (d.source.parentId === selectedNode ||
d.target.parentId === selectedNode) ? 'blue' : 'lightgray');
allLinks.exit().remove();
}
const getPoints = () => nodes.reduce((all, node) => {
node.points.forEach(point => all.push({
id: point.id, x: node.x + point.dx, y: node.y + point.dy, parentId: node.id}));
return all;
}, []);
const getLinks = points => links.map(link => {
const source = points.find(point => point.id === link.source);
const target = points.find(point => point.id === link.target);
return {source, target};
});
const renderAll = () => {
const svg = d3.select('svg');
const points = getPoints();
const _links = getLinks(points);
renderNodes(svg);
renderLinks(svg, _links);
renderConnectionPoints(svg, points);
}
renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width='400' height='180'>
</svg>
Hi,
Take a look at the snippet.
The data model is very simple: nodes, their connection points and links
Just provide your data and use the snippet's code to render it.
Good luck and feel free to ask any question :)
sorry by my bad english, I speak spanish...
I also look for something similar, researching I have achieved this, and I am missing:
Transform nodes circles into rectangles
Add text labels to each node and each link
Once everything is organized initially, they remain in a fixed location
Capture the click on each node and / or link to trigger another application
I leave the code below, observe especially the definition of nodes_data and links_data, and functions to define size and color:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: black ;
stroke-width: 0px;
}
</style>
<svg width="1000" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
//create somewhere to put the force directed graph
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//var radius = 15;
var nodes_data = [
{"name": "PO1", "entity": "PO"},
{"name": "PO2", "entity": "PO"},
{"name": "PO3", "entity": "PO"},
{"name": "PO4", "entity": "PO"},
{"name": "PO5", "entity": "PO"},
{"name": "PO6", "entity": "PO"},
{"name": "PO7", "entity": "PO"},
{"name": "PY1", "entity": "PY"},
{"name": "PY2", "entity": "PY"},
{"name": "L1", "entity": "X"},
{"name": "L2", "entity": "X"},
{"name": "L3", "entity": "X"},
{"name": "L4", "entity": "X"},
{"name": "TK1", "entity": "TK"},
{"name": "TK2", "entity": "TK"},
{"name": "PIL1", "entity": "TK"},
{"name": "BBA1", "entity": "BA"},
{"name": "BBA2", "entity": "BA"},
{"name": "ULAC1", "entity": "UL"},
{"name": "VtaYPF", "entity": "VTA"}
]
//Sample links data
//type: A for Ally, E for Enemy
var links_data = [
{"source": "PO1", "target": "L1", "type":"A" },
{"source": "PO2", "target": "L1", "type":"A" },
{"source": "PO3", "target": "L1", "type":"A"},
{"source": "PO4", "target": "L2", "type":"A"},
{"source": "PO5", "target": "L2", "type":"A"},
{"source": "PO6", "target": "L3", "type":"A"},
{"source": "PO7", "target": "L3", "type":"A"},
{"source": "L1", "target": "L3", "type":"A"},
{"source": "L2", "target": "L3", "type":"A"},
{"source": "L3", "target": "TK1", "type":"A"},
{"source": "L3", "target": "TK2", "type":"A"},
{"source": "TK1", "target": "L4", "type":"A"},
{"source": "TK2", "target": "L4", "type":"A"},
{"source": "L4", "target": "PIL1", "type":"A"},
{"source": "PIL1", "target": "ULAC1", "type":"A"},
{"source": "PIL1", "target": "BBA1", "type":"A"},
{"source": "PIL1", "target": "BBA2", "type":"A"},
{"source": "ULAC1", "target": "VtaYPF", "type":"A"},
{"source": "BBA1", "target": "PY1", "type":"A"},
{"source": "BBA2", "target": "PY2", "type":"A"}
]
//set up the simulation and add forces
var simulation = d3.forceSimulation()
.nodes(nodes_data);
var link_force = d3.forceLink(links_data)
.id(function(d) { return d.name; });
var charge_force = d3.forceManyBody()
.strength(-100);
var center_force = d3.forceCenter(width / 2, height / 2);
simulation
.force("charge_force", charge_force)
.force("center_force", center_force)
.force("links",link_force)
;
//add tick instructions:
simulation.on("tick", tickActions );
//add encompassing group for the zoom
var g = svg.append("g")
.attr("class", "everything");
//draw lines for the links
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter().append("line")
.attr("stroke-width", 2)
.style("stroke", linkColour);
//draw circles for the nodes
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", radius)
.attr("fill", circleColour);
//add drag capabilities
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
drag_handler(node);
//add zoom capabilities
var zoom_handler = d3.zoom()
.on("zoom", zoom_actions);
zoom_handler(svg);
/** Functions **/
//Function to choose what color circle we have
//Let's return blue for males and red for females
function circleColour(d){
var my_color
switch (d.entity) {
case "PO": my_color = "black"; break;
case "PY": my_color = "cyan"; break;
case "TK": my_color = "blue"; break;
case "UL": my_color = "green"; break;
case "VTA": my_color = "green"; break;
case "BA": my_color = "cyan"; break;
case "X": my_color = "grey"; break;
}
return my_color
}
//Function to choose the line colour and thickness
//If the link type is "A" return green
//If the link type is "E" return red
function linkColour(d){
if(d.type == "A"){
return "green";
} else {
return "red";
}
}
//Drag functions
//d is the node
function drag_start(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
//make sure you can't drag the circle outside the box
function drag_drag(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function drag_end(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
//Zoom functions
function zoom_actions(){
g.attr("transform", d3.event.transform)
}
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
//update link positions
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; });
}
function radius(d){
var my_rad
switch (d.entity) {
case "PO": my_rad = 10; break;
case "PY": my_rad = 10; break;
case "TK": my_rad = 20; break;
case "UL": my_rad = 20; break;
case "VTA": my_rad = 15; break;
case "BA": my_rad = 10; break;
case "X": my_rad = 3; break;
}
return my_rad
}
</script>
The above code generate this live image

How can I apply a clipPath in SVG with multiple paths and NOT clip out the area between the paths?

I have an SVG clipPath made of multiple path elements, which I need to apply to a group of elements. I only want to clip out the area under the path stroke itself, not in between the paths. (example that doesn't do what I want follows)
var lineData = [ { "x": 1, "y": 5}, { "x": 100, "y": 400},
{ "x": 300, "y": 100}, { "x": 600, "y": 600},
{ "x": 700, "y": 50} ];
var lineData2 = [ { "x": 1, "y": 500}, { "x": 100, "y": 100},
{ "x": 300, "y": 700}, { "x": 600, "y": 60},
{ "x": 700, "y": 700} ];
var lineFunction = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveBundle);
var svg = d3.select('body')
.append('svg')
.attr('id', 'svg')
.attr('width', 660)
.attr('height', 660)
.style('outline', '1px solid red')
.append('g')
.attr('clip-path', 'url(#clippy)');
var polygon = svg.append('polygon')
.attr('points', '230 10, 660 330, 230 650')
.attr('fill', '#c99');
var circle = svg.append('circle')
.attr('cx', 230)
.attr('cy', 330)
.attr('r', 200)
.attr('fill', '#9c6')
var clippy = d3.select('#svg')
.append('defs')
.append('clipPath')
.attr('id', 'clippy');
clippy.append("path")
.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 18)
.attr("fill", "none");
clippy.append("path")
.attr("d", lineFunction(lineData2))
.attr("stroke", "blue")
.attr("stroke-width", 18)
.attr("fill", "none");
Basically I want to accomplish something similar to what you get using letters, but instead using lines/paths.
var lineData = [ { "x": 1, "y": 5}, { "x": 100, "y": 400},
{ "x": 300, "y": 100}, { "x": 600, "y": 600},
{ "x": 700, "y": 50} ];
var lineData2 = [ { "x": 1, "y": 500}, { "x": 100, "y": 100},
{ "x": 300, "y": 700}, { "x": 600, "y": 60},
{ "x": 700, "y": 700} ];
var lineFunction = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveBundle);
var svg = d3.select('body')
.append('svg')
.attr('id', 'svg')
.attr('width', 660)
.attr('height', 660)
.style('outline', '1px solid red')
.append('g')
.attr('clip-path', 'url(#clippy)');
var polygon = svg.append('polygon')
.attr('points', '230 10, 660 330, 230 650')
.attr('fill', '#c99');
var circle = svg.append('circle')
.attr('cx', 230)
.attr('cy', 330)
.attr('r', 200)
.attr('fill', '#9c6')
var clippy = d3.select('#svg')
.append('defs')
.append('clipPath')
.attr('id', 'clippy');
clippy.append('text')
.attr('x', 120)
.attr('y', 320)
.attr('font-size', '4em')
.attr('font-weight', 'bold')
.attr('font-family', 'Georgia')
.text('This is a clip');
clippy.append('text')
.attr('x', 120)
.attr('y', 420)
.attr('font-size', '4em')
.attr('font-weight', 'bold')
.attr('font-family', 'Georgia')
.text('Also a clip')
Please help!
EDIT: Here's a Codepen with both examples.
From my reading of the SVG spec, it's not possible to use only a <path>'s stroke as the clipping path:
The raw geometry of each child element exclusive of rendering properties such as ‘fill’, ‘stroke’, ‘stroke-width’ within a ‘clipPath’ conceptually defines a 1-bit mask (with the possible exception of anti-aliasing along the edge of the geometry) which represents the silhouette of the graphics associated with that element. Anything outside the outline of the object is masked out.
However, converting the <clipPath> to a <mask> element produces what I think is your desired effect.
Here's a forked CodePen to demonstrate — the paths have have their strokes set to white, and the <text> elements were also given a fill of white to match the clip-path effect.

Can I move one data series down to a lower x axis, programmatically?

I have a d3 stacked column chart that I'm very happy with. The full code is in a JS Fiddle.
What I'd like to do is lop the last data series off, and set it on its own axis, but ensure that it maintains the same scale. So if this is my data:
var dataset = [
// apples
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 2 }, { "x": 4, "y": 7 }, { "x": 5, "y": 23 }],
// oranges
[{ "x": 1, "y": 10 }, { "x": 2, "y": 12 }, { "x": 3, "y": 19 }, { "x": 4, "y": 23 }, { "x": 5, "y": 17 }],
// grapes
[{ "x": 1, "y": 22 }, { "x": 2, "y": 28 }, { "x": 3, "y": 32 }, { "x": 4, "y": 35 }, { "x": 5, "y": 43 }],
// carrots
[{"x": 1, "y": 5 }, { "x": 2, "y": 4 }, { "x": 3, "y": 23 }, { "x": 4, "y": 2 }, { "x": 5, "y": 7 }]
];
I'd like to keep apples, oranges and grapes stacked, but I want carrots separated out. Carrots is always the last series. I was hoping I could draw the carrots into the same SVG with this:
var lower_svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", b);
var lower_rects = lower_svg.selectAll("rect")
.data(dataset[3])
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", h)
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
But a) that doesn't work (it doesn't draw anything) and b) that calls on the data series 3, which happens to be the last one in this example but isn't always.
And ... if it did work it would draw the carrots twice, once stacked with the other fruits and once below. I only want to draw it once, below.
What I want is to have this chart of various fruit: https://jsfiddle.net/oa7hho9q/17/
And this chart of carrots: https://jsfiddle.net/oa7hho9q/19/
Using the same x and y scales and pulling from the same dataset, where, carrots is just the last series in the set.
I have addressed your problem like this:
Step 1:
I pop out the carrot related data.
var carrots = dataset.pop(); store it in variable carrots
Step 2
I make 2 groups
//this g(group) will hold the stacked chart for carrot
var svgcarrot = svg.append("g").attr("transform", "translate(0,200)");
//this g(group) will hold the stacked chart for other fruits
var svg = svg.append("g").attr("transform", "translate(0,-150)");
//you may change the translate to move the chart as per your choice of positioning.
Step3
Make a function to make charts input svg group and its related dataset
//here svg is the group on which you wish to draw the chart.
//dataset is the data for which the chart need to be drawn.
function makeChart(dataset, svg) {
Step4
Inside your makeChart function your usual stack bar chart code.
function makeChart(dataset, svg) {
var stack = d3.layout.stack();
stack(dataset);//set data
xScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0,
d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})
])
.range([0, h / 2]);//2 chart so height/2
//make groups for fruits
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
//make rectangles
var rects = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d, i) {
return h - b - (yScale(d.y0 + d.y));
})
.attr("height", function(d) {
return yScale(d.y);
})
.attr("width", xScale.rangeBand());
}
Step 5
Now make your first chart
makeChart(dataset, svg);//note dataset has no carrot data as its popped in step1 also the svg container group made in step 2
makeChart([carrots], svgcarrot);//make carrot chart note the svgcarrot container group made in step 2
working example here

How to create d3.js Collapsible force layout with non tree data?

I have a d3 force directed layout with data in a similar structure below. Is it possible to apply collapsible force layout such as http://bl.ocks.org/mbostock/1062288 to it? I want a node to be collapsed /expanded on click.
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
]
}
If I understand correctly, perhaps this is what you are looking for. I edited the demo you linked to. Now when a source node is collapsed, we iterate over all edges and look for other nodes it has edges to.
for each target node that the source node has an edge to, we increment it's collapsing count. If a node has a collapsing count of greater than zero, it is not displayed.
When we uncollapse a node, we do the same thing, except we decrement from the collapsing count.
We need this collapsing count, since, as we are not in a tree, nodes can have more than one node which should cause them to collapse.
I made this work for directed graphs, though I'm not sure that's what you wanted.
Let me know what you think!
The json I used:
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2}
]
}
Modified tutorial code:
<!DOCTYPE html>
<meta charset="utf-8">
<title>Force-Directed Graph</title>
<style>
.node {
cursor: pointer;
stroke: #3182bd;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//Added markers to indicate that this is a directed graph
svg.append("defs").selectAll("marker")
.data(["arrow"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph.json", function(json) {
root = json;
//Give nodes ids and initialize variables
for(var i=0; i<root.nodes.length; i++) {
var node = root.nodes[i];
node.id = i;
node.collapsing = 0;
node.collapsed = false;
}
//Give links ids and initialize variables
for(var i=0; i<root.links.length; i++) {
var link = root.links[i];
link.source = root.nodes[link.source];
link.target = root.nodes[link.target];
link.id = i;
}
update();
});
function update() {
//Keep only the visible nodes
var nodes = root.nodes.filter(function(d) {
return d.collapsing == 0;
});
var links = root.links;
//Keep only the visible links
links = root.links.filter(function(d) {
return d.source.collapsing == 0 && d.target.collapsing == 0;
});
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = link.data(links, function(d) { return d.id; });
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("line", ".node")
.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; })
.attr("marker-end", "url(#arrow)");
// Update the nodes…
node = node.data(nodes, function(d){ return d.id; }).style("fill", color);
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; })
.style("fill", color)
.on("click", click)
.call(force.drag);
}
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; });
}
// Color leaf nodes orange, and packages white or blue.
function color(d) {
return d.collapsed ? "#3182bd" : d.children ? "#c6dbef" : "#fd8d3c";
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
//check if link is from this node, and if so, collapse
root.links.forEach(function(l) {
if(l.source.id == d.id) {
if(d.collapsed){
l.target.collapsing--;
} else {
l.target.collapsing++;
}
}
});
d.collapsed = !d.collapsed;
}
update();
}
</script>
Try this:
var width = 960,height = 500;
var force = d3.layout.force().size([width, height]).charge(-400)
.linkDistance(40)
.on("tick", tick);
var drag = force.drag().on("dragstart", dragstart);
var svg = d3.select("body").append("svg").attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("graph.json", function(error, graph) {
force.nodes(graph.nodes).links(graph.links)
.start();
link = link.data(graph.links).enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.call(drag);
});
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; });
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
You should use json file like this:
graph.json
{
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
]
}

Binding "mouseover" events to points on a line in d3.js

I would like to get the coordinates of a point on a line by clicking on the line using the following code:
var lineData = [ { "x": 1, "y": 5}, { "x": 20, "y": 20},
{ "x": 40, "y": 10}, { "x": 60, "y": 40},
{ "x": 80, "y": 5}, { "x": 100, "y": 60}];
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("linear");
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
var lineGraph = svgContainer.append("path")
.data([lineData]).attr("d", lineFunction)
//.attr("d", lineFunction(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none")
.on('mousedown', function(d) {
console.log({"x":d.x, "y":d.y})
});
(I updated the code to address the comments, but I still get "Object {x: undefined, y: undefined}")
I keep getting an "undefined" when clicking on the line. Am I missing a step?
You can get the coordinates of an event using d3.event:
.on("mousedown", function() {
console.log({"x": d3.event.x, "y": d3.event.y});
});
use mouse event
.on('mousedown', function(d) {
var m = d3.mouse(this);
console.log("x:"+m[0]+" y:"+m[1]);
});
in your function m[0] and m[1] gives you X and Y.

Resources