Skip to content

Instantly share code, notes, and snippets.

@kkdd
Last active April 27, 2022 12:40
Show Gist options
  • Save kkdd/1d8a098d299c763a3a49f8b4f0a60cd0 to your computer and use it in GitHub Desktop.
Save kkdd/1d8a098d299c763a3a49f8b4f0a60cd0 to your computer and use it in GitHub Desktop.
regl + d3.js: Zoomable WegGL scatter plot with reading data files dropped-onto

Run this example in a full screen, or click "Open".

A sample data file for dropping:

$ ./randomPointsGen.py 8000000 > points_data_for_dropping.txt
$ head -n 6 points_data_for_dropping.txt
121.762 17.147 848500ff
29.202 24.338 44b300ff
-63.768 106.568 418d00ff
-115.64 -0.136 bccf00ff
-75.727 10.618 b62c00ff
-63.024 94.299 b31000ff
$ cat randomPointsGen.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
from random import gauss
from random import randint

n = 10
if len(sys.argv) > 1:
  n = int(sys.argv[1])

sigma = 100

def randomGauss(n):
  return [round(gauss(0, sigma), 3) for _ in range(n)]

def randomHex(n):
  return ''.join([format(randint(0,255), '02x') for _ in range(n)])

for i in range(n):
  print(*randomGauss(2), randomHex(2) + "00ff")

