mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 21:28:12 +08:00
Graph/scatterplot improvements (#893)
* many graph/scatterplot fixes: 722, 882, 758, 886 * fix e2e test breakage caused by graph transform work * convert webgl point shaders to use flags rather than explicit point size * remove unused packages from dependencies * fix typo in regl prop name * factor common code into util helper file * legibility tweaks
This commit is contained in:
@@ -27,7 +27,7 @@ export const datasets = {
|
|||||||
lasso: [
|
lasso: [
|
||||||
{
|
{
|
||||||
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
||||||
count: "71"
|
count: "88"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
categorical: [
|
categorical: [
|
||||||
@@ -92,7 +92,7 @@ export const datasets = {
|
|||||||
},
|
},
|
||||||
lasso: {
|
lasso: {
|
||||||
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.65, y2: 0.15 },
|
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.65, y2: 0.15 },
|
||||||
count: "36"
|
count: "39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scatter: {
|
scatter: {
|
||||||
|
|||||||
Generated
+4
-103
@@ -4816,14 +4816,6 @@
|
|||||||
"integrity": "sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ==",
|
"integrity": "sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"canvas-fit": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/canvas-fit/-/canvas-fit-1.5.0.tgz",
|
|
||||||
"integrity": "sha1-rhO+Zq3kL1vg5IfjRfzjCl5bXl8=",
|
|
||||||
"requires": {
|
|
||||||
"element-size": "^1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"capture-exit": {
|
"capture-exit": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
|
||||||
@@ -5888,7 +5880,8 @@
|
|||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"deep-extend": {
|
"deep-extend": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
@@ -6200,11 +6193,6 @@
|
|||||||
"integrity": "sha512-lyoC8aoqbbDqsprb6aPdt9n3DpOZZzdz/T4IZKsR0/dkZIxnJVUjjcpOSwA66jPRIOyDAamCTAUqweU05kKNSg==",
|
"integrity": "sha512-lyoC8aoqbbDqsprb6aPdt9n3DpOZZzdz/T4IZKsR0/dkZIxnJVUjjcpOSwA66jPRIOyDAamCTAUqweU05kKNSg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"element-size": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/element-size/-/element-size-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-ZOXxWdlxIWMYRby67K8nnDm1404="
|
|
||||||
},
|
|
||||||
"elliptic": {
|
"elliptic": {
|
||||||
"version": "6.4.1",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
|
||||||
@@ -6890,11 +6878,6 @@
|
|||||||
"through": "~2.3.1"
|
"through": "~2.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"events": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
|
|
||||||
},
|
|
||||||
"evp_bytestokey": {
|
"evp_bytestokey": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
||||||
@@ -7618,11 +7601,6 @@
|
|||||||
"readable-stream": "^2.0.4"
|
"readable-stream": "^2.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"font-color-contrast": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/font-color-contrast/-/font-color-contrast-1.0.3.tgz",
|
|
||||||
"integrity": "sha1-d9iP+Xxr0JXPqd/rRGJLqK1xe2w="
|
|
||||||
},
|
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
@@ -11673,14 +11651,6 @@
|
|||||||
"array-includes": "^3.0.3"
|
"array-includes": "^3.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"key-pressed": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/key-pressed/-/key-pressed-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-1oL7THwQQpKaW8iGNnKMpxIg3rs=",
|
|
||||||
"requires": {
|
|
||||||
"vkey": "0.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||||
@@ -12311,22 +12281,6 @@
|
|||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mouse-position": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mouse-position/-/mouse-position-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-Ra8/RFn9e9/D2v9syhF5bLo4yiWvqZzcC/5bHOvkixDAkvO3WhlWN2xQRHSirMoYk2bagIae2upXXwxTxzI3BQ==",
|
|
||||||
"requires": {
|
|
||||||
"events": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mouse-pressed": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mouse-pressed/-/mouse-pressed-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-wB4UGA+NoSBQc5lZOeT5b+8ifjI=",
|
|
||||||
"requires": {
|
|
||||||
"events": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
@@ -12796,21 +12750,6 @@
|
|||||||
"wordwrap": "~1.0.0"
|
"wordwrap": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"orbit-camera": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/orbit-camera/-/orbit-camera-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-jaFhWrXB53eiv2W5XlzR9Rfx/DE=",
|
|
||||||
"requires": {
|
|
||||||
"gl-matrix": "^2.2.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"gl-matrix": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"os-browserify": {
|
"os-browserify": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
|
||||||
@@ -13512,16 +13451,6 @@
|
|||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"query-string": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg==",
|
|
||||||
"requires": {
|
|
||||||
"decode-uri-component": "^0.2.0",
|
|
||||||
"split-on-first": "^1.0.0",
|
|
||||||
"strict-uri-encode": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
@@ -14595,15 +14524,6 @@
|
|||||||
"ajv-keywords": "^3.1.0"
|
"ajv-keywords": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scroll-speed": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/scroll-speed/-/scroll-speed-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-3P5AlRbnYXVZe2BgACtRznLubtc=",
|
|
||||||
"requires": {
|
|
||||||
"events": "^1.0.0",
|
|
||||||
"wheel": "0.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||||
@@ -15070,11 +14990,6 @@
|
|||||||
"through": "2"
|
"through": "2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"split-on-first": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
|
|
||||||
},
|
|
||||||
"split-string": {
|
"split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||||
@@ -15269,11 +15184,6 @@
|
|||||||
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
|
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strict-uri-encode": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
|
|
||||||
},
|
|
||||||
"string-length": {
|
"string-length": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||||
@@ -16259,7 +16169,8 @@
|
|||||||
"urijs": {
|
"urijs": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz",
|
||||||
"integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg=="
|
"integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"urix": {
|
"urix": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -16395,11 +16306,6 @@
|
|||||||
"extsprintf": "^1.2.0"
|
"extsprintf": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vkey": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/vkey/-/vkey-0.0.3.tgz",
|
|
||||||
"integrity": "sha1-Y03XlS6ywRUxvxhZ8ObUGUeAM98="
|
|
||||||
},
|
|
||||||
"vm-browserify": {
|
"vm-browserify": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
|
||||||
@@ -16719,11 +16625,6 @@
|
|||||||
"webidl-conversions": "^4.0.2"
|
"webidl-conversions": "^4.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wheel": {
|
|
||||||
"version": "0.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wheel/-/wheel-0.0.0.tgz",
|
|
||||||
"integrity": "sha1-BP+b6YcXXese3U8kgui2V/MqQWU="
|
|
||||||
},
|
|
||||||
"which": {
|
"which": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||||
|
|||||||
+1
-10
@@ -34,23 +34,16 @@
|
|||||||
"@blueprintjs/core": "^3.15.1",
|
"@blueprintjs/core": "^3.15.1",
|
||||||
"@blueprintjs/icons": "^3.8.0",
|
"@blueprintjs/icons": "^3.8.0",
|
||||||
"@blueprintjs/select": "^3.8.0",
|
"@blueprintjs/select": "^3.8.0",
|
||||||
"canvas-fit": "^1.5.0",
|
|
||||||
"d3": "^4.10.0",
|
"d3": "^4.10.0",
|
||||||
"d3-scale-chromatic": "^1.3.0",
|
"d3-scale-chromatic": "^1.3.0",
|
||||||
"flatbuffers": "^1.10.2",
|
"flatbuffers": "^1.10.2",
|
||||||
"font-color-contrast": "^1.0.3",
|
|
||||||
"fuzzysort": "^1.1.4",
|
"fuzzysort": "^1.1.4",
|
||||||
"gl-mat4": "^1.1.4",
|
"gl-mat4": "^1.1.4",
|
||||||
"gl-matrix": "^3.0.0",
|
"gl-matrix": "^3.0.0",
|
||||||
"gl-vec3": "^1.1.3",
|
"gl-vec3": "^1.1.3",
|
||||||
"is-number": "^7.0.0",
|
"is-number": "^7.0.0",
|
||||||
"key-pressed": "0.0.1",
|
|
||||||
"lodash": "^4.17.14",
|
"lodash": "^4.17.14",
|
||||||
"memoize-one": "^5.0.4",
|
"memoize-one": "^5.0.4",
|
||||||
"mouse-position": "^2.0.1",
|
|
||||||
"mouse-pressed": "^1.0.0",
|
|
||||||
"orbit-camera": "^1.0.0",
|
|
||||||
"query-string": "^6.5.0",
|
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-autocomplete": "^1.7.2",
|
"react-autocomplete": "^1.7.2",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
@@ -59,9 +52,7 @@
|
|||||||
"react-redux": "^7.0.3",
|
"react-redux": "^7.0.3",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"regl": "^1.3.11",
|
"regl": "^1.3.11"
|
||||||
"scroll-speed": "^1.0.0",
|
|
||||||
"urijs": "^1.19.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.4.4",
|
"@babel/core": "^7.4.4",
|
||||||
|
|||||||
@@ -1,58 +1,64 @@
|
|||||||
// jshint esversion: 6
|
import { glPointFlags } from "../../util/glHelpers";
|
||||||
const mat4 = require("gl-mat4");
|
|
||||||
const vec3 = require("gl-vec3");
|
|
||||||
|
|
||||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
|
||||||
|
|
||||||
export default function(regl) {
|
export default function(regl) {
|
||||||
return regl({
|
return regl({
|
||||||
vert: `
|
vert: `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
attribute vec2 position;
|
attribute vec2 position;
|
||||||
attribute vec3 color;
|
attribute vec3 color;
|
||||||
attribute float size;
|
attribute float flag;
|
||||||
|
|
||||||
uniform float distance;
|
uniform float distance;
|
||||||
uniform mat4 projection, view;
|
uniform mat3 projView;
|
||||||
varying vec3 fragColor;
|
uniform float nPoints;
|
||||||
|
uniform float minViewportDimension;
|
||||||
|
|
||||||
|
varying vec4 fragColor;
|
||||||
|
|
||||||
|
const float zBottom = 0.99;
|
||||||
|
const float zMiddle = 0.;
|
||||||
|
const float zTop = -1.;
|
||||||
|
|
||||||
|
// import getFlags()
|
||||||
|
${glPointFlags}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
bool isNaN, isSelected, isHighlight;
|
||||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
getFlags(flag, isNaN, isSelected, isHighlight);
|
||||||
fragColor = color;
|
|
||||||
|
float size = isHighlight ? 8. : isSelected ? 4. : 1.;
|
||||||
|
gl_PointSize = (0.5 * pow(distance, 2.5)) + size;
|
||||||
|
|
||||||
|
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||||
|
vec3 xy = projView * vec3(position, 1.);
|
||||||
|
gl_Position = vec4(xy.xy, z, 1.);
|
||||||
|
|
||||||
|
float alpha = isNaN ? 0.9 : 1.0;
|
||||||
|
fragColor = vec4(color, alpha);
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
frag: `
|
frag: `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
varying vec3 fragColor;
|
varying vec4 fragColor;
|
||||||
void main() {
|
void main() {
|
||||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
gl_FragColor = vec4(fragColor, 1);
|
gl_FragColor = fragColor;
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
position: regl.prop("position"),
|
position: regl.prop("position"),
|
||||||
color: regl.prop("color"),
|
color: regl.prop("color"),
|
||||||
size: regl.prop("size")
|
flag: regl.prop("flag")
|
||||||
},
|
},
|
||||||
|
|
||||||
uniforms: {
|
uniforms: {
|
||||||
distance: regl.prop("distance"),
|
distance: regl.prop("distance"),
|
||||||
view: regl.prop("view"),
|
projView: regl.prop("projView"),
|
||||||
projection: ({ viewportWidth, viewportHeight }) => {
|
nPoints: regl.prop("nPoints"),
|
||||||
const aspectRatio = viewportWidth / viewportHeight;
|
minViewportDimension: regl.prop("minViewportDimension")
|
||||||
let m = mat4.perspective(
|
|
||||||
[],
|
|
||||||
Math.PI / 2,
|
|
||||||
viewportWidth / viewportHeight,
|
|
||||||
0.01,
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
if (aspectRatio < 1) {
|
|
||||||
m = mat4.scale(m, m, vec3.fromValues(1, 1, 1 / aspectRatio));
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
count: regl.prop("count"),
|
count: regl.prop("count"),
|
||||||
|
|||||||
@@ -2,21 +2,70 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import mat4 from "gl-mat4";
|
import { mat3, vec2 } from "gl-matrix";
|
||||||
import _regl from "regl";
|
import _regl from "regl";
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import setupSVGandBrushElements from "./setupSVGandBrush";
|
import setupSVGandBrushElements from "./setupSVGandBrush";
|
||||||
import setupCentroidSVG from "./setupCentroidSVG";
|
import setupCentroidSVG from "./setupCentroidSVG";
|
||||||
import actions from "../../actions";
|
|
||||||
import _camera from "../../util/camera";
|
import _camera from "../../util/camera";
|
||||||
import _drawPoints from "./drawPointsRegl";
|
import _drawPoints from "./drawPointsRegl";
|
||||||
import scaleLinear from "../../util/scaleLinear";
|
import { isTypedArray } from "../../util/typeHelpers";
|
||||||
|
|
||||||
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
|
/*
|
||||||
|
Simple 2D transforms control all point painting. There are three:
|
||||||
|
* model - convert from underlying per-point coordinate to a layout.
|
||||||
|
Currently used to move from data to webgl coordinate system.
|
||||||
|
* camera - apply a 2D camera transformation (pan, zoom)
|
||||||
|
* projection - apply any transformation required for screen size and layout
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createProjectionTF(viewportWidth, viewportHeight) {
|
||||||
|
/*
|
||||||
|
the projection transform accounts for the screen size & other layout
|
||||||
|
*/
|
||||||
|
const fractionToUse = 0.95; // fraction of min dimension to use
|
||||||
|
const topGutterSizePx = 32; // toolbar box height
|
||||||
|
const heightMinusGutter = viewportHeight - topGutterSizePx;
|
||||||
|
const minDim = Math.min(viewportWidth, heightMinusGutter);
|
||||||
|
const aspectScale = [
|
||||||
|
(fractionToUse * minDim) / viewportWidth,
|
||||||
|
(fractionToUse * minDim) / viewportHeight
|
||||||
|
];
|
||||||
|
const m = mat3.create();
|
||||||
|
mat3.fromTranslation(m, [
|
||||||
|
0,
|
||||||
|
-topGutterSizePx / viewportHeight / aspectScale[1]
|
||||||
|
]);
|
||||||
|
mat3.scale(m, m, aspectScale);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModelTF() {
|
||||||
|
/*
|
||||||
|
preallocate coordinate system transformation between data and gl.
|
||||||
|
Data arrives in a [0,1] range, and we operate elsewhere in [-1,1].
|
||||||
|
*/
|
||||||
|
const m = mat3.fromScaling(mat3.create(), [2, 2]);
|
||||||
|
mat3.translate(m, m, [-0.5, -0.5]);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderThrottle(callback) {
|
||||||
|
let rafCurrentlyInProgress = null;
|
||||||
|
return function f() {
|
||||||
|
if (rafCurrentlyInProgress) return;
|
||||||
|
const context = this;
|
||||||
|
rafCurrentlyInProgress = window.requestAnimationFrame(() => {
|
||||||
|
callback.apply(context);
|
||||||
|
rafCurrentlyInProgress = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
|
universe: state.universe,
|
||||||
world: state.world,
|
world: state.world,
|
||||||
crossfilter: state.crossfilter,
|
crossfilter: state.crossfilter,
|
||||||
responsive: state.responsive,
|
responsive: state.responsive,
|
||||||
@@ -29,14 +78,16 @@ import scaleLinear from "../../util/scaleLinear";
|
|||||||
colorAccessor: state.colors.colorAccessor
|
colorAccessor: state.colors.colorAccessor
|
||||||
}))
|
}))
|
||||||
class Graph extends React.Component {
|
class Graph extends React.Component {
|
||||||
computePointPositions = memoize((X, Y, scaleX, scaleY) => {
|
computePointPositions = memoize((X, Y, modelTF) => {
|
||||||
/*
|
/*
|
||||||
compute webgl coordinate buffer for each point
|
compute the model coordinate for each point
|
||||||
*/
|
*/
|
||||||
const positions = new Float32Array(2 * X.length);
|
const positions = new Float32Array(2 * X.length);
|
||||||
for (let i = 0, len = X.length; i < len; i += 1) {
|
for (let i = 0, len = X.length; i < len; i += 1) {
|
||||||
positions[2 * i] = scaleX(X[i]);
|
const p = vec2.fromValues(X[i], Y[i]);
|
||||||
positions[2 * i + 1] = scaleY(Y[i]);
|
vec2.transformMat3(p, p, modelTF);
|
||||||
|
positions[2 * i] = p[0];
|
||||||
|
positions[2 * i + 1] = p[1];
|
||||||
}
|
}
|
||||||
return positions;
|
return positions;
|
||||||
});
|
});
|
||||||
@@ -52,38 +103,65 @@ class Graph extends React.Component {
|
|||||||
return colors;
|
return colors;
|
||||||
});
|
});
|
||||||
|
|
||||||
computePointSizesFromCrossfilter = memoize((len, crossfilter) => {
|
computeSelectedFlags = memoize(
|
||||||
const sizes = new Float32Array(len);
|
(crossfilter, flagSelected, flagUnselected) => {
|
||||||
crossfilter.fillByIsSelected(sizes, 4, 0.2);
|
const x = crossfilter.fillByIsSelected(
|
||||||
|
new Float32Array(crossfilter.size()),
|
||||||
return sizes;
|
flagSelected,
|
||||||
});
|
flagUnselected
|
||||||
|
|
||||||
computePointSizes = memoize(
|
|
||||||
(len, crossfilter, metadataField, categoryField) => {
|
|
||||||
/*
|
|
||||||
compute webgl dot size for each point
|
|
||||||
*/
|
|
||||||
|
|
||||||
const selectionSizes = this.computePointSizesFromCrossfilter(
|
|
||||||
len,
|
|
||||||
crossfilter
|
|
||||||
);
|
);
|
||||||
let sizes;
|
return x;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (metadataField && categoryField) {
|
computePointFlags = memoize(
|
||||||
sizes = selectionSizes.slice();
|
(world, crossfilter, colorAccessor, centroidLabel) => {
|
||||||
const valuesArr = crossfilter.data.col(metadataField).asArray();
|
/*
|
||||||
|
We communicate with the shader using three flags:
|
||||||
|
- isNaN -- the value is a NaN. Only makes sense when we have a colorAccessor
|
||||||
|
- isSelected -- the value is selected
|
||||||
|
- isHightlighted -- the value is highlighted in the UI (orthogonal from selection highlighting)
|
||||||
|
|
||||||
for (let i = 0; i < len; i += 1) {
|
Due to constraints in webgl vertex shader attributes, these are encoded in a float, "kinda"
|
||||||
if (valuesArr[i] === categoryField) {
|
like bitmasks.
|
||||||
sizes[i] = 10;
|
|
||||||
|
We also have separate code paths for generating flags for categorical and
|
||||||
|
continuous metadata, as they rely on different tests, and some of the flags
|
||||||
|
(eg, isNaN) are meaningless in the face of categorical metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const flagSelected = 1;
|
||||||
|
const flagNaN = 2;
|
||||||
|
const flagHighlight = 4;
|
||||||
|
|
||||||
|
const flags = this.computeSelectedFlags(
|
||||||
|
crossfilter,
|
||||||
|
flagSelected,
|
||||||
|
0
|
||||||
|
).slice();
|
||||||
|
|
||||||
|
const { metadataField, categoryField } = centroidLabel;
|
||||||
|
const highlightData = metadataField
|
||||||
|
? world.obsAnnotations.col(metadataField)?.asArray()
|
||||||
|
: null;
|
||||||
|
const colorByColumn = colorAccessor
|
||||||
|
? world.obsAnnotations.col(colorAccessor)?.asArray() ||
|
||||||
|
world.varData.col(colorAccessor)?.asArray()
|
||||||
|
: null;
|
||||||
|
const colorByData =
|
||||||
|
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null;
|
||||||
|
|
||||||
|
if (colorByData || highlightData) {
|
||||||
|
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||||
|
if (highlightData) {
|
||||||
|
flags[i] += highlightData[i] === categoryField ? flagHighlight : 0;
|
||||||
|
}
|
||||||
|
if (colorByData) {
|
||||||
|
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
sizes = selectionSizes;
|
|
||||||
}
|
}
|
||||||
return sizes;
|
return flags;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,7 +175,8 @@ class Graph extends React.Component {
|
|||||||
Y: null,
|
Y: null,
|
||||||
positions: null,
|
positions: null,
|
||||||
colors: null,
|
colors: null,
|
||||||
sizes: null
|
sizes: null,
|
||||||
|
flags: null
|
||||||
};
|
};
|
||||||
this.state = {
|
this.state = {
|
||||||
toolSVG: null,
|
toolSVG: null,
|
||||||
@@ -108,55 +187,48 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// setup canvas and camera
|
// setup canvas, webgl draw function and camera
|
||||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
const camera = _camera(this.reglCanvas, {
|
||||||
|
pan: true,
|
||||||
|
scale: true,
|
||||||
|
rotate: false
|
||||||
|
});
|
||||||
const regl = _regl(this.reglCanvas);
|
const regl = _regl(this.reglCanvas);
|
||||||
|
|
||||||
const drawPoints = _drawPoints(regl);
|
const drawPoints = _drawPoints(regl);
|
||||||
|
|
||||||
// preallocate buffers
|
// preallocate webgl buffers
|
||||||
const pointBuffer = regl.buffer();
|
const pointBuffer = regl.buffer();
|
||||||
const colorBuffer = regl.buffer();
|
const colorBuffer = regl.buffer();
|
||||||
const sizeBuffer = regl.buffer();
|
const flagBuffer = regl.buffer();
|
||||||
|
|
||||||
// preallocate coordinate system transformation between data and gl
|
// create all default rendering transformations
|
||||||
const fractionToUse = 0.93; // fraction of dimension to use
|
const modelTF = createModelTF();
|
||||||
const shiftForMenuBar = 0.05;
|
const projectionTF = createProjectionTF(
|
||||||
const transform = {
|
this.reglCanvas.width,
|
||||||
glScaleX: scaleLinear([0, 1], [-1 * fractionToUse, 1 * fractionToUse]),
|
this.reglCanvas.height
|
||||||
glScaleY: scaleLinear(
|
);
|
||||||
[0, 1],
|
|
||||||
[
|
|
||||||
(1 + shiftForMenuBar) * fractionToUse,
|
|
||||||
(-1 + shiftForMenuBar) * fractionToUse
|
|
||||||
]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
/* first time, but this duplicates above function, should be possile to avoid this */
|
// initial draw to canvas
|
||||||
const reglRender = regl.frame(() => {
|
this.renderPoints(
|
||||||
this.reglDraw(
|
regl,
|
||||||
regl,
|
drawPoints,
|
||||||
drawPoints,
|
colorBuffer,
|
||||||
sizeBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
flagBuffer,
|
||||||
pointBuffer,
|
camera,
|
||||||
camera
|
projectionTF
|
||||||
);
|
);
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
regl,
|
regl,
|
||||||
drawPoints,
|
drawPoints,
|
||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
sizeBuffer,
|
flagBuffer,
|
||||||
camera,
|
camera,
|
||||||
reglRender,
|
modelTF,
|
||||||
transform
|
modelInvTF: mat3.invert([], modelTF),
|
||||||
|
projectionTF
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,50 +246,47 @@ class Graph extends React.Component {
|
|||||||
colorAccessor,
|
colorAccessor,
|
||||||
centroidLabel
|
centroidLabel
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { reglRender, mode, regl, toolSVG, centroidSVG } = this.state;
|
const { regl, toolSVG, centroidSVG } = this.state;
|
||||||
let stateChanges = {};
|
let stateChanges = {};
|
||||||
|
|
||||||
if (reglRender) {
|
|
||||||
if (
|
|
||||||
// If it IS RENDERING and it is NOT IN ZOOM mode, stop rendering.
|
|
||||||
this.reglRenderState === "rendering" &&
|
|
||||||
graphInteractionMode !== "zoom"
|
|
||||||
) {
|
|
||||||
reglRender.cancel();
|
|
||||||
this.reglRenderState = "paused";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
// If it is NOT RENDERING and it IS IN ZOOM mode, start rendering
|
|
||||||
this.reglRenderState !== "rendering" &&
|
|
||||||
graphInteractionMode === "zoom"
|
|
||||||
) {
|
|
||||||
this.restartReglLoop();
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regl && world) {
|
if (regl && world) {
|
||||||
/* update the regl state */
|
/* update the regl and point rendering state */
|
||||||
const { obsLayout, nObs } = world;
|
const { obsLayout, nObs } = world;
|
||||||
const {
|
const {
|
||||||
drawPoints,
|
drawPoints,
|
||||||
transform,
|
|
||||||
camera,
|
camera,
|
||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
sizeBuffer
|
flagBuffer,
|
||||||
|
modelTF
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
let { projectionTF } = this.state;
|
||||||
|
let needsRepaint = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.responsive.height !== responsive.height ||
|
||||||
|
prevProps.responsive.width !== responsive.width
|
||||||
|
) {
|
||||||
|
projectionTF = createProjectionTF(
|
||||||
|
this.reglCanvas.width,
|
||||||
|
this.reglCanvas.height
|
||||||
|
);
|
||||||
|
needsRepaint = true;
|
||||||
|
stateChanges = {
|
||||||
|
...stateChanges,
|
||||||
|
projectionTF
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/* coordinates for each point */
|
/* coordinates for each point */
|
||||||
const { glScaleX, glScaleY } = transform;
|
|
||||||
const X = obsLayout.col(layoutChoice.currentDimNames[0]).asArray();
|
const X = obsLayout.col(layoutChoice.currentDimNames[0]).asArray();
|
||||||
const Y = obsLayout.col(layoutChoice.currentDimNames[1]).asArray();
|
const Y = obsLayout.col(layoutChoice.currentDimNames[1]).asArray();
|
||||||
const newPositions = this.computePointPositions(X, Y, glScaleX, glScaleY);
|
const newPositions = this.computePointPositions(X, Y, modelTF);
|
||||||
if (renderCache.positions !== newPositions) {
|
if (renderCache.positions !== newPositions) {
|
||||||
/* update our cache & GL if the buffer changes */
|
/* update our cache & GL if the buffer changes */
|
||||||
renderCache.positions = newPositions;
|
renderCache.positions = newPositions;
|
||||||
pointBuffer({ data: newPositions, dimension: 2 });
|
pointBuffer({ data: newPositions, dimension: 2 });
|
||||||
|
needsRepaint = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* colors for each point */
|
/* colors for each point */
|
||||||
@@ -226,33 +295,35 @@ class Graph extends React.Component {
|
|||||||
/* update our cache & GL if the buffer changes */
|
/* update our cache & GL if the buffer changes */
|
||||||
renderCache.colors = newColors;
|
renderCache.colors = newColors;
|
||||||
colorBuffer({ data: newColors, dimension: 3 });
|
colorBuffer({ data: newColors, dimension: 3 });
|
||||||
|
needsRepaint = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sizes for each point */
|
/* flags for each point */
|
||||||
const { metadataField, categoryField } = centroidLabel;
|
const newFlags = this.computePointFlags(
|
||||||
const newSizes = this.computePointSizes(
|
world,
|
||||||
nObs,
|
|
||||||
crossfilter,
|
crossfilter,
|
||||||
metadataField,
|
colorAccessor,
|
||||||
categoryField
|
centroidLabel
|
||||||
);
|
);
|
||||||
if (renderCache.sizes !== newSizes) {
|
if (renderCache.flags !== newFlags) {
|
||||||
/* update our cache & GL if the buffer changes */
|
renderCache.flags = newFlags;
|
||||||
renderCache.size = newSizes;
|
flagBuffer({ data: newFlags, dimension: 1 });
|
||||||
sizeBuffer({ data: newSizes, dimension: 1 });
|
needsRepaint = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.count = nObs;
|
this.count = nObs;
|
||||||
|
|
||||||
regl._refresh();
|
if (needsRepaint) {
|
||||||
this.reglDraw(
|
this.renderPoints(
|
||||||
regl,
|
regl,
|
||||||
drawPoints,
|
drawPoints,
|
||||||
sizeBuffer,
|
colorBuffer,
|
||||||
colorBuffer,
|
pointBuffer,
|
||||||
pointBuffer,
|
flagBuffer,
|
||||||
camera
|
camera,
|
||||||
);
|
projectionTF
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createToolSVG = () => {
|
const createToolSVG = () => {
|
||||||
@@ -311,7 +382,8 @@ class Graph extends React.Component {
|
|||||||
stateChanges = { ...stateChanges, centroidSVG: newCentroidSVG };
|
stateChanges = { ...stateChanges, centroidSVG: newCentroidSVG };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Centroid SVG creation is disabled for now but should go into the first and third cases if enabled
|
// Centroid SVG creation is disabled for now but should go into the
|
||||||
|
// first and third cases if enabled
|
||||||
if (
|
if (
|
||||||
prevProps.responsive.height !== responsive.height ||
|
prevProps.responsive.height !== responsive.height ||
|
||||||
prevProps.responsive.width !== responsive.width
|
prevProps.responsive.width !== responsive.width
|
||||||
@@ -353,6 +425,14 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCanvasEvent = e => {
|
||||||
|
const { camera, projectionTF } = this.state;
|
||||||
|
if (e.type !== "wheel") e.preventDefault();
|
||||||
|
if (camera.handleEvent(e, projectionTF)) {
|
||||||
|
this.renderCanvas();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
brushToolUpdate(tool, container) {
|
brushToolUpdate(tool, container) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
@@ -433,76 +513,25 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
|
||||||
regl.clear({
|
|
||||||
depth: 1,
|
|
||||||
color: [1, 1, 1, 1]
|
|
||||||
});
|
|
||||||
drawPoints({
|
|
||||||
size: sizeBuffer,
|
|
||||||
distance: camera.distance,
|
|
||||||
color: colorBuffer,
|
|
||||||
position: pointBuffer,
|
|
||||||
count: this.count,
|
|
||||||
view: camera.view()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
restartReglLoop() {
|
|
||||||
const {
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
} = this.state;
|
|
||||||
const reglRender = regl.frame(() => {
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
reglRender
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mapScreenToPoint(pin) {
|
mapScreenToPoint(pin) {
|
||||||
/*
|
/*
|
||||||
Map an XY coordinates from screen domain to cell/point range,
|
Map an XY coordinates from screen domain to cell/point range,
|
||||||
accounting for current pan/zoom camera.
|
accounting for current pan/zoom camera.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { responsive } = this.props;
|
const { responsive } = this.props;
|
||||||
const { regl, camera, transform } = this.state;
|
const { camera, projectionTF, modelInvTF } = this.state;
|
||||||
const { glScaleX, glScaleY } = transform;
|
const cameraInvTF = camera.invView();
|
||||||
|
|
||||||
const gl = regl._gl;
|
/* screen -> gl */
|
||||||
|
|
||||||
// get aspect ratio
|
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
|
||||||
const scale = aspect < 1 ? 1 / aspect : 1;
|
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// transform screen coordinates -> cell coordinates
|
|
||||||
const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
||||||
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
||||||
const pout = [
|
|
||||||
x * inverse[14] * aspect * scale + inverse[12],
|
|
||||||
-(y * inverse[14] * scale + inverse[13])
|
|
||||||
];
|
|
||||||
|
|
||||||
const xy = [glScaleX.invert(pout[0]), glScaleY.invert(pout[1])];
|
const xy = vec2.fromValues(x, y);
|
||||||
|
const projectionInvTF = mat3.invert(mat3.create(), projectionTF);
|
||||||
|
vec2.transformMat3(xy, xy, projectionInvTF);
|
||||||
|
vec2.transformMat3(xy, xy, cameraInvTF);
|
||||||
|
vec2.transformMat3(xy, xy, modelInvTF);
|
||||||
return xy;
|
return xy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,29 +540,21 @@ class Graph extends React.Component {
|
|||||||
Map an XY coordinate from cell/point domain to screen range. Inverse
|
Map an XY coordinate from cell/point domain to screen range. Inverse
|
||||||
of mapScreenToPoint()
|
of mapScreenToPoint()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { responsive } = this.props;
|
const { responsive } = this.props;
|
||||||
const { regl, camera, transform } = this.state;
|
const { camera, projectionTF, modelTF } = this.state;
|
||||||
const { glScaleX, glScaleY } = transform;
|
const cameraTF = camera.view();
|
||||||
|
|
||||||
const gl = regl._gl;
|
const xy = vec2.transformMat3(vec2.create(), xyCell, modelTF);
|
||||||
|
vec2.transformMat3(xy, xy, cameraTF);
|
||||||
// get aspect ratio
|
vec2.transformMat3(xy, xy, projectionTF);
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
|
||||||
const scale = aspect < 1 ? 1 / aspect : 1;
|
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// variable names are choosen to reflect inverse of those used
|
|
||||||
// in mapScreenToPoint().
|
|
||||||
const pout = [glScaleX(xyCell[0]), glScaleY(xyCell[1])];
|
|
||||||
const x = (pout[0] - inverse[12]) / aspect / scale / inverse[14];
|
|
||||||
const y = (-pout[1] - inverse[13]) / scale / inverse[14];
|
|
||||||
|
|
||||||
const pin = [
|
const pin = [
|
||||||
Math.round(((x + 1) * (responsive.width - this.graphPaddingRight)) / 2),
|
|
||||||
Math.round(
|
Math.round(
|
||||||
-((y + 1) / 2 - 1) * (responsive.height - this.graphPaddingTop)
|
((xy[0] + 1) * (responsive.width - this.graphPaddingRight)) / 2
|
||||||
|
),
|
||||||
|
Math.round(
|
||||||
|
-((xy[1] + 1) / 2 - 1) * (responsive.height - this.graphPaddingTop)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
return pin;
|
return pin;
|
||||||
@@ -654,8 +675,61 @@ class Graph extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPoints(
|
||||||
|
regl,
|
||||||
|
drawPoints,
|
||||||
|
colorBuffer,
|
||||||
|
pointBuffer,
|
||||||
|
flagBuffer,
|
||||||
|
camera,
|
||||||
|
projectionTF
|
||||||
|
) {
|
||||||
|
const { universe } = this.props;
|
||||||
|
if (!this.reglCanvas || !universe) return;
|
||||||
|
const cameraTF = camera.view();
|
||||||
|
const projView = mat3.multiply(mat3.create(), projectionTF, cameraTF);
|
||||||
|
const { width, height } = this.reglCanvas;
|
||||||
|
regl.poll();
|
||||||
|
regl.clear({
|
||||||
|
depth: 1,
|
||||||
|
color: [1, 1, 1, 1]
|
||||||
|
});
|
||||||
|
drawPoints({
|
||||||
|
distance: camera.distance(),
|
||||||
|
color: colorBuffer,
|
||||||
|
position: pointBuffer,
|
||||||
|
flag: flagBuffer,
|
||||||
|
count: this.count,
|
||||||
|
projView,
|
||||||
|
nPoints: universe.nObs,
|
||||||
|
minViewportDimension: Math.min(width || 800, height || 600)
|
||||||
|
});
|
||||||
|
regl._gl.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCanvas = renderThrottle(() => {
|
||||||
|
const {
|
||||||
|
regl,
|
||||||
|
drawPoints,
|
||||||
|
colorBuffer,
|
||||||
|
pointBuffer,
|
||||||
|
flagBuffer,
|
||||||
|
camera,
|
||||||
|
projectionTF
|
||||||
|
} = this.state;
|
||||||
|
this.renderPoints(
|
||||||
|
regl,
|
||||||
|
drawPoints,
|
||||||
|
colorBuffer,
|
||||||
|
pointBuffer,
|
||||||
|
flagBuffer,
|
||||||
|
camera,
|
||||||
|
projectionTF
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { responsive, graphInteractionMode } = this.props;
|
const { responsive } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="graphWrapper">
|
<div id="graphWrapper">
|
||||||
@@ -676,6 +750,11 @@ class Graph extends React.Component {
|
|||||||
ref={canvas => {
|
ref={canvas => {
|
||||||
this.reglCanvas = canvas;
|
this.reglCanvas = canvas;
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={this.handleCanvasEvent}
|
||||||
|
onMouseUp={this.handleCanvasEvent}
|
||||||
|
onMouseMove={this.handleCanvasEvent}
|
||||||
|
onDoubleClick={this.handleCanvasEvent}
|
||||||
|
onWheel={this.handleCanvasEvent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
// jshint esversion: 6
|
import { glPointFlags } from "../../util/glHelpers";
|
||||||
const mat4 = require("gl-mat4");
|
|
||||||
|
|
||||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
|
||||||
|
|
||||||
export default function(regl) {
|
export default function(regl) {
|
||||||
return regl({
|
return regl({
|
||||||
vert: `
|
vert: `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
attribute vec2 position;
|
attribute vec2 position;
|
||||||
attribute vec3 color;
|
attribute vec3 color;
|
||||||
attribute float size;
|
attribute float flag;
|
||||||
uniform float distance;
|
|
||||||
uniform mat4 projection, view;
|
uniform mat3 projection;
|
||||||
varying vec3 fragColor;
|
uniform float nPoints;
|
||||||
|
uniform float minViewportDimension;
|
||||||
|
|
||||||
|
varying vec4 fragColor;
|
||||||
|
|
||||||
|
const float zBottom = 0.99;
|
||||||
|
const float zMiddle = 0.;
|
||||||
|
const float zTop = -1.;
|
||||||
|
|
||||||
|
// import getFlags()
|
||||||
|
${glPointFlags}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
bool isNaN, isSelected, isHighlight;
|
||||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
getFlags(flag, isNaN, isSelected, isHighlight);
|
||||||
fragColor = color;
|
|
||||||
|
float size = isHighlight ? 8. : isSelected ? 4. : 1.;
|
||||||
|
gl_PointSize = size;
|
||||||
|
|
||||||
|
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||||
|
vec3 xy = projection * vec3(position, 1.);
|
||||||
|
gl_Position = vec4(xy.xy, z, 1.);
|
||||||
|
|
||||||
|
float alpha = isNaN ? 0.9 : 1.0;
|
||||||
|
fragColor = vec4(color, alpha);
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
frag: `
|
frag: `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
varying vec3 fragColor;
|
varying vec4 fragColor;
|
||||||
void main() {
|
void main() {
|
||||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
gl_FragColor = vec4(fragColor, 1);
|
gl_FragColor = fragColor;
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
position: regl.prop("position"),
|
position: regl.prop("position"),
|
||||||
color: regl.prop("color"),
|
color: regl.prop("color"),
|
||||||
size: regl.prop("size")
|
flag: regl.prop("flag")
|
||||||
},
|
},
|
||||||
|
|
||||||
uniforms: {
|
uniforms: {
|
||||||
distance: regl.prop("distance"),
|
projection: regl.prop("projection"),
|
||||||
view: regl.prop("view"),
|
nPoints: regl.prop("nPoints"),
|
||||||
projection: () => mat4.perspective([], Math.PI / 2, 1, 0.01, 1000)
|
minViewportDimension: regl.prop("minViewportDimension")
|
||||||
},
|
},
|
||||||
|
|
||||||
count: regl.prop("count"),
|
count: regl.prop("count"),
|
||||||
|
|||||||
@@ -1,43 +1,40 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
|
|
||||||
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Button, ButtonGroup } from "@blueprintjs/core";
|
import { Button, ButtonGroup } from "@blueprintjs/core";
|
||||||
import _regl from "regl";
|
import _regl from "regl";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
import { mat3 } from "gl-matrix";
|
||||||
|
import memoize from "memoize-one";
|
||||||
|
import { isTypedArray } from "../../util/typeHelpers";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
|
|
||||||
import _camera from "../../util/camera";
|
|
||||||
|
|
||||||
import setupScatterplot from "./setupScatterplot";
|
import setupScatterplot from "./setupScatterplot";
|
||||||
import styles from "./scatterplot.css";
|
import styles from "./scatterplot.css";
|
||||||
|
|
||||||
import _drawPoints from "./drawPointsRegl";
|
import _drawPoints from "./drawPointsRegl";
|
||||||
import scaleLinear from "../../util/scaleLinear";
|
|
||||||
|
|
||||||
import { margin, width, height } from "./util";
|
import { margin, width, height } from "./util";
|
||||||
import finiteExtent from "../../util/finiteExtent";
|
import finiteExtent from "../../util/finiteExtent";
|
||||||
|
|
||||||
|
function createProjectionTF(viewportWidth, viewportHeight) {
|
||||||
|
/*
|
||||||
|
the projection transform accounts for the screen size & other layout
|
||||||
|
*/
|
||||||
|
const m = mat3.create();
|
||||||
|
return mat3.projection(m, viewportWidth, viewportHeight);
|
||||||
|
}
|
||||||
|
|
||||||
@connect(state => {
|
@connect(state => {
|
||||||
const { world, crossfilter } = state;
|
const { world, crossfilter, universe } = state;
|
||||||
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
|
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
|
||||||
const expressionX =
|
const expressionX =
|
||||||
world &&
|
|
||||||
scatterplotXXaccessor &&
|
scatterplotXXaccessor &&
|
||||||
world.varData.hasCol(scatterplotXXaccessor)
|
world.varData.col(scatterplotXXaccessor)?.asArray();
|
||||||
? world.varData.col(scatterplotXXaccessor).asArray()
|
|
||||||
: null;
|
|
||||||
const expressionY =
|
const expressionY =
|
||||||
world &&
|
|
||||||
scatterplotYYaccessor &&
|
scatterplotYYaccessor &&
|
||||||
world.varData.hasCol(scatterplotYYaccessor)
|
world.varData.col(scatterplotYYaccessor)?.asArray();
|
||||||
? world.varData.col(scatterplotYYaccessor).asArray()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
world,
|
world,
|
||||||
|
universe,
|
||||||
|
|
||||||
colorRGB: state.colors.rgb,
|
colorRGB: state.colors.rgb,
|
||||||
colorScale: state.colors.scale,
|
colorScale: state.colors.scale,
|
||||||
@@ -57,6 +54,64 @@ import finiteExtent from "../../util/finiteExtent";
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
class Scatterplot extends React.Component {
|
class Scatterplot extends React.Component {
|
||||||
|
computePointPositions = memoize((X, Y, xScale, yScale) => {
|
||||||
|
const positions = new Float32Array(2 * X.length);
|
||||||
|
for (let i = 0, len = X.length; i < len; i += 1) {
|
||||||
|
positions[2 * i] = xScale(X[i]);
|
||||||
|
positions[2 * i + 1] = yScale(Y[i]);
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
});
|
||||||
|
|
||||||
|
computePointColors = memoize(rgb => {
|
||||||
|
/*
|
||||||
|
compute webgl colors for each point
|
||||||
|
*/
|
||||||
|
const colors = new Float32Array(3 * rgb.length);
|
||||||
|
for (let i = 0, len = rgb.length; i < len; i += 1) {
|
||||||
|
colors.set(rgb[i], 3 * i);
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
});
|
||||||
|
|
||||||
|
computeSelectedFlags = memoize(
|
||||||
|
(crossfilter, flagSelected, flagUnselected) => {
|
||||||
|
const x = crossfilter.fillByIsSelected(
|
||||||
|
new Float32Array(crossfilter.size()),
|
||||||
|
flagSelected,
|
||||||
|
flagUnselected
|
||||||
|
);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
computePointFlags = memoize((world, crossfilter, colorAccessor) => {
|
||||||
|
const flagSelected = 1;
|
||||||
|
const flagNaN = 2;
|
||||||
|
// XXX - coming soon.
|
||||||
|
// const flagHighlight = 4;
|
||||||
|
|
||||||
|
const flags = this.computeSelectedFlags(
|
||||||
|
crossfilter,
|
||||||
|
flagSelected,
|
||||||
|
0
|
||||||
|
).slice();
|
||||||
|
|
||||||
|
const colorByColumn = colorAccessor
|
||||||
|
? world.obsAnnotations.col(colorAccessor)?.asArray() ||
|
||||||
|
world.varData.col(colorAccessor)?.asArray()
|
||||||
|
: null;
|
||||||
|
const colorByData =
|
||||||
|
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null;
|
||||||
|
|
||||||
|
if (colorByData) {
|
||||||
|
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||||
|
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
});
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
@@ -64,7 +119,7 @@ class Scatterplot extends React.Component {
|
|||||||
this.renderCache = {
|
this.renderCache = {
|
||||||
positions: null,
|
positions: null,
|
||||||
colors: null,
|
colors: null,
|
||||||
sizes: null,
|
flags: null,
|
||||||
xScale: null,
|
xScale: null,
|
||||||
yScale: null
|
yScale: null
|
||||||
};
|
};
|
||||||
@@ -85,39 +140,37 @@ class Scatterplot extends React.Component {
|
|||||||
this.renderCache = { ...this.renderCache, ...scales };
|
this.renderCache = { ...this.renderCache, ...scales };
|
||||||
}
|
}
|
||||||
|
|
||||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
|
||||||
const regl = _regl(this.reglCanvas);
|
const regl = _regl(this.reglCanvas);
|
||||||
|
|
||||||
const drawPoints = _drawPoints(regl);
|
const drawPoints = _drawPoints(regl);
|
||||||
|
|
||||||
|
// Create render transform
|
||||||
|
const projectionTF = createProjectionTF(
|
||||||
|
this.reglCanvas.width,
|
||||||
|
this.reglCanvas.height
|
||||||
|
);
|
||||||
|
|
||||||
// preallocate buffers
|
// preallocate buffers
|
||||||
const pointBuffer = regl.buffer();
|
const pointBuffer = regl.buffer();
|
||||||
const colorBuffer = regl.buffer();
|
const colorBuffer = regl.buffer();
|
||||||
const sizeBuffer = regl.buffer();
|
const flagBuffer = regl.buffer();
|
||||||
|
|
||||||
const reglRender = regl.frame(() => {
|
this.renderPoints(
|
||||||
this.reglDraw(
|
regl,
|
||||||
regl,
|
drawPoints,
|
||||||
drawPoints,
|
flagBuffer,
|
||||||
sizeBuffer,
|
colorBuffer,
|
||||||
colorBuffer,
|
pointBuffer,
|
||||||
pointBuffer,
|
projectionTF
|
||||||
camera
|
);
|
||||||
);
|
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
regl,
|
regl,
|
||||||
sizeBuffer,
|
flagBuffer,
|
||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
svg,
|
svg,
|
||||||
reglRender,
|
drawPoints,
|
||||||
camera,
|
projectionTF
|
||||||
drawPoints
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,22 +182,22 @@ class Scatterplot extends React.Component {
|
|||||||
scatterplotYYaccessor,
|
scatterplotYYaccessor,
|
||||||
expressionX,
|
expressionX,
|
||||||
expressionY,
|
expressionY,
|
||||||
colorRGB
|
colorRGB,
|
||||||
|
colorAccessor
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
reglRender,
|
|
||||||
regl,
|
regl,
|
||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
sizeBuffer,
|
flagBuffer,
|
||||||
svg,
|
svg,
|
||||||
drawPoints,
|
drawPoints,
|
||||||
camera
|
projectionTF
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor ||
|
||||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor ||
|
||||||
world !== prevProps.world // shape or clip of world changed
|
world !== prevProps.world // shape or clip of world changed
|
||||||
) {
|
) {
|
||||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||||
@@ -152,76 +205,54 @@ class Scatterplot extends React.Component {
|
|||||||
this.renderCache = { ...this.renderCache, ...scales };
|
this.renderCache = { ...this.renderCache, ...scales };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reglRender && this.reglRenderState === "rendering") {
|
if (world && regl) {
|
||||||
reglRender.cancel();
|
|
||||||
this.reglRenderState = "paused";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
world &&
|
|
||||||
regl &&
|
|
||||||
pointBuffer &&
|
|
||||||
colorBuffer &&
|
|
||||||
sizeBuffer &&
|
|
||||||
expressionX &&
|
|
||||||
expressionY &&
|
|
||||||
scatterplotXXaccessor &&
|
|
||||||
scatterplotYYaccessor
|
|
||||||
) {
|
|
||||||
const { renderCache } = this;
|
const { renderCache } = this;
|
||||||
const { xScale, yScale } = this.renderCache;
|
const { xScale, yScale } = this.renderCache;
|
||||||
const cellCount = expressionX.length;
|
let needsRepaint = false;
|
||||||
|
|
||||||
// Points change when expressionX or expressionY change.
|
const newPositions = this.computePointPositions(
|
||||||
if (
|
expressionX,
|
||||||
!renderCache.positions ||
|
expressionY,
|
||||||
expressionX !== prevProps.expressionX ||
|
xScale,
|
||||||
expressionY !== prevProps.expressionY
|
yScale
|
||||||
) {
|
|
||||||
if (!renderCache.positions) {
|
|
||||||
renderCache.positions = new Float32Array(2 * cellCount);
|
|
||||||
}
|
|
||||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
|
||||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
|
||||||
for (let i = 0, { positions } = renderCache; i < cellCount; i += 1) {
|
|
||||||
positions[2 * i] = glScaleX(xScale(expressionX[i]));
|
|
||||||
positions[2 * i + 1] = glScaleY(yScale(expressionY[i]));
|
|
||||||
}
|
|
||||||
pointBuffer({ data: renderCache.positions, dimension: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Colors for each point - change only when props.colorsRGB change.
|
|
||||||
if (!renderCache.colors || colorRGB !== prevProps.colorRGB) {
|
|
||||||
if (!renderCache.colors) {
|
|
||||||
renderCache.colors = new Float32Array(3 * cellCount);
|
|
||||||
}
|
|
||||||
for (let i = 0, { colors } = renderCache; i < cellCount; i += 1) {
|
|
||||||
colors.set(colorRGB[i], 3 * i);
|
|
||||||
}
|
|
||||||
colorBuffer({ data: renderCache.colors, dimension: 3 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sizes for each point - updates are triggered only when selected
|
|
||||||
// obs change
|
|
||||||
if (!renderCache.sizes || crossfilter !== prevProps.crossfilter) {
|
|
||||||
if (!renderCache.sizes) {
|
|
||||||
renderCache.sizes = new Float32Array(cellCount);
|
|
||||||
}
|
|
||||||
crossfilter.fillByIsSelected(renderCache.sizes, 4, 0.2);
|
|
||||||
sizeBuffer({ data: renderCache.sizes, dimension: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.count = cellCount;
|
|
||||||
|
|
||||||
regl._refresh();
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
);
|
||||||
|
if (renderCache.positions !== newPositions) {
|
||||||
|
renderCache.positions = newPositions;
|
||||||
|
pointBuffer({ data: renderCache.positions, dimension: 2 });
|
||||||
|
needsRepaint = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* colors for each point */
|
||||||
|
const newColors = this.computePointColors(colorRGB);
|
||||||
|
if (renderCache.colors !== newColors) {
|
||||||
|
renderCache.colors = newColors;
|
||||||
|
colorBuffer({ data: renderCache.colors, dimension: 3 });
|
||||||
|
needsRepaint = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFlags = this.computePointFlags(
|
||||||
|
world,
|
||||||
|
crossfilter,
|
||||||
|
colorAccessor
|
||||||
|
);
|
||||||
|
if (renderCache.flags !== newFlags) {
|
||||||
|
renderCache.flags = newFlags;
|
||||||
|
flagBuffer({ data: renderCache.flags, dimension: 1 });
|
||||||
|
needsRepaint = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.count = expressionX.length;
|
||||||
|
|
||||||
|
if (needsRepaint) {
|
||||||
|
this.renderPoints(
|
||||||
|
regl,
|
||||||
|
drawPoints,
|
||||||
|
flagBuffer,
|
||||||
|
colorBuffer,
|
||||||
|
pointBuffer,
|
||||||
|
projectionTF
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,22 +272,6 @@ class Scatterplot extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
|
||||||
regl.clear({
|
|
||||||
depth: 1,
|
|
||||||
color: [1, 1, 1, 1]
|
|
||||||
});
|
|
||||||
|
|
||||||
drawPoints({
|
|
||||||
size: sizeBuffer,
|
|
||||||
distance: camera.distance,
|
|
||||||
color: colorBuffer,
|
|
||||||
position: pointBuffer,
|
|
||||||
count: this.count,
|
|
||||||
view: camera.view()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
drawAxesSVG(xScale, yScale, svg) {
|
drawAxesSVG(xScale, yScale, svg) {
|
||||||
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
||||||
svg.selectAll("*").remove();
|
svg.selectAll("*").remove();
|
||||||
@@ -307,6 +322,34 @@ class Scatterplot extends React.Component {
|
|||||||
.text(scatterplotXXaccessor);
|
.text(scatterplotXXaccessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPoints(
|
||||||
|
regl,
|
||||||
|
drawPoints,
|
||||||
|
flagBuffer,
|
||||||
|
colorBuffer,
|
||||||
|
pointBuffer,
|
||||||
|
projectionTF
|
||||||
|
) {
|
||||||
|
if (!this.reglCanvas) return;
|
||||||
|
const { universe } = this.props;
|
||||||
|
const { width: cvWidth, height: cvHeight } = this.reglCanvas;
|
||||||
|
regl.poll();
|
||||||
|
regl.clear({
|
||||||
|
depth: 1,
|
||||||
|
color: [1, 1, 1, 1]
|
||||||
|
});
|
||||||
|
drawPoints({
|
||||||
|
flag: flagBuffer,
|
||||||
|
color: colorBuffer,
|
||||||
|
position: pointBuffer,
|
||||||
|
projection: projectionTF,
|
||||||
|
count: this.count,
|
||||||
|
nPoints: universe.nObs,
|
||||||
|
minViewportDimension: Math.min(cvWidth || width, cvHeight || height)
|
||||||
|
});
|
||||||
|
regl._gl.flush();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { minimized } = this.state;
|
const { minimized } = this.state;
|
||||||
@@ -367,7 +410,7 @@ class Scatterplot extends React.Component {
|
|||||||
height={height}
|
height={height}
|
||||||
data-testid="scatterplot"
|
data-testid="scatterplot"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: margin.left - 7,
|
marginLeft: margin.left,
|
||||||
marginTop: margin.top
|
marginTop: margin.top
|
||||||
}}
|
}}
|
||||||
ref={canvas => {
|
ref={canvas => {
|
||||||
|
|||||||
+141
-51
@@ -1,66 +1,156 @@
|
|||||||
// jshint esversion: 6
|
import { vec2, mat3 } from "gl-matrix";
|
||||||
const createCamera = require("orbit-camera");
|
|
||||||
const createScroll = require("scroll-speed");
|
|
||||||
const mp = require("mouse-position");
|
|
||||||
const mb = require("mouse-pressed");
|
|
||||||
const key = require("key-pressed");
|
|
||||||
|
|
||||||
const panSpeed = 1.0; // changed from 0.4 to 1.0 per issue #722
|
|
||||||
const scaleSpeed = 0.5;
|
const scaleSpeed = 0.5;
|
||||||
const scaleMax = 3;
|
const scaleMax = 3.0;
|
||||||
const scaleMin = 1.03;
|
const scaleMin = 0.5;
|
||||||
|
|
||||||
function attachCamera(canvas, opts) {
|
// private
|
||||||
opts = opts || {};
|
const scratch0 = new Float32Array(16);
|
||||||
opts.pan = opts.pan !== false;
|
const scratch1 = new Float32Array(16);
|
||||||
opts.scale = opts.scale !== false;
|
|
||||||
opts.rotate = opts.rotate !== false;
|
|
||||||
|
|
||||||
const scroll = createScroll(canvas, opts.scale);
|
class Camera {
|
||||||
const mbut = mb(canvas, opts.rotate);
|
constructor(canvas) {
|
||||||
const mpos = mp(canvas);
|
this.prevEvent = {
|
||||||
const camera = createCamera([0, 0, 1], [0, 0, -1], [0, 1, 0]);
|
clientX: 0,
|
||||||
|
clientY: 0,
|
||||||
|
type: 0
|
||||||
|
};
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.viewMatrix = mat3.create();
|
||||||
|
this.viewMatrixInv = mat3.create();
|
||||||
|
}
|
||||||
|
|
||||||
camera.tick = tick;
|
view() {
|
||||||
|
return this.viewMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
return camera;
|
invView() {
|
||||||
|
return this.viewMatrixInv;
|
||||||
|
}
|
||||||
|
|
||||||
function tick() {
|
distance() {
|
||||||
const ctrl = key("<control>") || key("<alt>");
|
return this.viewMatrix[0];
|
||||||
const alt = key("<shift>");
|
}
|
||||||
const { height, width } = canvas;
|
|
||||||
|
|
||||||
if (opts.rotate && mbut.left && ctrl && !alt) {
|
pan(dx, dy) {
|
||||||
camera.rotate(
|
mat3.translate(this.viewMatrix, this.viewMatrix, [dx, dy]);
|
||||||
[mpos.x / width - 0.5, mpos.y / height - 0.5],
|
mat3.invert(this.viewMatrixInv, this.viewMatrix);
|
||||||
[mpos.prevX / width - 0.5, mpos.prevY / height - 0.5]
|
}
|
||||||
);
|
|
||||||
|
zoomAt(d, x = 0, y = 0) {
|
||||||
|
/*
|
||||||
|
Set camera zoom at [x,y]
|
||||||
|
*/
|
||||||
|
const dClamped =
|
||||||
|
Math.max(Math.min(d * this.viewMatrix[0], scaleMax), scaleMin) /
|
||||||
|
this.viewMatrix[0];
|
||||||
|
if (dClamped === 1) return;
|
||||||
|
|
||||||
|
mat3.translate(this.viewMatrix, this.viewMatrix, [x, y]);
|
||||||
|
mat3.scale(this.viewMatrix, this.viewMatrix, [dClamped, dClamped]);
|
||||||
|
mat3.translate(this.viewMatrix, this.viewMatrix, [-x, -y]);
|
||||||
|
|
||||||
|
mat3.invert(this.viewMatrixInv, this.viewMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Event handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
flush(e) {
|
||||||
|
this.prevEvent.type = e.type;
|
||||||
|
this.prevEvent.clientX = e.clientX;
|
||||||
|
this.prevEvent.clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
localPosition(target, canvasX, canvasY, projectionInvTF) {
|
||||||
|
/*
|
||||||
|
Convert mouse position to local
|
||||||
|
*/
|
||||||
|
const { height, width } = target;
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
canvasX -= targetRect.left;
|
||||||
|
canvasY -= targetRect.top;
|
||||||
|
|
||||||
|
const pos = vec2.fromValues(
|
||||||
|
2 * (canvasX / width) - 1,
|
||||||
|
-2 * (canvasY / height) + 1
|
||||||
|
);
|
||||||
|
if (projectionInvTF) {
|
||||||
|
vec2.transformMat3(pos, pos, projectionInvTF);
|
||||||
}
|
}
|
||||||
|
vec2.transformMat3(pos, pos, this.invView());
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
if ((opts.pan && mbut.right) || (mbut.left && !ctrl && !alt)) {
|
mousePan(e, projectionTF) {
|
||||||
camera.pan([
|
const projectionInvTF = mat3.invert(scratch0, projectionTF);
|
||||||
((panSpeed * (mpos[0] - mpos.prev[0])) / width) * camera.distance,
|
const pos = this.localPosition(
|
||||||
((panSpeed * (mpos[1] - mpos.prev[1])) / height) * camera.distance
|
this.canvas,
|
||||||
]);
|
e.clientX,
|
||||||
|
e.clientY,
|
||||||
|
projectionInvTF
|
||||||
|
);
|
||||||
|
const prev = this.localPosition(
|
||||||
|
this.canvas,
|
||||||
|
this.prevEvent.clientX,
|
||||||
|
this.prevEvent.clientY,
|
||||||
|
projectionInvTF
|
||||||
|
);
|
||||||
|
|
||||||
|
const delta = vec2.sub(scratch1, pos, prev);
|
||||||
|
this.pan(delta[0], delta[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
wheelZoom(e, projectionTF) {
|
||||||
|
const { height } = this.canvas;
|
||||||
|
const { deltaY, deltaMode, clientX, clientY } = e;
|
||||||
|
const scale = scaleSpeed * (deltaMode === 1 ? 12 : 1) * (deltaY || 0);
|
||||||
|
|
||||||
|
const projectionInvTF = mat3.invert(scratch0, projectionTF);
|
||||||
|
const pos = this.localPosition(
|
||||||
|
this.canvas,
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
projectionInvTF
|
||||||
|
);
|
||||||
|
this.zoomAt(1 / Math.exp(scale / height), pos[0], pos[1]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(e, projectionTF) {
|
||||||
|
/*
|
||||||
|
process the event, and return true if camera view changed
|
||||||
|
*/
|
||||||
|
let viewChanged = false;
|
||||||
|
switch (e.type) {
|
||||||
|
case "mousemove": {
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
if (e.buttons & 0x1) {
|
||||||
|
viewChanged = this.mousePan(e, projectionTF);
|
||||||
|
}
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
this.flush(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "wheel": {
|
||||||
|
viewChanged = this.wheelZoom(e, projectionTF);
|
||||||
|
this.flush(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// noop
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return viewChanged;
|
||||||
if (opts.scale && scroll[1]) {
|
|
||||||
camera.distance *= Math.exp((scroll[1] * scaleSpeed) / height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.scale && (mbut.middle || (mbut.left && !ctrl && alt))) {
|
|
||||||
const d = mpos.y - mpos.prevY;
|
|
||||||
if (!d) return;
|
|
||||||
|
|
||||||
camera.distance *= Math.exp(d / height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (camera.distance > scaleMax) camera.distance = scaleMax;
|
|
||||||
if (camera.distance < scaleMin) camera.distance = scaleMin;
|
|
||||||
|
|
||||||
scroll.flush();
|
|
||||||
mpos.flush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function attachCamera(canvas) {
|
||||||
|
return new Camera(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
export default attachCamera;
|
export default attachCamera;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
Utility code for our WebGL shaders
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Point flags - used in graph & scatter plots
|
||||||
|
|
||||||
|
We want a bitmask-like flag structure, but due to webgl limitations
|
||||||
|
must emulate it with floats.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// for JS
|
||||||
|
export const flagSelected = 1;
|
||||||
|
export const flagNaN = 2;
|
||||||
|
export const flagHighlight = 4;
|
||||||
|
|
||||||
|
// for GLSL
|
||||||
|
export const glPointFlags = `
|
||||||
|
|
||||||
|
const float flagSelected = 1.;
|
||||||
|
const float flagNaN = 2.;
|
||||||
|
const float flagHighlight = 4.;
|
||||||
|
|
||||||
|
bool isLowBitSet(float f) {
|
||||||
|
f = mod(f, 2.);
|
||||||
|
return (f > 0.9 && f <= 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
float shiftRightOne(float f) {
|
||||||
|
return floor(f / 2.);
|
||||||
|
}
|
||||||
|
|
||||||
|
void getFlags(in float flag,
|
||||||
|
out bool isNaN,
|
||||||
|
out bool isSelected,
|
||||||
|
out bool isHighlight) {
|
||||||
|
isSelected = isLowBitSet(flag);
|
||||||
|
flag = shiftRightOne(flag);
|
||||||
|
isNaN = isLowBitSet(flag);
|
||||||
|
flag = shiftRightOne(flag);
|
||||||
|
isHighlight = isLowBitSet(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user