How to apply text transition to number in d3 - d3.js

I am new to d3. I created a bar chart. Append text and percentage in the bar chart with animation. When bar chart draw then the number and percentage go from bottom to top to the desired location. Here is the code
svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr("class", "g rect")
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.label); })
.attr("y", h)
.on("mouseover", onMouseOver) //Add listener for the mouseover event
... // attach other events
.transition()
.ease(d3.easeLinear)
.duration(2000)
.delay(function (d, i) {
return i * 50;
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)); })
.attr("width", x.bandwidth() - 15) // v4’s console.log(bands.bandwidth()) replaced v3’s console.log(bands.rangeband()).
.attr("height", function(d) { return h - y(d.percentage.slice(0, -1)); }) // use .slice to remove percentage sign at the end of number
.attr("fill", function(d) { return d.color; });
var legend = svg.append("g");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.value })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 15; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1) / 2);}) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.style("stroke", "papayawhip")
.style("fill", "papayawhip");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Now I want to apply text transition. Like instead of just printing say 90%(d.percentage). I want that it starts from 0 and goes to d.percentage gradually. How can I apply text transition in this case. I tried the following but it didn't work
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
d3.select(this).text(i(t));
};
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });

The problem is the this value.
Save it in the closure (that).
Use a number interpolator so you can round the result to a number of decimals.
var ptag = d3.select('body').append('p');
ptag.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, 90); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(2));
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>

Your problem is that you return function(t) { ... } and try to access this of parent function inside. The solution is to return arrow function, which does not shadow this value:
return t => d3.select(this).text(i(t));
(by the way, you may also want to round percentage before printing)
-------------Edit --------------
Here is the working code
var numberFormat = d3.format("d");
svg.selectAll(".g.rect").append("text")
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var element = d3.select(this);
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
var percent = numberFormat(i(t));
element.text(percent + "%");
};
//return t => element.text(format(i(t)));
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Thanks :)

Related

How Can I Use Symbols With a Legend

I've got a legend, with colored rectangles...
I'd like to replace the rectangles with symbols (i.e., circle, cross, diamond, square). I can't figure out how to do that.
I've been using variations of .attr("d", d3.svg.symbol().type('circle'). For instance, I tried:
legendRect
.attr("d", d3.svg.symbol().type(function (d) { return d[2] })
and I tried:
legendRect.append("svg:path")
.attr("d", d3.svg.symbol().type((d: any) => { return d[2] }))
d[2] is "supposed to be" pulling from legendData, as shown in the below code example...like it does with d[1] for the fill.
But I don't ever see anything change.
Here's the code I'm using for the legend, without the symbol stuff, below. What am I doing wrong and how can I change the rectangles to symbols? Where do I need to add what?
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var legend = this.svg.append("g")
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(-20,250)');
var legendRect = legend.selectAll('rect').data(legendData);
legendRect.enter()
.append("rect")
.attr("x", width - 65)
.attr("width", 10)
.attr("height", 10)
;
legendRect
.attr("y", function (d, i) {
return i * 20;
})
.style("fill", function (d) {
return d[1];
})
var legendText = legend.selectAll('text').data(legendData);
legendText.enter()
.append("text")
.attr("x", width - 52);
legendText
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d[0];
});
Here's how I would code it. Notice, that I data-bind to a wrapper g element and then place the symbol and text into it for each legend item. You can then position the g instead of positioning the text and "symbol" separately. This also removes the need for double-binding the data.
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var svg = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var legend = svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(20,20)');
var legendRect = legend
.selectAll('g')
.data(legendData);
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function(d,i){
return 'translate(0, ' + (i * 20) + ')';
});
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => { return d[2] }))
.style("fill", function (d) {
return d[1];
});
legendRectE
.append("text")
.attr("x", 10)
.attr("y", 5)
.text(function (d) {
return d[0];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
This is a implementation which uses symbols for your legend. You can use the symbols like the following:
svg.selectAll('.symbol')
.data(legendData)
.enter()
.append('path')
.attr('transform', function(d, i) {
return 'translate(' + (20) + ',' + ((i * 20) + 10) + ')';
})
.attr('d', d3.symbol().type(function(d, i) {
if (d[2] === "circle") {
return d3.symbolCircle;
} else if (d[2] === "cross") {
return d3.symbolCross;
} else if (d[2] === "diamond") {
return d3.symbolDiamond;
} else if (d[2] === "square") {
return d3.symbolSquare;
} else {
return d3.symbolTriangle;
}
})
.size(100))
.style("fill", function(d) {
return d[1];
});
Then you can set your legend labels like the following:
svg.selectAll('.label')
.data(legendData)
.enter()
.append('text')
.attr("x", "40")
.attr("y", function(d, i){ return ((i * 20)+15);})
.text(function(d) {
return d[0];
});
Check fiddle here - https://jsfiddle.net/zoxckLe3/
P.S. - Above solution uses d3 v4. To achieve the same in v3, use the following line .attr('d', d3.svg.symbol().type(function(d){return d[2];})) instead of the part where I match d[2] to the symbol name.
For adding image icons, you can use below code.
legend.append("**image**")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
**return "../assets/images/dev/"+d+".png";**
})
This works for me..