exit(0)
<!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%;
}
#debug {
z-index: 10;
}
</style>
</head>
<body>
<div id="canvas"></div>
<div id='debug' style='padding:4px;background-color:#fffc;position:absolute;right:0;top:0;color:#0af;font-family:courier;font-size:12px;user-select:none'></div>
</body>
<script src="https://npmcdn.com/regl/dist/regl.min.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script language="javascript">
const pointSize = 6, nTick = 10, tickPadding = {x: 12, y: 6};
const BYTESIZE = 1 // GL_UNSIGNED_BYTE
const FLOATSIZE = 4 // GL_FLOAT
const THOUSAND = 1000;
const container = "#canvas";
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});
let d3Transform = d3.zoomIdentity;
let [widthLatest, heightLatest] = widthHeight(canvas);
const maxPoints = 10 * THOUSAND * THOUSAND
const XYSIZE = FLOATSIZE * 2; // vec2 (x, y); position
const RGBASIZE = BYTESIZE * 4; // vec4 (r, g, b, a); uint8 is normalized and converted to float in regl().
const reglData = {
count: 0, // the number of points to draw
offset: 0,
position: regl.buffer({
usage: 'dynamic',
type: 'float',
length: XYSIZE * maxPoints
}),
color: regl.buffer({
usage: 'dynamic',
type: 'uint8',
length: RGBASIZE * maxPoints
}),
transform: {}, // reglTransform
}
const addPoints = (points) => {
const nPoints = points.length;
if (nPoints > maxPoints) return;
if (nPoints + reglData.offset > maxPoints) {
reglData.count = reglData.offset;
reglData.offset = 0;
}
const max = (a, b) => a>b?a:b;
const position = points.map(v=>[v[0],v[1]]);
const color = points.map(v=>hex2rgb(v[2]));
reglData.position.subdata(position, reglData.offset * XYSIZE);
reglData.color.subdata(color, reglData.offset * RGBASIZE);
reglData.offset += nPoints
reglData.count = max(reglData.count, reglData.offset);
showDebug();
}
const reglTransform = () => {
const [width, height] = widthHeight(canvas);
const scale = [d3Transform.k/width*2, d3Transform.k/height*2];
const offset = [d3Transform.x/width*2 + (d3Transform.k-1), -d3Transform.y/height*2 - (d3Transform.k-1)];
return {scale: scale, offset: offset};
}
const drawPoints = regl({
profile: true,
depth: {enable: false},
stencil: {enable: false},
primitive: 'points',
count: regl.prop('count'),
uniforms: {
scale: regl.prop('transform.scale'),
offset: regl.prop('transform.offset'),
pointSize: pointSize,
},
attributes: {
position: { // vec2
buffer: regl.prop('position'),
},
color: { // vec4
buffer: regl.prop('color'),
normalized: true, // uint8 is normalized and converted to float
}
},
frag: `
precision mediump float;
varying vec4 fill;
void main() {
gl_FragColor = fill;
}
`,
vert: `
precision mediump float;
attribute vec2 position;
attribute vec4 color;
uniform vec2 scale;
uniform vec2 offset;
uniform float pointSize;
varying vec4 fill;
void main() {
gl_PointSize = pointSize;
gl_Position = vec4(position*scale+offset, 0, 1);
fill = color;
}
`,
});
let redrawRequested = false;
regl.frame(()=>{
if (redrawRequested) {drawPoints(reglData); redrawRequested=false;}
});
const drawNewPoints = (newPoints) => {
addPoints(newPoints);
redrawRequested = true
}
const render = () => {
const [width, height] = widthHeight(canvas);
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 / height * nTick)
.tickSize(height)
.tickPadding(-tickPadding.x);
const yAxis = d3.axisRight(yScale)
.ticks(nTick)
.tickSize(width)
.tickPadding(-width + tickPadding.y);
const zoomed = (event, d) => {
d3Transform = event.transform;
gX.call(xAxis.scale(d3Transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3Transform.rescaleY(yScale)));
reglData.transform = reglTransform();
redrawRequested = true;
}
const zoom = d3.zoom().on("zoom", zoomed);
svg.call(zoom).call(zoom.transform, d3Transform);
};
const resizeRender = () => {
const [width, height] = widthHeight(canvas);
const v = (1/d3Transform.k-1)/2;
d3Transform = d3Transform.translate((width-widthLatest)*v, (height-heightLatest)*v);
[widthLatest, heightLatest] = [width, height];
regl.poll();
render();
}
render();
window.addEventListener('resize', resizeRender);
const dropArea = document.getElementById('canvas');
dropArea.ondragover = () => false;
dropArea.ondragend = () => false;
dropArea.ondrop = (e) => {
[...e.dataTransfer.files].forEach(readLines);
e.preventDefault();
return false;
};
const transformStream = (file) => {
const name = file.name.split('.');
const ext = name.pop();
const compressed = (ext == 'gz');
return compressed?new DecompressionStream('gzip'):new TransformStream();
}
const readLines = (file) => {
const stream = file.stream().pipeThrough(transformStream(file));
const reader = stream.getReader();
const decoder = new TextDecoder();
let remaining = "";
reader.read().then(function doChunk({value, done}) {
if (done) return;
const lines = (remaining + decoder.decode(value)).split("\n");
remaining = lines.pop();
(async () => {
drawNewPoints(lines.map(line2array));
await wait();
reader.read().then(doChunk);
})();
});
}
function wait() {return new Promise(resolve => setTimeout(resolve, 0))};
function line2array(line) {return line.split(" ").map(w => {const v = Number(w); return isNaN(v)?w:v})};
function hex2rgb (hex) {
if (!hex) return randomColor();
if (hex.charAt(0)=="#") hex=hex.slice(1);
const rgb = [...Array(hex.length/2).keys()].map(i=>parseInt(hex.slice(i*2,(i+1)*2),16));
if (rgb.length == 3) {rgb.push(255)}
return rgb;
}
function randomColor() {
const rand = n => Math.floor(Math.random() * n);
return [rand(256), rand(256), 0, 255]
}
function widthHeight(canvas) {
const r = canvas.getBoundingClientRect();
return [r.width, r.height];
};
function showDebug() {
const str = `total = ${reglData.count.toLocaleString()}`;
document.getElementById("debug").innerText = str;
}
const data_initial = [[0,0,"ff0000"], [100,100,"ff0000"]];
drawNewPoints(data_initial);
</script>
</html>
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from random import gauss
from random import randint
n = 10
if len(sys.argv) > 1:
n = int(sys.argv[1])
sigma = 100
def randomGauss(n):
return [round(gauss(0, sigma), 3) for _ in range(n)]
def randomHex(n):
return ''.join([format(randint(0,255), '02x') for _ in range(n)])
for i in range(n):
print(*randomGauss(2), randomHex(2) + "00ff")
exit(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment