Skip to content

Commit 80383f9

Browse files
committed
fixes phx 235 - dont drop selection on click - make sure drag is called before calling dragend
1 parent 30d1f85 commit 80383f9

File tree

1 file changed

+140
-119
lines changed

1 file changed

+140
-119
lines changed

src/traces/parcoords/axisbrush.js

+140-119
Original file line numberDiff line numberDiff line change
@@ -206,137 +206,158 @@ function getInterval(d, y) {
206206
return out;
207207
}
208208

209+
function dragstart(lThis, d) {
210+
d3.event.sourceEvent.stopPropagation();
211+
var y = d.height - d3.mouse(lThis)[1] - 2 * c.verticalPadding;
212+
var unitLocation = d.unitToPaddedPx.invert(y);
213+
var b = d.brush;
214+
var interval = getInterval(d, y);
215+
var unitRange = interval.interval;
216+
var s = b.svgBrush;
217+
s.wasDragged = false; // we start assuming there won't be a drag - useful for reset
218+
s.grabbingBar = interval.region === 'ns';
219+
if(s.grabbingBar) {
220+
var pixelRange = unitRange.map(d.unitToPaddedPx);
221+
s.grabPoint = y - pixelRange[0] - c.verticalPadding;
222+
s.barLength = pixelRange[1] - pixelRange[0];
223+
}
224+
s.clickableOrdinalRange = interval.clickableOrdinalRange;
225+
s.stayingIntervals = (d.multiselect && b.filterSpecified) ? b.filter.getConsolidated() : [];
226+
if(unitRange) {
227+
s.stayingIntervals = s.stayingIntervals.filter(function(int2) {
228+
return int2[0] !== unitRange[0] && int2[1] !== unitRange[1];
229+
});
230+
}
231+
s.startExtent = interval.region ? unitRange[interval.region === 's' ? 1 : 0] : unitLocation;
232+
d.parent.inBrushDrag = true;
233+
s.brushStartCallback();
234+
}
235+
236+
var dragging = false;
237+
238+
function drag(lThis, d) {
239+
dragging = true;
240+
241+
d3.event.sourceEvent.stopPropagation();
242+
var y = d.height - d3.mouse(lThis)[1] - 2 * c.verticalPadding;
243+
var s = d.brush.svgBrush;
244+
s.wasDragged = true;
245+
246+
if(s.grabbingBar) { // moving the bar
247+
s.newExtent = [y - s.grabPoint, y + s.barLength - s.grabPoint].map(d.unitToPaddedPx.invert);
248+
} else { // south/north drag or new bar creation
249+
s.newExtent = [s.startExtent, d.unitToPaddedPx.invert(y)].sort(sortAsc);
250+
}
251+
252+
d.brush.filterSpecified = true;
253+
s.extent = s.stayingIntervals.concat([s.newExtent]);
254+
s.brushCallback(d);
255+
renderHighlight(lThis.parentNode);
256+
}
257+
258+
function dragend(lThis, d) {
259+
if(!dragging) { // i.e. click
260+
// mock zero drag
261+
mousemove(lThis, d);
262+
drag(lThis, d);
263+
// remember it is a click not a drag
264+
d.brush.svgBrush.wasDragged = false;
265+
}
266+
dragging = false;
267+
268+
var e = d3.event;
269+
e.sourceEvent.stopPropagation();
270+
var brush = d.brush;
271+
var filter = brush.filter;
272+
var s = brush.svgBrush;
273+
var grabbingBar = s.grabbingBar;
274+
s.grabbingBar = false;
275+
s.grabLocation = undefined;
276+
d.parent.inBrushDrag = false;
277+
clearCursor(); // instead of clearing, a nicer thing would be to set it according to current location
278+
if(!s.wasDragged) { // a click+release on the same spot (ie. w/o dragging) means a bar or full reset
279+
s.wasDragged = undefined; // logic-wise unneeded, just shows `wasDragged` has no longer a meaning
280+
if(s.clickableOrdinalRange) {
281+
if(brush.filterSpecified && d.multiselect) {
282+
s.extent.push(s.clickableOrdinalRange);
283+
} else {
284+
s.extent = [s.clickableOrdinalRange];
285+
brush.filterSpecified = true;
286+
}
287+
} else if(grabbingBar) {
288+
s.extent = s.stayingIntervals;
289+
if(s.extent.length === 0) {
290+
brushClear(brush);
291+
}
292+
} else {
293+
brushClear(brush);
294+
}
295+
s.brushCallback(d);
296+
renderHighlight(lThis.parentNode);
297+
s.brushEndCallback(brush.filterSpecified ? filter.getConsolidated() : []);
298+
return; // no need to fuse intervals or snap to ordinals, so we can bail early
299+
}
300+
301+
var mergeIntervals = function() {
302+
// Key piece of logic: once the button is released, possibly overlapping intervals will be fused:
303+
// Here it's done immediately on click release while on ordinal snap transition it's done at the end
304+
filter.set(filter.getConsolidated());
305+
};
306+
307+
if(d.ordinal) {
308+
var a = d.unitTickvals;
309+
if(a[a.length - 1] < a[0]) a.reverse();
310+
s.newExtent = [
311+
ordinalScaleSnap(0, a, s.newExtent[0], s.stayingIntervals),
312+
ordinalScaleSnap(1, a, s.newExtent[1], s.stayingIntervals)
313+
];
314+
var hasNewExtent = s.newExtent[1] > s.newExtent[0];
315+
s.extent = s.stayingIntervals.concat(hasNewExtent ? [s.newExtent] : []);
316+
if(!s.extent.length) {
317+
brushClear(brush);
318+
}
319+
s.brushCallback(d);
320+
if(hasNewExtent) {
321+
// merging intervals post the snap tween
322+
renderHighlight(lThis.parentNode, mergeIntervals);
323+
} else {
324+
// if no new interval, don't animate, just redraw the highlight immediately
325+
mergeIntervals();
326+
renderHighlight(lThis.parentNode);
327+
}
328+
} else {
329+
mergeIntervals(); // merging intervals immediately
330+
}
331+
s.brushEndCallback(brush.filterSpecified ? filter.getConsolidated() : []);
332+
}
333+
334+
function mousemove(lThis, d) {
335+
var y = d.height - d3.mouse(lThis)[1] - 2 * c.verticalPadding;
336+
var interval = getInterval(d, y);
337+
338+
var cursor = 'crosshair';
339+
if(interval.clickableOrdinalRange) cursor = 'pointer';
340+
else if(interval.region) cursor = interval.region + '-resize';
341+
d3.select(document.body)
342+
.style('cursor', cursor);
343+
}
344+
209345
function attachDragBehavior(selection) {
210346
// There's some fiddling with pointer cursor styling so that the cursor preserves its shape while dragging a brush
211347
// even if the cursor strays from the interacting bar, which is bound to happen as bars are thin and the user
212348
// will inevitably leave the hotspot strip. In this regard, it does something similar to what the D3 brush would do.
213349
selection
214350
.on('mousemove', function(d) {
215351
d3.event.preventDefault();
216-
if(!d.parent.inBrushDrag) {
217-
var y = d.height - d3.mouse(this)[1] - 2 * c.verticalPadding;
218-
var interval = getInterval(d, y);
219-
220-
var cursor = 'crosshair';
221-
if(interval.clickableOrdinalRange) cursor = 'pointer';
222-
else if(interval.region) cursor = interval.region + '-resize';
223-
d3.select(document.body)
224-
.style('cursor', cursor);
225-
}
352+
if(!d.parent.inBrushDrag) mousemove(this, d);
226353
})
227354
.on('mouseleave', function(d) {
228355
if(!d.parent.inBrushDrag) clearCursor();
229356
})
230357
.call(d3.behavior.drag()
231-
.on('dragstart', function(d) {
232-
d3.event.sourceEvent.stopPropagation();
233-
var y = d.height - d3.mouse(this)[1] - 2 * c.verticalPadding;
234-
var unitLocation = d.unitToPaddedPx.invert(y);
235-
var b = d.brush;
236-
var interval = getInterval(d, y);
237-
var unitRange = interval.interval;
238-
var s = b.svgBrush;
239-
s.wasDragged = false; // we start assuming there won't be a drag - useful for reset
240-
s.grabbingBar = interval.region === 'ns';
241-
if(s.grabbingBar) {
242-
var pixelRange = unitRange.map(d.unitToPaddedPx);
243-
s.grabPoint = y - pixelRange[0] - c.verticalPadding;
244-
s.barLength = pixelRange[1] - pixelRange[0];
245-
}
246-
s.clickableOrdinalRange = interval.clickableOrdinalRange;
247-
s.stayingIntervals = (d.multiselect && b.filterSpecified) ? b.filter.getConsolidated() : [];
248-
if(unitRange) {
249-
s.stayingIntervals = s.stayingIntervals.filter(function(int2) {
250-
return int2[0] !== unitRange[0] && int2[1] !== unitRange[1];
251-
});
252-
}
253-
s.startExtent = interval.region ? unitRange[interval.region === 's' ? 1 : 0] : unitLocation;
254-
d.parent.inBrushDrag = true;
255-
s.brushStartCallback();
256-
})
257-
.on('drag', function(d) {
258-
d3.event.sourceEvent.stopPropagation();
259-
var y = d.height - d3.mouse(this)[1] - 2 * c.verticalPadding;
260-
var s = d.brush.svgBrush;
261-
s.wasDragged = true;
262-
263-
if(s.grabbingBar) { // moving the bar
264-
s.newExtent = [y - s.grabPoint, y + s.barLength - s.grabPoint].map(d.unitToPaddedPx.invert);
265-
} else { // south/north drag or new bar creation
266-
s.newExtent = [s.startExtent, d.unitToPaddedPx.invert(y)].sort(sortAsc);
267-
}
268-
269-
d.brush.filterSpecified = true;
270-
s.extent = s.stayingIntervals.concat([s.newExtent]);
271-
s.brushCallback(d);
272-
renderHighlight(this.parentNode);
273-
})
274-
.on('dragend', function(d) {
275-
var e = d3.event;
276-
e.sourceEvent.stopPropagation();
277-
var brush = d.brush;
278-
var filter = brush.filter;
279-
var s = brush.svgBrush;
280-
var grabbingBar = s.grabbingBar;
281-
s.grabbingBar = false;
282-
s.grabLocation = undefined;
283-
d.parent.inBrushDrag = false;
284-
clearCursor(); // instead of clearing, a nicer thing would be to set it according to current location
285-
if(!s.wasDragged) { // a click+release on the same spot (ie. w/o dragging) means a bar or full reset
286-
s.wasDragged = undefined; // logic-wise unneeded, just shows `wasDragged` has no longer a meaning
287-
if(s.clickableOrdinalRange) {
288-
if(brush.filterSpecified && d.multiselect) {
289-
s.extent.push(s.clickableOrdinalRange);
290-
} else {
291-
s.extent = [s.clickableOrdinalRange];
292-
brush.filterSpecified = true;
293-
}
294-
} else if(grabbingBar) {
295-
s.extent = s.stayingIntervals;
296-
if(s.extent.length === 0) {
297-
brushClear(brush);
298-
}
299-
} else {
300-
brushClear(brush);
301-
}
302-
s.brushCallback(d);
303-
renderHighlight(this.parentNode);
304-
s.brushEndCallback(brush.filterSpecified ? filter.getConsolidated() : []);
305-
return; // no need to fuse intervals or snap to ordinals, so we can bail early
306-
}
307-
308-
var mergeIntervals = function() {
309-
// Key piece of logic: once the button is released, possibly overlapping intervals will be fused:
310-
// Here it's done immediately on click release while on ordinal snap transition it's done at the end
311-
filter.set(filter.getConsolidated());
312-
};
313-
314-
if(d.ordinal) {
315-
var a = d.unitTickvals;
316-
if(a[a.length - 1] < a[0]) a.reverse();
317-
s.newExtent = [
318-
ordinalScaleSnap(0, a, s.newExtent[0], s.stayingIntervals),
319-
ordinalScaleSnap(1, a, s.newExtent[1], s.stayingIntervals)
320-
];
321-
var hasNewExtent = s.newExtent[1] > s.newExtent[0];
322-
s.extent = s.stayingIntervals.concat(hasNewExtent ? [s.newExtent] : []);
323-
if(!s.extent.length) {
324-
brushClear(brush);
325-
}
326-
s.brushCallback(d);
327-
if(hasNewExtent) {
328-
// merging intervals post the snap tween
329-
renderHighlight(this.parentNode, mergeIntervals);
330-
} else {
331-
// if no new interval, don't animate, just redraw the highlight immediately
332-
mergeIntervals();
333-
renderHighlight(this.parentNode);
334-
}
335-
} else {
336-
mergeIntervals(); // merging intervals immediately
337-
}
338-
s.brushEndCallback(brush.filterSpecified ? filter.getConsolidated() : []);
339-
})
358+
.on('dragstart', function(d) { dragstart(this, d); })
359+
.on('drag', function(d) { drag(this, d); })
360+
.on('dragend', function(d) { dragend(this, d); })
340361
);
341362
}
342363

0 commit comments

Comments
 (0)