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.
Related
I am working with the Zoomable Sunburst on D3 Observable. I want to show the clicked label in the center circle.
So instead of this:
I need to see something like this:
But as you can see the label isn't remaining centered. I am currently using the following to add the label in the center circle on click, as you see in the previous GIF:
d3.select("#title").remove()
const main_title = g.append("text").attr("id", "title")
.attr("dx", function(d) {
return -20
})
.style("font-size", "18px").attr("text-anchor", "center")
.attr("transform", function(d, i) {
p.x = 9,
p.y = 10;
return "translate(" + p.x + "," + p.y + ")";
})
.text(p.data.name);
This is placed inside the clicked(p) function.
How can I ensure the label is centered regardless of text length. Also, ensure the label appears when the visual first loads, so you don't have to click on it to see the first one.
I'm setting up a page with bootstrap. I have the layout working perfectly but one of the elements is a zoomable map of the US (using d3). The zoom function I am using requires the width and height of the div in pixels in order to calculate how to translate and scale the map. I have tried using percentages but I can't get anything going that way. Is there any way to dynamically get the height and width of the div. I have searched all over but the search terms are too generic (or I'm not clever enough to phrase it correctly).
Alternatively, how else might I get the necessary values.
Here is my implementation using hard coded width and height (which won't work if the page resizes).
//make the map element
var width = 1000;
var height = 1000;
var svg = d3.select("#Map")
.append("svg")
.attr("id", "chosvg")
.attr("height", height)
//.attr("viewBox", "0 0 600 600")
.attr("width", width)
.style("preserveAspectRatio", "true");
cbsa = svg.append("g");
d3.json("data/us-cbsa.json", function(json) {
cbsa.selectAll("path")
.attr("id", "cbsa")
.data(json.features)
.enter()
.append("path")
.attr("class", data ? quantize : null) //data ? value_if_true : value_if_false -- ternary operator
.attr("d", path)
.on("click", clicked);
});
in the clicked() function, I have the zoom like this which works, but only
with a certain window width
cbsa.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
For clarity, I am ideally loooking for something like:
var width = column.width() //or something using percentages
I can include my html as well if it helps.
You can get the width of the column by calling:
var bb = document.querySelector ('#Map')
.getBoundingClientRect(),
width = bb.right - bb.left;
Depending on the browser, the bb might already have an width property. Keep in mind that the column might appear wider because the initial size of the svg is too big, so its parent column might bee, too.
https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
Can I set the text element of a D3 shape to display only if a variable has a value? In the code below, a rectangle renders but the width will change depending on the value of selections (an array of document ids). If selections is empty, I still want the shape to render, but don't want any text label. Right now, I am seeing $NaN. Not sure where to include the if statement here.
Template.Box.onRendered (function () {
const self = this;
//List of variables to calculate dimensions
var value = //Result of calculations
var selections = Template.parentData(0).selections;
var boxContainer = d3.select("#box" + boxId)
.append("svg")
.attr("id", "svg-box");
var box = boxContainer.append("rect")
.attr("x", start + "%")
.attr("y", 0)
.attr("height", 50)
.attr("width", range + "%")
.attr("id", "box" + boxId);
var boxValue = boxContainer.append("text")
.attr("x", boxSpace + "%")
.attr("y", "20px")
.text(value)
.attr("id", "low" + boxId);
self.autorun(function() {
//repeat code from above
});
});
You can add this to your text attribute.
node.append("text")
.style("display", function (d) { return (d.data.i) ? null : "none" })
If there is no d.data.i then it display is set to none. Null means it will be displayed.
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);
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);