Click Event Not Firing After Drag (sometimes) in d3.js - d3.js

Observed Behavior
I'm using d3.js, and I'm in a situation where I'd like to update some data based on a drag event, and redraw everything after the dragend event. The draggable items also have some click behavior.
Draggable items can only move along the x-axis. When an item is dragged, and the cursor is directly above the draggable item on dragend/mouseup, the item must be clicked twice after it is re-drawn for the click event to fire. When an item is dragged, but dragend/mouseup does not occur directly above the item, the click event fires as expected (on the first try) after the redraw.
Desired Behavior
I'd like the click event to always fire on the first click after dragging, regardless of where the cursor is.
If I replace the click event on the draggable items with a mouseup event, everything works as expected, but click is the event I'd really like to handle.
A Demonstration
Here is a self-contained example: http://jsfiddle.net/RRCyq/2/
And here is the relevant javascript code:
var data, click_count,did_drag;
// this is the data I'd like to render
data = [
{x : 100, y : 150},
{x : 200, y : 250}
];
// these are some elements I'm using for debugging
click_count = d3.select('#click-count');
did_drag = d3.select('#did-drag');
function draw() {
var drag_behavior,dragged = false;
// clear all circles from the svg element
d3.select('#test').selectAll('circle')
.remove();
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
});
d3.select('#test').selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function(d) { return d.x; })
.attr('cy',function(d) { return d.y; })
.attr('r',20)
.on('click',function() {
did_drag.text(dragged.toString());
if(!dragged) {
// increment the click counter
click_count.text(parseInt(click_count.text()) + 1);
}
}).call(drag_behavior);
}
draw();

A little late to the party, buuuut...
The documentations suggests that you use d3.event.defaultPrevented in your click event to know whether or not the element was just dragged. If you combine that with your drag and dragend events, a much cleaner approach is to call the exact function you want when necessary (see when and how flashRect is called):
http://jsfiddle.net/langdonx/fE5gN/
var container,
rect,
dragBehavior,
wasDragged = false;
container = d3.select('svg')
.append('g');
rect = container.append('rect')
.attr('width', 100)
.attr('height', 100);
dragBehavior = d3.behavior.drag()
.on('dragstart', onDragStart)
.on('drag', onDrag)
.on('dragend', onDragEnd);
container
.call(dragBehavior)
.on('click', onClick);
function flashRect() {
rect.attr('fill', 'red').transition().attr('fill', 'black');
}
function onDragStart() {
console.log('onDragStart');
}
function onDrag() {
console.log('onDrag');
var x = (d3.event.sourceEvent.pageX - 50);
container.attr('transform', 'translate(' + x + ')');
wasDragged = true;
}
function onDragEnd() {
if (wasDragged === true) {
console.log('onDragEnd');
// always do this on drag end
flashRect();
}
wasDragged = false;
}
function onClick(d) {
if (d3.event.defaultPrevented === false) {
console.log('onClick');
// only do this on click if we didn't just finish dragging
flashRect();
}
}
I didn't like the global variable, so I made a revision to use data: http://jsfiddle.net/langdonx/fE5gN/1/

After observing that the click required before my svg circles would start responding to click events again could happen anywhere in the document, I settled on a hack whereby I simulate a click event on the document (thanks to https://stackoverflow.com/a/2706236/1015178) after the drag ends. It's ugly, but it works.
Here's the function to simulate an event (again, thanks to https://stackoverflow.com/a/2706236/1015178)
function eventFire(el, etype){
if (el.fireEvent) {
(el.fireEvent('on' + etype));
} else {
var evObj = document.createEvent('Events');
evObj.initEvent(etype, true, false);
el.dispatchEvent(evObj);
}
}
And here's the updated drag behavior:
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
// simulate a click anywhere, so the svg circles
// will start responding to click events again
eventFire(document,'click');
});
Here's the full working example of my hackish "fix":
http://jsfiddle.net/RRCyq/3/

Related

Disable resize of brush on range chart from focus charts (dc.js, d3.js) - Solved

