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.
Related
I have a project where i am trying to use transitions to display data for years 1800-1805
I have created a bar chart and could get the transition, but the problem here is that i am trying to sort the bar chart so that the data will be displayed in descending order. how ever when i sort the data and do the transition instead of changing the "y" attribute values of rectangles my code is replacing the existing rectangles to the sorted ones for every single transition.
I want my code in such a way that the rectangles should move its position to its new position from current one .
How could i do that.
My data is as below
year 1800
China - 20000
USA - 80000
France - 15000
year 1801
China - 25000
USA -90000
France - 35000
now for this data my code is replacing France and china data it is not moving up and down. what should i add in my code to do that?
main.js
-------------
/*
* main.js
* Mastering Data Visualization with D3.js
* 2.5 - Activity: Adding SVGs to the screen
*/
var margin = { left:80, right:20, top:50, bottom:100 };
var width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var g = d3.select("#chart-area")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
var time = 0;
/*var xLabel = g.append("text")
.attr("y", height + 200)
.attr("x", width / 2)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("GDP Per Capita ($)");
var yLabel = g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("x", -170)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Life Expectancy (Years)")*/
var timeLabel = g.append("text")
.attr("y", height +100)
.attr("x", width + 100)
.attr("font-size", "40px")
.attr("opacity", "0.4")
.attr("text-anchor", "middle")
.text("1800");
var data = d3.json("buildings.json").then(function(data){
// console.log(data);
// Clean data
data.forEach(function(d) {
d.Year = +d.Year;
d.Budget=+d.Budget;
});
const formattedData = data.map(function(year){
return year["countries"].filter(function(country){
var dataExists = (country.budget);
return dataExists
})
});
// Run the code every 0.1 second
d3.interval(function(){
// At the end of our data, loop back
time = (time < 5) ? time+1 : 0
update(formattedData[time]);
}, 10000);
// First run of the visualization
update(formattedData[0]);
})
function update(data) {
// Standard transition time for the visualization
var t = d3.transition()
.duration(10000);
// JOIN new data with old elements.
var rects = g.selectAll("rect").data(data, function(d){
return d;
});
// EXIT old elements not present in new data.
//rects.exit()
//.attr("class", "exit")
//.remove();
// ENTER new elements present in new data.
rects.enter()
.append("rect")
.attr("class", "enter")
//.attr("fill", function(d) { return continentColor(d.continent); })
// .merge(rects)
.attr("x", 0)
.attr("y", function(d,i){
return i*20;
}).transition(t)
.attr("width", function(d,i){
return d.budget/100;
console.log(d.budget);
})
.attr("height", 18)
.attr("fill",function(d,i){
if(d.country=="Italy")
{
return "green";
}else if(d.country=="Australia"){
return "blue";
}
});
// Update the time label
timeLabel.text(+(time + 1800))
}
Your problem is just the key function, which is fundamental for achieving object constancy.
Right now you have this:
var rects = g.selectAll("rect").data(data, function(d){
return d;
});
Which returns d, that is, the whole datum. Given your data structure, the datum is an object.
However, the API is clear about the value returned by the key function:
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element. (emphasis mine)
Therefore, change the key function to:
var rects = g.selectAll("rect").data(data, function(d){
return d.country;
//string---^
});
I am trying some basic d3 and i have been trying to get the attributes of each of the rect using d3 but I am not able to get anything.
When i try d3.selectAll("rect"), I get
How do can i access attributes of rect by using something like d3.selectAll("rect").select("part1").attr(...) or something similar? I want to access different attributes of all rect.
You can get any attribute of an element using a getter:
d3.select(foo).attr("bar")
Which is basically the attr() function with just one argument.
Here is a demo. There are two classes of rectangles, part1 and part2. I'm selecting all part1 rectangles and getting their x positions:
var svg = d3.select("svg");
var rects = svg.selectAll(null)
.data(d3.range(14))
.enter()
.append("rect")
.attr("fill", "teal")
.attr("y", 20)
.attr("x", d => 10 + 12 * d)
.attr("height", 40)
.attr("width", 10)
.attr("class", d => d % 2 === 0 ? "part1" : "part2");
d3.selectAll(".part1").each(function(d,i) {
console.log("The x position of the rect #" + i + " is " + d3.select(this).attr("x"))
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I am working on one project in which I need to plot data on USA map.
Here is the link to the code.
I am getting logical error in the output. In the drop down menu of attributes, when you first select attribute as DAMAGE_PROPERTY then I get the legend which I want. But as soon as you select different attribute, previous legend gets appended to the new one. You can test it on the link. I have used .remove() property in the code to remove previously added elements.
Here is my Legend code-
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.ordinal()
.domain(["<10", "10-20", "20-30", "30-40", "40-50", "50-60", "60-70", "70-80", ">80"])
.range(["#1a9850", "#66bd63", "#a6d96a","#d9ef8b","#ffffbf","#fee08b","#fdae61","#f46d43","#d73027"]);
var colorforbig=d3.scale.ordinal()
.domain(["<100","100-1000","1000-5000","5000-50000","50000-100000","100000-500000","5000000-10000000","10000000-50000000",">50000000"])
.range(["#1a9850", "#66bd63", "#a6d96a","#d9ef8b","#ffffbf","#fee08b","#fdae61","#f46d43","#d73027"]);
initlegend();
function initlegend(){
Legend=d3.select("svg")
.append("g")
.attr("id","legend");
}
function loadlegend(){
alert("in loadlegend");
var remove=d3.select("#legend")
.selectAll("g")
.remove();
console.log(remove);
var legendBox=Legend.selectAll("g")
.data(function(){
if(attr=="DAMAGE_PROPERTY"){
return colorforbig.domain();
}
else{
return color.domain();
}
})
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
var height = legendRectSize;
var horz = 20;
var vert = i * height;
return "translate(" + horz + "," + vert + ")";
});
//Append a rectangle to each legend element to display the colors from the domain in the color variable
legendBox.append("rect")
.attr("width", legendRectSize)
.attr("height", legendRectSize)
.style("fill", color)
.style("stroke", color);
//Append a text element to each legend element based on the listed domains in the color variable
legendBox.append("text")
.attr("x", legendRectSize + legendSpacing)
.attr("y", legendRectSize - legendSpacing)
.text(function(d) { return d; });
}
What I want is when you select attribute DAMAGE_PROPERTY it should show different legend. For other four properties it should show different legend. So there are total two legends.
The code for my scaling function is:
var innerWidth = 117;
var innerHeight = 14;
// create a scaling function
var max = .8;
var min = -.7;
var wScale = d3.scale.linear()
.domain([0, max])
.range([0, innerWidth]);
The code for creating rectangle is
var sel = selection.selectAll("svg")
.selectAll("rect")
.transition().duration(500)
.attr("width", function(d) { return wScale(Math.abs(d))});
However this doesn't seem to work and for negative values of d no rectangle is formed when I want the width of the rectangle to be according to the absolute value of d.
EDIT
My question is with reference to this app generously provided here.and is related to the horizontal blue bars. Being naïve with JS, after a bit of exploring I have identified the chunk of code in the app which is creating the horizontal blue bars. However Instead of the same bars I want it slightly differently
This is the code creating the plot.
col_3 = JS('function makeGraph(selection){
// find out which table and column
var regex = /(col_\\d+)/;
var col = regex.exec(this[0][0].className)[0];
var regex = /tbl_(\\S+)/;
var tbl = regex.exec(this[0][0].className)[1];
var innerWidth = 117;
var innerHeight = 14;
// create a scaling function
var max = colMax(tbl, col);
var min = colMin(tbl, col);
var wScale = d3.scale.linear()
.domain([0, max])
.range([0, innerWidth]);
// text formatting function
var textformat = d3.format(".1f");
// column has been initialized before, update function
if(tbl + "_" + col + "_init" in window) {
var sel = selection.selectAll("svg")
.selectAll("rect")
.transition().duration(500)
.attr("width", function(d) { return wScale(d.value)});
var txt = selection
.selectAll("text")
.text(function(d) { return textformat(d.value); });
return(null);
}
// can remove padding here, but still can't position text and box independently
this.style("padding", "5px 5px 5px 5px");
// remove text. will be added back later
selection.text(null);
var svg = selection.append("svg")
.style("position", "absolute")
.attr("width", innerWidth)
.attr("height", innerHeight);
var box = svg.append("rect")
.style("fill", "lightblue")
.attr("stroke","none")
.attr("height", innerHeight)
.attr("width", min)
.transition().duration(500)
.attr("width", function(d) { return wScale(d.value); });
// format number and add text back
var textdiv = selection.append("div");
textdiv.style("position", "relative")
.attr("align", "right");
textdiv.append("text")
.text(function(d) { return textformat(d.value); });
window[tbl + "_" + col + "_init"] = true;
}')
There is a lot of missing element in your code. The selection is not defined. You have to call the data function with data. There must be an enter section with append.
I created a fiddle with some made up parameter from your code.
It works more or less.
var data =[1];
var sel = d3.select("svg")
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width",10)
.attr("height",40)
.attr('x', 1.5)
.attr('y', 1.5)
.transition().duration(500)
.attr("width", function(d) { return wScale(Math.abs(d))});
http://jsfiddle.net/acerola/symcuccm/
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.