After dynamically updating the data for a force simulation in d3, the node's g element is initialized in a different place than where the links point to, and they do not move in sync when I drag them.
Here is what the graph looks like after adding a node.
I followed this tutorial for the update pattern: https://bl.ocks.org/mbostock/1095795.
I create my links and nodes like so:
// build the arrow
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // different link/path types can be defined here
.enter().append("svg:marker") // this section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// create the link drawing
var link = svg.append("g")
.attr("class", "links")
.attr("marker-end", "url(#end)")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke-width", "4px");
// node is a group element
var node = svg.selectAll(".node")
.data(graph.nodes, function (d) { return d.id; })
.enter().append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// add circle to the node
node
.append("circle")
.attr("r", function (d) { return d.id == undefined ? 0 : d.width / 2 + 2 * padding; })
.attr("fill", function (d) { return color(d.type); })
.attr('stroke', '#000000')
.attr('stroke-width', '3');
// add the text to the group
node
.append('text')
.text(node => node.type)
.attr('font-size', 15)
.attr('font-family', fontFamily)
.attr('dx', function (d) { return -d.width / 2 })
.attr('dy', 4);
and update the graph when the node and link data is changed like so:
// update graph for nodes
node = node.data(graph.nodes, function (d) { return d.id; });
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.merge(node);
node
.append("circle")
.attr("r", function (d) { return d.id == undefined ? 0 : d.width / 2 + 2 * padding; })
.attr("fill", function (d) { return color(d.type); })
.attr('stroke', '#000000')
.attr('stroke-width', '3');
console.log(graph.nodes);
// add the text to the group
node
.append('text')
.text(node => node.type)
.attr('font-size', 15)
.attr('font-family', fontFamily)
.attr('dx', function (d) { return -d.width / 2 })
.attr('dy', 4);
// update links
link = link.data(graph.links);
link.exit().remove();
link = link.enter()
.append("line")
.attr("stroke-width", "4px")
.merge(link);
// restart simulation
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alpha(0.01);
simulation.restart();
Related
I have a multi-line chart that shows data of two different categories. I have generated the graph but the x-axis is only showing the starting date and ending date instead of all the dates in between. How do I show all of those dates?
I have tried this code with a dummy data and it works perfectly. The only difference I see between the datas is the date format. In the dummy data, it only has the year but with the actual data it has the month and day.
/* Scale */
var xScale = d3.scaleBand()
.domain(d3.extent(arr[0].data, d => d.date_visited))
.range([0, width - margin]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(arr[0].data, d => d.doc_count)])
.rangeRound([height - margin, 0]);
var color = d3.scaleOrdinal(d3.schemeCategory10);
/* Add SVG */
var svg = d3.select(".lc_top").append("svg")
.attr("width", (width + margin))
.attr("height", (height + margin))
.append('g')
.attr("transform", `translate(${margin}, ${margin})`);
/* Add line into SVG */
var area = d3.area()
.x(d => xScale(d.date_visited))
.y0(height - margin)
.y1(d => yScale(d.doc_count));
let lines = svg.append('g')
.attr('class', 'lines')
.attr('width', svgWidth + 200)
.attr('height', svgHeight);
lines.selectAll('.line-group')
.data(arr).enter()
.append('g')
.attr('class', 'line-group')
.on("mouseover", function (d, i) {
svg.append("text")
.attr("class", "title-text")
.text(d.doc_count)
.attr("text-anchor", "middle")
.attr("x", (width - margin) / 2)
.attr("y", 5);
})
.on("mouseout", function (d) {
svg.select(".title-text").remove();
})
.append('path')
.attr('fill', (d, i) => color(i))
.attr('d', area)
.style('stroke', (d, i) => color(i))
.style('opacity', lineOpacity)
.on("mouseover", function (d) {
d3.selectAll('.area')
.style('opacity', otherLinesOpacityHover);
d3.selectAll('.circle')
.style('opacity', circleOpacityOnLineHover);
d3.select(this)
.style('opacity', lineOpacityHover)
.style("stroke-width", lineStrokeHover)
.style("cursor", "pointer");
})
.on("mouseout", function (d) {
d3.selectAll(".area")
.style('opacity', lineOpacity);
d3.selectAll('.circle')
.style('opacity', circleOpacity);
d3.select(this)
.style("stroke-width", lineStroke)
.style("cursor", "none");
});
/* Add circles in the line */
lines.selectAll("circle-group")
.data(arr).enter()
.append("g")
.style("fill", (d, i) => color(i))
.selectAll("circle")
.data(d => d.data).enter()
.append("g")
.attr("class", "circle")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer")
.append("text")
.attr("class", "text")
.text(`${d.doc_count}`)
.attr("x", d => xScale(d.date_visited))
.attr("y", d => yScale(d.doc_count) - 10);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "none")
.transition()
.duration(duration)
.selectAll(".text").remove();
})
.append("circle")
.attr("cx", d => xScale(d.date_visited))
.attr("cy", d => yScale(d.doc_count))
.attr("r", circleRadius)
.style('opacity', circleOpacity)
.on("mouseover", function (d) {
d3.select(this)
.transition()
.duration(duration)
.attr("r", circleRadiusHover);
})
.on("mouseout", function (d) {
d3.select(this)
.transition()
.duration(duration)
.attr("r", circleRadius);
});
/* Add Axis into SVG */
var xAxis = d3.axisBottom(xScale).ticks(7);
var yAxis = d3.axisLeft(yScale).ticks(7);
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${height - margin})`)
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append('text')
.attr("y", 15)
.attr("transform", "rotate(-90)")
.attr("fill", "#000")
.text("Total value");
Here's the graph:
You can use d3 timescale.
https://github.com/d3/d3-scale/blob/v2.2.2/README.md#time-scales
var x = d3.scaleTime()
.domain([new Date(2000, 0, 1), new Date(2000, 0, 2)])
.range([0, 960]);
I'm learning about d3.js and the system of forces. I have a blocker because I am not able to add a text and it is perfectly centered within the circle. I had tried to create <text> but it does not work. How can I achieve it?
In this piece I tried to create a text element but it does not work.
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 20)
.attr("fill", function(d){
return colorNode(d.group);
})
.style("stroke", function(d){
return colorNode(d.group);
})
UPDATE
I know that I must somehow make that within a g element, this content the circle and a text, but I can not make it work. obviously, I also do not know how to center the text inside the circle. My result is that the circle appears outside the force diagram. I have tried this, but not works:
var node = g.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter()
.append("g");
node.append("circle")
.attr("r", 20)
.attr("fill", function(d){
return colorNode(d.group);
})
.style("stroke", function(d){
return colorNode(d.group);
})
this is my full code:
https://plnkr.co/edit/JhjhFKQgKVtmYQXmgbzF?p=preview
All you need is setting the dominant-baseline to central, which centers the text vertically, and text-anchor to middle, which centers the text horizontally. Also, get rid of those x and y attributes.
This should be the selection:
var text = g.append("g")
.attr("class", "labels")
.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.style("dominant-baseline", "central")
.style("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", "0.7em")
.text(function(d) {
return d.lotid;
});
Here is your code with those changes:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .9;
}
</style>
<body>
<div id="grafica_back" style="width:100wh"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
width = w.innerWidth || e.clientWidth || g.clientWidth,
height = w.innerHeight || e.clientHeight || g.clientHeight;
var colorNode = d3.scaleOrdinal()
.range(d3.schemeCategory20),
colorLink = d3.scaleOrdinal()
.range(d3.schemeCategory10)
var svg = d3.select("#grafica_back").append("svg")
.attr("width", width)
.attr("height", height);
var graph = {
"nodes": [{
"id": "18362286",
"lotid": "TEST",
"epoch": 1511295513000,
"group": 1,
"sourceOnly": true
},
{
"id": "18362287",
"lotid": "TEST",
"epoch": 1511324313000,
"group": 2,
"sourceOnly": false
}
],
"links": [{
"source": "18362286",
"target": "18362287",
"reltype": "GENEALOGY"
}]
};
var width = 400,
height = 200;
var simulation = d3.forceSimulation()
.nodes(graph.nodes);
simulation
.force("charge_force", d3.forceManyBody().strength(-100))
.force("center_force", d3.forceCenter(width / 2, height / 2))
.force("links", d3.forceLink(graph.links).id(function(d) {
return d.id;
}).distance(100).strength(0.1))
.force("collide", d3.forceCollide().radius(2))
;
simulation
.on("tick", ticked);
//add encompassing group for the zoom
var g = svg.append("g")
.attr("class", "everything");
//Create deffinition for the arrow markers showing relationship directions
g.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 23)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke", function(d) {
console.log(d);
return colorLink(d.group);
})
.attr("marker-end", "url(#arrow)");
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 20)
.attr("fill", function(d) {
return colorNode(d.group);
})
.style("stroke", function(d) {
return colorNode(d.group);
})
//add drag capabili
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
drag_handler(node);
var text = g.append("g")
.attr("class", "labels")
.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.style("dominant-baseline", "central")
.style("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", "0.7em")
.text(function(d) {
return d.lotid;
});
node.on("click", function(d) {
d3.event.stopImmediatePropagation();
self.onNodeClicked.emit(d.id);
});
node.append("title")
.text(function(d) {
d.lotid;
});
//add zoom capabilities
var zoom_handler = d3.zoom()
.on("zoom", zoom_actions);
zoom_handler(svg);
//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 ticked() {
//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;
});
text
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
</script>
I have 2 lines in the form of waves plotted in x-y axis based on randomly generated data and i am showing circles on the waves denoting the data points on it.
Based on setInterval of 200 ms, I am updating the original data and the lines(waves) are moving to the left, but the issue is that the only circles which are there in the initial interval are moving and for 2nd interval onward the circles are not showing up on the waves.
see the jsfiddle for the running code : https://jsfiddle.net/rajatmehta/tm5166e1/10/
here is the code :
chartBody.append("path") // Add the valueline path
.datum(globalData)
.attr("id", "path1")
.attr("class", "line")
.attr("d", valueline);
chartBody.selectAll(null)
.data(globalData)
.enter()
.append("circle")
.attr("class", "dot1")
.attr("r", 3)
.attr("cx", function(d) {
console.log(d);
return x(d.timestamp);
})
.attr("cy", function(d) {
return y(d.value);
});
chartBody.selectAll(null)
.data(globalDataNew)
.enter()
.append("circle")
.attr("class", "dot2")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.timestamp);
})
.attr("cy", function(d) {
return y(d.value);
});
chartBody.append("path") // Add the valueline path
.datum(globalDataNew)
.attr("id", "path2")
.attr("class", "line")
.attr("d", valueline2);
any idea how to do that ?
You need to create new circles based on the updated data. Currently, you are only updating the data to selection, but not appending circles, and then moving existing circles to the left.
For example, you could to this:
chartBody.selectAll(".dot1")
.data(globalData, function(d){ return d.timestamp; })
.enter()
.append("circle")
.attr("class", "dot1")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.timestamp);
})
.attr("cy", function(d) {
return y(d.value);
});
chartBody.selectAll(".dot2")
.data(globalDataNew, function(d){ return d.timestamp; })
.enter()
.append("circle")
.attr("class", "dot2")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.timestamp);
})
.attr("cy", function(d) {
return y(d.value);
});
d3.selectAll(".dot1")
//.data(globalData)
.transition()
.duration(duration)
.ease("linear")
.attr("transform", "translate(" + String(dx) + ")");
d3.selectAll(".dot2")
//.data(globalDataNew)
.transition()
.ease("linear")
.duration(duration)
.attr("transform", "translate(" + String(dx) + ")");
See here: https://jsfiddle.net/tm5166e1/11/
This appends the data, using the timestamp as a key so you only create new circles for newly added datums.
(There is an issue when they are first added which is beyond the scope of this question, but it will be worth checking out these examples: https://bl.ocks.org/tomshanley/15a2b09a95ccaf338650e50fd207fcbf and https://bl.ocks.org/mbostock/1642874)
I've been working on a sunburst visualization example provided by the following link http://bl.ocks.org/mbostock/4063423. I want the label to display the name of the partition in question that has been moused over. Right now whenever I mouseover a partition it shows "flare" in the middle only. Is there a way for me to access the names of the children?
d3.json("flare.json", function(error, root) {
var path = svg.datum(root).selectAll("path")
.data(partition.nodes) //access the nodes
.enter()
.append("path")
.attr("display", function(d) { return d.depth ? null : 'none';}) //hide inner ring
.attr("d", arc)//used whenever I come across a path
.style("stroke", "#fff")
.style("fill", function(d) { return color((d.children ? d : d.parent).name);})
.style("fill-rule", "evenodd")
.each(stash)
.on("mouseover", mouseover)
.on("mouseout", mouseout);
var label = svg.append("text")
.attr("id", "tooltip")
.attr("x", 10)
.attr("y", 50)
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", "black")
.style("opacity", 0)
.text(function(d) { return d.name; });
function mouseover(d) {
d3.select(this)
.transition()
.duration(100)
.style("opacity", 0.3);
label.style("opacity", .9);
console.log('mouseover', mouseover);
};
function mouseout(d) {
d3.select(this)
.transition()
.duration(100)
.style("opacity", 1);
label.style("opacity", 0);
console.log('mouseout', mouseout);
};
This problem is solved by first of all appending text elements to a g element and not svg element. Second you want to create a text element outside the mousehandler with a specific id then call it using that Id within the event handler like so.
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path =
g.append("path")
.attr("display", function(d) { return d.depth ? null : 'none';}) //hide inner ring
.attr("d", arc)//used whenever I come across a path
.attr("id", "part")
.style("stroke", "#fff")
.style("fill", function(d) { return color((d.children ? d : d.parent).name);})
.style("fill-rule", "evenodd")
.each(stash)
.on("mouseover", mouseover)
.on("mouseout", mouseout);
var text = g.selectAll("text")
.data(partition.nodes(root))
.enter()
.append("text")
.attr("id", "tip")
.attr("x", 10)
.attr("y", 50)
.attr("font-size", "11px")
.style("opacity", 0);
function mouseover(d) {
d3.select(this)
.transition()
.duration(1000)
.ease('elastic')
.style("opacity", 0.3);
//label.style("opacity", .9);
d3.select("#tip").text(d.name).style("opacity", 0.9);
console.log('mouseover', mouseover);
};
function mouseout(d) {
d3.select(this)
.transition()
.duration(100)
.style("opacity", 1);
d3.select("#tip").text(d.name).style("opacity", 0);
console.log('mouseout', mouseout);
};
I have been setting up graphs to dynamically scale in reference to the width and height of the svg or bounding box, and with nodes and node borders this is relatively simple. By using d.depth and assigning the desired ratio to assign a relative radius to a node at a certain depth, this is giving me no problems and nodes at lower depths become smaller to avoid overlap. Then, for the node border I assign a ratio between the node radius and the thickness of the border.
This has proven to be an ideal solution to a problem that arises when adding a zoom feature by scaling the container. This is the problem:
The text, the nodes, and the node borders no longer bunch up, but the links remain the same weight.
I would also like to scale the thickness of the edges between nodes but I do not know how to identify edges so that I can either assign a thickness in relation to the radius of the parent or child node
d3.json("aviation.json", function(root) {
var nodes = balloon.nodes(root),
links = balloon.links(nodes);
var link= svg.selectAll("path")
.data(links)
.enter().append("path")
.attr("d",diagonal)
.attr("stroke", "black")
.attr("shape-rendering", "auto")
.attr("fill", "none")
.data(nodes)
.attr("stroke-width", function(d) {return (1/5000*diameter);});
var node = svg.selectAll("g.node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node");
node.append("circle")
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("fill", "white")
.attr("opacity", "0.05")
.attr("stroke-width", function(d) {return d.r/600;}) //here I am referecing d.r
//to find the stroke width.
.attr("stroke-opacity", ".5")
.on("Click", function(d) { return zoomClick});
var text = svg.selectAll("g.text")
.data(nodes)
.enter()
.append("text")
.attr("dx", function(d) { return d.x })
.attr("dy", function(d) { return d.y })
.attr("font-family", "Helvetica")
.attr("font-size", function(d) {return d.children ? d.r/7 : d.r/4;})
.attr("stroke-width", function(d) {return d.r/400;})
.attr("stroke-opacity", "1")
.attr("fill", "white")
.attr("pointer-events", "all")
.style("text-anchor", "middle")
.text(function(d) { return d.name; })