mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-22 12: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:
@@ -1,44 +1,62 @@
|
||||
// jshint esversion: 6
|
||||
const mat4 = require("gl-mat4");
|
||||
|
||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
||||
import { glPointFlags } from "../../util/glHelpers";
|
||||
|
||||
export default function(regl) {
|
||||
return regl({
|
||||
vert: `
|
||||
precision mediump float;
|
||||
|
||||
attribute vec2 position;
|
||||
attribute vec3 color;
|
||||
attribute float size;
|
||||
uniform float distance;
|
||||
uniform mat4 projection, view;
|
||||
varying vec3 fragColor;
|
||||
attribute float flag;
|
||||
|
||||
uniform mat3 projection;
|
||||
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() {
|
||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
||||
fragColor = color;
|
||||
bool isNaN, isSelected, isHighlight;
|
||||
getFlags(flag, isNaN, isSelected, isHighlight);
|
||||
|
||||
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: `
|
||||
precision mediump float;
|
||||
varying vec3 fragColor;
|
||||
varying vec4 fragColor;
|
||||
void main() {
|
||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||
discard;
|
||||
}
|
||||
gl_FragColor = vec4(fragColor, 1);
|
||||
gl_FragColor = fragColor;
|
||||
}`,
|
||||
|
||||
attributes: {
|
||||
position: regl.prop("position"),
|
||||
color: regl.prop("color"),
|
||||
size: regl.prop("size")
|
||||
flag: regl.prop("flag")
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
distance: regl.prop("distance"),
|
||||
view: regl.prop("view"),
|
||||
projection: () => mat4.perspective([], Math.PI / 2, 1, 0.01, 1000)
|
||||
projection: regl.prop("projection"),
|
||||
nPoints: regl.prop("nPoints"),
|
||||
minViewportDimension: regl.prop("minViewportDimension")
|
||||
},
|
||||
|
||||
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 { connect } from "react-redux";
|
||||
import { Button, ButtonGroup } from "@blueprintjs/core";
|
||||
import _regl from "regl";
|
||||
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 _camera from "../../util/camera";
|
||||
|
||||
import setupScatterplot from "./setupScatterplot";
|
||||
import styles from "./scatterplot.css";
|
||||
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import scaleLinear from "../../util/scaleLinear";
|
||||
|
||||
import { margin, width, height } from "./util";
|
||||
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 => {
|
||||
const { world, crossfilter } = state;
|
||||
const { world, crossfilter, universe } = state;
|
||||
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
|
||||
const expressionX =
|
||||
world &&
|
||||
scatterplotXXaccessor &&
|
||||
world.varData.hasCol(scatterplotXXaccessor)
|
||||
? world.varData.col(scatterplotXXaccessor).asArray()
|
||||
: null;
|
||||
world.varData.col(scatterplotXXaccessor)?.asArray();
|
||||
const expressionY =
|
||||
world &&
|
||||
scatterplotYYaccessor &&
|
||||
world.varData.hasCol(scatterplotYYaccessor)
|
||||
? world.varData.col(scatterplotYYaccessor).asArray()
|
||||
: null;
|
||||
world.varData.col(scatterplotYYaccessor)?.asArray();
|
||||
|
||||
return {
|
||||
world,
|
||||
universe,
|
||||
|
||||
colorRGB: state.colors.rgb,
|
||||
colorScale: state.colors.scale,
|
||||
@@ -57,6 +54,64 @@ import finiteExtent from "../../util/finiteExtent";
|
||||
};
|
||||
})
|
||||
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) {
|
||||
super(props);
|
||||
this.count = 0;
|
||||
@@ -64,7 +119,7 @@ class Scatterplot extends React.Component {
|
||||
this.renderCache = {
|
||||
positions: null,
|
||||
colors: null,
|
||||
sizes: null,
|
||||
flags: null,
|
||||
xScale: null,
|
||||
yScale: null
|
||||
};
|
||||
@@ -85,39 +140,37 @@ class Scatterplot extends React.Component {
|
||||
this.renderCache = { ...this.renderCache, ...scales };
|
||||
}
|
||||
|
||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
||||
const regl = _regl(this.reglCanvas);
|
||||
|
||||
const drawPoints = _drawPoints(regl);
|
||||
|
||||
// Create render transform
|
||||
const projectionTF = createProjectionTF(
|
||||
this.reglCanvas.width,
|
||||
this.reglCanvas.height
|
||||
);
|
||||
|
||||
// preallocate buffers
|
||||
const pointBuffer = regl.buffer();
|
||||
const colorBuffer = regl.buffer();
|
||||
const sizeBuffer = regl.buffer();
|
||||
const flagBuffer = regl.buffer();
|
||||
|
||||
const reglRender = regl.frame(() => {
|
||||
this.reglDraw(
|
||||
regl,
|
||||
drawPoints,
|
||||
sizeBuffer,
|
||||
colorBuffer,
|
||||
pointBuffer,
|
||||
camera
|
||||
);
|
||||
camera.tick();
|
||||
});
|
||||
|
||||
this.reglRenderState = "rendering";
|
||||
this.renderPoints(
|
||||
regl,
|
||||
drawPoints,
|
||||
flagBuffer,
|
||||
colorBuffer,
|
||||
pointBuffer,
|
||||
projectionTF
|
||||
);
|
||||
|
||||
this.setState({
|
||||
regl,
|
||||
sizeBuffer,
|
||||
flagBuffer,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
svg,
|
||||
reglRender,
|
||||
camera,
|
||||
drawPoints
|
||||
drawPoints,
|
||||
projectionTF
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,22 +182,22 @@ class Scatterplot extends React.Component {
|
||||
scatterplotYYaccessor,
|
||||
expressionX,
|
||||
expressionY,
|
||||
colorRGB
|
||||
colorRGB,
|
||||
colorAccessor
|
||||
} = this.props;
|
||||
const {
|
||||
reglRender,
|
||||
regl,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
sizeBuffer,
|
||||
flagBuffer,
|
||||
svg,
|
||||
drawPoints,
|
||||
camera
|
||||
projectionTF
|
||||
} = this.state;
|
||||
|
||||
if (
|
||||
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
||||
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor ||
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor ||
|
||||
world !== prevProps.world // shape or clip of world changed
|
||||
) {
|
||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||
@@ -152,76 +205,54 @@ class Scatterplot extends React.Component {
|
||||
this.renderCache = { ...this.renderCache, ...scales };
|
||||
}
|
||||
|
||||
if (reglRender && this.reglRenderState === "rendering") {
|
||||
reglRender.cancel();
|
||||
this.reglRenderState = "paused";
|
||||
}
|
||||
|
||||
if (
|
||||
world &&
|
||||
regl &&
|
||||
pointBuffer &&
|
||||
colorBuffer &&
|
||||
sizeBuffer &&
|
||||
expressionX &&
|
||||
expressionY &&
|
||||
scatterplotXXaccessor &&
|
||||
scatterplotYYaccessor
|
||||
) {
|
||||
if (world && regl) {
|
||||
const { renderCache } = this;
|
||||
const { xScale, yScale } = this.renderCache;
|
||||
const cellCount = expressionX.length;
|
||||
let needsRepaint = false;
|
||||
|
||||
// Points change when expressionX or expressionY change.
|
||||
if (
|
||||
!renderCache.positions ||
|
||||
expressionX !== prevProps.expressionX ||
|
||||
expressionY !== prevProps.expressionY
|
||||
) {
|
||||
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
|
||||
const newPositions = this.computePointPositions(
|
||||
expressionX,
|
||||
expressionY,
|
||||
xScale,
|
||||
yScale
|
||||
);
|
||||
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) {
|
||||
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
||||
svg.selectAll("*").remove();
|
||||
@@ -307,6 +322,34 @@ class Scatterplot extends React.Component {
|
||||
.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() {
|
||||
const { dispatch } = this.props;
|
||||
const { minimized } = this.state;
|
||||
@@ -367,7 +410,7 @@ class Scatterplot extends React.Component {
|
||||
height={height}
|
||||
data-testid="scatterplot"
|
||||
style={{
|
||||
marginLeft: margin.left - 7,
|
||||
marginLeft: margin.left,
|
||||
marginTop: margin.top
|
||||
}}
|
||||
ref={canvas => {
|
||||
|
||||
Reference in New Issue
Block a user