D3.js range selection (d3.brush) touch panel usage

Introduces how to use D3 range selection on touch panels.

The basic usage of range selection was introduced here. When supporting touch panels, changing it like this sample makes it easier to use.

Example program

Touch the graph. Unlike the example here, the range selection moves to the touched location.

View code

Example code

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 Brush Touch</title>
  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body>
  <!-- 1. Set style on selection -->
  <style>
    .selected {
      fill: red;
      stroke: brown;
    }
  </style>
  <script>
    // 2. Display scatter plot
    var width = 800; // Graph width
    var height = 600; // Graph height
    var margin = { "top": 30, "bottom": 30, "right": 30, "left": 30 };

    var randomX = d3.randomUniform(0.5, 10);
    var randomY = d3.randomNormal(0.5, 0.12);
    var data = d3.range(500).map(function () { return [randomX(), randomY()]; });

    var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var xScale = d3.scaleLinear()
      .domain([0, 10])
      .range([0, width - margin.right - margin.left]);

    var yScale = d3.scaleLinear()
      .domain([0, 1])
      .range([height - margin.bottom - margin.top, 0]);

    var dot = g.append("g")
      .attr("fill-opacity", 0.2)
      .selectAll("circle")
      .data(data)
      .enter()
      .append("circle")
      .attr("cx", function (d) { return xScale(d[0]) })
      .attr("cy", function (d) { return yScale(d[1]) })
      .attr("r", 5);

    svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + (height - margin.bottom) + ")")
      .call(d3.axisBottom(xScale));

    svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(d3.axisLeft(yScale));

    // 3. Brush settings
    var brush = d3.brush()
      .extent([
        [0, 0],
        [width - margin.left - margin.right, height - margin.top - margin.bottom]
      ])
      .on("start brush", brushed)

    var g2 = g.append("g")
      .call(brush)
      .call(brush.move, [
        [xScale(2), yScale(0.8)],
        [xScale(5), yScale(0.3)]
      ])
      // Important section starts here -----------------------------
      .selectAll(".overlay")
      .each(function (d) { d.type = "selection"; })
      .on("mousedown touchstart", brushcentered);
    // Important section ends here -----------------------------

    function brushed() {
      var x0 = xScale.invert(d3.event.selection[0][0]);
      var y1 = yScale.invert(d3.event.selection[0][1]);
      var x1 = xScale.invert(d3.event.selection[1][0]);
      var y0 = yScale.invert(d3.event.selection[1][1]);
      dot.classed("selected",
        function (d) {
          return (x0 <= d[0] && d[0] <= x1) && (y0 <= d[1] && d[1] <= y1);
        }
      );
    }

    // Important section starts here -----------------------------
    function brushcentered() {
      var mouse = d3.mouse(this);

      var dx = xScale(2) - xScale(0);
      var dy = yScale(0.2) - yScale(0);

      var x0 = mouse[0] - dx / 2;
      var x1 = mouse[0] + dx / 2;
      var y0 = mouse[1] - dy / 2;
      var y1 = mouse[1] + dy / 2; //

      var xMax = xScale.range()[1];
      var yMax = yScale.range()[0];
      var x = x1 > xMax ? [xMax - dx, xMax] : x0 < 0 ? [0, dx] : [x0, x1];
      var y = y1 > yMax ? [yMax - dy, yMax] : y0 < 0 ? [0, dy] : [y0, y1];
      d3.select(this.parentNode).call(brush.move, [
        [x[0], y[1]],
        [x[1], y[0]]
      ]);
    }
    // Important section ends here -----------------------------
  </script>
</body>

<html>

Code explanation

The basic program is the same as this range selection example. This section explains the parts marked as important.

Event settings on click and touch

var g2 = g.append("g")
  .call(brush)
  .call(brush.move, [
    [xScale(2), yScale(0.8)],
    [xScale(5), yScale(0.3)]
  ])
  .selectAll(".overlay")
  .each(function(d) { d.type = "selection"; })
  .on("mousedown touchstart", brushcentered);

When brush is set with the call method, the following SVG elements are set.

<g class="brush" fill="none" pointer-events="all" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">
  <rect class="overlay" pointer-events="all" cursor="crosshair" x="0" y="0" width="960" height="500"></rect>
  <rect class="selection" cursor="move" fill="#777" fill-opacity="0.3" stroke="#fff" shape-rendering="crispEdges" x="112" y="194" width="182" height="83"></rect>
  <rect class="handle handle--n" cursor="ns-resize" x="107" y="189" width="192" height="10"></rect>
  <rect class="handle handle--e" cursor="ew-resize" x="289" y="189" width="10" height="93"></rect>
  <rect class="handle handle--s" cursor="ns-resize" x="107" y="272" width="192" height="10"></rect>
  <rect class="handle handle--w" cursor="ew-resize" x="107" y="189" width="10" height="93"></rect>
  <rect class="handle handle--nw" cursor="nwse-resize" x="107" y="189" width="10" height="10"></rect>
  <rect class="handle handle--ne" cursor="nesw-resize" x="289" y="189" width="10" height="10"></rect>
  <rect class="handle handle--se" cursor="nwse-resize" x="289" y="272" width="10" height="10"></rect>
  <rect class="handle handle--sw" cursor="nesw-resize" x="107" y="272" width="10" height="10"></rect>
</g>

Set the event handler brushcentered on the rect element with the "overlay" class, which represents the selectable range. At that time, if you change the type of "overlay" to selection, the selection range can be moved with the move method.

Event handler on click and touch

Set the event handler, which is the function called when the event occurs.

  function brushcentered() {
    var mouse = d3.mouse(this);

    var dx = xScale(2) - xScale(0);
    var dy = yScale(0.2) - yScale(0);

    var x0 = mouse[0] - dx / 2;
    var x1 = mouse[0] + dx / 2;
    var y0 = mouse[1] - dy / 2;
    var y1 = mouse[1] + dy / 2; //

    var xMax = xScale.range()[1];
    var yMax = yScale.range()[0];
    var x = x1 > xMax ? [xMax - dx, xMax] : x0 < 0 ? [0, dx] : [x0, x1];
    var y = y1 > yMax ? [yMax - dy, yMax] : y0 < 0 ? [0, dy] : [y0, y1];
    d3.select(this.parentNode).call(brush.move, [
      [x[0], y[1]],
      [x[1], y[0]]
    ]);

  }

d3.mouse() is a method that retrieves the mouse position. It returns the mouse coordinates relative to the element passed as an argument in the form [x, y]. dx and dy are the size of the selection range on touch. Select this.parentNode to reference the parent element of "overlay", where brush is set.

Closing

The selection area moved by touch can be resized by dragging the four corners.

With touch operations, range selection can be difficult when using drag operations, so the method introduced on this page can be used. When the screen does not scroll, the method in D3.js range selection (d3.brush) usage can also be used.