D3 - Draw arrow on bar to reflect the data - d3.js

I have a plunker here - https://plnkr.co/edit/qDi8bm3xh3hdaV059AXX?p=preview
Its a bar chart where the bars are drawn using a start and finish position.
The start position could be higher than the finish position so I have logic to still draw the bars in the correct position.
I'd like to draw an arrow on the bars to indicate if the start position is higher or lower than the finish position.
I have added a data-arrow to the bars (I've already used a class) which indicates if the arrow should be up or down.
.attr('data-arrow', (d, i) => {
return d.start > d.finish ? 'down' : 'up'
})
Is it possible to draw some element on the bars to indicate the start position.
Is is possible to draw an arrow on each bar and then use the data attribute and css to rotate it the correct way

Just draw the line in the other direction
bar.enter()
.append("line")
.attr("x1", d => x(d.phase) + x.bandwidth()/2)
.attr("y1", d => y(d.start) + ((d.start < d.finish) ? -10 : 10) )
.attr("x2", d => x(d.phase) + x.bandwidth()/2)
.attr("y2", d => y(d.finish) + ((d.start < d.finish) ? 15 : -15) )
.attr('class', d => d.start > d.finish ? 'arrow-down' : 'arrow-up' )
.attr("stroke","red")
.attr("stroke-width",2)
.attr("marker-end","url(#arrow)");

Here's a quik solution: enter bars again and append arrows (or other shapes) to the start point of the bars:
bar.enter()
.append("circle")
.attr("cy", d => y(d.start))
.attr("cx", d => x(d.phase) + x.bandwidth()/2)
.attr("r", 5)
.attr("fill", "blue")
Updated plankr
In this solution, there is no need for additional data attributes although it doesn't harm either. If it is important to show direction, in addition to the start point, I would also consider gradients because they generally add less clutter to the graph.
Note that the direction is implicitly known because of the y-axis. Adding an arrow does not really give new information unless we have data regarding the rate of change from min to max.

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.

How to create proportionally spaced horizontal grid lines?

I have a demo here
It's a simple D3 chart in an Angular app.
I would like to have four horizontal grid lines across the chart and have them proportionally space so a line at 25%, 50%, 75% and 100% the height of the chart.
I'm not concerned about the scale on the y-axis I just ned them proportionally space on the height on the chart.
I sort of have it working here but using some dodge math
const lines = chart.append('g')
.classed('lines-group', true);
lines.selectAll('line')
.data([0,1.33,2,4])
.enter()
.append('line')
.classed('hor-line', true)
.attr("y1", (d) => {
return height/d
})
.attr("y2", (d) => {
return height/d
})
.attr("x1", 0)
.attr("x2", width)
Is there a better way to do this or a proper D3 way to space the lines
Use your y scale. If you want to keep the data as percentages, all you need is:
lines.selectAll('line')
.data([25, 50, 75, 100])
.enter()
.append('line')
.attr("y1", (d) => {
return y(y.domain()[1] * (d / 100))
})
.attr("y2", (d) => {
return y(y.domain()[1] * (d / 100))
})
//etc...
As you can see we're just multiplying the maximum value in the y scale domain, which is y.domain()[1], by any value you want (in this case the percentage, represented by d / 100).
Here is the forked StackBlitz: https://stackblitz.com/edit/d3-start-above-zero-9b389s
You can customize a d3.axisRight to get this to work. Instead of adding the custom lines, try adding something like this after you add your axisBottom:
const maxVal = d3.max( graphData, (d) => d.value )
yAxis.call(
d3.axisRight(y).tickSize(width).tickValues([0.25*maxVal, 0.5*maxVal, 0.75*maxVal, maxVal])
).call(
g => g.select(".domain").remove()
).call(
g => g.selectAll(".tick text").remove()
)
Note that I am passing in the exact tick values you want using .tickValues, which allows this customization. See this post for more details on customization.

D3.js shape gets translated in display after transition as if coordinate system had changed

This animation tries to illustrate balls following a curved line "falling" into a bucket:
(1) https://bl.ocks.org/max-l/ddfef6f8415675878baba32080d6a874/bae06bead60551cdae7488faccaa0d9c5624455c
For a reason that I can't understand, in (1), the balls get "teleported" outside the rectangle, it's as if the display suddenly had changed coordinate system.
The following code illustrates what should happen at the end of the transition: the balls should bounce in the rectangle that represents a bucket:
(2) https://bl.ocks.org/max-l/cda07bafcf7970e724b3aa00aefe9a02/8230c5db14e666efcb833c6c41c3c941f836729f
Why do the circles get "teleported" on the display, while the x,y coordinate shows no such "teleportation" ?
function redraw(data){
var circle = svg.selectAll("circle")
.data(data)
circle.enter().append("circle")
.attr("r", radius)
.transition()
.ease(d3.easeQuad)
.delay(rndDelay)
.duration(2000)
.attrTween("transform", translateAlong(path.node()))
.on("end", d => {
const lastP = faucet[2]
d.state = 1
d.x = lastP[0]
d.y = lastP[1]
console.log("a1",[d.x,d.y])
})
circle.filter(d => d.state == 1)
.attr("r", radius)
.attr("cx", d => d.x)
.attr("cy", d => {
console.log("a2",[d.x,d.y])
return d.y
})
}
After the transition is complete, you are both transforming with translate and positioning with cx/cy, which results in the position being off.
During the transition you set the transform for each circle:
.attrTween("transform", translateAlong(path.node()))
Afterwards you position by:
.attr("cx", d => d.x)
.attr("cy", d => d.y)
But this is added to the end transition point/translation (the end of the faucet). This is why everything appears normal except off by a fixed amount.
Just reset the transform after the transition.
Example
Or alternatively, update the translate with the new x/y values rather than using cx/cy.

d3 force directed graph nodes stay at fixed position after filter

In my d3 force directed scatter plot i try to make points disappear and re-appear by clicking on a legend key. After clicking the legend key, i would like the remaining points to regroup and not to stay fixed in the same position, leaving blank spaces (screenshots). When clicking again on the legend, they should fly in again.
I tried to remove the fill of the circles upon clicking on a legend key, which is working, but obviouly does not make the force do its work..
My code on blockbuilder.org: http://blockbuilder.org/dwoltjer/04a84646720e1f82c16536d5ef9848e8
You can treat the filtered data as new data and apply the update, enter and exit pattern:
var node = svg.selectAll(".dot")
.data(data);
node.exit().remove();
node.enter().append("circle")
.attr("class", "dot")
.attr("r", radius)
......
The click event for legend:
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.on("click", function (d) {
visible[d] = !visible[d];
var newdata = data.filter(function(e) { return visible[e.bank];});
DrawNode(newdata);
});
Here is the update blocks
Simply deleting the nodes should be enough to make the force rearrange itself and group the nodes again. But, you will want to save the nodes to bring them back (possibly using a temporary array).
However, if you want the nodes to fly off screen (and back on), then what I'd do (using V4) is move the nodes to a new forcePoint that's way off screen:
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.on("click", function (d) {
node.filter(function () {
return this.dataset.bank === d;
})
position
.force('x', d3.forceX(width/2).strength(20))
.force('y', d3.forceY(height*2).strength(20));//should be twice the height of the svg, so way off the y axis. Or whichever direction you choose.
});

Legend transition not working properly

I have a graph with a legend, and the legend is always the same. The only thing that changes with the transitions is the size of the legend tiles, that are the same as this example.
When I update my graph, its size changes, and so does the legend's. Here is what I have for the legend :
var couleurs = ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"];
var legende = ["0-15", "15-30", "30-45", "45-60", "60-75", "75-90", "90-100"];
canevas.append("g").selectAll(".legende").data(legende).enter()
.append("rect")
.attr("class", "legende")
.attr("width", cellPosX / 7)
.attr("height", largeurCellule / 2)
.attr("x", (d, i) => i * (cellPosX / 7))
.attr("y", cellPosY + 10)
.attr("fill", ((d, i) => couleurs[i]));
canevas.selectAll(".legende").data(legende).transition()
.duration(transitionTime)
.attr("width", cellPosX / 7)
.attr("x", (d, i) => i * (cellPosY / 7))
.attr("y", cellPosY + 10)
.attr("fill", ((d, i) => couleurs[i]));
canevas.selectAll(".legende").data(legende).exit()
.remove();
This works fine, except that when the graph is updated, for a while there are 2 legends at the same time. One that goes from the old position to the new one, which is expected, but there is also one that instantly appears to the new position. Here is a very low fps gif that I quickly made to show an example.
How would I go about having the legend going from its initial position to the other one, without it also appearing instantly at the new position?

Resources