I'm having trouble understanding using D3 events and dispatch functions. I have a chart example that I've been working on called: "Vertical Bar Charts With Legends."
Drawing the charts and the legends was easy enough but I'd like to add the ability to highlight each bar as I mouseover its correlating text legend, located to the right of the chart.
I've read through all of the event documentation and even looked at a number of examples, most of which are pretty complicated, but I seem to be missing something. Would anyone know how to best accomplish the text legend mouseover functionality that dispatches events to automatically change colors of the corresponding vertical bars?
This question is similar to the one you posted in the d3-js Google Group. Without duplicating what I wrote there, I would reiterate that you probably don't want d3.dispatch; that is intended for custom event abstractions (such as brushes and behaviors). It'll be simpler to use native events.
If you want your legend to change the color of the corresponding bar on mouseover, then breakdown the problem into steps:
Detect mouseover on the legend.
Select the corresponding bar.
Change the bar's fill color.
First, use selection.on to listen for "mouseover" events on the legend elements. Your listener function will be called when the mouse goes over a legend element, and will be called with two arguments: the data (d) and the index (i). You can use this information to select the corresponding bar via d3.select. Lastly, use selection.style to change the "fill" style with the new color.
If you're not sure how to select the corresponding bar on legend mouseover, there are typically several options. The most straightforward is to select by index, assuming that the number of legend elements and number of rect elements are the same, and they are in the same order. In that case, if a local variable rect contains the rect elements, you could say:
function mouseover(d, i) {
d3.select(rect[0][i]).style("fill", "red");
}
If you don't want to rely on index, another option is to scan for the matching bar based on identical data. This uses selection.filter:
function mouseover(d, i) {
rect.filter(function(p) { return d === p; }).style("fill", "red");
}
Yet another option is to give each rect a unique ID, and then select by id. For example, on initialization, you could say:
rect.attr("id", function(d, i) { return "rect-" + i; });
Then, you could select the rect by id on mouseover:
function mouseover(d, i) {
d3.select("#rect-" + i).style("fill", "red");
}
The above example is contrived since I used the index to generate the id attribute (in which case, it's simpler and faster to use the first technique of selecting by index). A more realistic example would be if your data had a name property; you could then use d.name to generate the id attribute, and likewise select by id. You could also select by other attributes or class, if you don't want to generate a unique id.
Mike's answer is great.
I used it come up with this for selecting a cell in a grid I was drawing:
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y) #displays the cell x y location
d3.select(this).style("fill", "red");
So when I am entering the data in I added the event listener and using d3.select(this).
See the code in context below:
vis.selectAll("rect")
.data(singleArray)
.enter().append("svg:rect")
.attr("stroke", "none")
.attr("fill", (d) ->
if d.lifeForm
return "green"
else
return "white")
.attr("x", (d) -> xs(d.x))
.attr("y", (d) -> ys(d.y))
.attr("width", cellWidth)
.attr("height", cellHeight)
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y)
d3.select(this).style("fill", "red");
return
)
Related
I'm creating a 'hand-drawn' style chart in d3:
Each bar below is a path inside of a g of class bar. When I hover over each g.bar, I highlight all paths inside with a basic mouseover function:
d3.selectAll('g.bar')
.on('mouseover', function() {
d3.select(this).selectAll('path').style('stroke', 'red')
})
The problem is, the highlighting only occurs when I hover over the paths, not the entire g.bar.
This makes the highlighting look super glitchy (running a mouse across it repeatedly highlights/unhighlights the path).
My question is: Is there a way to have all associated paths highlight whenever I'm hovering over the entire g.bar outlining the bar itself, and not just when I highlight over the path elements themselves?
A live-demo of my code is here: https://blockbuilder.org/jwilber/4bd8f5dd73666cdc5a30d7d6481e231a
Thanks for any help!
You could just add the following css:
g.bar {
pointer-events: bounding-box;
}
or directly set the g.bar elements' pointer-events attribute, which in your code would look like:
bar.setAttribute('pointer-events', 'bounding-box');
this sets up the g.bar elements to listen to events anywhere within the actual space that they take up (the bounding box).
However, the above only works in Chrome.
Another alternative that seems to work in all the browsers I've tried is to add a transparent rect element to each g.bar element (just as a sibling of the path).
data.forEach(function (d) {
let node = rc.rectangle(0, y(d.trick), x(d.count), y.bandwidth());
bar = roughSvg.appendChild(node);
bar.setAttribute('class', 'bar');
});
d3.selectAll('g.bar')
.data(data)
.append('rect')
.attr('x', 0)
.attr('y', d => y(d.trick))
.attr('width', d => x(d.count))
.attr('height', y.bandwidth())
.attr('fill', 'transparent');
I'm guessing that this works because rather than the g.bar mouseout event happening whenever it falls between the strokes of the path, there is a solid rect element filling the space, even if it is transparent.
I have made a compound bar chart representing footballers within football teams. The chart is here: http://andybarefoot.com/football/path.html
I used d3 and built the page to work in two stages. Firstly I load the data and create a rectangle for each player. I then update the parameters of the rectangles based on the data assigned to each element depending on which view is chosen. This means that the different navigation options resize and rearrange the rectangles based on existing data mapped to the elements but no additional data is loaded in.
Whilst the resizing of the rectangles works correctly I am unable to reorder the rectangles based on the data.
The vertical position of each rectangle is set simply by "i" multiplied by a set spacing variable. To change the order I thought I could selectAll all elements, sort based on the relevant data, and then set the new vertical position in the same way. (i.e. the value of "i" would have changed). However I can't get this to work.
Here is my (unsuccessful) attempt:
// select all elements and then reorder
svg
.selectAll(".team")
.sort(function(a, b) {
return b.totalClubContractDistance - a.totalClubContractDistance;
})
;
// select all elements and reposition according to new order
svg
.selectAll(".team")
.duration(750)
.attr("transform", function(d,i) {
return "translate(0,"+teamSpacing*i+")";
})
;
In d3 there are 4 core concepts. Join, Update, Enter, Exit. You can read more here: https://bost.ocks.org/mike/join/
Basically, every time you want to update the position of an element, you should change the data, then do a join followed by an update.
So the code would look like this:
function render (data) {
// join
// this joins the new data to the existing data
var teams = svg.selectAll('.team')
.data(data);
// update
// this will update existing teams that have a different location
teams.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// enter
// this will add new teams that were added to the data set
teams.enter()
.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// exit
// this will remove all the teams that are no longer part of the data set
teams.exit()
.remove();
}
Hope this helps
I am trying to add the ability to drag a circle from within one parent into another using this as the starting point: https://bl.ocks.org/mbostock/7607535
I've added this 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 ] + ")"
})
});
and then tried attaching it like this:
svg.selectAll("circle").call(drag);
or
node.call(drag);
I've two problems, one is positioning the circle correctly as I drag (which I think is because I need to save the origin) and the other one is how to select a sub circle to drag as it is assuming I want the parent circle. Ideally I'd like to be able to select any circle and be able to drop it into any other circle. Is there a way of attaching the drag behaviour so this just works or do I need to look at the data structure in order to work out the lowest level circle I could be trying to drag?
There is a lot going on in the Zoomable example and some of it is colliding with your intent. The quick fixes:
One is positioning the circle correctly as I drag (which I think is because I need to save the origin).
Positioning the circle will require an update during the ondrag event. Here is what I used:
var drag = d3.behavior.drag()
.on("drag", function(d, i) {
d.x += d3.event.dx;
d.y += d3.event.dy;
draw();
});
function draw() {
var k = diameter / (root.r * 2 + margin);
node.attr("transform", function(d) {
return "translate(" + (d.x - root.x) * k + "," + (d.y - root.y) * k + ")";
});
circle.attr("r", function(d) {
return d.r * k;
});
}
This draw function replaces the zoomTo function from the Zoomable example. Getting zoom to work with dragging is possible but it requires some extra thought.
The other one is how to select a sub circle to drag as it is assuming I want the parent circle.
The Zoomable example has this CSS:
.label,
.node--root,
.node--leaf {
pointer-events: none;
}
If you want to target the leaf nodes, you will have to remove the .node--leaf portion of the CSS rule. You can also add a new rule to turn off events for the non-leaf nodes:
.attr("class", function(d) {
return d.parent ? d.children ? "node node--middle" : "node node--leaf" : "node node--root";
})
Note the addition of node--middle.
Ideally I'd like to be able to select any circle and be able to drop it into any other circle. Is there a way of attaching the drag behaviour so this just works or do I need to look at the data structure in order to work out the lowest level circle I could be trying to drag?
You can drag it around without much trouble but the drag behavior will not automatically propagate changes into the data structure. If you want the actual hierarchy to change then there are a few extra steps required:
Detect where the leaf node ended
Change the parent of the leaf node appropriately
Recalculate the entire packing
Redraw/animate the new packing
Step 1 will be the tricky one. I do not have an example handy for helping with this. You can quickly detect the location that a drag event ended using the dragstop event — the trick will be figuring out which node is underneath that stopping point.
It's not allowing you to drag the leaf-most children nodes because there's a css style with pointer-events: none applied to those nodes, so they never trigger drag. So you need to take out some or all of these classes:
.label,
.node--root,
.node--leaf {
pointer-events: none;
}
That'll make it possible to drag the children, but it will not prevent the ability to drag the parents. If you don't want the parents to be draggable, you have to NOT .call(drag) on them. The way to do it is to call drag on a subset of all circles, using filter.
So, after creating the circles, you can do this:
circle
.filter( function(d) { return d.children ? false : true; } )
.call(drag);
That will apply drag only to things that do not have children.
You are right about the incorrect positioning during drag being caused by not knowing the origin. I'm pretty sure you need to save the original circle position on dragstart and then add d3.event.dx to that position during drag.
I would like to be able to place each data object (in this case 'moreData' array) inside a group element. So in the very simplified example below I would end up with three groups with 2 or 3 circles inside.
I'm using the node D3 provides with 'this' in a call to each (second one) to construct a selector.
Although the first call to each is correct (console.log tells me so)... the selector I create is obviously not doing the right thing as its creating 5 circles outside the body element and the second console.log never reports the first element.
Here is a fiddle simple use of this
From this simple data set of three objects:
data = [{'data':10, 'moreData':[1,2]}, {'data': 12, 'moreData':[3,4,5]},{'data':6, 'moreData':[7,8,9]}];
I expect and get three groups but no circles inside the groups.
var svg = d3.select("body").append("svg");
var shapes = svg.selectAll("g")
.data(data).enter();
shapes.append("g").each(add);
function add(d, i) {
console.log(i, d);
// this is where we go south!!
d3.select(this).data(d.moreData).enter() // help with this!!
.append("circle")
.attr("cx", function (d, i) {
return (i + 1) * 25;
})
.attr("cy", 10)
.attr("r", 10)
.each(function (d, i) {
console.log(i, d); // this is not good!
})
thanks for any insight into what I'm doing wrong....
The above fiddle shows no output, but if you inspect the 'results' tab you can see the correct empty groups and the circle elements outside the body tag ... at least in Chrome.
You need to select the empty set of circles before setting the data.
Right now, you are calling:
d3.select(this).data(d.moreData)
Replace that line with:
d3.select(this).selectAll("circle").data(d.moreData)
The general d3 enter paradigm is select a group -> attach data to that group -> use enter/exit, where enter will run for each item in the group which has data, but no DOM element, and exit for each element which has a DOM element but no data.
Fiddle.
Also, you should use different variables for i and d for your inner function, since right now they're the same as the variables on your outer function. (Perhaps use function(D, I) instead.)
I have a dataset, each item has been linked to svg rects using D3.
var bars = svg_content.selectAll("rect")
.data(dataset);
.enter()
.append("rect")
Assume the generation is complete (i.e. the .enter() process is complete and the rects have been generated).
How would I access the rect associated with a specific index of that dataset (for instance, the rect linked to the third piece of data)?
You can use selection.filter or the function form of the commonly used selection.select depending on your needs:
var third = selection.filter(function(d, i) { return i == 2; });
// Equivalently
var third = selection.select(function(d, i) { return i == 2; });
There are a few ways to do this. Generally, in d3, you tend to access the data from within a selection. So you would see something like:
var bars = svg_content.selectAll("rect")
.data(dataset);
.enter()
.append("rect")
.attr('class', function(d) { return d.myName; });
Here d is the data item from dataset that is associated with a particular rect. That code would class each rect with the "myName" property of each data item.
Let's say some you want to place one of these rects specially. One with myName='aName'. We will select that rectangle and set the 'tranform' attribute based on the associated data.
svg.content.selectAll('rect.aName')
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + 20 ')'; })
Note that in both cases you can also access the item's index and if it's relevant also the parent index (use function(d,i,j) {...})
Finally, though I don't encourage it in general, I have for unit tests directly accessed the data associated with an element with __data__. For example with jQuery:
$.find("svg rect.aName")[0].__data__;
You can play with a quick fiddle here