Why a horizontal line is not working correctly in stack bar chart - d3.js

I have a stacked bar chart. You can see the fiddle here.
I have drawn a line that is actually a horizontal line leveling the current stack of a bar. Below is the code.
.on('mouseenter', function (actual, i) {
const y = yScale(actual.y + actual.y0);
debugger;
line = svg.append('line')
.attr('id', 'limit')
.attr('x1', 0)
.attr('y1', y)
.attr('x2', width)
.attr('y2', y);
And the output is,
Here, you can see that, for the monthly data, the line is correct. But for the quarterly data, the line is a bit above the actual position. And for the yearly data, the line is not showing.
What is the problem here?
And how can I show a tooltip along with the line?

Looking at the fiddle, it seems that the scale you are using to render the rectangles is not yScale, but actually just y
Changing the following fragment:
const y = yScale(actual.y + actual.y0)
line = svg.append('line')
.attr('id', 'limit')
.attr('x1', 0)
.attr('y1', y)
.attr('x2', width)
.attr('y2', y);
To:
const limitY = y(actual.y + actual.y0);
line = svg.append('line')
.attr('id', 'limit')
.attr('x1', 0)
.attr('y1', limitY)
.attr('x2', width)
.attr('y2', limitY);
Adjusts the position of the line to match the rectangles, because it is now using the same scale that the bars and the axis are using.
Regarding the tooltip, I see there is a rectangle you want to append:
line.append("rect")
.attr("width", "10px")
.attr("height", "10px")
.style("fill", "red");
However, a <line> can not have a <rect> element inside. What you actually want is to add the <rect> to the <svg>:
svg.append("rect")
.attr('id', 'myId') // Also give it an Id for clean up
.attr("width", "10px")
.attr("height", "10px")
.attr("y", limitY) // The limitY is available to position the tooltip under the line
.style("fill", "red");
Don't forget to remove it in the mouseout event, as you are doing with <line#limit>:
.on("mouseout", function() {
svg.selectAll('#limit').remove();
// clean the rectangle on mouseout:
svg.selectAll('#myId').remove();
})
You can use the same premise of the above <rect> in a <g> element to create a full tooltip with text and background, but coding it is outside of the scope of this answer. I hope the above explanations can give you a direction.
Here is a fiddle with the changes.

Related

Add text in rect svg and append it to arc in donut chart

I wanted to add labels to each arc in donut chart. I've added by taking the centroid of each arc and adding, but somehow it is not adding in correct position. I can't figure it out so I need some help regarding it. I've added my code in codepen. The link is here.
My donut should look like this.
Sample code is:
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
var centroid = arc.centroid(d);
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
.attr('x', (centroid[0]) - 28)
.attr('y', (centroid[1]) - 5)
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
.attr("dx", (centroid[0]) - 7)
.attr("dy", centroid[1] + 7)
.text(Math.round((d.data.value.value)) + "%");
});
Thanks in advance.
The difference between the "bad" state on codepen and the desired state is that in the one you don't like, you take the centroid and then you center your text on it. The centroid of a thick arc is the midpoint of the arc that runs from the midpoint of one line-segment cap to the other. This is roughly "center of mass" of the shape if it had some finite thickness and were a physical object. I don't think it's what you want. What you want is the midpoint of the outer arc. There's no function to generate it, but it's easy enough to calculate. Also, I think you want to justify your text differently for arcs whose text-anchor point is on the left hand of the chart from those on the right half. I'm going copy your code and modify it, with comments explaining.
// for some reason I couldn't get Math.Pi to work in d3.js, so
// I'm just going to calculate it once here in the one-shot setup
var piValue = Math.acos(-1);
// also, I'm noting the inner radius here and calculating the
// the outer radius (this is similar to what you do in codepen.)
var innerRadius = 40
var thickness = 30
var outerRadius = innerRadius + thickness
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
// I'm renaming "centroid" to "anchor - just a
// point that relates to where you want to put
// the label, regardless of what it means geometrically.
// no more call to arc.centroid
// var centroid = arc.centroid(d);
// calculate the angle halfway between startAngle and
// endAngle. We can just average them because the convention
// seems to be that angles always increase, even if you
// if you pass the 2*pi/0 angle, and that endAngle
// is always greater than startAngle. I subtract piValue
// before dividing by 2 because in "real" trigonometry, the
// convention is that a ray that points in the 0 valued
// angles are measured against the positive x-axis, which
// is angle 0. In D3.pie conventions, the 0-angle points upward
// along the y-axis. Subtracting pi/2 to all angles before
// doing any trigonometry fixes that, because x and y
// are handled normally.
var bisectAngle = (d.startAngle + d.endAngle - piValue) / 2.0
var anchor = [ outerRadius * Math.cos(bisectAngle), outerRadius * Math.sin(bisectAngle) ];
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
// now if you stopped and didn't change anything more, you'd
// have something kind of close to what you want, but to get
// it closer, you want the labels to "swing out" from the
// from the circle - to the left on the left half of the
// the chart and to the right on the right half. So, I'm
// replacing your code with fixed offsets to code that is
// sensitive to which side we're on. You probably also want
// to replace the constants with something related to the
// the dynamic size of the label background, but I leave
// that as an "exercise for the reader".
// .attr('x', anchor[0] - 28)
// .attr('y', anchor[1] - 5)
.attr('x', anchor[0] < 0 ? anchor[0] - 48 : anchor[0] - 2)
.attr('y', anchor[1] - 10
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
// changing the text centering code to match the box
// box-centering code above. Again, rather than constants,
// you're probably going to want something a that
// that adjusts to the size of the background box
// .attr("dx", anchor[0] - 7)
// .attr("dy", anchor[1] + 7)
.attr("dx", anchor[0] < 0 ? anchor[0] - 28 : anchor[0] + 14)
.attr("dy", anchor[1] + 4)
.text(Math.round((d.data.value.value)) + "%");
});
I tested. this code on your codepen example. I apologize if I affected your example for everyone - I'm not familiar with codepen and I don't know the collaboration rules. This is all just meant by way of suggestion, it can be made a lot more efficient with a few tweaks, but I wanted to keep it parallel to make it clear what I was changing and why. Hope this gives you some good ideas.