How to append value at the top of rect

I have made a vertical bar chart and have the following to append the text onto the bars.
If i set y attribute as 0,the text is shown at the top, irrespective of the length of bars i.e all values stick together on top even though bar length is 50,1,1,3.
What i need is that the value should display at the immediate top of the bar.
I tried text anchor end also but it didnt work.
sets.append("text")
.attr("class", "global")
//.attr("y", function(d) {
// return yScale(d.global) ;
// })
.attr("dy", 5)
.attr("dx",(xScale.rangeBand() / 4))
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("fill", "black")
.text(function(d) {
return ( commaFormat(d.global) > 0) ? commaFormat(d.global) : "";
});
here is the rect code
sets.append("rect")
.attr("class","global")
.attr("width", xScale.rangeBand()/2)
.attr("y", function(d) {
return yScale((d.global/total)*100);
})
.attr("height", function(d){
return h - yScale((d.global/total)*100);
})
.attr('fill', function (d, i) {
return color(d.global);
})
.append("text")
.text(function(d) {
return commaFormat((d.global/total)*100);
})
//.attr('x', function(d, i) {
// return xScale(i) + xScale.rangeBand() / 2;
//})
//.attr('y', function(d) {
// return h - yScale((d.global/total)*100) + 14;
//})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")

How to add text to Cluster Force Layout bubbles

I am trying to add some text from the name field in my JSON file to each bubble in a cluster.
https://plnkr.co/edit/hwxxG34Z2wYZ0bc51Hgu?p=preview
I have added what I thought was correct attributes to the nodes with
node.append("text")
.text(function (d) {
return d.name;
})
.attr("dx", -10)
.attr("dy", "5em")
.text(function (d) {
return d.name
})
.style("stroke", "white");
function tick(e) {
node.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("transform", function (d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
Everything works fine except the labels.
What am I missing here?
Thanks
Kev
For that you will need to make a group like this:
var node = svg.selectAll("g")
.data(nodes)
.enter().append("g").call(force.drag);//add drag to the group
To the group add circle.
var circles = node.append("circle")
.style("fill", function(d) {
return color(d.cluster);
})
To the group add text:
node.append("text")
.text(function(d) {
return d.name;
})
.attr("dx", -10)
.text(function(d) {
return d.name
})
.style("stroke", "white");
Add tween to the circle in group like this:
node.selectAll("circle").transition()
.duration(750)
.delay(function(d, i) {
return i * 5;
})
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) {
return d.radius = i(t);
};
});
Now the tick method will translate the group and with the group the circle and text will take its position.
working code here
The problem: a text SVG element cannot be child of a circle SVG element.
The solution is creating another selection for the texts:
var nodeText = svg.selectAll(".nodeText")
.data(nodes)
.enter().append("text")
.text(function (d) {
return d.name;
})
.attr("text-anchor", "middle")
.style("stroke", "white")
.call(force.drag);
Here is the plunker: https://plnkr.co/edit/qnx7CQox0ge89zBL9jxc?p=preview

D3.js nodes not appearing on page load

