mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 07:28:11 +08:00
renaming backend, cellxgene to server, client respectively
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// jshint esversion: 6
|
||||
const mat4 = require("gl-mat4");
|
||||
|
||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
||||
|
||||
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;
|
||||
void main() {
|
||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
||||
fragColor = color;
|
||||
}`,
|
||||
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec3 fragColor;
|
||||
void main() {
|
||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||
discard;
|
||||
}
|
||||
gl_FragColor = vec4(fragColor, 1);
|
||||
}`,
|
||||
|
||||
attributes: {
|
||||
position: regl.prop("position"),
|
||||
color: regl.prop("color"),
|
||||
size: regl.prop("size")
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
distance: regl.prop("distance"),
|
||||
view: regl.prop("view"),
|
||||
projection: (context, props) => {
|
||||
return mat4.perspective(
|
||||
[],
|
||||
Math.PI / 2,
|
||||
context.viewportWidth * props.scale / context.viewportHeight,
|
||||
0.01,
|
||||
1000
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
count: regl.prop("count"),
|
||||
|
||||
primitive: "points"
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.graphCanvas path {
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
#graphWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#graphAttachPoint,
|
||||
.graphSVG,
|
||||
.graphCanvas {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#graphSVG {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.axis path,
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
circle {
|
||||
stroke-width: 4px;
|
||||
stroke: #000;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./graph.css";
|
||||
import { setupSVGandBrushElements } from "./setupSVGandBrush";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
import { connect } from "react-redux";
|
||||
import actions from "../../actions";
|
||||
|
||||
import mat4 from "gl-mat4";
|
||||
import fit from "canvas-fit";
|
||||
import _camera from "../../util/camera.js";
|
||||
import _regl from "regl";
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { scaleLinear } from "../../util/scaleLinear";
|
||||
|
||||
import FaCrosshair from "react-icons/lib/fa/crosshairs";
|
||||
import FaZoom from "react-icons/lib/fa/search-plus";
|
||||
import FaSave from "react-icons/lib/fa/download";
|
||||
|
||||
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
responsive: state.responsive,
|
||||
crossfilter: state.controls.crossfilter
|
||||
};
|
||||
})
|
||||
class Graph extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.count = 0;
|
||||
this.inverse = mat4.identity([]);
|
||||
this.graphPaddingTop = 100;
|
||||
this.renderCache = {
|
||||
positions: null,
|
||||
colors: null
|
||||
};
|
||||
this.state = {
|
||||
drawn: false,
|
||||
svg: null,
|
||||
ctx: null,
|
||||
brush: null,
|
||||
mode: "brush"
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// setup canvas and camera
|
||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
||||
const regl = _regl(this.reglCanvas);
|
||||
|
||||
const drawPoints = _drawPoints(regl);
|
||||
|
||||
// preallocate buffers
|
||||
const pointBuffer = regl.buffer();
|
||||
const colorBuffer = regl.buffer();
|
||||
const sizeBuffer = regl.buffer();
|
||||
|
||||
regl.frame(({ viewportWidth, viewportHeight }) => {
|
||||
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(),
|
||||
scale: viewportHeight / viewportWidth
|
||||
});
|
||||
|
||||
this.setState({ camera });
|
||||
camera.tick();
|
||||
});
|
||||
|
||||
this.setState({
|
||||
regl,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
sizeBuffer
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.state.regl && nextProps.crossfilter) {
|
||||
/* update the regl state */
|
||||
const crossfilter = nextProps.crossfilter.cells;
|
||||
const cells = crossfilter.all();
|
||||
const cellCount = cells.length;
|
||||
|
||||
// X/Y positions for each point - a cached value that only
|
||||
// changes if we have loaded entirely new cell data
|
||||
//
|
||||
if (
|
||||
!this.renderCache.positions ||
|
||||
this.props.crossfilter.cells != nextProps.crossfilter.cells
|
||||
) {
|
||||
if (!this.renderCache.positions)
|
||||
this.renderCache.positions = new Float32Array(2 * cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||
// d3.scaleLinear().domain([0,1]).range([1,-1])
|
||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||
|
||||
for (
|
||||
let i = 0, positions = this.renderCache.positions;
|
||||
i < cellCount;
|
||||
i++
|
||||
) {
|
||||
positions[2 * i] = glScaleX(cells[i].__x__);
|
||||
positions[2 * i + 1] = glScaleY(cells[i].__y__);
|
||||
}
|
||||
this.state.pointBuffer({
|
||||
data: this.renderCache.positions,
|
||||
dimension: 2
|
||||
});
|
||||
}
|
||||
|
||||
// Colors for each point - a cached value that only changes when
|
||||
// the cell metadata changes (done by updateCellColors middleware).
|
||||
// NOTE: this is a slightly pessimistic assumption, as the metadata
|
||||
// could have changed for some other reason, but for now color is
|
||||
// the only metadata that changes client-side. If this is problematic,
|
||||
// we could add some sort of color-specific indicator to the app state.
|
||||
if (
|
||||
!this.renderCache.colors ||
|
||||
this.props.cellsMetadata != nextProps.cellsMetadata
|
||||
) {
|
||||
if (!this.renderCache.colors)
|
||||
this.renderCache.colors = new Float32Array(3 * cellCount);
|
||||
for (let i = 0, colors = this.renderCache.colors; i < cellCount; i++) {
|
||||
colors.set(cells[i].__colorRGB__, 3 * i);
|
||||
}
|
||||
this.state.colorBuffer({ data: this.renderCache.colors, dimension: 3 });
|
||||
}
|
||||
|
||||
// Sizes for each point - this is presumed to change each time the
|
||||
// component receives new props. Almost always a true assumption, as
|
||||
// most property upates are due to changes driving a crossfilter
|
||||
// selection set change.
|
||||
//
|
||||
if (
|
||||
!this.renderCache.sizes ||
|
||||
this.props.crossfilter.cells != nextProps.crossfilter.cells
|
||||
) {
|
||||
this.renderCache.sizes = new Float32Array(cellCount);
|
||||
}
|
||||
crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2);
|
||||
this.state.sizeBuffer({ data: this.renderCache.sizes, dimension: 1 });
|
||||
|
||||
this.count = cellCount;
|
||||
}
|
||||
|
||||
if (
|
||||
/* invisibly handles the initial null vs integer case as well as resize events */
|
||||
nextProps.responsive.height !== this.props.responsive.height ||
|
||||
nextProps.responsive.width !== this.props.responsive.width
|
||||
) {
|
||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
||||
d3
|
||||
.select("#graphAttachPoint")
|
||||
.selectAll("*")
|
||||
.remove();
|
||||
const { svg } = setupSVGandBrushElements(
|
||||
this.handleBrushSelectAction.bind(this),
|
||||
this.handleBrushDeselectAction.bind(this),
|
||||
nextProps.responsive,
|
||||
this.graphPaddingTop
|
||||
);
|
||||
this.setState({ svg });
|
||||
}
|
||||
}
|
||||
handleBrushSelectAction() {
|
||||
/*
|
||||
No idea why d3 event scope works like this
|
||||
but apparently
|
||||
it does
|
||||
https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f
|
||||
*/
|
||||
const s = d3.event.selection;
|
||||
/*
|
||||
event describing brush position:
|
||||
@-------|
|
||||
| |
|
||||
| |
|
||||
|-------@
|
||||
*/
|
||||
|
||||
// compute inverse view matrix
|
||||
const inverse = mat4.invert([], this.state.camera.view());
|
||||
|
||||
// transform screen coordinates -> cell coordinates
|
||||
const invert = pin => {
|
||||
const x =
|
||||
2 * pin[0] / (this.props.responsive.height - this.graphPaddingTop) - 1;
|
||||
const y =
|
||||
2 *
|
||||
(1 - pin[1] / (this.props.responsive.height - this.graphPaddingTop)) -
|
||||
1;
|
||||
const pout = [x + inverse[12], y + inverse[13]];
|
||||
return [(pout[0] + 1) / 2, (pout[1] + 1) / 2];
|
||||
};
|
||||
|
||||
const brushCoords = {
|
||||
northwest: invert([s[0][0], s[0][1]]),
|
||||
southeast: invert([s[1][0], s[1][1]])
|
||||
};
|
||||
|
||||
this.props.dispatch({
|
||||
type: "graph brush selection change",
|
||||
brushCoords
|
||||
});
|
||||
}
|
||||
handleBrushDeselectAction() {
|
||||
if (!d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
type: "graph brush deselect"
|
||||
});
|
||||
}
|
||||
}
|
||||
handleOpacityRangeChange(e) {
|
||||
this.props.dispatch({
|
||||
type: "change opacity deselected cells in 2d graph background",
|
||||
data: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="graphWrapper">
|
||||
<div style={{ position: "fixed", right: 0, top: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "baseline"
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.dispatch(actions.resetGraph());
|
||||
}}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
padding: "10px 20px",
|
||||
marginRight: 10,
|
||||
borderRadius: 2,
|
||||
backgroundColor: globals.brightBlue,
|
||||
border: "none",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
reset graph
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.dispatch(actions.regraph());
|
||||
}}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
padding: "10px 20px",
|
||||
marginRight: 10,
|
||||
borderRadius: 2,
|
||||
backgroundColor: globals.brightBlue,
|
||||
border: "none",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
regraph selection
|
||||
</button>
|
||||
<div>
|
||||
<span style={{ position: "relative", top: 3 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.setState({ mode: "brush" });
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border:
|
||||
this.state.mode === "brush"
|
||||
? "1px solid black"
|
||||
: "1px solid white",
|
||||
backgroundColor: "white",
|
||||
padding: 5,
|
||||
borderRadius: 3
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<FaCrosshair />{" "}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.setState({ mode: "zoom" });
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
border:
|
||||
this.state.mode === "zoom"
|
||||
? "1px solid black"
|
||||
: "1px solid white",
|
||||
backgroundColor: "white",
|
||||
padding: 5,
|
||||
borderRadius: 3
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<FaZoom />{" "}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
padding: "10px 20px",
|
||||
backgroundColor: globals.lightGrey,
|
||||
border: "none",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginRight: 50,
|
||||
marginTop: 50,
|
||||
zIndex: -9999,
|
||||
position: "fixed",
|
||||
right: 20,
|
||||
bottom: 20
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: this.state.mode === "brush" ? "inherit" : "none"
|
||||
}}
|
||||
id="graphAttachPoint"
|
||||
/>
|
||||
<div style={{ padding: 0, margin: 0 }}>
|
||||
<canvas
|
||||
width={this.props.responsive.height - this.graphPaddingTop}
|
||||
height={this.props.responsive.height - this.graphPaddingTop}
|
||||
ref={canvas => {
|
||||
this.reglCanvas = canvas;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Graph;
|
||||
|
||||
// <span style={{ marginRight: 10, fontSize: 12 }}>
|
||||
// deselected opacity
|
||||
// </span>
|
||||
// <input
|
||||
// style={{ position: "relative", top: 6, marginRight: 20 }}
|
||||
// type="range"
|
||||
// onChange={this.handleOpacityRangeChange.bind(this)}
|
||||
// min={0}
|
||||
// max={1}
|
||||
// step="0.01"
|
||||
// />
|
||||
@@ -0,0 +1,43 @@
|
||||
// jshint esversion: 6
|
||||
import styles from "./graph.css";
|
||||
import _ from "lodash";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
/******************************************
|
||||
*******************************************
|
||||
put svg & brush in DOM
|
||||
*******************************************
|
||||
******************************************/
|
||||
|
||||
export const setupSVGandBrushElements = (
|
||||
handleBrushSelectAction,
|
||||
handleBrushDeselectAction,
|
||||
responsive,
|
||||
graphPaddingTop
|
||||
) => {
|
||||
const side = responsive.height - graphPaddingTop;
|
||||
const svg = d3
|
||||
.select("#graphAttachPoint")
|
||||
.append("svg")
|
||||
.attr("width", side)
|
||||
.attr("height", side)
|
||||
.attr("class", `${styles.graphSVG}`);
|
||||
|
||||
svg.append("g").call(
|
||||
d3
|
||||
.brush()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[
|
||||
responsive.height - graphPaddingTop,
|
||||
responsive.height - graphPaddingTop
|
||||
]
|
||||
])
|
||||
.on("brush", handleBrushSelectAction)
|
||||
.on("end", handleBrushDeselectAction)
|
||||
);
|
||||
|
||||
return {
|
||||
svg
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// jshint esversion: 6
|
||||
// createExpressionsCountsMap () {
|
||||
//
|
||||
// const CHANGE_ME_MAGIC_GENE_INDEX = 5;
|
||||
//
|
||||
// const expressionsCountsMap = {};
|
||||
//
|
||||
// /* currently selected gene */
|
||||
// expressionsCountsMap.geneName = this.state.expressions.data.genes[3];
|
||||
//
|
||||
// let maxExpressionValue = 0;
|
||||
//
|
||||
// /* create map of expressions for every cell */
|
||||
// this.state.expressions.data.cells.map((c) => {
|
||||
// /* cellname = 234 */
|
||||
// expressionsCountsMap[c.cellname] = c["e"][CHANGE_ME_MAGIC_GENE_INDEX];
|
||||
// /* collect the maximum value as we iterate */
|
||||
// if (c["e"][CHANGE_ME_MAGIC_GENE_INDEX] > maxExpressionValue) {
|
||||
// maxExpressionValue = c["e"][CHANGE_ME_MAGIC_GENE_INDEX]
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// expressionsCountsMap.maxValue = maxExpressionValue;
|
||||
//
|
||||
// return expressionsCountsMap;
|
||||
// }
|
||||
Reference in New Issue
Block a user