How can i display tooltip at the top of each vertical bar - d3.js

I have tried writing couple of equations for the same but unabe to get it aligned well. I need to display tooltip at the top of each bar.
Here is fiddle of the same
I am using mouseover events to display tooltips
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);
})
.on('mouseover', function(d, i) {
var xPos = xScale.rangeBand()*i;
//console.log(xScale(i)); 6 190 282
console.log(xScale.rangeBand()*i);
var yPos = yScale((d.global / total) * 100);
d3.select('#hor_tooltip')
.style('left', xPos + 'px')
.style('top', yPos + 'px')
.style('display', 'block')
.html(d.global);
})
.on('mouseout', function() {
d3.select('#hor_tooltip').style('display', 'none');
})
.append("text")
.text(function(d) {
return commaFormat((d.global/total)*100);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")

You should use d3.event inside the mouseover event handler. You can use the x,y coordinates of the event or request the bounding box of the evnet target element.
The following code should help but you still need to adjust for the height of the tootip itself:
var px = d3.event.pageX;
var py = d3.event.target.getBoundingClientRect().top;

You could also try foxTooltip.js. It has an option to always position on a specific side of an element.
https://github.com/MichaelRFox/foxToolTip.js

Related

D3.js .each() with function that passes data

I would like to put my tooltip into a function such that I can re-use it for multiple elements. When I call the tooltip function for the label element the tooltip displays only the first x value to all labels instead of looping over the X value array. How do I properly access the data in the function?
const tooltip = d3.select('body').append('div')
.attr('id', 'rect-tooltip');
function mouseover(data-x){
d3.select('g')
area.selectAll("text")
.on('mouseover', (d) => {
rect-tooltip.transition()
.duration(100)
.style('opacity', .9)
rect-tooltip.html(`${data-x}`) //Pass in X-values
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
})
.on('mouseout', (d) => {
rect-tooltip.transition()
.duration(400)
.style('opacity', 0);
})
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.each(function(d) {
mouseover(d.x);}); // Only first data point is added to each label?
Without an example of the this, I may be mis-reading your issue.
Problem
The key problem stems from iterating through the text elements twice:
area.each(function (d) {
// do something with each element/datum in the selection
})
d3.selectAll("text")
.on("mouseover", function(d) {
// apply an event listner and corrsesponding function to each text element.
})
The problem is you nest the second in the first. For every element in area you select all the text elements: if you have 2 elements you're selecting all the text twice. You only need to select each text element once.
In the pattern you have, for each element in area we pass that element's datum to the nested function which takes a property of that datum and with d3.selectAll("text").on("mouseover" ... applies that single datum to all text mouseover events. Since you do this for every element in area, we end up overwriting the event listeners multiple times.
No where do you reference the current datum in the chain following d3.selectAll("text"), so we only have a value from the current datum in the current iteration of .each().
Solution
You shouldn't need to use .each() here to apply an event listener, .on() should be sufficient.
We have our mouseover and mouseout functions:
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
Then we can call it with:
selection.on("mouseover",mouseover)
.on("mouseout",mouseout);
And we can resuse this on multiple selections or elements. The datum specific to each element will be used to define the tooltip text.
var tooltip = d3.select(".tooltip");
var svg = d3.select("svg");
var data = [{x: 10},{x:50},{x:90},{x:130},{x:170},{x:210},{x:250},{x:290},{x:330}]
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+[d.x,0]+")"; })
var rect = g.append("rect")
.attr("width", 35)
.attr("height", 100)
.attr("fill","steelblue")
.on("mousemove",mouseover)
.on("mouseout",mouseout)
var text = g.append("text")
.attr("y", 120)
.attr("x", 18)
.style("text-anchor","middle")
.text(function(d) { return d.x; })
.on("mouseover",mouseover)
.on("mouseout",mouseout)
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
.tooltip {
position: absolute;
padding: 5px;
background: yellow;
}
rect, text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="tooltip"></div>
<svg width="400" height="300"></svg>
Based on Andrew his comment I changed my code to:
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip');
function mouseover(d){
tooltip.transition()
.duration(100)
.style('opacity', .9)
tooltip.html(d)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout(){
tooltip.transition()
.duration(400)
.style('opacity', 0);
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.on("mouseover", function(d) { mouseover(d.x); })
.on("mouseout", mouseout);

Can't add title to stacked bar graph on D3

I creating a stacked bar chart using this example. The chart works and renders but I can't add a mouseover label.
I tried this...
DATE.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
.append("svg:title")
.text(functino(d){return "foo"});
But this after adding the .append("svg:title... the graph stops rendering. If I remove the .style("fill... line, the graph renders, however it's not stacked and there's no mouseover feature.
I have also tried using the tooltip route. (Source)
.on("mouseover", function() { tooltip.style("display", null); })
.on("mouseout", function() { tooltip.style("display", "none"); })
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
});
// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);
tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold");
But still not luck. Is there a library I need to load? Not sure what's going on.
The graph stop rendering when you try to append the title because you have a typo: it's function, not functino.
Besides that, this is what you need to get the value of each stacked bar:
.append("title")
.text(function(d){
return d[1]-d[0]
});
Here is the demo: https://bl.ocks.org/anonymous/raw/886d1749c4e01e191b94df23d97dcaf7/
But I don't like <title>s. They are not very versatile. Thus, instead of creating another <text>, as the second code you linked does, I prefer creating a div:
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Which we position and set the HTML text this way:
.on("mousemove", function(d) {
tooltip.html("Value: " + (d[1] - d[0]))
.style('top', d3.event.pageY - 10 + 'px')
.style('left', d3.event.pageX + 10 + 'px')
.style("opacity", 0.9);
}).on("mouseout", function() {
tooltip.style("opacity", 0)
});
And here is the demo: https://bl.ocks.org/anonymous/raw/f6294c4d8513dbbd8152770e0750efd9/

Appending another element when a sibling element's transition ends

I have a bar chart, which I am using transitions to animate the heights of rect elements like so:
//Create a layer for each category of data that exists, as per dataPointLegend values
//e.g. DOM will render <g class="successful"><g>
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
})
//transform below is used to shift the entire layer up by one pixel to allow
//x-axis to appear clearly, otherwise bars inside layer appear over the top.
.attr('transform', 'translate(0,-1)');
//Create a layer for each datapoint object
//DOM will render <g class="successful"><g></g><g>
barLayers = layers.selectAll('g.layer')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('g');
//Create rect elements inside each of our data point layers
//DOM will render <g class="successful"><g><rect></rect></g></g>
barLayers
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', height - margin.bottom - margin.top)
.attr('height', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs;
})
.duration(transitionDurationMs)
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
I then have a further selection used for appending text elements
//Render any point labels if present
//DOM will render <g><g><rect></rect><text></text></g></g>
if (width > miniChartWidth) {
barLayers
.append('text')
.text(function(d) {
return d.pointLabel
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
}
This fades in the text elements nicely after all the rects have finished growing in height. What I wondered, was whether its possible to append a text element to the corresponding layer as each bar finishes its transition?
I have seen the answer on this SO - Show text only after transition is complete d3.js
Which looks to be along the lines of what I am after, I tried adding an .each('end',...) in my rect rendering cycle like so
.each('end', function(d){
barLayers
.append('text')
.text(function() {
return d.pointLabel
})
.attr('x', function() {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function() {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
});
But I end up with lots of text elements for each of my g that holds a single rect for each of my datapoints.
I feel like I'm close, but need some assistance from you wise people :)
Thanks
whateverTheSelectionIs
.each('end', function(d){
barLayers
.append('text')
.each runs separately for every element in your selection, and inside the each you're adding text elements to every barLayer (barLayers). So you're going to get a (barLayers.size() * selection.size()) number of text elements added overall. You need to add only one text element in the each to the right bar / g.
The below is a fudge that might work. It's tricky because the text you want to add is a sibling of the rects in the selection that calls the .each function..., d3.select(this.parentNode) should move you up to the parent of the rect, which would be the right barLayer.
whateverTheSelectionIs
.each('end', function(d,i){
d3.select(this.parentNode)
.append('text')

How to add space between bars in a grouped bar chart in a nvd3 grouped multibar chart?

I'm trying to add some space/padding for a nvd3 multi bar chart. "groupSpacing" is not what I need, since it only adds space between groups. I'll need space between each bar inside group. I found one link in github support. Can you post any solution or tweak?
I also found a d3 example of grouped bar chart. Any help in this example also very helpful to me.
Thanks.
I have draw a d3 group barchart:
fiddle
You can adjust the groupSpacing by change the code on line 56:
var groupSpacing = 6;
Technically i just achieve it by change the width of each rects' width:
var barsEnter = bars.enter().append('rect')
.attr('class', 'stm-d3-bar')
.attr('x', function(d,i,j) {
return (j * x1.rangeBand() );
})
.attr('y', function(d) { return y(d.y); })
.attr('height', function(d) { return height - y(d.y); })
.attr('width', x0.rangeBand() / barData.length - groupSpacing )
.attr('transform', function(d,i) {
return 'translate(' + x0(d.x) + ',0)';
})
.style("fill", function(d, i, j) {
return color(data[j].key);
});
Hope it helps you understand how you can achieve it in d3.
I minus the number of group spacing from the "width" attribute also. I found that the x-axis label looks a little off after I did that so I add the (group spacing / 2) to the "x" attribute. Here is the example of my code.
var groupSpacing = 15;
var rect = groups.selectAll("rect")
.data(function (d) { return d; })
.enter()
.append("rect")
.attr("x", function (d) { return x(d.x) + (groupSpacing / 2) ; })
.attr("y", function (d) { return y(d.y0 + d.y); })
.attr("height", function (d) { return y(d.y0) - y(d.y0 + d.y); })
.attr("width", x.rangeBand() - groupSpacing)

D3 drag error 'Cannot read property x of undefined'

I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);

Resources