Here is a JSFiddle of what I've done so far
The graph is not showing he nodes on load...I am not able to figure out what has gone wrong with the code...
var zoom = null; //the d3.js zoom object
var zoomWidgetObj = null; //the zoom widget draghandeler object
var zoomWidgetObjDoZoom = true;
var oldzoom = 0;
var w = 1060,
h = 800,
radius = d3.scale.log().domain([0, 312000]).range(["10", "50"]);
var color = d3.scale.category20();
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("idx", -1)
.attr("idsel", -1);
//d3.json(data, function(json) {
var force = self.force = d3.layout.force()
.nodes(data.nodes)
.links(data.links)
.distance(100)
.linkDistance(1)
.linkStrength(0.1)
.charge(-1000)
.size([w,h])
.start();
var link = vis.selectAll("line.link")
.data(data.links)
.enter().append("line")
.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; })
.style("stroke-width", function(d) { return Math.sqrt(d.value); })
.on("mouseover", function(d) {
var selection = d3.select(this);
var initialWidth = Number( selection.style("stroke-width") );
selection.transition().style("stroke-width", initialWidth + Number(1) )
.style("stroke-opacity", 1.0).duration(5)
.style("stroke","green")
})
.on("mouseout", function(d) {
var selection = d3.select(this);
selection.transition().style("stroke-width", getLinkStroke( selection.data()[0]))
.style("stroke-opacity", conf.link_def_opacity)
.style("stroke", "black")});
var node = vis.selectAll("g.node")
.data(data.nodes)
.enter().append("svg:g")
.attr("class", "node")
.attr("r", 4.5)
.call(force.drag)
.on("mousedown", function(d) {
d.fixed = true;
d3.select(this).classed("sticky", true)})
.on("mouseover", mouseover)
.on("mouseout", mouseout);
node.append("circle")
.attr("class", function(d){ return "node type"+d.type})
.attr("r", function(d) { return radius(d.value) || 10 })
.call(force.drag)
.style("stroke", "gray")
.attr('stroke', '#fff')
.attr('stroke-width', '2.5px');
node.append("svg:image")
.attr("class", "circle")
.attr("xlink:href", function(d){ return d.img_href})
.attr("x", "-16px")
.attr("y", "-16px")
.attr("width", "32px")
.attr("height", "32px");
node.append("svg:title")
.text(function(d) { return d.name; });
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 16)
.attr("dy", ".35em")
.text(function(d) { return d.name });
node.select("circle").style("fill", function(d) { return d.name=="Salvation Army"?"white":"blue"; });
force.on("tick", function() {
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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
function mouseover() {
d3.select(this).select("circle").transition()
.duration(75)
.attr("r", 16)
.style("fill", "red");
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(75)
.attr("r", 8);
}
One of your circles has a radius of NaN - this is most likely why they aren't rendering. Put a breakpoint where you're setting the radius and isolate the node / the cause of the NaN.

Why is my update function creating an entirely new force-directed graph?

I'm looking at this other question and this example provided in that question, and I cannot quite get my own update function to work:
update = function() {
path = svg.append("svg:g").selectAll("path")
.data(force.links());
path.enter().append("svg:path")
.attr("class", "link")
.attr("fill", "none")
.attr("stroke", "#666")
.attr("stroke-width",
function(d) {
return (Math.floor(Math.log(d.value - min_val) / Math.log(2)) + 1) + "px"
})
.attr("marker-end", "url(#generic_arrow_marker)")
.on("mouseover",
function(d, i) {
if(d3.select(this).attr("stroke") != "#666") {
mousedOut = false;
}
})
.on("mouseout",
function(d, i) {
if(d3.select(this).attr("stroke") != "#666") {
mousedOut = true;
restoreGraph();
}
});
path.exit().remove();
circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes());
circle.enter().append("svg:circle")
.attr("r",
function(d) {
return (20 + Math.floor(Math.log(d.pagerank) / Math.log(2)) * 2) + "px"
})
.attr("fill", "#ccc")
.attr("stroke", "#333")
.attr("stroke-width", "1.5px")
.on("mouseover", // select relevant data nodes on click
function(d, i) {
mousedOut = false;
d3.select(this).attr("class", "selected");
transitions(d);
$("span#user").text(d.name)
$("span#pagerank").text(d.pagerank)
})
.on("click",
function(d, i) {
incoming = !incoming;
transitions(d);
})
.on("mouseout",
function(d, i) {
mousedOut = true;
d3.select(this).attr("class", "");
restoreGraph();
})
.call(force.drag);
circle.exit().remove();
text = svg.append("svg:g").selectAll("g")
.data(force.nodes());
textEnter = text.enter().append("svg:g");
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function(d) { return d.name; })
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.name; })
text.exit().remove();
force.start();
}
Whenever I call update(), it creates an entirely new copy of the existing D3 graph, even if I didn't change anything.
Ideas on what I might be doing wrong?
I figured it out as soon as I posted the question...
It was because I had svg.append("svg:g") for path, circle, and text.
I think I'll leave this question up in case it helps anyone else...

Resources