block by 1wheel 77c660a764ab55a496c4e37623be9069

canvas-interaction-sorted

Full Screen

With data sorted along the x axis, we can find the closest point even faster.

var [px, py] = d3.mouse(this)
var index = bisect.left(data, px)

var minPoint = null
var minDist = Infinity
var lxDist = 0
var rxDist = 0
var i = 0
while (lxDist < minDist && rxDist < minDist){
  lxDist = checkPoint(data[index - i])
  rxDist = checkPoint(data[index + i])
  i++
}

function checkPoint(d){
  if (!d) return Infinity

  var dx = d.px - px
  var dy = d.py - py
  var dist = Math.sqrt(dx*dx + dy*dy)

  if (dist < minDist){
    minDist = dist
    minPoint = d
  }

  return Math.abs(px - d.px)
}

_script.js

var ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden')

var sel = d3.select('#graph').html('')
var c = d3.conventions({
  sel, 
  margin: {left: 30},
  layers: 'cs',
})

var [ctx, svg] = c.layers
d3.drawAxis(c)

var colorScale = d3.scaleLinear().range(['#f0f', '#0f0'])

var n = 1000000
var data = d3.range(n).map(i => {
  var r = i/n
  var px = c.x(r)
  var py = c.y(Math.random())
  var color = colorScale(r + Math.random()/5)

  ctx.fillStyle = color
  ctx.fillRect(px, py, 1, 1)

  return {px, py, color}
})

var highlightCircle = svg.append('circle')
  .at({r: 10, fill: 'none'})
  .st({pointerEvents: 'none'})


var bisect = d3.bisector(d => d.px)

svg.append('rect')
  .at({width: c.width, height: c.height, fillOpacity: 0})
  .call(d3.attachTooltip)
  .on('mousemove', function(){
    var startT = performance.now()

    var [px, py] = d3.mouse(this)
    var index = bisect.left(data, px)

    var minPoint = null
    var minDist = Infinity
    var lxDist = 0
    var rxDist = 0
    var i = 0
    while (lxDist < minDist && rxDist < minDist){
      lxDist = checkPoint(data[index - i])
      rxDist = checkPoint(data[index + i])
      i++
    }

    function checkPoint(d){
      if (!d) return Infinity

      var dx = d.px - px
      var dy = d.py - py
      var dist = Math.sqrt(dx*dx + dy*dy)

      if (dist < minDist){
        minDist = dist
        minPoint = d
      }

      return Math.abs(px - d.px)
    }

    var timeDelta = performance.now() - startT
    ttSel.text(' found in ' + d3.format('.3f')(timeDelta) + 'ms with ' + d3.format('05')(i*2) + ' comparisons')

    highlightCircle
      .translate([minPoint.px, minPoint.py])
      .at({stroke: minPoint.color, strokeWidth: 4})
  })
  .on('mouseout', function(){
    highlightCircle.at({strokeWidth: 0})
  })

index.html

<!DOCTYPE html>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">

<div id='graph'></div>
<script src='d3_.js'></script>

<script type="text/javascript">
</script>

<script src='_script.js'></script>

style.css

body{
  font-family: menlo, Consolas, 'Lucida Console', monospace; 
  margin: 0px;
}

.tooltip {
  top: -1000px;
  position: fixed;
  padding: 10px;
  background: rgba(255, 255, 255, .90);
  border: 1px solid lightgray;
  pointer-events: none;
}
.tooltip-hidden{
  opacity: 0;
  transition: all .3s;
  transition-delay: .1s;
}

@media (max-width: 590px){
  div.tooltip{
    bottom: -1px;
    width: calc(100%);
    left: -1px !important;
    right: -1px !important;
    top: auto !important;
    width: auto !important;
  }
}

svg{
  overflow: visible;
}

.domain{
  display: none;
}

text{
  pointer-events: none;
  text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}