d3 version 4 zoom behaviour on g element

The attached fiddle shows that on zoom the blue rectangles resize with the scale as expected but the yellow rectangles don't! The main difference is that the yellow rectangles were added to a 'g' element with text included. Any ideas why?
https://jsfiddle.net/sjp700/u6rj20jc/1/
var group = svg.selectAll(".rectangle")
.data(data);
gEnter = group.enter()
.append("g")
.attr("class", "rectangle")
.attr("fill", "yellow")
.attr("transform", function (d) { return "translate(" + x(d.start) + "," + y(d.finish) + ")"; });
gEnter.append("rect")
.attr("class", "rectband")
.merge(gEnter)
.attr("width", 50)
.attr("height", 18)
//.attr("rx", 10)
//.attr("ry", 10)
.style("opacity", .5) // set the element opacity
.style("stroke", "black");
Your yellow rectangles and text is not contained in an element that the zoom is applied to. Simple fix is to append them to gMain (which is the element on which the zoom is applied):
var group = gMain
.selectAll(".rectangle")
.data(data);
Updated fiddle here.

Can I use images as the background rectangles for d3 treemaps?

Is it possible to make a treemap in d3 with the background of each rectangle be an image? I am looking for something similar to what was done in Silverlight here, but for d3. If it is possible, are there any recommended tutorials that walk through the process of connecting the background to an image?
Yes, there are several ways of using images in SVGs. You probably want to define the image as a pattern and then use it to fill the rectangle. For more information, see e.g. this question (the procedure is the same regardless of the element you want to fill).
In D3 code, it would look something like this (simplified).
svg.append("defs")
.append("pattern")
.attr("id", "bg")
.append("image")
.attr("xlink:href", "image.jpg");
svg.append("rect")
.attr("fill", "url(#bg)");
Its important to note, that the image needs to have width, height attributes
chart.append("defs")
.append('pattern')
.attr('id', 'locked2')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 4)
.attr('height', 4)
.append("image")
.attr("xlink:href", "locked.png")
.attr('width', 4)
.attr('height', 4);
Using patterns to add an image in a rectangle can make your visualisation quite slow.
You can do something like that instead, this is the code I used for my rectangular nodes into a force layout, I wanted to put rectangles filled by an image as nodes:
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node");
node.append("rect")
.attr("width", 80)
.attr("height", 120)
.attr("fill", 'none')
.attr("stroke", function (d) {
return colors(d.importance);
});
node.append("image")
.attr("xlink:href", function (d) { return d.cover;})
.attr("x", 2)
.attr("width", 76)
.attr("height", 120)
.on('dblclick', showInfo);

