D3.js 범위 선택(d3.brush) 사용법

D3의 범위 선택의 사용법을 소개한다.

예제 프로그램

예제 프로그램에 표시된 회색 사각형을 드래그하거나 그래프의 아무것도 없는 곳을 클릭하고 드래그하여 범위를 선택할 수 있다. 회색 사각형 모서리를 드래그하여 범위의 크기를 변경할 수 있다.

코드 확인

예제 코드

<!DOCTYPE html>
<html>

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

<body>
  <!-- 1. 선택 시 스타일 설정 -->
  <style>
    .selected {
      fill: red;
      stroke: brown;
    }
  </style>
  <script>
    // 2. 산점도 표시
    var width = 800; // 그래프 넓이
    var height = 600; // 그래프 높이
    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 설정
    var brush = d3.brush()
      .extent([
        [0, 0],
        [width - margin.left - margin.right, height - margin.top - margin.bottom]
      ])
      .on("start brush", brushed);

    g.append("g")
      .call(brush)
      .call(brush.move, [
        [xScale(2), yScale(0.8)],
        [xScale(5), yScale(0.3)]
      ]);

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

</html>

코드 설명

1. 선택시 스타일 설정

선택되었을 때, 플롯에 설정할 스타일을 정의한다.

<style>
.selected {
  fill: red;
  stroke: brown;
}
</style>

2. 산포도 표시

산포도를 만드는 방법에 대한 자세한 내용은 여기를 참조한다. 관련 부분을 설명한다.

산포도의 원본 데이터를 만든다.

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()]; });

d3.randomUniform은 인수에 설정한 범위에서 난수를 발생시키는 함수를 설정하는 것이고, d3.randomNormal은 평균값과 표준편차를 인수로 하여 가우스 분포에 따른 난수를 발생시키는 함수를 설정하는 것이다. 이 함수들과 d3.range(500).map()를 사용하여 500점의 2차원 배열 데이터를 작성한다.

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

body 요소 내에 SVG 요소를 설정하고 그 안에 "g" 요소를 설정한다. 플롯 표시 및 범위 선택을 위한 브러시를 "g" 요소 내에 설정한다.

3. brush 설정

범위 선택을 위한 brush를 설정하는 프로토타입을 설정하고 call 메소드로 호출하여 brush를 설정한다.

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

g.append("g")
  .call(brush)
  .call(brush.move, [
    [xScale(2), yScale(0.8)],
    [xScale(5), yScale(0.3)]
  ]);

call 메소드에서 brush를 설정하면 다음 SVG 요소가 선택한 요소 내(이번에는 "g" 요소)로 설정된다.

<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>

또한 d3.brush에는 다음 설정이 가능하다.

설정 설명
d3.brush.move() 브러시 선택 영역을 이동한다.
[[x0, y0], [x1, y1]]의 2차원 배열로 설정한.
d3.brush.extent() 브러시의 이동 가능 범위를 설정한다.
[[x0, y0], [x1, y1]]의 2차원 배열로 설정한다.
d3.brush.handleSize() 브러시의 핸들 크기를 설정한다.
기본값은 6이다.
d3.brush.on(typenames , function) 이벤트 시 호출할 함수를 설정한다. typenames 는 다음의 3종류로부터 설정한다.
start - 선택 개시시.
brush - 범위 선택의 변경시.
end - 선택 종료시.

이번에는 on 메소드를 사용하여 start, brush시에 호출하는 이벤트 리스너(brushed)를 설정한다.

.on("start brush", brushed);

이벤트 리스너(이벤트 시에 호출되는 함수)는 이벤트 리스너가 불려 갔을 때에 설정되는 event의 필드를 사용해 설정한다.

이벤트 필드 설명
event.target 관련 brush 비헤이비어에 대한 참조
event.type 현재 이벤트. "start", "brush", "end" 중 어느 것?
event.selection 현재 brush의 선택 범위. [[x0, y0], [x1, y1]]의 2차원 배열.
event.sourceEvent mousemove, touchmove 등 brush 이벤트의 근원이 되는 이벤트 종류

필드 선택을 사용하여 선택한 범위 내의 산포도 플롯에 스타일을 설정한다. .invert는 산포도상의 좌표를 화면상의 좌표로 변환하는 함수 xScale, yScale을 역변환하여 화면상의 좌표에서 산포도상의 좌표로 변환하는 함수로 변경하는 메소드이다.

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

classed 메서드를 사용하여 두 번째 인수의 조건식이 참인 경우 "selected" 클래스를 설정하고 거짓인 경우 제외한다.

마무리

터치 패널의 범위 선택이 여기서 소개한 벙법으로는 어렵고, D3.js 범위 선택(d3.brush) 사용법 - 터치 패널 대응 이 페이지를 참고하길 바란다.




최종 수정 : 2024-01-18