大量の点の 2 次元プロット（グリッドライン付き）（regl + d3.js）

こんにちは。
大量の点の 2 次元プロットを行いました。regl (WelGL ライブラリ）の利用に加え[^1]、d3.js を利用し[^2]、マウスイベント処理（ズーム、スクロール）およびグリッドライン描画を行いました。

[^1]: 参考：「[大量の点の 2 次元プロット（regl）](https://qiita.com/kkdd/items/8b46c5e7362397a9c627)」
[^2]: 参考：「[Zoomable Scatterplot (d3.js)](https://qiita.com/kkdd/items/3ba7c54d581467cb34af)」

<img width="260" alt="scatter.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54617/5b0e5eb1-4651-4121-7eb6-818d8a22b182.jpeg">


```scatter.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <style>
    html,body{
        height: 100%;
        margin: 0;
    }
	.axis path {
	    display: none;
	}
	.axis line {
	    stroke-opacity: 0.1;
	    shape-rendering: crispEdges;
	}
	svg,
    #canvas {
      position: absolute;
      width: 100%;
      height: 100%;
    }
    </style>
  </head>
  <body>
    <div id="canvas"></div>
  </body>
  <script language="javascript" src="https://npmcdn.com/regl/dist/regl.min.js"></script>
  <script src="https://d3js.org/d3.v6.min.js"></script>
  <script language="javascript">
	const pointSize = 8, container = "#canvas";
	const nPoints =   1000;
	let transform = d3.zoomIdentity;
	const canvas = document.querySelector(container);
    const svg = d3.select(container).append('svg');
    const gX = svg.append("g").attr("class", "axis axis--x");
    const gY = svg.append("g").attr("class", "axis axis--y");
	const regl = createREGL({container: canvas});

	const createScatterPoints = () => {
		const generatePoints = (nPoints) => {
		  const rng = d3.randomNormal(0, 100);
		  return d3.range(nPoints).map(() => (
		      { x: rng(),
		        y: rng(),
		        color: [Math.random(), Math.random(), 0]})
		  );
		}
		
		const points = generatePoints(nPoints);
		
		const frag = `
		  precision highp float;
		  varying vec3 fragColor;
		  void main() {
		    gl_FragColor = vec4(fragColor, 1);
		  }`;
		
		const vert = `
		  varying vec3 fragColor;
		  attribute vec2 pos;
		  attribute vec3 color;
		  uniform float pointSize;
		  uniform vec2 scale;
		  uniform vec2 offset;

		  void main() {
		    fragColor = color;
		    gl_PointSize = pointSize;
		    gl_Position = vec4(pos * scale + offset, 0, 1);
		  }`;
		
		const drawPoints = regl({
		    frag: frag,
		    vert: vert,
		    primitive: 'points',
		    count: points.length,
		    attributes: {
		      pos: points.map(d => [d.x, d.y]),
		      color: points.map(d => d.color)
		    },
		    uniforms: {
		      pointSize: pointSize,
		      scale: regl.prop('scale'),
		      offset: regl.prop('offset'),
		    }
		});
		
		const drawScatterPoints = () => {
			const scale = [transform.k/window.innerWidth*2, transform.k/window.innerHeight*2];
			const offset = [transform.x/window.innerWidth*2 + (transform.k-1), -transform.y/window.innerHeight*2 - (transform.k-1)];
			regl.poll();
			drawPoints({scale: scale, offset: offset});
		};
		
		return drawScatterPoints;
	};
	

	const nTick = 10;
    const drawPoints = createScatterPoints();

	const render = () => {
		drawPoints();
		
		const {width, height} = canvas.getBoundingClientRect();
		const xScale = d3.scaleLinear()
			.domain([-width / 2, width / 2])
			.range([0, width]);
		const yScale = d3.scaleLinear()
			.domain([-height / 2, height / 2])
			.range([height, 0]);
			
		const xAxis = d3.axisBottom(xScale)
			.ticks((width + 2) / (height + 2) * nTick)
			.tickSize(height)
			.tickPadding(-12);
		const yAxis = d3.axisRight(yScale)
			.ticks(nTick)
			.tickSize(width)
			.tickPadding(-width + 6);

		const zoomed = (event, d) => {
			transform = event.transform;
			gX.call(xAxis.scale(transform.rescaleX(xScale)));
			gY.call(yAxis.scale(transform.rescaleY(yScale)));
			drawPoints();
		}

		const zoom = d3.zoom().on("zoom", zoomed);
		svg.call(zoom).call(zoom.transform, transform);
	};

    render();
    window.addEventListener('resize', render);

  </script>
</html>
```