How to properly get text width to center labels above graph bars?

I currently have a graph that has associated bar values displaying above each bar, but I'm having difficulty centering the value labels, due to not being able to fetch each text element's width.
This is how my graph is drawing at the moment:
All I need to do is to subtract half of each text element's width, but I can't seem to do so with the following Coffeescript:
#Drawing value labels
svg.selectAll("rect")
.data(data)
.enter()
.append("text")
.text((d)-> d.Total)
.attr("width", x.rangeBand())
.attr("x", (d)->
textWidth = d3.selectAll("text").attr("width")
x(d.Year) + (x.rangeBand() / 2) - (textWidth / 2)
)
.attr("y", (d)-> y(d.Total) - 5)
.attr("font-size", "10px")
.attr("font-family", "sans-serif")
#Drawing bars
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", (d)-> x(d.Year))
.attr("width", x.rangeBand())
.attr("y", (d)-> y(d.Total))
.attr("height", (d)-> height - y(d.Total))
Is there a way that I can access each text element's width attribute to set a value to offset?
A perhaps simpler way is to use a text-anchor of middle with x set to the left side of the bar plus half the width of the bar:
svg.selectAll(".bar-label")
.data(data)
.enter().append("text")
.text((d)-> d.Total)
.attr("class", "bar-label")
.attr("text-anchor", "middle")
.attr("x", (d)-> x(d.Year) + x.rangeBand()/2)
.attr("y", (d)-> y(d.Total) - 5)
You can get the bounding box of the text and use those values to apply a transform to center it. An example for getting the bounding box is here. The code would look something like
.text(function(d) { return d.Total; })
.attr("x", function(d) {
return x(d.Year) + (x.rangeBand() / 2) - (this.getBBox().width / 2);
}
...

D3.js ignores duplicate values

I'm exploring D3.js. I primarily used their tutorial to get what I have. But I have made some adjustments.
What I'm doing is counting the number of active & inactive items in a specific table. It then displays a graph with those values. Most everything works fines. I have 2 issues with it though:
It doesn't update automatically with my AJAX call when items are deleted. Only updates when items are added. But I'm not concerned about this for this post.
My primary issue: duplicate values aren't being treated as individual numbers. Instead it sees [10,10] and outputs it as a single bar in the graph as 10 (instead of 2 bars).
Looking at the D3 docs, it would seem the issue lies with .data. Documentation mentions that it joins data.
$(document).on("DOMSubtreeModified DOMNodeRemoved", ".newsfeed", function() {
var columns = ['created','deleted'];
var data = [numN, numD];
var y = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
var x = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
//.enter().append("rect")
.attr("x", x)
.attr("height", y)
.attr("width", x.rangeBand())
.attr("rx", 10)
.attr("ry", 10);
chart.selectAll("text")
.data(data)
//.enter().append("text")
.attr("y", y)
.attr("x", function(d) {
return x(d) + x.rangeBand() / 2;
}) //offset
.attr("dy", -10) // padding-right
.attr("dx", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text(String);
});
How can I make each value to display? If I pass in two different values, the chart displays as it should.
Your problem is at .attr("x", x). So the way you're doing it assigns the same x coordinate for both rects.
So try offsetting x coordinate.
.attr("x", function(d, i) { x + i * width_of_your_rect); })

Resources