8

I am using D3 JavaScript library to construct an SVG diagram and assign mouse events to them. The SVG diagram can be complex, made with many arbitrary path elements. I have been struggling to figure out a way to determine all the nearby elements on a mouseover event. So, for example, when the mouse cursor moves over the diagram I can determine all the component SVG elements within a radius of N pixels from the cursor.

I am completely stumped on how to tackle this. I am not even sure if there is an API that allows me determine if an SVG constituent element is within a bounding region?

Any hints on how to tackle this would be most appreciated.

pliske
  • 81
  • 1
  • 2
  • To clarify: Do you just want the nearest elements to respond to an event, or do you actually need to find all the elements that have an edge/center/corner within some radius of the mouse pointer? – ZachB Sep 29 '12 at 06:32
  • I'd like to find all the elements that have an edge/center/corner within some radius of the mouse pointer. From a UI perspective, the user will click/tap close to what they want and then get a list of all the nearby elements so that they can select the actual one they want. – pliske Sep 29 '12 at 14:58
  • 1
    `.getIntersectionList(rect)` provides what you want, but it doesn't work in Firefox. SVG Edit provides an implementation of it https://github.com/duopixel/Method-Draw/blob/master/editor/svgcanvas.js#L600 – methodofaction Sep 29 '12 at 17:53

2 Answers2

10

Preface: @Duopixel's comment on your original post is great -- I didn't know that method exists. However, it looks like it only supports a rectangular region per the MSDN docs, whereas it seems more intuitive to have a circle around the mouse.

A couple of other techniques:

  1. Use an invisible, thick stroke that "expands" your element, if you're not otherwise using the stroke for styling, or draw a transparent line/shape on top of the element that is some amount larger. Listen to the 'click' event for these elements.

  2. Do a pairwise comparison of every element with that of the mouse's click radius. I made an example here: http://jsfiddle.net/AkPN2/5/. I only implemented circles because they were easy -- you'd need to add the geometry for rectangles.

ZachB
  • 13,051
  • 4
  • 61
  • 89
  • Thank you so much. Some good ideas here and I found your example #2 code very instructive. I can see how that solution will work for the predefined shapes, but not sure if it can work with "path" svg elements. But it gives me a place to start, as does .getIntersectionList(rect). Thanks – pliske Sep 30 '12 at 03:55
  • Thanks for the ideas. I did an implementation of Technique 1 to answer [another question](http://stackoverflow.com/a/16620775/151212) – explunit May 18 '13 at 04:48
1

I ended up using the method in SVGEdit suggested by DuoPixel. It uses other SVGEdit method defined in svgcanvas.js so it's not really meant as a standalone. Here's a working link to DuoPixel method suggested in the comments: https://github.com/duopixel/Method-Draw/blob/master/method-draw/src/svgcanvas.js#L600

With the specific implementation in case the link changes:

// This method sends back an array or a NodeList full of elements that
// intersect the multi-select rubber-band-box on the current_layer only.
//
// Since the only browser that supports the SVG DOM getIntersectionList is Opera,
// we need to provide an implementation here. We brute-force it for now.
//
// Reference:
// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421
// Webkit does not implement getIntersectionList(), see https://bugs.webkit.org/show_bug.cgi?id=11274
var getIntersectionList = this.getIntersectionList = function(rect) {
if (rubberBox == null) { return null; }
var parent = current_group || getCurrentDrawing().getCurrentLayer();
if(!curBBoxes.length) {
// Cache all bboxes
curBBoxes = getVisibleElementsAndBBoxes(parent);
}
var resultList = null;
try {
resultList = parent.getIntersectionList(rect, null);
} catch(e) { }
if (resultList == null || typeof(resultList.item) != "function") {
resultList = [];
if(!rect) {
var rubberBBox = rubberBox.getBBox();
var bb = {};
for(var o in rubberBBox) {
bb[o] = rubberBBox[o] / current_zoom;
}
rubberBBox = bb;
} else {
var rubberBBox = rect;
}
var i = curBBoxes.length;
while (i--) {
if(!rubberBBox.width || !rubberBBox.width) continue;
if (svgedit.math.rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {
resultList.push(curBBoxes[i].elem);
}
}
}
// addToSelection expects an array, but it's ok to pass a NodeList
// because using square-bracket notation is allowed:
// http://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
return resultList;
};
Jean-Philippe Jodoin
  • 4,536
  • 1
  • 25
  • 28