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

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);

Related

D3 Grouped Bar Chart - Selecting entire group?

I have a grouped bar chart similar to https://bl.ocks.org/mbostock/3887051
I used a mouseover function to fade the bars the mouse is currently not over
function mouseover(bar)
{
d3.selectAll(".bar")
.filter(function(d){ return (d != bar);})
.transition(t)
.style("opacity", 0.5);
}
While this works nicely to highlight a single bar, I now need to highlight the entire group / fade everything but this group.
So far I haven't been able to figure out though how to get from the datum element d passed via .on("mouseover", function(d) ... back to the entire group this element belongs to.
Is there a simple way to achieve this in D3v4?
In D3 4.0 the callback function for the .on() method is passed 3 arguments: the current datum (d), the current index (i), and the current group (nodes).
Within the mouseover callback, you can selectAll("rect"), and filter out items which are in the current group (node). With this selection, you then set opacity to 0.5. On mouseout, you just need to set all opacity back to 1.0. The pertinent code is:
...
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
with a small helper function to check if a value is present in an array (array of DOM elements in our case):
function isInArray(value, array) {
return array.indexOf(value) > -1;
}
The full code in context (given your linked example):
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", function(d) { return z(d.key); })
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
One solution could be:
Make a function which selects all group and gives it a transition of opacity 0.
The DOM on which mouse is over give opacity 1.
function hoverIn(){
d3.selectAll(".group-me").transition()
.style("opacity", 0.01);//all groups given opacity 0
d3.select(this).transition()
.style("opacity", 1);//give opacity 1 to group on which it hovers.
}
Make a function which selects all group and gives it a transition of opacity 1, when the mouse is out.
function hoverOut(){
d3.selectAll(".group-me").transition()
.style("opacity", 1);
}
On the group add a class and add the mouse out and in function like
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.classed("group-me", true)//add a class for selection.
.on("mouseover", hoverIn)
.on("mouseout", hoverOut)
working code here

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')

general update pattern III, update values not showing up

I am working on the modification of Mike Bostock's general update pattern III block and having a hard time understanding why, though the enter and exit values show up, the update values are not. I've read that assigning the specific value instead of using the data array value will help, as with a key, but this did not work. How do I modify this so entering values show up with their fill style, red color? I have read SO posts and re-read "How Selections Work" but still can't make it work.
Here is the code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: bold 28px monospace;
}
.enter {
fill: green;
}
.update {
fill: red;
}
.exit {
fill: blue;
}
</style>
<body>
<script src="../d3.v3.js"></script>
<script>
function randomData() {
return Math.floor(Math.random() * 200);
}
var the_values = [];
function randomEntry() {
var numlist = [];
var randomEntry;
var maximum,minimum;
maximum = 10; minimum = 1
var random_in_range = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
var length_of_array = random_in_range;
console.log("length_of_array", length_of_array);
for (i = 0; i < length_of_array; i++) {
numlist.push([randomData(), randomData()]);
}
return numlist;
}
the_values = randomEntry();
console.log("the_values", the_values);
var width = 360,
height = 400;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
var circles = svg.selectAll("circle")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
// UPDATE
// Update old elements as needed.
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d, i) {
return d[0];
})
.attr("y", function(d, i) {
return d[1];
})
// ENTER
// Create new elements as needed.
circles.enter().append("circle")
.attr("class", "enter")
.attr("opacity", 0.3)
.attr("r", 25)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.attr("r", 30)
.style("fill-opacity", 1);
text.enter().append("text")
.attr("class", "enter")
.attr("dy", ".25em")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.style("fill-opacity", 1e-6)
.text(function(d) {
return d[0];
})
.transition()
.duration(750)
.style("fill-opacity", 1);
// EXIT
// Remove old elements as needed.
text.exit()
.attr("class", "exit")
.transition()
.duration(750)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.remove();
circles.exit()
.attr("class", "exit")
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
}
// The initial display.
update(the_values);
// Grab a random sample of letters from the alphabet, in alphabetical order.
setInterval(function() {
update(randomEntry());
}, 1500);
</script>
From a quick glance at your code, it seems to be doing what you are looking for. Your enter circles are actually filled green, so you are actually seeing those. Updates are changed to red, but you don't see many of those because you are picking a few random numbers from 1-200. It's just unlikely that you will end up with any in the update selection, because that means that you selected the same number twice in a row.
To see some update circles, change:
return Math.floor(Math.random() * 200);
To:
return Math.floor(Math.random() * 10);
This throws the positions off, but you should soon see some red circles.
The reason is that in the update function you are always changing the whole array of input.
You are doing:
setInterval(function() {
update(randomEntry());//this will change the full array set
}, 1500);
This should have been:
setInterval(function() {
the_values.forEach(function(d){
//change the data set for update
})
update(the_values);
}, 1500);
Please note above i have not created a new array but I am passing the same array with changes to the update function.
Working fiddle here
Hope this helps!