Please note - there is a solution for part of this problem by Gordon Woodhull in Disable brush on range chart before selecting a scale from the dropdown/on page load(dc.js,d3.js)
In addition,there is a partial solution at the end this question.
Furthermore there are two fiddles:
1) https://jsfiddle.net/dani2011/4dsjjdan/1/
2) https://jsfiddle.net/dani2011/uzg48yk7/1/ (with partial solution)
Need to disable resize of the brush on range chart (timeSlider) by dragging the line within the focus charts (bitChart,bitChart2). As Gordon Woodhull suggested (Disable brush resize (DC.js, D3.js) try to enable only pan without zoom.
Current behavior:
1)
Dragging the line on bitChart2 (focus chart) pans the brush until the borders of the timeChart. Once reaching the borders,the brush shrinks. The other focus chart (bitChart) resizes the brush of the range chart during drag of its line.
2)
When selecting a span for the brush from the dropdown only the .on('zoomed', function (chart, filter) { of bitChart2 is loaded and not the .on("zoomed"... of bitChart.
Print screens from the console:
a) Selecting scale from the dropdown
b) Dragging line on bitChart:
c) Dragging line on bitChart2:
3)
For both bitChart and bitChart2 the value of scale is 1 and the position
is 0,0:
.on('zoomed', function (chart, filter) {
//var zoom = d3.behavior.zoom()
// .translate([0, 0])
//.scale(1).scaleExtent([1, 1])
var zoom = d3.behavior.zoom()
var scale = zoom.scale(); var position = zoom.translate();
js file
The following implementations did not solve the issue:
**option 1**
bitChart.on('zoomed', function (chart, filter) {
d3.behavior.zoom().on("zoom", null);//doesn't stop zoom
//event needs svg element(tried different options),doesn't work
d3.behavior.zoom().scale(1).scaleExtent([1,1]).translate([0,0]).event(chart.select('g.stack_0')))
**option 2**
//Applied on timeslider,bitChart,bitChart2 to eliminate zoom and
//maintain pan
.zoomScale([1, 1])//dc.js
//And also
.on('zoomed', function (chart, filter) {
bitChart.zoomScale([1, 1]);
//Nothing pans with
chart.focus(chart.xOriginalDomain())
**option 3**
bitChart.on('zoomed', function (chart, filter) {
var svg = d3.select("body")
.append("svg")
.call(d3.behavior.zoom().on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" +"
scale(" + 1 + ")")
}))
//.append("g")
**option 4**
.on('zoomed', function (chart, filter) {
var chartBody = chart.select('g.stack _0');
var path = chartBody.selectAll('path.line').data([extra_data]);
path.enter().append('path').attr({
class: 'line',
});
path.attr('transform', 'translate(0,50)');
**option 5**
bitChart.on('zoomed', function (chart, filter) {
var zoom = d3.behavior.zoom()
.scaleExtent([1, 1])
chart.select('g.stack _0').call(zoom);
zoom.scale(1);
zoom.event(chart.select('g.stack _0'));
**option 6**
bitChart.on('zoomed', function (chart, filter) {
svg.call(d3.behavior.zoom().scale(1));
**option 7**
var min_zoom = 1;
var max_zoom = 1;
var svg = d3.select("body").append("svg");
var zoom = d3.behavior.zoom().scaleExtent([min_zoom, max_zoom])
bitChart.on('zoomed', function (chart, filter) {
svg.call(zoom);
My fiddle:
https://jsfiddle.net/dani2011/4dsjjdan/1/ was forked from https://jsfiddle.net/gordonwoodhull/272xrsat/9/.
When selecting span from the dropdown and clicking on the range chart,The range chart (timeSlider) acts strange on the fiddle, but behaves as expected when run it in my environment. Please note in this fiddle that bitChart2 pans the brush as expected.The resize of the brush when reaching the edge happens in my enviroment. bitChart still resizes the brush.
A partial solution:
To enable multi focus charts on a single range chart as in https://github.com/dc-js/dc.js/blob/master/web/examples/multi-focus.html written by Gordon Woodhull.Used the focus chart which worked properly in my code (bitChart2) as the main reference chart:
bitChart2.focusCharts = function (chartlist) {
if (!arguments.length) {
return this._focusCharts;
}
this._focusCharts = chartlist; // only needed to support the getter above
this.on('filtered', function (range_chart) {
if (!range_chart.filter()) {
dc.events.trigger(function () {
chartlist.forEach(function(focus_chart) {
focus_chart.x().domain(focus_chart.xOriginalDomain());
});
});
} else chartlist.forEach(function(focus_chart) {
if (!rangesEqual(range_chart.filter(), focus_chart.filter())) {
dc.events.trigger(function () {
focus_chart.focus(range_chart.filter());
});
}
});
});
return this;
};
bitChart2.focusCharts([bitChart]);
My second fiddle:
https://jsfiddle.net/dani2011/uzg48yk7/1/ was forked from https://jsfiddle.net/gordonwoodhull/272xrsat/9/.
1) When clicking on the range chart in the fiddle it does not function properly, but works in my environment.
2) The brush does not resize at the edges of the range chart in the fiddle as it does in my environment
3) It does show in the fiddle that the whole range chart is selected when panning/clicking on the lines in the focus charts and when clicking in the range chart
4) It does show in the fiddle that after selecting the brush span from the dropdown, panning the lines in the focus charts moves the brush properly on the range chart.
5) It does show in the fiddle that dragging the brush on the range chart is possible again in no span is selected from the dropdown
Still needs to solve:
1) When reaching the ends of the range chart (timeSlider) the brush resizes
solved by updating versions to be the same as the version of the external resources in the fiddle https://jsfiddle.net/gordonwoodhull/272xrsat/9/. Thank you Gordon!
2) Before selecting a scale from the dropdown:
a) When panning /translating the line of the focus charts(bitChart,bitChart2) the brush resizes
b) It is possible again to drag the brush on the range chart
Any help would be appreciated !

d3 update number - count up/down instead of replacing number immediately

Hei,
I'm updating a bar chart when user presses a button. That works fine with the .transition- property. However, if I do that on text, it replaces the text immediately. Instead what I'd like to happen is that it would count from the old to the new number (while the label moves with the bar). So as an example: a bar is updated from value 1453 to 1102. Instead of replacing 1453 immediately when the user clicks it should count up from 1102 to 1453 over the specified transition time.
Can I achieve that? Is there any d3 function for that?
I uploaded a quick example of text interpolation on bl.ocks. The relevant parts are the custom interpolator:
function interpolateText(a, b) {
var regex = /^(\d+), (\d+)$/;
var matchA = regex.exec(a);
var matchB = regex.exec(b);
if (matchA && matchB) {
var x = d3.interpolateRound(+matchA[1], +matchB[1]);
var y = d3.interpolateRound(+matchA[2], +matchB[2]);
return function(t) {
var result = [x(t), y(t)].join(", ");
return result;
};
}
}
d3.interpolators.push(interpolateText);
And using d3.transition.tween:
.on("dragend", function(d, i) {
var prev = [d.x, d.y].join(", ");
d.x = d.origin[0];
d.y = d.origin[1];
var next = [d.x, d.y].join(", ");
var selection = d3.select(this)
.transition()
.duration(1000)
.call(draw);
selection
.select("text")
.tween("textTween", function() {
var i = d3.interpolate(prev, next);
return function(t) {
this.textContent = i(t);
}
});
});
In my case, I am listening for a drag start/end but you can hook it up to a button press very easily.
The reason the above code works is because .tween will get the same animation "ticks" that the standard interpolators use. This causes the inner t parameter to match the progress of the animation and when you set this.textContent it will update the inner value of the DOM element.
The example I use is interpolating between two points which is fairly trivial but if all you want is to update text containing exactly one number it is even easier.

How to prevent d3 from triggering drag on right click?

I have defined some drag behaviour that works as expected as follows (code in CoffeeScript):
nodeDrag = d3.behavior.drag()
.on("dragstart", (d, i) ->
force.stop())
.on("drag", (d, i) ->
d.px += d3.event.dx
d.py += d3.event.dy
d.x += d3.event.dx
d.y += d3.event.dy
tick())
.on("dragend", (d, i) ->
force.resume()
d.fixed = true
tick())
// ...
nodes = vis.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
// ...
.call(nodeDrag)
I now try to create custom behaviour for right clicks on nodes. However, this triggers "dragstart" and "drag", i.e. after I call e.preventDefault() on the "contextmenu" event, the node in question is stuck to my mouse pointer and follows it around until I do another (left) click to force a release (I assume e.preventDefault() also causes "dragend" to never fire).
I found a brief discussion of this issue in a thread on Google Groups and a discussion in d3's issues on Github. However, I cannot figure out from those comments how to prevent this behaviour.
How can I not trigger dragging on right click?
I found a possibility to limit the drag gestures to left mouse button only.
It involves an additional field that records when a gesture has been initiated:
dragInitiated = false
The rest of the code is then modified to register initiation and termination of a desired drag gestures on "dragstart" and "dragend", respectively. Actions for "drag" are then only performed if a drag gestures was properly initiated.
nodeDrag = d3.behavior.drag()
.on "dragstart", (d, i) ->
if (d3.event.sourceEvent.which == 1) # initiate on left mouse button only
dragInitiated = true # -> set dragInitiated to true
force.stop()
.on "drag", (d, i) ->
if (dragInitiated) # perform only if a drag was initiated
d.px += d3.event.dx
d.py += d3.event.dy
d.x += d3.event.dx
d.y += d3.event.dy
tick()
.on "dragend", (d, i) ->
if (d3.event.sourceEvent.which == 1) # only take gestures into account that
force.resume() # were valid in "dragstart"
d.fixed = true
tick()
dragInitiated = false # terminate drag gesture
I am not sure whether this is the most elegant solution, but it does work and is not exceptionally clumsy or a big hack.
for D3 v4 see drag.filter([filter])
If filter is specified, sets the filter to the specified function and returns the drag behavior. If filter is not specified, returns the current filter, which defaults.
drag.filter([filter])
for the right click:
.filter(['touchstart'])
Filters available
mousedown, mousemove, mouseup, dragstart, selectstart, click, touchstart, touchmove, touchend, touchcancel
Little late to the party, I had the same problem and I used the following method to make sure my drag only works for left click.
var drag = d3.behavior.drag()
.on('drag', function () {
console.log(d3.event.sourceEvent.button);
if(d3.event.sourceEvent.button == 0){
var mouse = d3.mouse(this);
d3.select(this)
.attr('x', mouse[0])
.attr('y', mouse[1]);
}
});
To handle all listeners of drag event, you can use the code below:
function dragChartStart() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragStart");
}
function dragChartEnd() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragEnd");
}
function dragChartMove() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragMove");
}
var dragBehavior = d3.behavior.drag()
.on("drag", dragChartMove)
.on("dragstart", dragChartStart)
.on("dragend", dragChartEnd);
Actually with different version of d3 you might want to use d3.event.which instead of d3.event.sourceEvent.which to detect which mouse button you clicked.

Is there a select query to grab all but my current selection?

In my d3js app, when the user hovers over a particular circle, I have it enlarge. That is no problem. At the same time, I want to select "all the others" and make them smaller. What is a good query to grab "all the other circles"?
You can use selection.filter or the lesser known functional form of the commonly used selection.select depending on your needs.
If you bind your DOM elements to data using key functions, which is the recommended way, then you can filter on a selection's key: http://jsfiddle.net/9TmXs/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ...
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return d.id != x.id; })
.transition() // ...
});
Another general approach is comparing the DOM elements themselves: http://jsfiddle.net/FDt8S/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ..
var self = this;
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return self != this; })
.transition()
// ...
});

