I have sliders that modify the S command of a path. I want the source name to appear on the path which it does; however how do I remove the previously created text element? I have tried to remove it (see code below) but it doesn't work. The dom just fills up with extra text elements and the text on the path gets darker and darker as they start to pile up on each other. I have even tried to check for the text element by id as shown but no go. Hope you can shed any light on how to remove the text element so there is just one as each S command is modified.
I have added a fiddle here (append text at very bottom of code window):
fiddle...
graph.links.forEach(function (d, i) {
//console.log(obj[0].text, graph.links[i].source.name, graph.links[i].linkid);
if (graph.links[i].source.name == obj[0].text) {
var linkid = graph.links[i].linkid;
var the_path = $("#" + linkid).attr("d");
var arr = $("#" + linkid).attr("d").split("S");
//update S command
$("#" + linkid).attr("d", arr[0] + " S" + scommand_x2 + "," + scommand_y2 + " " + scommand_x + "," + scommand_y);
svg.select("#txt_" + linkid).remove();
svg.selectAll("#" + linkid).data(graph.links).enter()
.append("text")
.attr("id", "txt_" + linkid)
.append("textPath")
.attr("xlink:href", function (d) {
return "#" + linkid;
})
.style("font-size", fontSize + "px")
.attr("startOffset", "50%")
.text("")
.text(graph.links[i].source.name);
}
});
Here is a solution:
https://jsfiddle.net/kx3u23oe/
I did a couple of things. First, you don't need to bind this text to data the way you did. Second, I move the variable outside the update function, with all the append:
var someText = svg.append("text").append("textPath");
Then I kept only this inside update function:
someText.attr("xlink:href", "#L0")
.style("font-size", "12px")
.attr("startOffset", "50%")
.text("some text");
You can remove a text element using 'remove' function. Here is a working code snippet for the same.
var text = d3.select("body")
.append("text")
.text("Hello...");
setTimeout(function() {
text.remove();
}, 800);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
By the way, the problem in your code is, you are iterating over each link (graph.links.forEach(function (d, i) {) and creates a text element for all links(.data(graph.links).enter()) in each iteration. This creates n*n number of text labels; where n is the number of links. So I assume your code should be as follows.
svg.select("#txt_" + linkid).remove();
svg.selectAll("#" + linkid)
.append("text")
.attr("id", "txt_" + linkid)
.append("textPath")
.attr("xlink:href", function (d) {
return "#" + linkid;
})
.style("font-size", fontSize + "px")
.attr("startOffset", "50%")
.text(graph.links[i].source.name);
Related
I just have a quick question regarding changing the color of a path of a stroke of the stock radial tree by Mike Bostock
https://bl.ocks.org/mbostock/4063550
For example, if I can change the color of sub links such as:
var link = g.selectAll(".link")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("class", "link")
.style("fill", "none")
.attr("d", function(d) {
return "M" + project(d.x, d.y)
+ "C" + project(d.x, (d.y + d.parent.y) / 2)
+ " " + project(d.parent.x, (d.y + d.parent.y) / 2)
+ " " + project(d.parent.x, d.parent.y);
});
link.attr('stroke', function(d) {
if (d.id.startsWith("Root.Item1")){
return "#386eff";
}
if (d.id.startsWith("Root.Item2")){
return "#45cbf2";
}
else return '#70f2ad';
});
This will change all the link colors for data that starts with Root.Item2
I.e. Root.Item2.Child1 and Root.Item2.Child2
will have the same color.
However, what If I wish to highlight the paths for Root.Item2.Child2 only and leave the other links the same color?
The concept is something like highlight the path that starts with Root and ends in Child2?
Thanks
I was able to figure this out in a roundabout way by checking the d.children of the node. Not sure if it's ideal but it works if anyone else wishes to do something similar.
if (d.id.startsWith("Root.Item2")) {
for (var i = 0; i < d.children.length; i++ ) {
if (d.id.startsWith("Root.Item2.Child1") |
) {
return "red";
}
}
I am working with the code at CodePen as a base to build a Gantt Chart for my needs. However, I am trying to modify the code so that the actual Rectangle heights accommodate the text that is given to them. In the example shown, all of the task texts are one word long so they fit within the rectangle. However, if a task is several words long and the rectangle width is short, the text does not wrap and overflows.
How could I modify the code so that the rectangle heights are drawn to fit the text within them, or alternatively have the text wrap and have the rectangle grow (in height) to accommodate the text?
Right now the rectangle heights are hard-coded in the CodePen example:
var barHeight = 20;
The example also adds the text in the following way to the rectangles (see below). I've experimented with trying to put html in the rectangle instead of text to no avail:
var rectText = rectangles.append("text")
.text(function(d){
return d.task;
})
You are really asking two questions. One, how do you wrap the text and then two, how do you scale the rect height to that wrapped text.
1.) The canonical example of wrapping text is presented by M. Bostock here.
2.) To scale the height of the rect to the text then, you can use .getBBox() as #BenLyall hints at. You first wrap the text, then call .getBBox() on the text node and apply the height to the rect.
Here's a complete example:
var someWidth = Math.random() * 250;
var longText = "Now is the time for all good men to come to the aid of their country";
var rect = g.append('rect')
.style('fill','steelblue')
.attr('width', someWidth) //<-- random width we don't know it
.attr('height', 1); // <-- start with arbitrary height
var txt = g.append('text')
.text(longText) //<-- our super long text
.attr('x', 4)
.attr('y', 10)
.attr('dy', '.71em')
.style('fill', 'white')
.call(wrap, someWidth); //<-- wrap it according to our width
var height = txt.node().getBBox().height + 15; //<-- get our height plus a margin
rect.attr('height', height); //<-- change our rect
Here's a working example.
Here is a modified version of the example that you linked to.
http://codepen.io/anon/pen/JdyGGp
The interesting bits:
var innerRects = rectangles.append("g")
.attr("class", "rectangle");
innerRects.append("rect")
.attr("rx", 3)
.attr("ry", 3)
.attr("x", function(d){
return timeScale(dateFormat.parse(d.startTime)) + theSidePad;
})
.attr("y", function(d, i){
return i*theGap + theTopPad;
})
.attr("width", function(d){
return (timeScale(dateFormat.parse(d.endTime))-timeScale(dateFormat.parse(d.startTime)));
})
//.attr("height", theBarHeight)
.attr("stroke", "none")
.attr("fill", function(d){
for (var i = 0; i < categories.length; i++){
if (d.type == categories[i]){
return d3.rgb(theColorScale(i));
}
}
})
var rectText = innerRects.append("text")
.text(function(d){
return d.task;
})
.attr("x", function(d){
return (timeScale(dateFormat.parse(d.endTime))-timeScale(dateFormat.parse(d.startTime)))/2 + timeScale(dateFormat.parse(d.startTime)) + theSidePad;
})
.attr("y", function(d, i){
return i*theGap + 14+ theTopPad;
})
.attr("font-size", 11)
.attr("text-anchor", "middle")
.attr("text-height", theBarHeight)
.attr("fill", "#fff");
innerRects.each(function(r) {
var bBox = d3.select(this).select("text").node().getBBox();
d3.select(this).select("rect").attr("height", function(d) {
return bBox.height;
}).attr("y", function(d) {
return bBox.y;
});
});
Instead of just appending the rect and text elements directly into a single group, I've created a new group for each one, so that they can be linked together, since for each rect we need to find out the height of it's corresponding text element.
After the rect and text elements have been created, I'm then looping over the g elements that I created and getting the bounding box (getBBox() function call) of the text and setting the height of the associated rect element to the height value returned from the bounding box. I'm also setting the y element of the rect to match.
Additionally, the new g elements to group the rect and text elements together break the tooltip positioning code, so that is updated accordingly with:
innerRects.on('mouseover', function(e) {
var tag = "";
if (d3.select(this).data()[0].details != undefined){
tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
"Type: " + d3.select(this).data()[0].type + "<br/>" +
"Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
"Ends: " + d3.select(this).data()[0].endTime + "<br/>" +
"Details: " + d3.select(this).data()[0].details;
} else {
tag = "Task: " + d3.select(this).data()[0].task + "<br/>" +
"Type: " + d3.select(this).data()[0].type + "<br/>" +
"Starts: " + d3.select(this).data()[0].startTime + "<br/>" +
"Ends: " + d3.select(this).data()[0].endTime;
}
var output = document.getElementById("tag");
var item = d3.select(this).select("rect").node();
var x = (item.x.animVal.value + item.width.animVal.value/2) + "px";
var y = item.y.animVal.value + 25 + "px";
output.innerHTML = tag;
output.style.top = y;
output.style.left = x;
output.style.display = "block";
}).on('mouseout', function() {
var output = document.getElementById("tag");
output.style.display = "none";
});
The changes here are :
var item = d3.select(this).select("rect").node();
var x = (item.x.animVal.value + item.width.animVal.value/2) + "px";
var y = item.y.animVal.value + 25 + "px";
Previously, this was using this to grab the x and y positions of the rect elements. Since these are now grouped with a text the code needs to be updated to reference the rect child element from the group. This code does just that.
There are a whole bunch more issues when you start wrapping the text as well. The background rectangles will also need to grow dynamically to contain them. I'd suggest refactoring the example to deal with this.
You'll need to do the following:
Add placeholder elements for all the rects. For the background and task rects, you should be able to set their width in advance (the background rect widths are just the width of the containing element. The width of the task rects are set by the start and end date of the tasks.
Using the width of the task rects you can add the text elements and wrap at the appropriate width. (This example may help in doing that http://bl.ocks.org/mbostock/7555321)
Go back and set the heights of all of the background and task rects based on the computed heights of all their children text elements.
Visualization Goal: Build a D3 Tree that has text at, both, nodes and links, and that transitions cleanly, when nodes are selected/deselected.
Problem: While I can get link text, called "predicates," to show up along the centroid of all link paths, I can't seem to get them to transition in and out "smoothly."
Question: Can someone please help me please help me clean up the code and better understand how tree "link" transitions are behaving so I understand the theory behind the code?
Visualization and Source Location: http://bl.ocks.org/Guerino1/raw/ed80661daf8e5fa89b85/
The existing code looks as follows...
var linkTextItems = vis.selectAll("g.linkText")
.data(tree.links(nodes), function(d) { return d.target.id; })
var linkTextEnter = linkTextItems.enter().append("svg:g")
.attr("class", "linkText")
.attr("transform", function(d) { return "translate(" + (d.target.y + 20) + "," + (getCenterX(d)) + ")"; });
// Add Predicate text to each link path
linkTextEnter.append("svg:foreignObject")
.attr("width", "120")
.attr("height", "40")
.append("xhtml:body")
.attr("xmlns", "http://www.w3.org/1999/xhtml")
.html(function(d){ return "<p>" + (linksByIdHash[d.source.id + ":" + d.target.id].predicate) + "</p>"; });
// Transition nodes to their new position.
//var linkTextUpdate = linkTextItems.transition()
//.duration(duration)
//.attr("transform", function(d) { return "translate(" + d.source.x + "," + d.source.y + ")"; })
//linkTextUpdate.select("linkText")
//.style("fill-opacity", 1);
// Transition exiting linkText to the new position of the parents.
var linkTextExit = linkTextItems.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.source.y + 20 + "," + (getCenterX(d)) + ")"; })
.remove();
linkTextExit.select("linkText")
.style("fill-opacity", 1e-6);
function getCenterX(d) {
var xS = d.source.x;
var xT = d.target.x;
if(xS == xT)
{ return (xS - (xS - xT)/2); }
else if(xS > xT)
{return (xS - (xS - xT)/2); }
else
{ return (xT - (xT - xS)/2); }
}
Some Symptoms...
When link text transitions in or out, it's choppy / not smooth
When a branch is collapse, link text doesn't transition to appropriate path centroids
My frustration is that I feel like I'm very close but that I'm missing something very simple/basic. Any help is greatly appreciate.
I'm drawing a pie chart with d3.js. I want to transition the pie slices when new data is added. (i'm using the reusable chart API). I'm first creating a chart group using enter and then appending the chart arc path to that:
http://jsfiddle.net/EuK6H/4/
var arcGroup = svg.select(".pie").selectAll(".arc " + selector)
.data(pie(data))
.enter().append("g")
.attr("class", "arc " + selector);
if (options.left) {
arcGroup.attr('transform', 'translate(' + options.left + ',' + options.top + ')');
} else {
arcGroup.attr('transform', 'translate(' + options.width / 2 + ',' + options.height / 2 + ')');
}
//append an arc path to each group
arcGroup.append("path")
.attr("d", arc)
//want to add the transition in here somewhere
.attr("class", function (d) { return 'slice-' + d.data.type; })
.style("fill", function (d, i) {
return color(d.data.amount)
});
//...
Problem is when new data comes in I need to be able to transition the path (and also the text nodes shown in the the fiddle) but the enter selection is made on the the parent group. How can I add a transition() so it applies to the path?
You can use .select(), which will propagate the data to the selected element. Then you can apply a transition based on the new data. The code would look something like this:
var sel = svg.selectAll(".pie").data(pie(newData));
// handle enter + exit selections
// update paths
sel.select("path")
.transition()
.attrTween("d", arcTween);
I have a function to draw circles (canvasCPI and canvasGDP are my svgs):
var CPIforecircles = canvasCPI.append("g");
var GDPforecircles = canvasGDP.append("g");
function drawGDPForecastCircles(theNum){
GDPforecircles.append("circle")
.attr("r", 3)
.attr("class", "circleGDPFore")
.style("display", null)
.attr("transform", "translate(" + xScaleQuarterly(dataForecast[theNum].date) + "," + yScaleGDP(dataForecast[theNum].GDPforecast) + ")");
}
function drawCPIForecastCircles(theNum){
CPIforecircles.append("circle")
.attr("r", 3)
.attr("class", "circleCPIFore")
.style("display", null)
.attr("transform", "translate(" + xScaleQuarterly(dataForecast[theNum].date) + "," + yScaleCPI(dataForecast[theNum].CPIforecast) + ")");
}
then through my script call this function to draw more and more circles:
function generateCirclesFore(indexNum){
for (var i=indexNum; i<counterFore+1; i++){
drawGDPForecastCircles(i);
drawCPIForecastCircles(i);
}
}
eventually i have two group elements (CPIforecircles and GDPforecircles) with lots of circles inside the tags but how to I select these circles as an array and then apply a style of display to none (.style("display", "none")) to only certain circles in that array?
I decided to put my comment as an answer so this is not left officially without an answer. Also, I believe the answer is accurate. So, here it is:
selectAll(".circleCPIFore")
.filter(function(d) { d.someProp == someCriteria;})
.style("display","none");