d3.js Bar Chart facing issue with tooltip

I have the following d3.js script to create bar chart which works fine.
I added functionality to show tool tip (not sure whether i added it at right place or not) which works fine but it has created an issue with existing mouseout event.
Issue:
The issue is that the following code is not working anymore. When i mouse over it does not turn into grey.
.on('mouseout', function (d) {
d3.select(this)
.attr('fill', 'blue');
However, if i comment the following lines than above mouseout event works perfect.
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
Complete Script
var jsonData = #Html.Raw(Json.Encode(Model));
data = jsonData;
InitChart();
function InitChart() {
var barData = data;
var vis = d3.select('#SummaryChart'),
WIDTH = 500,
HEIGHT = 375,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 150
},
xRange = d3.scale.ordinal().rangeRoundBands([MARGINS.left, WIDTH - MARGINS.right], 0.1).domain(barData.map(function (d) {
return d.Date;
})),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([0,
d3.max(barData, function (d) {
return d.Duration;
})
]),
xAxis = d3.svg.axis()
.scale(xRange)
.tickSize(0)
.tickSubdivide(true),
yAxis = d3.svg.axis()
.scale(yRange)
.tickSize(0)
.orient("left")
.tickSubdivide(true);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Duration:</strong> <span style='color:red'>" + d.Duration + "</span>";
})
vis.call(tip);
vis.append('svg:g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (HEIGHT - MARGINS.bottom) + ')')
.call(xAxis);
vis.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + (MARGINS.left) + ',0)')
.call(yAxis);
vis.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("font-size", "20px")
.attr("x", WIDTH)
.attr("y", HEIGHT + 20)
.text("Time");
vis.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("font-size", "20px")
.attr("y", 100)
.attr("x",-100)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("Hours:");
vis.selectAll('rect')
.data(barData)
.enter()
.append('rect')
.attr('x', function (d) {
return xRange(d.Date);
})
.attr('y', function (d) {
return yRange(d.Duration);
})
.attr('width', xRange.rangeBand())
.attr('height', function (d) {
return ((HEIGHT - MARGINS.bottom) - yRange(d.Duration));
})
.attr('fill', 'blue')
.on('mouseover', function (d) {
d3.select(this)
.attr('fill', 'grey');
})
.on('mouseout', function (d) {
d3.select(this)
.attr('fill', 'blue');
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
}
</script>
Can someone point out what is wrong and how i cam make it working with both tooltip and on mouse turning into grey working both together?
Unlike jQuery, D3 allows only a single callback per action. Therefore if you attach two .on('mouseout') callbacks, only the last one will execute. See:
d3.select('div')
.on('mouseout', function() {console.log('A')})
.on('mouseout', function() {console.log('B')})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div style='width:250px;height:250px;background:red'></div>
You have two ways around this. One, as suggested in comments, would be to call both attr and tooltip in the callback, as this:
.on('mouseout', function (d) {
d3.select(this).attr('fill', 'blue');
tip.hide();
}
Second would be to use the dot . notation, as described in the API, second paragraph
If an event listener was already registered for the same type on the selected element, the existing listener is removed before the new listener is added. To register multiple listeners for the same event type, the type may be followed by an optional namespace, such as "click.foo" and "click.bar".
So in your case
.on('mouseout.attr', function (d) {
d3.select(this)
.attr('fill', 'blue');
})
.on('mouseout.tip', tip.hide)
Working example:
d3.select('div')
.on('mouseout.logA', function() {console.log('A')})
.on('mouseout.logB', function() {console.log('B')})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div style='width:250px;height:250px;background:red'></div>

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