Basically I have a graph where the user can click different SVG shapes and by doing so a .on('click') function will change the color fills. However, at some point it's going to be important to give the user the option to revert the graph to its initial state. I think the intuitive way to go about this for my case is by clicking anywhere outside the graph, i.e somewhere on the document body that is not in the graph coordinate plane (margins, padding, ect).
I tried this:
d3.select('body').on('click', function() {
d3.selectAll('circle').style('fill', function(d) {
return d.color;
})
});
It did not have any effect. I'm guessing that my existing on click effects of the shapes are overriding my d3.select('body').on('click') that I tried above. Either that or my approach is just flat out wrong.
Any suggestions here would be great.
Also I am aware that .attr('fill') and .style('fill') should almost always be consistent throughout, I do need .style('fill') here. I tried .attr('fill') just to be safe as well.
You might be able to use something like
window.onclick = function(event){
if(!(event.target.className.baseVal=="circleClass")){
d3.selectAll('circle').style('fill', function(d) {
return d.color;
})
}
}
Related
Note: I asked this question about interrupting transitions during a scroll, but am trying a different technique now that is resulting in a similar issue that doesn't get resolved with the accepted (and working) answer.
This time, rather than initializing all the graphs with 0 opacity and having a separate function to change the opacity that gets called on each step, I'd like to use selection.remove() in each drawing function. I want to do this so that out-of-view graphs don't get in the way of any mouseover interactions that I might want on the current graph.
For example, I have functions that clear the existing graphs and then draw the current one with some transition:
var makeCircle0 = function() {
d3.selectAll(".nancygraphs").interrupt().remove()
g.append("circle")
.attr("cx", 50)
.attr("cy", 100)
.attr("r", 20)
.attr("fill", "red")
.attr("id", "red")
.attr("opacity", 0)
.transition()
.duration(1000)
.attr("opacity", 1)
.attr("class", "nancygraphs")
}
These functions are put in a list
var activateFunctions = [];
activateFunctions[0] = makeCircle0;
activateFunctions[1] = makeCircle1;
activateFunctions[2] = makeCircle2;
activateFunctions[3] = makeCircle3;
And depending on the step, the function gets called to draw the correct graph
function handleStepEnter(response) {
console.log(response)
step.classed('is-active', function(d, i) {
return i === response.index;
})
figure.select('p').text(response.index);
figure.call(activateFunctions[response.index]) // HERE!
}
Here is a jsfiddle to illustrate. Basically, if you scroll back-and-forth quickly then old graphs don't get cleared and you'll notice several graphics in view simultaneously. Why isn't d3.selectAll(".nancygraphs").interrupt().remove() doing the job?
Three observations regarding your approach:
First, according to the d3 manual on
transitions:
remove: remove the selected elements when the transition ends.
The remove will not interrupt already running transitions - it only removes when all transitions have stopped. More specifically it seems to act when __transition__.count of an element reaches 0. You could consider using a non-d3 remove implementation here, e. g. jQuery.
Second, from the same manual:
Interrupting a transition on an element has no effect on any transitions on any descendant elements. (...) you must therefore interrupt the descendants: selection.selectAll("*")
You should call interrupt on both by doing d3.selectAll(".nancygraphs").interrupt().selectAll("*").interrupt().
Third, it is never a good idea to directly couple mouse or scroll input to your logic (when you directly couple input events to e. g. attaching a transition, you might be doing so many thousands of times): did you use a debounce function? The lodash implementation is highly recommended.
After trying these modifications I would assume your current problem is solved. If it is not, a further way of debugging would be to log / overwrite the __transition__.count attribute of your elements.
I am trying to make just the axis label of the selected item bold, so that it is more apparent to users what they have selected. I am using a composite bar chart to compare two values and have turned the labels -90 so that they are inside the bars. I have been able to make the labels clickable, with help from Gordon here: dc.js barChart - click on x-axis ticks and labels
I am counting clicks so that users can select and deselect from clicking on the label, however with some of the smaller values, users cannot tell which items they have filtered on in any specific chart.
I am able to select, de-select on clicks, I have tried select('tick-text').attr('style', 'font-weight: bold;');
This bolds the first item in the x-axis, no matter which selection is made.
When I use selectAll('.x text) it changes all.
Relevant portion of the code:
ByTopLvl.on('pretransition',function() {
ByTopLvl.selectAll('g.x text')
.style('text-anchor', 'start')
.attr('transform', 'rotate(-90),translate(10, -10)');
ByTopLvl.select('.axis.x')
.selectAll('.tick text')
.on('click.custom', function (d) {
var clicks = $(this).data('clicks');
if (!clicks) {
ByTopLvl.replaceFilter(d)
.select('.tick text')
.attr('style', 'font-weight: bold;');
ByTopLvl.redrawGroup();
} else {
ByTopLvl.select('.tick text')
.attr('style', 'font-weight: normal;');
ByTopLvl.filterAll();
dc.redrawAll();
}
$(this).data("clicks", !clicks);
});
I would expect that when I click on the label, only the label clicked would be bold.
First off, whenever possible, I would suggest using the built-in selection/filter state rather than trying to keep track of clicks yourself. Otherwise they are bound to get out of sync at some point.
If you have the boldness of the ticks driven by the active filters, then you'll get the same behavior whether the bar or the tick was clicked, and you can be certain that exactly the ticks in the filters are the bolded ones:
CSS
.dc-chart g.axis.x text.selected {
font-weight: bold;
}
JS
chart.on('filtered', function(chart) {
var filters = chart.filters();
chart.selectAll('.axis.x .tick text').classed('selected', function(d) {
return filters.includes(d);
})
})
[Side note since I'm not answering your exact question: if you want to make your code work, you could do something like filter the selection based on d:
ByTopLvl.select('.tick text').filter(function(d2) { return d2 === d; })
or in your case, this is the clicked tick, so d3.select(this) should also work. But I think you'll run into a lot of bugs that way.]
Similarly, you can simplify your click behavior by tying into the built-in filter behavior, which already toggles:
chart.on('pretransition', function(chart) {
chart.select('.axis.x')
.selectAll('.tick text')
.on('click.select', function(d) {
chart.filter(d);
chart.redrawGroup();
});
});
Yeah, it's weird that the built-in filter function toggles, but that's just the way that dc.js evolved.
Here's a demo fiddle.
In a composite
Composite charts in dc.js are a little bit of a confusing mess.
The filter selection is shared between the parent and child, except they sort also handle it separately.
Unfortunately when I have to troubleshoot composite charts, I just try different things until it works, and never fully understand what's going on. I think it's just too complicated for my brain. :-O
Anyway, this worked...
Keep a reference to the inner bar chart:
var chart = dc.compositeChart('#test'), bar;
chart
.width(768)
.height(380)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal)
.brushOn(false)
.xAxisLabel('Fruit')
.yAxisLabel('Quantity Sold')
.dimension(fruitDimension)
.group(sumGroup)
._rangeBandPadding(0.05)
.compose([
bar = dc.barChart(chart)
.barPadding(0.1)
.outerPadding(0.05)
])
When responding to the click, filter the child bar chart instead of the parent:
chart.on('pretransition', function(chart) {
chart.select('.axis.x')
.selectAll('.tick text')
.on('click.select', function(d) {
bar.filter(d);
chart.redrawGroup();
});
});
Listen to the filtered event of the child bar chart and apply axis bolding to the parent composite chart:
bar.on('filtered', function(chart) {
var filters = chart.filters();
chart.selectAll('.axis.x .tick text').classed('selected', function(d) {
return filters.includes(d);
})
})
Whoo. I guess it's sorta.. consistent? The child bar chart is the source of truth for the filters. Maybe I'm rationalizing.
New fiddle version.
I'm looking for some advice on how to get two elements in a visualization, which are linked by a common data value, to respond simultaneously.
Here is the visualization as it stands now.
http://bl.ocks.org/natemiller/2686e5c0d9a1a4bb0895
Note that the different colored points are for the 50 US states in 2005 (green) and 2013 (blue), so there is a blue point and a green point for each state. I have two things I would like to get working here.
I would like to be able to mouseover either a blue point or a green point and have the corresponding point (for the same state) highlighted.
I would like a tooltip with some basic data to appear next to both points, providing point specific data.
Regarding the first point above. Right now when you mouseover a blue point the corresponding green point is highlighted, however, when you mouseover a green point only that point is highlighted and not its corresponding blue point. I imagine this is a simple fix, but for the life of me I can't figure out to reverse the reference so I get green to blue references as well.
Regarding the second point. Right now a tooltip with relevant information appears near the moused-over point, but I would like to have a similar tooltip appear next to the corresponding point from the alternate year of data, so that direct comparisons across years are easier. I am quite new to adding HTML tooltips so I'm not clear on how to do this and suspect it may require a new method for adding tooltips. Can any help to steer me in the correct direction for how to have a tooltip appear near the moused-over element and a corresponding linked element?
1) Remember that ids are unique and you're creating multiple circles with the same id, use a class instead
circles.attr("class", function(d) { return d.state })
2) You're creating a single tooltip, if you want to show one for each pair of states create multiple tooltips
Assuming that you make these changes you can easily create multiple tooltips for each pair of states
circles.on('mouseover', function (d) {
// selection for 2 states
var states = d3.selectAll('circle.' + d.state)
// code to style those nodes goes here ...
// tooltips for the states
var tooltips = d3.select('svg').selectAll('text.tooltip')
.data(states.data())
// initial styling of the tooltips goes here...
tooltips
.enter()
.append('text')
.attr('class', 'tooltip')
// update
tooltips
.html(function (d) {
// text of the tooltip
return 'something'
})
// positioning, it requires some margin fixes I guess
.attr('x', function (d) { return xScale(d.child_pov) })
.attr('y', function (d) { return yScale(d.non_math_prof) })
})
Finally remove the tooltips created on mouseover when the mouseout event is triggered
circles.on('mouseout', function (d) {
d3.select('svg').selectAll('text.tooltip').remove()
})
You cannot have multiple elements with the same id. Use a class .circleHawaii instead of an id #circleHawaii.
I was looking at the NYTimes interactive on state subsidies this morning and noticed that even when a state is obscured by a dot it is brought forward on hover.
For example the dot covering Massachusetts also partially covers New Hampshire, yet when you when you mouse into the covered part of New Hampshire, New Hampshire is brought forward.
How do you suppose they achieve this? The dots are in front of the state outlines based on their order in the DOM. I thought there might be a second set of state outlines on top of everything, listening for mouseovers that would trigger the underlying shape, but that doesn't seem to be the case.
I need to implement similar functionality in an application I'm working on and am curious about an elegant way with SVG elements.
Thanks.
As noted by Andy the circles in that NYT graphic have a CSS pointer-events property of none:
circle {
pointer-events: none;
}
the bring to front behavior can be achieved in a the mouseover function using this method:
stateShape.on("mouseover",function(){this.parentNode.appendChild(this);})
Now you can just use the raise() method:
Example in a function:
highlightSite(site) {
let selectedSite = d3.selectAll(`[siteName=${site}]`)
selectedSite.raise()
selectedSite
.attr('stroke', 'black')
.style('opacity', 1)
}
I love the way this scatterchart highlights circles when you mouse over them: http://novus.github.com/nvd3/examples/scatterWithLegend.html
But there's a lot of code there (looks like the author has defined his/her own standard libraries) and I can't figure out exactly how the effect is achieved.
Is it something to do with the .hover class and the stroke-width property?
I'd like to achieve the same effect on my own scatter graph, although I'm using circles rather than paths, so it may not be possible.
In the example, the effect seems to be realised in scatter.js from line 136.
Just highlighting individual circles is much easier though and doesn't need all the other stuff the example does. All you need to do is add a mouseover handler to the circles and (for example) increase stroke-width. That would look something like
d3.selectAll("circle").data(...).enter()
.append(...)
.classed("circle", true)
.on("mouseover", function() { d3.select(d3.event.target).classed("highlight", true); })
.on("mouseout", function() { d3.select(d3.event.target).classed("highlight", false); });
I'm assuming that a CSS class highlight defines the style. Alternatively, you can just use (in this example) CSS class circle:hover without the need for the event handlers.