Raphael JS : how to remove events?

I use the Raphael .mouseover() and .mouseout() events to highlight some elements in my SVG.
This works fine, but after I click on an element, I want it to stop highlighting.
In the Raphael documentation I found :
To unbind events use the same method names with “un” prefix, i.e. element.unclick(f);
but I can't get this to work and I also don't understand the 'f' parameter.
This doesn't work , but what does??
obj.click( function() {
this.unmouseover();
});
Ok, what you have to do is pass the handler function to the unmouseover request:
// Creates canvas 320 × 200 at 10, 50
var paper = Raphael(10, 50, 320, 200);
// Creates circle at x = 50, y = 40, with radius 10
var circle = paper.circle(50, 40, 10);
// Sets the fill attribute of the circle to red (#f00)
circle.attr("fill", "#f00");
// Sets the stroke attribute of the circle to white
circle.attr("stroke", "#fff");
var mouseover = function (event) {
this.attr({fill: "yellow"});
}
var mouseout = function (event) {
this.attr({fill: "red"});
}
circle.hover(mouseover, mouseout);
circle.click(function (event) {
this.attr({fill: "blue"});
this.unmouseover(mouseover);
this.unmouseout(mouseout);
});
http://jsfiddle.net/GexHj/1/
That's what f is about. You can also use unhover():
circle.click(function (event) {
this.attr({fill: "blue"});
this.unhover(mouseover, mouseout);
});
http://jsfiddle.net/GexHj/2/

Resources