Detailed graphics present a challenge for hover interactions because small target areas corresponding to individual data points are hard for the user to hit, making it difficult to access additional data revealed through mouseovers. One solution can be to increase the size of the targets such that effectively everything becomes a target, and it is assumed that the user’s intention was to hit whichever target is closest to the mouse. The two scatter plots here illustrate the difference between these two approaches; it’s certainly much easier to navigate the second.

One popular way to implement this kind of interaction is by overlaying a Voronoi diagram, turning each point into a polygon for the purposes of capturing user interactions. Nadieh Brehmer explains this technique in detail.

However, using a Voronoi diagram significantly complicates the markup of an SVG, which then essentially needs to contain a secondary DOM which serves as the interaction layer. If this rubs you the wrong way, it is also possible to arrive at the same behavior using using computations entirely in memory, without any rendering artifacts. That’s how this second example works.

This project is written in a heavily annotated style called literate programming. The code blocks from this Markdown document are being executed as JavaScript by lit-web.

First, an anonymous function to guard the rest of the code in this script within a closure.

`(() => {`

Start with a bunch of static configuration values assigned to variables which we can then reference semantically throughout the rest of the script.

```
const min = 0
const max = 1000
const height = 300
const width = 300
const grid = 20
const radius = 3
const margin = {
top: grid,
right: grid,
bottom: grid,
left: grid
}
let data
```

Scales are among those shared values. For a typical graphic these would probably be inside the `chart()`

function, but for this technique they’re needed in order to calculate the interaction target so they have been moved to the outer scope.

```
const x = d3.scaleLinear()
.domain([0, max])
.range([margin.left, width])
const y = d3.scaleLinear()
.domain([max, 0])
.range([margin.bottom, height])
```

A simple tooltip function which prints the values on top of the data point. This obviously isn’t particularly useful since it just replicates the information from the chart axes, but the point of this exercise is to change how this function is triggered by user interactions.

```
const tooltip = (wrapper, circle) => {
const { x, y } = circle.node().getBBox()
const text = wrapper.select('text.tooltip')
text
.attr('transform', `
translate(
${x + radius},
${y}
)
`)
text
.text(`
${circle.datum().x}
×
${circle.datum().y}
`)
}
```

One of the primary costs of this in-memory technique is that it tracks all mouse movement, not the borders between Voronoi areas. Your browser can efficiently track mouse movement across DOM node boundaries, and there are many more mouse movements than DOM node boundary crossings, so this technique would be comparatively inefficient without some corrective action. We need to *debounce* the function tracking the mouse movements by limiting the rate of its successive invocations.

Here’s a simple debounce function which takes an input function and adds a rate limit.

```
const milliseconds = 1000
const debounce = (fn) => {
var timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
timeout = null
fn.apply(this, args)
}, milliseconds)
}
}
```

With conventional user interactions the tooltip function would usually just fire on mouseover.

```
const mouseoverStandard = selection => {
selection
.selectAll('circle')
.on('mouseenter', function() {
tooltip(selection, d3.select(this))
})
}
```

The key to this technique is to first determine the hit point closest to the user interaction, and then run the tooltip function there.

```
const mouseoverSnap = wrapper => {
wrapper
.on('mousemove', function() {
// capture position relative to data rectangle
const rect = d3.select(this).select('rect').node()
const mouse = d3.mouse(rect)
// convert interaction point into data space
const datum = {
x: x.invert(mouse[0]),
y: y.invert(mouse[1])
}
// find closest data point
const distances = data
.map(item => {
// essentially just the pythagorean theorem
const dx = Math.abs(datum.x - item.x)
const dy = Math.abs(datum.y - item.y)
const distance = Math.sqrt(
Math.pow(dy, 2) +
Math.pow(dx, 2)
)
return distance
})
// closest datum
const index = distances.indexOf(d3.min(distances))
const closest = data[index]
// corresponding node
const match = wrapper
.selectAll('circle')
.filter(d => {
return d.x === closest.x && d.y === closest.y
})
// run tooltip function
tooltip(wrapper, match)
})
}
```

Let’s actually put all the above pieces together and render so it’s easier to actually see the difference between them.

Generate a set of random points to plot in the chart.

```
const pad = 0.05 * max
const random = d3.randomUniform(min + pad, max - pad)
const item = () => ({
x: Math.round(random()),
y: Math.round(random())
})
data = Array.from({length: 100}).map(item)
```

Here’s a function which creates a scatter plot of the data points generated above. I won’t bother annotating this part of the code because the chart over which you deploy this technique isn’t really the point here.

```
const chart = selection => {
const dimensions = ['x', 'y']
const translate = `translate(${margin.left}, ${margin.top})`
const wrapper = selection
.append('g')
.classed('wrapper', true)
.attr('transform', translate)
wrapper
.append('rect')
.attr('x', margin.left)
.attr('y', margin.top)
.attr('height', height - margin.top)
.attr('width', width - margin.left)
const axes = wrapper
.append('g')
.classed('axes', true)
dimensions.forEach(dimension => {
const dir = dimension === 'x'
const position = dir ? 'Bottom' : 'Left'
const axis = d3[`axis${position}`]()
.scale(dimension === 'x' ? x : y)
axes.append('g')
.classed(`axis-${dimension}`, true)
.attr('transform', `
translate(
${dir ? 0 : margin.left},
${dir ? height : 0}
)
`)
.call(axis)
})
const points = wrapper
.append('g')
.classed('points', true)
wrapper.append('text').classed('tooltip', true)
const point = points
.selectAll('circle')
.data(data)
.enter()
.append('circle')
point
.attr('r', radius)
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
}
```

Run the chart function twice to generate two copies of the same chart.

```
const example = d3.select('main')
.selectAll('div.example')
.data(['standard', 'snapping'])
.enter()
.append('div')
.classed('example', true)
example
.append('h2')
.text(d => `${d} mouseovers`)
example
.append('svg')
.attr('height', height + margin.bottom + margin.top)
.attr('width', width + margin.left + margin.right)
.attr('class', d => `chart-${d}`)
.call(chart)
```

With our debounce helper at the ready, we can now attach the mouseover functions to our two charts.

```
d3.select('div.example:nth-child(1)')
.call(debounce(mouseoverStandard))
d3.select('div.example:nth-child(2)')
.call(debounce(mouseoverSnap))
```

Close the anonymous function opened at the very beginning.

`})()`