mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 01:28:12 +08:00
remove old heatmap webgl code
This commit is contained in:
@@ -1,251 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import DeckGL, {
|
|
||||||
PointCloudLayer,
|
|
||||||
ScreenGridLayer,
|
|
||||||
COORDINATE_SYSTEM
|
|
||||||
} from "deck.gl";
|
|
||||||
import OrbitController from "./orbit-control";
|
|
||||||
import { Popup } from "./popup";
|
|
||||||
|
|
||||||
class Heatmap extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onChangeViewport = this.onChangeViewport.bind(this);
|
|
||||||
this.onInitialized = this.onInitialized.bind(this);
|
|
||||||
this.onResize = this.onResize.bind(this);
|
|
||||||
this.onUpdate = this.onUpdate.bind(this);
|
|
||||||
this.onHover = this.onHover.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
points: [],
|
|
||||||
sampleExpressionMatrix: [
|
|
||||||
{ color: [0, 255, 0], position: [100, 100] },
|
|
||||||
{ color: [0, 255, 0], position: [100, 100] },
|
|
||||||
{ color: [0, 255, 0], position: [100, 100] }
|
|
||||||
],
|
|
||||||
progress: 0,
|
|
||||||
popup: {
|
|
||||||
displayed: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
title: ""
|
|
||||||
},
|
|
||||||
viewport: {
|
|
||||||
lookAt: [0, 0, 0],
|
|
||||||
distance: 1,
|
|
||||||
rotationX: 0,
|
|
||||||
rotationY: 0,
|
|
||||||
fov: 30,
|
|
||||||
minDistance: 0.5,
|
|
||||||
maxDistance: 3
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getColor(cluster) {
|
|
||||||
let color = [0, 0, 0];
|
|
||||||
|
|
||||||
switch (cluster) {
|
|
||||||
case 0:
|
|
||||||
color = [166, 206, 227];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
color = [31, 120, 180];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
color = [178, 223, 138];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
color = [51, 160, 44];
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
color = [251, 154, 153];
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
color = [227, 26, 28];
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
color = [253, 191, 111];
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
color = [255, 127, 0];
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
color = [202, 178, 214];
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
color = [106, 61, 154];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
this.onResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.canvas.fitBounds([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
|
|
||||||
|
|
||||||
this.fetchData();
|
|
||||||
|
|
||||||
window.requestAnimationFrame(this.onUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData() {
|
|
||||||
fetch(
|
|
||||||
"https://raw.githubusercontent.com/zdenekhynek/data-science-capstone-visualisation/master/public/clusters.json"
|
|
||||||
).then(res => {
|
|
||||||
res.json().then(obj => {
|
|
||||||
const clusters = Object.keys(obj).map(k => obj[k]);
|
|
||||||
const points = clusters.map(cluster => {
|
|
||||||
const position = [cluster.x, cluster.y, cluster.z];
|
|
||||||
const color = [255, 0, 0];
|
|
||||||
const id = cluster.id;
|
|
||||||
const title = cluster.webTitle;
|
|
||||||
return { id, title, position, color };
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ points, progress: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onHover(d) {
|
|
||||||
let popup = { displayed: false };
|
|
||||||
console.log("d", d);
|
|
||||||
|
|
||||||
if (d.object) {
|
|
||||||
const object = d.object;
|
|
||||||
popup = {
|
|
||||||
id: object.id,
|
|
||||||
title: object.title,
|
|
||||||
displayed: true,
|
|
||||||
x: d.x,
|
|
||||||
y: d.y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ popup });
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize() {
|
|
||||||
const { innerWidth: width, innerHeight: height } = window;
|
|
||||||
this.setState({ width: width / 1.5, height: height / 1.5 });
|
|
||||||
}
|
|
||||||
|
|
||||||
onInitialized(gl) {
|
|
||||||
gl.clearColor(0, 0, 0, 1);
|
|
||||||
gl.enable(gl.DEPTH_TEST);
|
|
||||||
gl.depthFunc(gl.LEQUAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeViewport(viewport) {
|
|
||||||
this.setState({
|
|
||||||
rotating: !viewport.isDragging,
|
|
||||||
viewport: { ...this.state.viewport, ...viewport }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate() {
|
|
||||||
const { viewport } = this.state;
|
|
||||||
window.requestAnimationFrame(this.onUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPointCloudLayer() {
|
|
||||||
return (
|
|
||||||
this.state.points.length &&
|
|
||||||
new PointCloudLayer({
|
|
||||||
id: "point-cloud-layer",
|
|
||||||
data: this.state.points,
|
|
||||||
projectionMode: COORDINATE_SYSTEM.IDENTITY,
|
|
||||||
pickable: true,
|
|
||||||
onHover: this.onHover,
|
|
||||||
getPosition: d => d.position,
|
|
||||||
getNormal: d => [0, 0.5, 0.2],
|
|
||||||
getColor: d => d.color,
|
|
||||||
radiusPixels: 2
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGridLayer() {
|
|
||||||
/**
|
|
||||||
* Data format:
|
|
||||||
* [
|
|
||||||
* {position: [-122.4, 37.7]},
|
|
||||||
* ...
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
const screenGridLayer = new ScreenGridLayer({
|
|
||||||
id: "screen-grid-layer",
|
|
||||||
data: this.state.sampleExpressionMatrix,
|
|
||||||
projectionMode: COORDINATE_SYSTEM.IDENTITY,
|
|
||||||
pickable: true,
|
|
||||||
getPosition: d => d.position,
|
|
||||||
getColor: d => d.color,
|
|
||||||
cellSizePixels: 40
|
|
||||||
});
|
|
||||||
|
|
||||||
return screenGridLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDeckGLCanvas() {
|
|
||||||
const { width, height, viewport } = this.state;
|
|
||||||
const canvasProps = { width, height, ...viewport };
|
|
||||||
const glViewport = OrbitController.getViewport(canvasProps);
|
|
||||||
|
|
||||||
return (
|
|
||||||
width &&
|
|
||||||
height && (
|
|
||||||
<OrbitController
|
|
||||||
{...canvasProps}
|
|
||||||
ref={canvas => {
|
|
||||||
this.canvas = canvas;
|
|
||||||
}}
|
|
||||||
onChangeViewport={this.onChangeViewport}
|
|
||||||
>
|
|
||||||
<DeckGL
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
viewport={glViewport}
|
|
||||||
layers={[
|
|
||||||
// this.renderPointCloudLayer(),
|
|
||||||
this.renderGridLayer()
|
|
||||||
].filter(Boolean)}
|
|
||||||
onWebGLInitialized={this.onInitialized}
|
|
||||||
/>
|
|
||||||
</OrbitController>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { width, height, popup } = this.state;
|
|
||||||
if (!width || !height) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedPopup = popup.displayed ? <Popup {...popup} /> : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="heatmap">
|
|
||||||
{this.renderDeckGLCanvas()}
|
|
||||||
{renderedPopup}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Heatmap;
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
/* global window */
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import { PerspectiveViewport } from "deck.gl";
|
|
||||||
import { vec3 } from "gl-matrix";
|
|
||||||
|
|
||||||
/* Utils */
|
|
||||||
// constrain number between bounds
|
|
||||||
function clamp(x, min, max) {
|
|
||||||
if (x < min) {
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
if (x > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ua =
|
|
||||||
typeof window.navigator !== "undefined"
|
|
||||||
? window.navigator.userAgent.toLowerCase()
|
|
||||||
: "";
|
|
||||||
const firefox = ua.indexOf("firefox") !== -1;
|
|
||||||
|
|
||||||
/* Interaction */
|
|
||||||
|
|
||||||
export default class OrbitController extends Component {
|
|
||||||
static getViewport({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
lookAt,
|
|
||||||
distance,
|
|
||||||
rotationX,
|
|
||||||
rotationY,
|
|
||||||
fov
|
|
||||||
}) {
|
|
||||||
const cameraPos = vec3.add([], lookAt, [0, 0, distance]);
|
|
||||||
vec3.rotateX(cameraPos, cameraPos, lookAt, rotationX / 180 * Math.PI);
|
|
||||||
vec3.rotateY(cameraPos, cameraPos, lookAt, rotationY / 180 * Math.PI);
|
|
||||||
|
|
||||||
return new PerspectiveViewport({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
lookAt,
|
|
||||||
far: 1000,
|
|
||||||
near: 0.1,
|
|
||||||
fovy: fov,
|
|
||||||
eye: cameraPos
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this._dragStartPos = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragStart(evt) {
|
|
||||||
const { pageX, pageY } = evt;
|
|
||||||
this._dragStartPos = [pageX, pageY];
|
|
||||||
this.props.onChangeViewport({ isDragging: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDrag(evt) {
|
|
||||||
if (this._dragStartPos) {
|
|
||||||
const { pageX, pageY } = evt;
|
|
||||||
const { width, height } = this.props;
|
|
||||||
const dx = (pageX - this._dragStartPos[0]) / width;
|
|
||||||
const dy = (pageY - this._dragStartPos[1]) / height;
|
|
||||||
|
|
||||||
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey) {
|
|
||||||
// pan
|
|
||||||
const { lookAt, distance, rotationX, rotationY, fov } = this.props;
|
|
||||||
|
|
||||||
const unitsPerPixel = distance / Math.tan(fov / 180 * Math.PI / 2) / 2;
|
|
||||||
|
|
||||||
const newLookAt = vec3.add([], lookAt, [
|
|
||||||
-unitsPerPixel * dx,
|
|
||||||
unitsPerPixel * dy,
|
|
||||||
0
|
|
||||||
]);
|
|
||||||
vec3.rotateX(newLookAt, newLookAt, lookAt, rotationX / 180 * Math.PI);
|
|
||||||
vec3.rotateY(newLookAt, newLookAt, lookAt, rotationY / 180 * Math.PI);
|
|
||||||
|
|
||||||
this.props.onChangeViewport({
|
|
||||||
lookAt: newLookAt
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// rotate
|
|
||||||
const { rotationX, rotationY } = this.props;
|
|
||||||
const newRotationX = clamp(rotationX - dy * 180, -90, 90);
|
|
||||||
const newRotationY = (rotationY - dx * 180) % 360;
|
|
||||||
|
|
||||||
this.props.onChangeViewport({
|
|
||||||
rotationX: newRotationX,
|
|
||||||
rotationY: newRotationY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._dragStartPos = [pageX, pageY];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragEnd() {
|
|
||||||
this._dragStartPos = null;
|
|
||||||
this.props.onChangeViewport({ isDragging: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWheel(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
let value = evt.deltaY;
|
|
||||||
// Firefox doubles the values on retina screens...
|
|
||||||
if (firefox && evt.deltaMode === window.WheelEvent.DOM_DELTA_PIXEL) {
|
|
||||||
value /= window.devicePixelRatio;
|
|
||||||
}
|
|
||||||
if (evt.deltaMode === window.WheelEvent.DOM_DELTA_LINE) {
|
|
||||||
value *= 40;
|
|
||||||
}
|
|
||||||
if (value !== 0 && value % 4.000244140625 === 0) {
|
|
||||||
// This one is definitely a mouse wheel event.
|
|
||||||
// Normalize this value to match trackpad.
|
|
||||||
value = Math.floor(value / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { distance, minDistance, maxDistance } = this.props;
|
|
||||||
const newDistance = clamp(
|
|
||||||
distance * Math.pow(1.01, value),
|
|
||||||
minDistance,
|
|
||||||
maxDistance
|
|
||||||
);
|
|
||||||
|
|
||||||
this.props.onChangeViewport({
|
|
||||||
distance: newDistance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// public API
|
|
||||||
fitBounds(min, max) {
|
|
||||||
const { fov } = this.props;
|
|
||||||
const size = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
|
||||||
const newDistance = size / Math.tan(fov / 180 * Math.PI / 2) / 2;
|
|
||||||
|
|
||||||
this.props.onChangeViewport({
|
|
||||||
distance: newDistance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ position: "relative", userSelect: "none" }}
|
|
||||||
onMouseDown={this._onDragStart.bind(this)}
|
|
||||||
onMouseMove={this._onDrag.bind(this)}
|
|
||||||
onMouseLeave={this._onDragEnd.bind(this)}
|
|
||||||
onMouseUp={this._onDragEnd.bind(this)}
|
|
||||||
onWheel={this._onWheel.bind(this)}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OrbitController.defaultProps = {
|
|
||||||
lookAt: [0, 0, 0],
|
|
||||||
rotationX: 0,
|
|
||||||
rotationY: 0,
|
|
||||||
minDistance: 0,
|
|
||||||
maxDistance: Infinity,
|
|
||||||
fov: 50
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React, { PureComponent } from "react";
|
|
||||||
|
|
||||||
export class Popup extends PureComponent {
|
|
||||||
render() {
|
|
||||||
const { title, x, y } = this.props;
|
|
||||||
const style = {
|
|
||||||
position: "absolute",
|
|
||||||
top: y,
|
|
||||||
left: x,
|
|
||||||
maxWidth: "200px",
|
|
||||||
padding: "10px",
|
|
||||||
color: "white",
|
|
||||||
backgroundColor: "black",
|
|
||||||
pointerEvents: "none",
|
|
||||||
transform: "translate(10px, -50%)"
|
|
||||||
};
|
|
||||||
|
|
||||||
const arrowStyle = {
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "-14px",
|
|
||||||
width: "7px",
|
|
||||||
height: "5px",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
border: "7px solid transparent",
|
|
||||||
borderRight: "7px solid black"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={style}>
|
|
||||||
<div style={arrowStyle} />
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Popup.defaultProps = {
|
|
||||||
id: "id",
|
|
||||||
title: "",
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user