mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 03:58:11 +08:00
renaming backend, cellxgene to server, client respectively
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
// 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;
|
||||
@@ -0,0 +1,170 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import Helmet from "react-helmet";
|
||||
import Container from "./framework/container";
|
||||
import { connect } from "react-redux";
|
||||
import PulseLoader from "halogen/PulseLoader";
|
||||
|
||||
import LeftSideBar from "./leftsidebar";
|
||||
import Parallel from "./continuous/parallel";
|
||||
import Legend from "./continuousLegend";
|
||||
import Joy from "./joy/joy";
|
||||
import Graph from "./graph/graph";
|
||||
import * as globals from "../globals";
|
||||
import actions from "../actions";
|
||||
|
||||
import SectionHeader from "./framework/sectionHeader";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cells: state.cells
|
||||
};
|
||||
})
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
_onURLChanged() {
|
||||
this.props.dispatch({ type: "url changed", url: document.location.href });
|
||||
}
|
||||
componentDidMount() {
|
||||
/* listen for url changes, fire one when we start the app up */
|
||||
window.addEventListener("popstate", this._onURLChanged);
|
||||
this._onURLChanged();
|
||||
|
||||
this.props.dispatch(actions.initialize());
|
||||
|
||||
/*
|
||||
first request includes query straight off the url bar for now
|
||||
*/
|
||||
this.props.dispatch(actions.requestCells(window.location.search));
|
||||
/* listen for resize events */
|
||||
window.addEventListener("resize", () => {
|
||||
this.props.dispatch({
|
||||
type: "window resize",
|
||||
data: {
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth
|
||||
}
|
||||
});
|
||||
});
|
||||
this.props.dispatch({
|
||||
type: "window resize",
|
||||
data: {
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
// console.log('app:', this.props, this.state)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
{this.props.cells.loading ? (
|
||||
<div
|
||||
style={{ position: "fixed", left: window.innerWidth / 2, top: 150 }}
|
||||
>
|
||||
<PulseLoader color="rgb(0,0,0)" size="10px" margin="4px" />
|
||||
<span
|
||||
style={{ fontFamily: globals.accentFont, fontStyle: "italic" }}
|
||||
>
|
||||
loading cells
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.cells.error ? "Error loading cells" : null}
|
||||
{false ? (
|
||||
<Joy data={this.state.expressions && this.state.expressions.data} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div>
|
||||
<LeftSideBar />
|
||||
<div
|
||||
style={{
|
||||
padding: 15,
|
||||
width: 1440 - 410 /* but responsive */,
|
||||
marginLeft: 350 /* but responsive */
|
||||
}}
|
||||
>
|
||||
<Graph />
|
||||
<Legend />
|
||||
{/*<Parallel/>*/}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,208 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./categorical.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
import FaArrowRight from "react-icons/lib/fa/angle-right";
|
||||
import FaArrowDown from "react-icons/lib/fa/angle-down";
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
colorAccessor: state.controls.colorAccessor
|
||||
};
|
||||
})
|
||||
class Category extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isChecked: true,
|
||||
isExpanded: false
|
||||
};
|
||||
}
|
||||
|
||||
handleColorChange() {
|
||||
this.props.dispatch({
|
||||
type: "color by categorical metadata",
|
||||
colorAccessor: this.props.metadataField
|
||||
});
|
||||
}
|
||||
toggleAll() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter all of these",
|
||||
metadataField: this.props.metadataField
|
||||
});
|
||||
this.setState({ isChecked: true });
|
||||
}
|
||||
toggleNone() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter none of these",
|
||||
metadataField: this.props.metadataField,
|
||||
value: this.props.value
|
||||
});
|
||||
this.setState({ isChecked: false });
|
||||
}
|
||||
renderCategoryItems() {
|
||||
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
|
||||
return (
|
||||
<Value
|
||||
key={v}
|
||||
metadataField={this.props.metadataField}
|
||||
count={this.props.values[v]}
|
||||
value={v}
|
||||
i={i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// display: "flex",
|
||||
// alignItems: "baseline",
|
||||
maxWidth: globals.maxControlsWidth
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline"
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
// flexShrink: 0,
|
||||
fontWeight: 500,
|
||||
// textAlign: "right",
|
||||
// fontFamily: globals.accentFont,
|
||||
// fontStyle: "italic",
|
||||
margin: "3px 10px 3px 0px"
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
position: "relative",
|
||||
top: 2
|
||||
}}
|
||||
onClick={() => {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
}}
|
||||
>
|
||||
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight />}
|
||||
</span>
|
||||
{this.props.metadataField}
|
||||
<input
|
||||
onChange={
|
||||
this.state.isChecked
|
||||
? this.toggleNone.bind(this)
|
||||
: this.toggleAll.bind(this)
|
||||
}
|
||||
checked={this.state.isChecked}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
onClick={this.handleColorChange.bind(this)}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
marginLeft: 4,
|
||||
// padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto",
|
||||
borderRadius: 3,
|
||||
color:
|
||||
this.props.colorAccessor === this.props.metadataField
|
||||
? globals.brightBlue
|
||||
: "black",
|
||||
// backgroundColor: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "inherit",
|
||||
display: "inline-block",
|
||||
position: "relative",
|
||||
top: 2,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
<FaPaintBrush />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>{this.state.isExpanded ? this.renderCategoryItems() : null}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@connect(state => {
|
||||
const ranges =
|
||||
state.cells.cells && state.cells.cells.data.ranges
|
||||
? state.cells.cells.data.ranges
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges
|
||||
};
|
||||
})
|
||||
class Categories extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.ranges) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 310,
|
||||
marginRight: 40,
|
||||
paddingRight: 20,
|
||||
flexShrink: 0
|
||||
// height: 700,
|
||||
// overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.options && key !== "CellName" && !isColorField) {
|
||||
return (
|
||||
<Category key={key} metadataField={key} values={value.options} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Categories;
|
||||
|
||||
/*
|
||||
<SectionHeader text="Categorical Metadata"/>
|
||||
[on off] toggle hide deselected filters (shows a menu vs shows what you ordered in compact/narrative form. fold out animation.)
|
||||
|
||||
<p> Sort by: Alphabetical / Count || Show counts: true / false || Collapse: all / none</p>
|
||||
|
||||
|
||||
<p> <button> Field name [initial state] [create 2d graph] [explain part of 2d graph] </button> </p>
|
||||
<p> <button> Field name [initial state] [create hypothesis] [validate hypothesis] </button> </p>
|
||||
<p> <button> Field name [total] [selected in current filters] [selected in graph selection] </button> </p>
|
||||
<p> <button> Tumor [400] [40] [4] </button> </p>
|
||||
|
||||
might be interesting to show them as an 👁 icon, or with a slash through it, to allow for visible or hidden state
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
Each category has a color associated with it - ie., color by location should show up on these buttons,
|
||||
Does colorby live here? Like a global, with the category labels at the top? If not, we have to
|
||||
duplicate the category names somewhere else - but then, not all (like Cluster_2d_color) won't be listed here.
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,8 @@
|
||||
// jshint esversion: 6
|
||||
export const alphabeticallySortedValues = values => {
|
||||
return Object.keys(values).sort((a, b) => {
|
||||
var textA = a.toUpperCase();
|
||||
var textB = b.toUpperCase();
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
// jshint esversion: 6
|
||||
import { connect } from "react-redux";
|
||||
import React from "react";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
|
||||
colorScale: state.controls.colorScale,
|
||||
colorAccessor: state.controls.colorAccessor
|
||||
};
|
||||
})
|
||||
class CategoryValue extends React.Component {
|
||||
toggleOff() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter deselect",
|
||||
metadataField: this.props.metadataField,
|
||||
value: this.props.value
|
||||
});
|
||||
}
|
||||
|
||||
toggleOn() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter select",
|
||||
metadataField: this.props.metadataField,
|
||||
value: this.props.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.categoricalAsBooleansMap) return null;
|
||||
|
||||
const selected = this.props.categoricalAsBooleansMap[
|
||||
this.props.metadataField
|
||||
][this.props.value];
|
||||
const c =
|
||||
this.props.metadataField ===
|
||||
this.props
|
||||
.colorAccessor; /* this is the color scale, so add swatches below */
|
||||
|
||||
return (
|
||||
<div
|
||||
key={this.props.i}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
fontWeight: selected ? 700 : 400
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
paddingLeft: 15,
|
||||
width: 200,
|
||||
flexShrink: 0,
|
||||
margin: 0
|
||||
// lineHeight: "1em"
|
||||
}}
|
||||
>
|
||||
<input
|
||||
style={{ position: "relative", top: 1 }}
|
||||
onChange={
|
||||
selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)
|
||||
}
|
||||
checked={selected}
|
||||
type="checkbox"
|
||||
/>
|
||||
{this.props.value}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
padding: "1px 10px",
|
||||
width: 80,
|
||||
textAlign: "center",
|
||||
backgroundColor: c
|
||||
? this.props.colorScale(this.props.value)
|
||||
: "inherit",
|
||||
color: c ? "white" : "black",
|
||||
margin: 0
|
||||
// lineHeight: "1em"
|
||||
}}
|
||||
>
|
||||
{this.props.count}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryValue;
|
||||
|
||||
// toggleOnlyThis() {
|
||||
// this.props.dispatch({
|
||||
// type: "categorical metadata filter none of these",
|
||||
// metadataField: this.props.metadataField,
|
||||
// value: this.props.value
|
||||
// })
|
||||
// }
|
||||
// <span
|
||||
// onClick={this.toggleOnlyThis.bind(this)}
|
||||
// style={{
|
||||
// fontFamily: globals.accentFont,
|
||||
// fontSize: 10,
|
||||
// fontWeight: 100,
|
||||
// fontStyle: "italic",
|
||||
// cursor: "pointer",
|
||||
// }}>
|
||||
// {"only"}
|
||||
// </span>
|
||||
|
||||
// onClick={selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)}
|
||||
// toggleOn() {
|
||||
// this.props.dispatch(
|
||||
// actions.attemptCategoricalMetadataSelection(
|
||||
// this.props.metadataField,
|
||||
// this.props.value
|
||||
// ))
|
||||
// }
|
||||
// toggleOff() {
|
||||
// this.props.dispatch(
|
||||
// actions.attemptCategoricalMetadataDeselection(
|
||||
// this.props.metadataField,
|
||||
// this.props.value
|
||||
// ))
|
||||
// }
|
||||
@@ -0,0 +1,94 @@
|
||||
// jshint esversion: 6
|
||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
||||
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import styles from "./parallelCoordinates.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
|
||||
import setupParallelCoordinates from "./setupParallelCoordinates";
|
||||
import drawAxes from "./drawAxes";
|
||||
import drawLinesCanvas from "./drawLinesCanvas";
|
||||
|
||||
import HistogramBrush from "./histogramBrush";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges =
|
||||
state.cells.cells && state.cells.cells.data.ranges
|
||||
? state.cells.cells.data.ranges
|
||||
: null;
|
||||
const metadata =
|
||||
state.cells.cells && state.cells.cells.data.metadata
|
||||
? state.cells.cells.data.metadata
|
||||
: null;
|
||||
|
||||
const initializeRanges =
|
||||
state.initialize.data && state.initialize.data.data.ranges
|
||||
? state.initialize.data.data.ranges
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
};
|
||||
})
|
||||
class Continuous extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {}
|
||||
componentWillReceiveProps(nextProps) {}
|
||||
|
||||
componentDidMount() {}
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.range && key !== "CellName" && !isColorField) {
|
||||
return (
|
||||
<HistogramBrush
|
||||
key={key}
|
||||
metadataField={key}
|
||||
ranges={value.range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Continuous;
|
||||
|
||||
// <SectionHeader text="Continuous Metadata"/>
|
||||
@@ -0,0 +1,88 @@
|
||||
// jshint esversion: 6
|
||||
import styles from "./parallelCoordinates.css";
|
||||
import { yAxis, brushstart } from "./util";
|
||||
|
||||
const drawAxes = (
|
||||
svg,
|
||||
ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
height,
|
||||
width,
|
||||
handleBrushAction,
|
||||
handleColorAction
|
||||
) => {
|
||||
/*****************************************
|
||||
******************************************
|
||||
Handles a brush event, toggling the display of foreground lines.
|
||||
******************************************
|
||||
******************************************/
|
||||
|
||||
function brush() {
|
||||
var actives = [];
|
||||
svg
|
||||
.selectAll(".parcoords_axis .parcoords_brush")
|
||||
.filter(function(d) {
|
||||
return d3.brushSelection(this);
|
||||
})
|
||||
.each(function(d) {
|
||||
actives.push({
|
||||
dimension: d,
|
||||
extent: d3.brushSelection(this)
|
||||
});
|
||||
});
|
||||
/* fire action, with selected dimensions & their values */
|
||||
handleBrushAction(actives);
|
||||
}
|
||||
|
||||
var axes = svg
|
||||
.selectAll(".parcoords_axis")
|
||||
.data(dimensions)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} parcoords_axis`)
|
||||
.attr("transform", (d, i) => {
|
||||
return "translate(" + xscale(i) + ")";
|
||||
});
|
||||
|
||||
axes
|
||||
.append("g")
|
||||
.each(function(d) {
|
||||
var renderAxis =
|
||||
"axis" in d
|
||||
? d.axis.scale(d.scale) // custom axis
|
||||
: yAxis.scale(d.scale); // default axis
|
||||
d3.select(this).call(renderAxis);
|
||||
})
|
||||
.append("text")
|
||||
.on("click", d => {
|
||||
handleColorAction(d.key);
|
||||
})
|
||||
.attr("class", styles.title)
|
||||
.attr("text-anchor", "start")
|
||||
.text(function(d) {
|
||||
return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️";
|
||||
});
|
||||
|
||||
// Add and store a brush for each axis.
|
||||
axes
|
||||
.append("g")
|
||||
.attr("class", `${styles.brush} parcoords_brush`)
|
||||
.each(function(d) {
|
||||
d3.select(this).call(
|
||||
(d.brush = d3
|
||||
.brushY()
|
||||
.extent([[-10, 0], [10, height]])
|
||||
.on("start", brushstart)
|
||||
.on("brush", brush)
|
||||
.on("end", brush))
|
||||
);
|
||||
})
|
||||
.selectAll("rect")
|
||||
.attr("x", -8)
|
||||
.attr("width", 16);
|
||||
|
||||
return axes;
|
||||
};
|
||||
|
||||
export default drawAxes;
|
||||
@@ -0,0 +1,97 @@
|
||||
// jshint esversion: 6
|
||||
import _ from "lodash";
|
||||
import { project } from "./util";
|
||||
|
||||
import renderQueue from "../../util/renderQueue";
|
||||
|
||||
/*****************************************
|
||||
******************************************
|
||||
draw loop
|
||||
******************************************
|
||||
******************************************/
|
||||
|
||||
const drawLinesCanvas = (
|
||||
ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
colorAccessor,
|
||||
colorScale
|
||||
) => {
|
||||
return d => {
|
||||
ctx.globalAlpha = 0.1;
|
||||
|
||||
if (d["__selected__"]) {
|
||||
ctx.strokeStyle = d["__color__"];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
var coords = project(d, dimensions, xscale);
|
||||
coords.forEach((p, i) => {
|
||||
// this tricky bit avoids rendering null values as 0
|
||||
if (p === null) {
|
||||
// this bit renders horizontal lines on the previous/next
|
||||
// dimensions, so that sandwiched null values are visible
|
||||
if (i > 0) {
|
||||
var prev = coords[i - 1];
|
||||
if (prev !== null) {
|
||||
ctx.moveTo(prev[0], prev[1]);
|
||||
ctx.lineTo(prev[0] + 6, prev[1]);
|
||||
}
|
||||
}
|
||||
if (i < coords.length - 1) {
|
||||
var next = coords[i + 1];
|
||||
if (next !== null) {
|
||||
ctx.moveTo(next[0] - 6, next[1]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
ctx.moveTo(p[0], p[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.lineTo(p[0], p[1]);
|
||||
});
|
||||
ctx.stroke();
|
||||
};
|
||||
};
|
||||
|
||||
const drawCellLinesUsingRenderQueue = (
|
||||
metadata,
|
||||
dimensions,
|
||||
xscale,
|
||||
ctx,
|
||||
colorAccessor,
|
||||
colorScale
|
||||
) => {
|
||||
const _renderLinesWithQueue = renderQueue(
|
||||
drawLinesCanvas(ctx, dimensions, xscale, colorAccessor, colorScale)
|
||||
).rate(50);
|
||||
_renderLinesWithQueue(metadata);
|
||||
return _renderLinesWithQueue;
|
||||
};
|
||||
|
||||
const drawCellLinesSync = (
|
||||
metadata,
|
||||
dimensions,
|
||||
xscale,
|
||||
ctx,
|
||||
colorAccessor,
|
||||
colorScale
|
||||
) => {
|
||||
const _draw = drawLinesCanvas(
|
||||
ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
colorAccessor,
|
||||
colorScale
|
||||
);
|
||||
_.each(metadata, _draw);
|
||||
};
|
||||
|
||||
export default drawCellLinesUsingRenderQueue;
|
||||
// export default drawCellLinesUsingRenderQueue;
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
https://bl.ocks.org/mbostock/4341954
|
||||
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
|
||||
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
|
||||
*/
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@connect(state => {
|
||||
const ranges =
|
||||
state.cells.cells && state.cells.cells.data.ranges
|
||||
? state.cells.cells.data.ranges
|
||||
: null;
|
||||
const metadata =
|
||||
state.cells.cells && state.cells.cells.data.metadata
|
||||
? state.cells.cells.data.metadata
|
||||
: null;
|
||||
|
||||
const initializeRanges =
|
||||
state.initialize.data && state.initialize.data.data.ranges
|
||||
? state.initialize.data.data.ranges
|
||||
: null;
|
||||
|
||||
return {
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
cellsMetadata: state.controls.cellsMetadata
|
||||
};
|
||||
})
|
||||
class HistogramBrush extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = 300;
|
||||
this.height = 100;
|
||||
this.marginBottom = 20;
|
||||
this.histogramCache = {};
|
||||
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
brush: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {}
|
||||
componentDidUpdate() {}
|
||||
|
||||
calcHistogramCache(nextProps) {
|
||||
// recalculate expensive stuff
|
||||
const allValuesForContinuousFieldAsArray = _.map(
|
||||
nextProps.cellsMetadata,
|
||||
nextProps.metadataField
|
||||
);
|
||||
|
||||
this.histogramCache.x = d3
|
||||
.scaleLinear()
|
||||
.domain([nextProps.ranges.min, nextProps.ranges.max])
|
||||
.range([0, this.width]);
|
||||
|
||||
this.histogramCache.y = d3
|
||||
.scaleLinear()
|
||||
.range([this.height - this.marginBottom, 0]);
|
||||
// .range([height - margin.bottom, margin.top]);
|
||||
|
||||
this.histogramCache.bins = d3
|
||||
.histogram()
|
||||
.domain(this.histogramCache.x.domain())
|
||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||
|
||||
this.histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.calcHistogramCache(this.props);
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.metadataField !== nextProps.metadataField ||
|
||||
!this.histogramCache.x
|
||||
) {
|
||||
this.calcHistogramCache(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
onBrush(selection, x) {
|
||||
return () => {
|
||||
if (d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
range: null
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
drawHistogram(svgRef) {
|
||||
const x = this.histogramCache.x;
|
||||
const y = this.histogramCache.y;
|
||||
const bins = this.histogramCache.bins;
|
||||
const numValues = this.histogramCache.numValues;
|
||||
|
||||
d3
|
||||
.select(svgRef)
|
||||
.insert("g", "*")
|
||||
.attr("fill", "#bbb")
|
||||
.selectAll("rect")
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("x", function(d) {
|
||||
return x(d.x0) + 1;
|
||||
})
|
||||
.attr("y", function(d) {
|
||||
return y(d.length / numValues);
|
||||
})
|
||||
.attr("width", function(d) {
|
||||
return Math.abs(x(d.x1) - x(d.x0) - 1);
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return y(0) - y(d.length / numValues);
|
||||
});
|
||||
|
||||
if (!this.state.brush && !this.state.axis) {
|
||||
const brush = d3
|
||||
.select(svgRef)
|
||||
.append("g")
|
||||
.attr("class", "brush")
|
||||
.call(
|
||||
d3
|
||||
.brushX()
|
||||
.on(
|
||||
"end",
|
||||
this.onBrush(this.props.metadataField, x.invert).bind(this)
|
||||
)
|
||||
);
|
||||
|
||||
const xAxis = d3
|
||||
.select(svgRef)
|
||||
.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr(
|
||||
"transform",
|
||||
"translate(0," + (this.height - this.marginBottom) + ")"
|
||||
)
|
||||
.call(d3.axisBottom(x).ticks(5))
|
||||
.append("text")
|
||||
.attr("x", 300)
|
||||
.attr("y", -6)
|
||||
.attr("fill", "#000")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("font-weight", "bold")
|
||||
.text(this.props.metadataField);
|
||||
|
||||
this.setState({ brush, xAxis });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{ marginTop: 10 }}
|
||||
id={"histogram_" + this.props.metadataField}
|
||||
>
|
||||
<svg
|
||||
width={this.width}
|
||||
height={this.height}
|
||||
ref={svgRef => {
|
||||
this.drawHistogram(svgRef);
|
||||
}}
|
||||
>
|
||||
{this.props.ranges.min}
|
||||
{" to "}
|
||||
{this.props.ranges.max}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HistogramBrush;
|
||||
@@ -0,0 +1,155 @@
|
||||
// jshint esversion: 6
|
||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
||||
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import styles from "./parallelCoordinates.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
|
||||
import setupParallelCoordinates from "./setupParallelCoordinates";
|
||||
import drawAxes from "./drawAxes";
|
||||
import drawLinesCanvas from "./drawLinesCanvas";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges =
|
||||
state.cells.cells && state.cells.cells.data.ranges
|
||||
? state.cells.cells.data.ranges
|
||||
: null;
|
||||
const metadata =
|
||||
state.cells.cells && state.cells.cells.data.metadata
|
||||
? state.cells.cells.data.metadata
|
||||
: null;
|
||||
|
||||
const initializeRanges =
|
||||
state.initialize.data && state.initialize.data.data.ranges
|
||||
? state.initialize.data.data.ranges
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
};
|
||||
})
|
||||
class Parallel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
|
||||
this.setState({ svg, ctx });
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeDrawAxes(nextProps);
|
||||
this.maybeDrawLines(nextProps);
|
||||
}
|
||||
maybeDrawAxes(nextProps) {
|
||||
if (
|
||||
!this.state.axes &&
|
||||
nextProps.initializeRanges /* axes are created on full range of data */
|
||||
) {
|
||||
const dimensions = createDimensions(nextProps.initializeRanges);
|
||||
|
||||
const xscale = d3
|
||||
.scalePoint()
|
||||
.domain(d3.range(dimensions.length))
|
||||
.range([0, width]);
|
||||
|
||||
const axes = drawAxes(
|
||||
this.state.svg,
|
||||
this.state.ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
height,
|
||||
width,
|
||||
this.handleBrushAction.bind(this),
|
||||
this.handleColorAction.bind(this)
|
||||
);
|
||||
|
||||
this.setState({
|
||||
axes,
|
||||
xscale,
|
||||
dimensions
|
||||
});
|
||||
|
||||
this.props.dispatch({
|
||||
type: "parallel coordinates axes have been drawn"
|
||||
});
|
||||
}
|
||||
}
|
||||
maybeDrawLines = _.debounce(nextProps => {
|
||||
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
|
||||
if (
|
||||
nextProps.ranges &&
|
||||
nextProps.cellsMetadata &&
|
||||
nextProps.axesHaveBeenDrawn
|
||||
) {
|
||||
if (this.state._drawLinesCanvas) {
|
||||
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
|
||||
}
|
||||
|
||||
this.state.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const _drawLinesCanvas = drawLinesCanvas(
|
||||
nextProps.cellsMetadata,
|
||||
this.state.dimensions,
|
||||
this.state.xscale,
|
||||
this.state.ctx,
|
||||
nextProps.colorAccessor,
|
||||
nextProps.colorScale
|
||||
);
|
||||
|
||||
this.setState({
|
||||
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="parcoords_wrapper">
|
||||
<div
|
||||
className={styles.parcoords}
|
||||
id="parcoords"
|
||||
style={{
|
||||
width: width + margin.left + margin.right + "px",
|
||||
height: height + margin.top + margin.bottom + "px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Parallel;
|
||||
|
||||
// <SectionHeader text="Continuous Metadata"/>
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
|
||||
This code can be found: https://bl.ocks.org/syntagmatic/05a5b0897a48890133beb59c815bd953
|
||||
|
||||
body {
|
||||
min-width: 760px;
|
||||
}
|
||||
*/
|
||||
|
||||
.parcoords {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.parcoords svg,
|
||||
.parcoords canvas {
|
||||
font: 10px sans-serif;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.parcoords canvas {
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.axis .title {
|
||||
font-size: 10px;
|
||||
transform: rotate(-21deg) translate(-5px,-6px);
|
||||
fill: #222;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* -webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%); */
|
||||
|
||||
.axis line,
|
||||
.axis path {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.axis .tick text {
|
||||
fill: #222;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
old, from reference bl.ocks
|
||||
.axis.manufac_name .tick text,
|
||||
.axis.food_group .tick text {
|
||||
opacity: 1;
|
||||
}
|
||||
*/
|
||||
|
||||
.axis:hover line,
|
||||
.axis:hover path,
|
||||
.axis.active line,
|
||||
.axis.active path {
|
||||
fill: none;
|
||||
stroke: #222;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.axis:hover .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.axis:hover .tick text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.axis.active .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.axis.active .tick text {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.brush .extent {
|
||||
fill-opacity: .3;
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.pre {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
margin: 6px 12px;
|
||||
tab-size: 40;
|
||||
font-size: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*****************************************
|
||||
******************************************
|
||||
Setup SVG & Canvas elements
|
||||
******************************************
|
||||
******************************************/
|
||||
// jshint esversion: 6
|
||||
const setupParallelCoordinates = (width, height, margin) => {
|
||||
var container = d3.select("#parcoords");
|
||||
|
||||
var svg = container
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var canvas = container
|
||||
.append("canvas")
|
||||
.attr("width", width * devicePixelRatio)
|
||||
.attr("height", height * devicePixelRatio)
|
||||
.style("width", width + "px")
|
||||
.style("height", height + "px")
|
||||
.style("margin-top", margin.top + "px")
|
||||
.style("margin-left", margin.left + "px");
|
||||
|
||||
var ctx = canvas.node().getContext("2d");
|
||||
ctx.globalCompositeOperation = "darken";
|
||||
ctx.globalAlpha = 0.15;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
|
||||
return {
|
||||
svg,
|
||||
ctx
|
||||
};
|
||||
};
|
||||
|
||||
export default setupParallelCoordinates;
|
||||
@@ -0,0 +1,57 @@
|
||||
// jshint esversion: 6
|
||||
import _ from "lodash";
|
||||
|
||||
const paddingRight = 120;
|
||||
const continuousChartWidth = 1200;
|
||||
|
||||
export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
|
||||
export const width =
|
||||
continuousChartWidth - margin.left - margin.right - paddingRight;
|
||||
export const height = 340 - margin.top - margin.bottom;
|
||||
export const innerHeight = height - 2;
|
||||
|
||||
export const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
export const createDimensions = data => {
|
||||
const newArr = [];
|
||||
_.each(data, (value, key) => {
|
||||
if (value.range) {
|
||||
newArr.push({
|
||||
key: key /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */,
|
||||
type: {
|
||||
within: (d, extent, dim) => {
|
||||
return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1];
|
||||
}
|
||||
},
|
||||
scale: d3
|
||||
.scaleSqrt()
|
||||
.range([innerHeight, 0])
|
||||
.domain([0, value.range.max])
|
||||
});
|
||||
}
|
||||
});
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export const yAxis = d3.axisLeft();
|
||||
|
||||
export const brushstart = () => {
|
||||
d3.event.sourceEvent.stopPropagation();
|
||||
};
|
||||
|
||||
export const d3_functor = v => {
|
||||
return typeof v === "function"
|
||||
? v
|
||||
: () => {
|
||||
return v;
|
||||
};
|
||||
};
|
||||
|
||||
export const project = (d, dimensions, xscale) => {
|
||||
return dimensions.map((p, i) => {
|
||||
// check if data element has property and contains a value
|
||||
if (!(p.key in d) || d[p.key] === null) return null;
|
||||
|
||||
return [xscale(i), p.scale(d[p.key])];
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
// create continuous color legend
|
||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
||||
const continuous = (selector_id, colorscale) => {
|
||||
var legendheight = 200,
|
||||
legendwidth = 80,
|
||||
margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
|
||||
var canvas = d3
|
||||
.select(selector_id)
|
||||
.style("height", legendheight + "px")
|
||||
.style("width", legendwidth + "px")
|
||||
// .style("position", "relative")
|
||||
.append("canvas")
|
||||
.attr("height", legendheight - margin.top - margin.bottom)
|
||||
.attr("width", 1)
|
||||
.style("height", legendheight - margin.top - margin.bottom + "px")
|
||||
.style("width", legendwidth - margin.left - margin.right + "px")
|
||||
// .style("border", "1px solid #000")
|
||||
.style("position", "absolute")
|
||||
.style("top", margin.top + 1 + "px")
|
||||
.style("left", margin.left + 1 + "px")
|
||||
.style(
|
||||
"transform",
|
||||
"scale(1,-1)"
|
||||
) /* flip it! dark is high value light is low. we flip the color scale as well [1, 0] instead of [0, 1] */
|
||||
.node();
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var legendscale = d3
|
||||
.scaleLinear()
|
||||
.range([1, legendheight - margin.top - margin.bottom])
|
||||
.domain([
|
||||
colorscale.domain()[1],
|
||||
colorscale.domain()[0]
|
||||
]); /* we flip this to make viridis colors dark if high in the color scale */
|
||||
|
||||
// image data hackery based on http://bl.ocks.org/mbostock/048d21cf747371b11884f75ad896e5a5
|
||||
var image = ctx.createImageData(1, legendheight);
|
||||
d3.range(legendheight).forEach(function(i) {
|
||||
var c = d3.rgb(colorscale(legendscale.invert(i)));
|
||||
image.data[4 * i] = c.r;
|
||||
image.data[4 * i + 1] = c.g;
|
||||
image.data[4 * i + 2] = c.b;
|
||||
image.data[4 * i + 3] = 255;
|
||||
});
|
||||
ctx.putImageData(image, 0, 0);
|
||||
|
||||
// A simpler way to do the above, but possibly slower. keep in mind the legend width is stretched because the width attr of the canvas is 1
|
||||
// See http://stackoverflow.com/questions/4899799/whats-the-best-way-to-set-a-single-pixel-in-an-html5-canvas
|
||||
/*
|
||||
d3.range(legendheight).forEach(function(i) {
|
||||
ctx.fillStyle = colorscale(legendscale.invert(i));
|
||||
ctx.fillRect(0,i,1,1);
|
||||
});
|
||||
*/
|
||||
|
||||
var legendaxis = d3
|
||||
.axisRight()
|
||||
.scale(legendscale)
|
||||
.tickSize(6)
|
||||
.ticks(8);
|
||||
|
||||
var svg = d3
|
||||
.select(selector_id)
|
||||
.append("svg")
|
||||
.attr("height", legendheight + "px")
|
||||
.attr("width", legendwidth + "px")
|
||||
.style("position", "absolute")
|
||||
.style("left", "0px")
|
||||
.style("top", "0px");
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", "axis")
|
||||
.attr(
|
||||
"transform",
|
||||
"translate(" +
|
||||
(legendwidth - margin.left - margin.right + 3) +
|
||||
"," +
|
||||
margin.top +
|
||||
")"
|
||||
)
|
||||
.call(legendaxis);
|
||||
};
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
responsive: state.responsive
|
||||
};
|
||||
})
|
||||
class ContinuousLegend extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.colorAccessor !== this.props.colorAccessor) {
|
||||
/* always remove it, if it's not continuous we don't put it back. */
|
||||
d3
|
||||
.select("#continuous_legend")
|
||||
.selectAll("*")
|
||||
.remove();
|
||||
}
|
||||
|
||||
if (nextProps.colorAccessor && nextProps.colorScale) {
|
||||
/* fragile! continuous range is 0 to 1, not [#fa4b2c, ...], make this a flag? */
|
||||
if (nextProps.colorScale.range()[0][0] !== "#") {
|
||||
continuous(
|
||||
"#continuous_legend",
|
||||
d3
|
||||
.scaleSequential(d3.interpolateViridis)
|
||||
.domain(nextProps.colorScale.domain())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
drawScale() {}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
id="continuous_legend"
|
||||
style={{
|
||||
position: "fixed",
|
||||
display: this.props.colorAccessor ? "inherit" : "none",
|
||||
right: 0,
|
||||
top: this.props.responsive.height / 2
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ContinuousLegend;
|
||||
@@ -0,0 +1,77 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "../framework/buttons.css";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect()
|
||||
class CellSetButton extends React.Component {
|
||||
set() {
|
||||
const set = _.map(this.props.crossfilter.cells.allFiltered(), "CellName");
|
||||
|
||||
this.props.dispatch({
|
||||
type:
|
||||
"store current cell selection as differential set " +
|
||||
this.props.eitherCellSetOneOrTwo,
|
||||
data: set
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button
|
||||
style={{
|
||||
color: "#FFF",
|
||||
borderRadius: 2,
|
||||
padding: "0px 10px",
|
||||
height: 30,
|
||||
backgroundColor: globals.brightBlue,
|
||||
border: "none",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={this.set.bind(this)}
|
||||
>
|
||||
<span style={{ fontSize: 24, fontWeight: 700 }}>
|
||||
{" "}
|
||||
{this.props.eitherCellSetOneOrTwo}{" "}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "Georgia",
|
||||
fontStyle: "italic",
|
||||
marginLeft: 8,
|
||||
position: "relative",
|
||||
top: -3
|
||||
}}
|
||||
>
|
||||
{this.props.differential[
|
||||
"celllist" + this.props.eitherCellSetOneOrTwo
|
||||
]
|
||||
? this.props.differential[
|
||||
"celllist" + this.props.eitherCellSetOneOrTwo
|
||||
].length + " cells"
|
||||
: 0 + " cells"}
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CellSetButton;
|
||||
|
||||
//
|
||||
// <button style={{
|
||||
// marginLeft: 3,
|
||||
// color: "#FFF",
|
||||
// padding: "0px 10px",
|
||||
// height: 30,
|
||||
// backgroundColor: globals.brightBlue,
|
||||
// border: "none",
|
||||
// cursor: "pointer",
|
||||
// }}>
|
||||
// <span style={{fontSize: 24, fontWeight: 700}}> X </span>
|
||||
//
|
||||
// </button>
|
||||
@@ -0,0 +1,323 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./expression.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
import actions from "../../actions";
|
||||
import ReactAutocomplete from "react-autocomplete"; /* http://emilebres.github.io/react-virtualized-checkbox/ */
|
||||
import getContrast from "font-color-contrast"; // https://www.npmjs.com/package/font-color-contrast
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
|
||||
class HeatmapSquare extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const contrastColor = getContrast(
|
||||
this.props.backgroundColor
|
||||
.substring(4, this.props.backgroundColor.length - 1)
|
||||
.replace(/ /g, "")
|
||||
.split(",")
|
||||
);
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
padding: "12px 6px",
|
||||
textAlign: "center",
|
||||
color: contrastColor,
|
||||
width: 40,
|
||||
flexShrink: 0,
|
||||
fontSize: 12,
|
||||
margin: 0,
|
||||
backgroundColor: this.props.backgroundColor
|
||||
}}
|
||||
>
|
||||
{this.props.text}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************
|
||||
***********************************
|
||||
***********************************
|
||||
Row
|
||||
***********************************
|
||||
***********************************
|
||||
**********************************/
|
||||
@connect(state => {
|
||||
return {
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
colorAccessor: state.controls.colorAccessor
|
||||
};
|
||||
})
|
||||
class HeatmapRow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
handleGeneColorScaleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch(
|
||||
actions.requestSingleGeneExpressionCountsForColoringPOST(
|
||||
this.props.gene
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
handleSetGeneAsScatterplotX(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
type: "set scatterplot x",
|
||||
data: this.props.gene
|
||||
});
|
||||
};
|
||||
}
|
||||
handleSetGeneAsScatterplotY(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
type: "set scatterplot y",
|
||||
data: this.props.gene
|
||||
});
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 220,
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "baseline"
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 150, flexShrink: 0 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
{this.props.gene}
|
||||
</span>
|
||||
</div>
|
||||
<HeatmapSquare
|
||||
backgroundColor={this.props.greyColorScale(this.props.set1exp)}
|
||||
text={this.props.set1exp}
|
||||
/>
|
||||
<HeatmapSquare
|
||||
backgroundColor={this.props.greyColorScale(this.props.set2exp)}
|
||||
text={this.props.set2exp}
|
||||
/>
|
||||
<span
|
||||
title={this.props.aveDiff}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
>
|
||||
{this.props.aveDiff.toFixed(2)}
|
||||
</span>
|
||||
<span
|
||||
onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(
|
||||
this
|
||||
)}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color:
|
||||
this.props.scatterplotXXaccessor === this.props.gene
|
||||
? "white"
|
||||
: globals.brightBlue,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 700,
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
padding: "2px 3px",
|
||||
backgroundColor:
|
||||
this.props.scatterplotXXaccessor === this.props.gene
|
||||
? globals.brightBlue
|
||||
: "inherit"
|
||||
}}
|
||||
>
|
||||
X
|
||||
</span>
|
||||
<span
|
||||
onClick={this.handleSetGeneAsScatterplotY(this.props.gene).bind(
|
||||
this
|
||||
)}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color:
|
||||
this.props.scatterplotYYaccessor === this.props.gene
|
||||
? "white"
|
||||
: globals.brightBlue,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 700,
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
padding: "2px 3px",
|
||||
backgroundColor:
|
||||
this.props.scatterplotYYaccessor === this.props.gene
|
||||
? globals.brightBlue
|
||||
: "inherit"
|
||||
}}
|
||||
>
|
||||
Y
|
||||
</span>
|
||||
<span
|
||||
onClick={this.handleGeneColorScaleClick(this.props.gene).bind(this)}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
marginRight: 6,
|
||||
borderRadius: 3,
|
||||
padding: "0px 2px 2px 2px",
|
||||
color:
|
||||
this.props.colorAccessor === this.props.gene
|
||||
? "white"
|
||||
: "inherit",
|
||||
backgroundColor:
|
||||
this.props.colorAccessor === this.props.gene
|
||||
? globals.brightBlue
|
||||
: "inherit"
|
||||
}}
|
||||
>
|
||||
<FaPaintBrush style={{ display: "inline-block" }} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************
|
||||
***********************************
|
||||
***********************************
|
||||
DiffExp Heatmap
|
||||
***********************************
|
||||
***********************************
|
||||
**********************************/
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
allGeneNames: state.controls.allGeneNames
|
||||
};
|
||||
})
|
||||
class Heatmap extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
render() {
|
||||
if (!this.props.differential.diffExp)
|
||||
return <p>Select cells & compute differential to see heatmap</p>;
|
||||
|
||||
const topGenesForCellSet1 = this.props.differential.diffExp.data.celllist1;
|
||||
const topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2;
|
||||
|
||||
const extent = d3.extent(
|
||||
_.union(
|
||||
topGenesForCellSet1.mean_expression_cellset1,
|
||||
topGenesForCellSet1.mean_expression_cellset2,
|
||||
topGenesForCellSet2.mean_expression_cellset1,
|
||||
topGenesForCellSet2.mean_expression_cellset2
|
||||
)
|
||||
);
|
||||
|
||||
const greyColorScale = d3
|
||||
.scaleSequential()
|
||||
.domain(extent)
|
||||
.interpolator(d3.interpolateGreys);
|
||||
|
||||
return (
|
||||
<div>
|
||||
Color by any gene:
|
||||
<ReactAutocomplete
|
||||
items={this.props.allGeneNames}
|
||||
shouldItemRender={(item, value) =>
|
||||
item.toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}
|
||||
getItemValue={item => item}
|
||||
renderItem={(item, highlighted) => (
|
||||
<div
|
||||
key={item}
|
||||
style={{ backgroundColor: highlighted ? "#eee" : "transparent" }}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)}
|
||||
value={this.state.value}
|
||||
onChange={e => this.setState({ value: e.target.value })}
|
||||
onSelect={value => {
|
||||
this.setState({ value });
|
||||
this.props.dispatch(
|
||||
actions.requestSingleGeneExpressionCountsForColoringPOST(value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
width: 400,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
<p style={{ marginRight: 110 }}>Gene</p>
|
||||
<p style={{ marginRight: 25 }}>1</p>
|
||||
<p style={{ marginRight: 20 }}>2</p>
|
||||
<p>ave diff</p>
|
||||
</div>
|
||||
{topGenesForCellSet1.topgenes.map((gene, i) => {
|
||||
return (
|
||||
<HeatmapRow
|
||||
key={gene}
|
||||
gene={gene}
|
||||
greyColorScale={greyColorScale}
|
||||
aveDiff={topGenesForCellSet1.ave_diff[i]}
|
||||
set1exp={Math.floor(
|
||||
topGenesForCellSet1.mean_expression_cellset1[i]
|
||||
)}
|
||||
set2exp={Math.floor(
|
||||
topGenesForCellSet1.mean_expression_cellset2[i]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{topGenesForCellSet2.topgenes.map((gene, i) => {
|
||||
return (
|
||||
<HeatmapRow
|
||||
key={gene}
|
||||
gene={gene}
|
||||
greyColorScale={greyColorScale}
|
||||
aveDiff={topGenesForCellSet2.ave_diff[i]}
|
||||
set1exp={Math.floor(
|
||||
topGenesForCellSet2.mean_expression_cellset1[i]
|
||||
)}
|
||||
set2exp={Math.floor(
|
||||
topGenesForCellSet2.mean_expression_cellset2[i]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Heatmap;
|
||||
@@ -0,0 +1,94 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
import CellSetButton from "./cellSetButtons";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
crossfilter: state.controls.crossfilter
|
||||
};
|
||||
})
|
||||
class Expression extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
handleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
type: "color by expression",
|
||||
gene: gene
|
||||
});
|
||||
};
|
||||
}
|
||||
computeDiffExp() {
|
||||
this.props.dispatch(
|
||||
actions.requestDifferentialExpression(
|
||||
this.props.differential.celllist1,
|
||||
this.props.differential.celllist2
|
||||
)
|
||||
);
|
||||
}
|
||||
render() {
|
||||
if (!this.props.differential) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div style={{ margin: 10 }}>
|
||||
<div style={{ marginBottom: 15, width: 300 }}>
|
||||
There are currently
|
||||
{" " +
|
||||
(this.props.crossfilter
|
||||
? this.props.crossfilter.cells.countFiltered()
|
||||
: 0) +
|
||||
" "}
|
||||
cells selected, click a cell set button to store them.
|
||||
</div>
|
||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
||||
</div>
|
||||
<div>
|
||||
{this.props.differential.celllist1 &&
|
||||
this.props.differential.celllist2 ? (
|
||||
<button
|
||||
style={{
|
||||
fontSize: 18,
|
||||
margin: 10,
|
||||
fontWeight: 700,
|
||||
color: "#FFF",
|
||||
padding: "12px 20px",
|
||||
backgroundColor: globals.brightBlue,
|
||||
border: "none",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={this.computeDiffExp.bind(this)}
|
||||
>
|
||||
Compute differential expression
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
style={{
|
||||
fontSize: 18,
|
||||
margin: 10,
|
||||
fontWeight: 700,
|
||||
color: "#FFF",
|
||||
padding: "12px 20px",
|
||||
backgroundColor: globals.mediumGrey,
|
||||
border: "none"
|
||||
}}
|
||||
>
|
||||
Compute differential expression
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Expression;
|
||||
@@ -0,0 +1,6 @@
|
||||
.btn {
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: "#FFF";
|
||||
border: "none";
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
|
||||
import styles from "./container.css";
|
||||
|
||||
const Container = props => (
|
||||
<div className={styles.container}>{props.children}</div>
|
||||
);
|
||||
|
||||
export default Container;
|
||||
@@ -0,0 +1,3 @@
|
||||
.header {
|
||||
background: red;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
|
||||
import Container from "./container";
|
||||
import styles from "./header.css";
|
||||
|
||||
const Header = () => (
|
||||
<header className={styles.header}>
|
||||
<Container />
|
||||
</header>
|
||||
);
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,15 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
|
||||
const SectionHeader = ({ text }) => (
|
||||
<p
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
);
|
||||
|
||||
export default SectionHeader;
|
||||
@@ -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;
|
||||
// }
|
||||
@@ -0,0 +1,163 @@
|
||||
// jshint esversion: 6
|
||||
import styles from "./joy.css";
|
||||
|
||||
/*
|
||||
via https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3
|
||||
*/
|
||||
|
||||
var margin = { top: 30, right: 10, bottom: 30, left: 100 },
|
||||
width = 400 - margin.left - margin.right,
|
||||
height = 600 - margin.top - margin.bottom;
|
||||
|
||||
// Percent two area charts can overlap
|
||||
var overlap = 0.4;
|
||||
|
||||
var formatTime = d3.timeFormat("%I %p");
|
||||
|
||||
var x = function(d) {
|
||||
return d.time;
|
||||
},
|
||||
xScale = d3.scaleTime().range([0, width]),
|
||||
xValue = function(d) {
|
||||
return xScale(x(d));
|
||||
},
|
||||
xAxis = d3.axisBottom(xScale).tickFormat(formatTime);
|
||||
|
||||
var y = function(d) {
|
||||
return d.value;
|
||||
},
|
||||
yScale = d3.scaleLinear(),
|
||||
yValue = function(d) {
|
||||
return yScale(y(d));
|
||||
};
|
||||
|
||||
var activity = function(d) {
|
||||
return d.key;
|
||||
},
|
||||
activityScale = d3.scaleBand().range([0, height]),
|
||||
activityValue = function(d) {
|
||||
return activityScale(activity(d));
|
||||
},
|
||||
activityAxis = d3.axisLeft(activityScale);
|
||||
|
||||
var area = d3
|
||||
.area()
|
||||
.x(xValue)
|
||||
.y1(yValue);
|
||||
|
||||
var line = area.lineY1();
|
||||
|
||||
function parseTime(offset) {
|
||||
var date = new Date(2017, 0, 1); // chose an arbitrary day
|
||||
return d3.timeMinute.offset(date, offset);
|
||||
}
|
||||
|
||||
function row(d) {
|
||||
return {
|
||||
activity: d.activity,
|
||||
time: parseTime(d.time),
|
||||
value: +d.p_smooth
|
||||
};
|
||||
}
|
||||
|
||||
const drawJoy = data => {
|
||||
console.log("drawJoy: ", data);
|
||||
|
||||
var svg = d3
|
||||
.select("#joyplot")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
d3.tsv(
|
||||
"https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv",
|
||||
row,
|
||||
function(error, dataFlat) {
|
||||
// Sort by time
|
||||
dataFlat.sort(function(a, b) {
|
||||
return a.time - b.time;
|
||||
});
|
||||
|
||||
var data = d3
|
||||
.nest()
|
||||
.key(function(d) {
|
||||
return d.activity;
|
||||
})
|
||||
.entries(dataFlat);
|
||||
|
||||
// Sort activities by peak activity time
|
||||
function peakTime(d) {
|
||||
var i = d3.scan(d.values, function(a, b) {
|
||||
return y(b) - y(a);
|
||||
});
|
||||
return d.values[i].time;
|
||||
}
|
||||
data.sort(function(a, b) {
|
||||
return peakTime(b) - peakTime(a);
|
||||
});
|
||||
|
||||
console.log("sorted", data);
|
||||
|
||||
xScale.domain(d3.extent(dataFlat, x));
|
||||
|
||||
activityScale.domain(
|
||||
data.map(function(d) {
|
||||
return d.key;
|
||||
})
|
||||
);
|
||||
|
||||
var areaChartHeight =
|
||||
(1 + overlap) * (height / activityScale.domain().length);
|
||||
|
||||
yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
|
||||
|
||||
area.y0(yScale(0));
|
||||
|
||||
var gActivity = svg
|
||||
.append("g")
|
||||
.attr("class", "activities")
|
||||
.selectAll(".activity")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", function(d) {
|
||||
return `${styles.activity} ${styles.activity["--" + d.key]}`;
|
||||
})
|
||||
.attr("transform", function(d) {
|
||||
var ty = activityValue(d) - activityScale.bandwidth() + 5;
|
||||
return "translate(0," + ty + ")";
|
||||
});
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.area)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", area);
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.line)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", line);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--x"]}`)
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
|
||||
.call(activityAxis);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default drawJoy;
|
||||
@@ -0,0 +1,42 @@
|
||||
svg {
|
||||
display: block;
|
||||
/*margin: 0 auto;*/
|
||||
}
|
||||
|
||||
.axis .domain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--x text {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.axis--x line {
|
||||
stroke: #aaa;
|
||||
}
|
||||
|
||||
.axis--activity .tick line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--activity text {
|
||||
font-size: 12px;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
/*.axis--activity .tick:nth-child(odd) text {
|
||||
fill: #222;
|
||||
}*/
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.area {
|
||||
fill: #448cab;
|
||||
}
|
||||
|
||||
.activity:nth-child(odd) .area {
|
||||
fill: #5ca3c1;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import styles from "./joy.css";
|
||||
import drawJoy from "./drawJoy";
|
||||
import joyParser from "./joyParser";
|
||||
|
||||
class Joy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data) {
|
||||
console.log("joyplot data 44", nextProps.data);
|
||||
drawJoy(joyParser(nextProps.data));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="joyplot_wrapper" style={{ marginTop: 50 }}>
|
||||
<h3> Joy </h3>
|
||||
<p>
|
||||
{" "}
|
||||
Cell expression distribution per gene & if differential expression,
|
||||
Ie., cells for cluster 5, top genes expressed by cluster 8
|
||||
</p>
|
||||
<div id="joyplot"> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Joy;
|
||||
@@ -0,0 +1,29 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
const joyParser = (data, count = 20) => {
|
||||
const genes = [];
|
||||
|
||||
/* setup */
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const gene = {
|
||||
key:
|
||||
data.genes[
|
||||
i
|
||||
] /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */,
|
||||
values: []
|
||||
};
|
||||
|
||||
data.cells.forEach(cell => {
|
||||
gene.values.push({
|
||||
value: cell["e"][i]
|
||||
});
|
||||
});
|
||||
|
||||
genes.push(gene);
|
||||
}
|
||||
|
||||
return genes;
|
||||
};
|
||||
|
||||
export default joyParser;
|
||||
@@ -0,0 +1,109 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import Categorical from "./categorical/categorical";
|
||||
import Continuous from "./continuous/continuous";
|
||||
import ExpressionButtons from "./expression/expressionButtons";
|
||||
import { connect } from "react-redux";
|
||||
import Heatmap from "./expression/diffExpHeatmap";
|
||||
import * as globals from "../globals";
|
||||
import DynamicScatterplot from "./scatterplot/scatterplot";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
responsive: state.responsive
|
||||
};
|
||||
})
|
||||
class LeftSideBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.metadataSectionPadding = 300;
|
||||
this.state = {
|
||||
currentTab: "metadata"
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div style={{ position: "fixed" }}>
|
||||
<p
|
||||
style={{
|
||||
margin: 10,
|
||||
fontSize: 24,
|
||||
color: globals.lightGrey,
|
||||
fontWeight: 700,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
CELLxGENE {globals.datasetTitle}{" "}
|
||||
</p>
|
||||
<div style={{ padding: 10 }}>
|
||||
<button
|
||||
style={{
|
||||
padding: "none",
|
||||
outline: 0,
|
||||
fontSize: 14,
|
||||
fontWeight: this.state.currentTab === "metadata" ? 700 : 400,
|
||||
fontStyle:
|
||||
this.state.currentTab === "metadata" ? "inherit" : "italic",
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
backgroundColor: "#FFF",
|
||||
borderTop: "none",
|
||||
borderBottom: "none",
|
||||
borderRight: "none",
|
||||
borderLeft: "none"
|
||||
}}
|
||||
onClick={() => {
|
||||
this.setState({ currentTab: "metadata" });
|
||||
}}
|
||||
>
|
||||
Metadata
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: "none",
|
||||
outline: 0,
|
||||
fontSize: 14,
|
||||
fontWeight: this.state.currentTab === "expression" ? 700 : 400,
|
||||
fontStyle:
|
||||
this.state.currentTab === "expression" ? "inherit" : "italic",
|
||||
cursor: "pointer",
|
||||
border: "none",
|
||||
backgroundColor: "#FFF",
|
||||
borderTop: "none",
|
||||
borderBottom: "none",
|
||||
borderRight: "none",
|
||||
borderLeft: "none"
|
||||
}}
|
||||
onClick={() => {
|
||||
this.setState({ currentTab: "expression" });
|
||||
}}
|
||||
>
|
||||
Expression
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: this.props.responsive.height - this.metadataSectionPadding,
|
||||
width: 400,
|
||||
padding: 10,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden"
|
||||
}}
|
||||
>
|
||||
{this.state.currentTab === "metadata" ? <Categorical /> : null}
|
||||
{this.state.currentTab === "metadata" ? <Continuous /> : null}
|
||||
{this.state.currentTab === "expression" ? <Heatmap /> : null}
|
||||
</div>
|
||||
<div style={{ position: "fixed", bottom: 0, left: 0 }}>
|
||||
{this.state.currentTab === "metadata" ? <ExpressionButtons /> : null}
|
||||
{this.state.currentTab === "expression" ? (
|
||||
<DynamicScatterplot />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LeftSideBar;
|
||||
@@ -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,15 @@
|
||||
|
||||
.scatterplot {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scatterplot svg,
|
||||
.scatterplot canvas {
|
||||
font: 10px sans-serif;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.scatterplot canvas {
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// jshint esversion: 6
|
||||
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
|
||||
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
|
||||
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import scatterplot from "./scatterplot";
|
||||
import setupScatterplot from "./setupScatterplot";
|
||||
import styles from "./scatterplot.css";
|
||||
|
||||
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 { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges =
|
||||
state.cells.cells && state.cells.cells.data.ranges
|
||||
? state.cells.cells.data.ranges
|
||||
: null;
|
||||
const metadata =
|
||||
state.cells.cells && state.cells.cells.data.metadata
|
||||
? state.cells.cells.data.metadata
|
||||
: null;
|
||||
const initializeRanges =
|
||||
state.initialize.data && state.initialize.data.data.ranges
|
||||
? state.initialize.data.data.ranges
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
crossfilter: state.controls.crossfilter,
|
||||
differential: state.differential,
|
||||
expression: state.expression
|
||||
};
|
||||
})
|
||||
class Scatterplot extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.count = 0;
|
||||
this.state = {
|
||||
svg: null,
|
||||
// ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
xScale: null,
|
||||
yScale: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { svg } = setupScatterplot(width, height, margin);
|
||||
this.setState({
|
||||
svg
|
||||
});
|
||||
|
||||
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({
|
||||
distance: camera.distance,
|
||||
color: colorBuffer,
|
||||
position: pointBuffer,
|
||||
size: sizeBuffer,
|
||||
count: this.count,
|
||||
view: camera.view(),
|
||||
scale: viewportHeight / viewportWidth
|
||||
});
|
||||
|
||||
camera.tick();
|
||||
});
|
||||
|
||||
this.setState({
|
||||
regl,
|
||||
sizeBuffer,
|
||||
pointBuffer,
|
||||
colorBuffer
|
||||
});
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeSetupScalesAndDrawAxes(nextProps);
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.state.xScale &&
|
||||
this.state.yScale &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
||||
) {
|
||||
this.drawAxesSVG(this.state.xScale, this.state.yScale);
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.regl &&
|
||||
this.state.pointBuffer &&
|
||||
this.state.colorBuffer &&
|
||||
this.state.sizeBuffer &&
|
||||
this.props.expression.data &&
|
||||
this.props.expression.data.genes &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
this.state.xScale &&
|
||||
this.state.yScale
|
||||
) {
|
||||
const crossfilter = this.props.crossfilter.cells;
|
||||
const data = this.props.expression.data;
|
||||
const cells = data.cells;
|
||||
const genes = data.genes;
|
||||
const cellCount = cells.length;
|
||||
const positions = new Float32Array(2 * cellCount);
|
||||
const colors = new Float32Array(3 * cellCount);
|
||||
const sizes = new Float32Array(cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0, width]).range([-0.95, 0.95])
|
||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
||||
|
||||
// d3.scaleLinear().domain([0, height]).range([-1, 1])
|
||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
||||
|
||||
const geneXXaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotXXaccessor
|
||||
);
|
||||
const geneYYaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
|
||||
/*
|
||||
Construct Vectors
|
||||
*/
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const cell = cells[i];
|
||||
|
||||
positions[2 * i] = glScaleX(
|
||||
this.state.xScale(cell.e[geneXXaccessorIndex])
|
||||
); /* scale each point first to the window as we calculate extents separately below, so no need to repeat */
|
||||
positions[2 * i + 1] = glScaleY(
|
||||
this.state.yScale(cell.e[geneYYaccessorIndex])
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const metadata = this.props.metadata[i];
|
||||
colors.set(metadata.__colorRGB__, 3 * i);
|
||||
}
|
||||
|
||||
crossfilter.fillByIsFiltered(sizes, 4, 0.2);
|
||||
|
||||
this.state.pointBuffer({ data: positions, dimension: 2 });
|
||||
this.state.colorBuffer({ data: colors, dimension: 3 });
|
||||
this.state.sizeBuffer({ data: sizes, dimension: 1 });
|
||||
this.count = cellCount;
|
||||
}
|
||||
}
|
||||
maybeSetupScalesAndDrawAxes(nextProps) {
|
||||
if (
|
||||
nextProps.expression &&
|
||||
nextProps.expression.data &&
|
||||
nextProps.scatterplotXXaccessor &&
|
||||
nextProps.scatterplotYYaccessor
|
||||
) {
|
||||
const xScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(nextProps.expression.data.cells, (cell, i) => {
|
||||
return cell.e[
|
||||
nextProps.expression.data.genes.indexOf(
|
||||
nextProps.scatterplotXXaccessor
|
||||
)
|
||||
];
|
||||
})
|
||||
)
|
||||
.range([0, width]);
|
||||
|
||||
const yScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(nextProps.expression.data.cells, cell => {
|
||||
return cell.e[
|
||||
nextProps.expression.data.genes.indexOf(
|
||||
nextProps.scatterplotYYaccessor
|
||||
)
|
||||
];
|
||||
})
|
||||
)
|
||||
.range([height, 0]);
|
||||
|
||||
this.setState({
|
||||
xScale,
|
||||
yScale
|
||||
});
|
||||
}
|
||||
}
|
||||
drawAxesSVG(xScale, yScale) {
|
||||
this.state.svg.selectAll("*").remove();
|
||||
|
||||
// the axes are much cleaner and easier now. No need to rotate and orient the axis, just call axisBottom, axisLeft etc.
|
||||
var xAxis = d3.axisBottom().scale(xScale);
|
||||
|
||||
var yAxis = d3.axisLeft().scale(yScale);
|
||||
|
||||
// adding axes is also simpler now, just translate x-axis to (0,height) and it's alread defined to be a bottom axis.
|
||||
this.state.svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.attr("class", "x axis")
|
||||
.call(xAxis);
|
||||
|
||||
// y-axis is translated to (0,0)
|
||||
this.state.svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(0,0)")
|
||||
.attr("class", "y axis")
|
||||
.call(yAxis);
|
||||
|
||||
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
|
||||
this.state.svg
|
||||
.append("text")
|
||||
.attr("x", 10)
|
||||
.attr("y", 10)
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotYYaccessor);
|
||||
|
||||
this.state.svg
|
||||
.append("text")
|
||||
.attr("x", width)
|
||||
.attr("y", height - 10)
|
||||
.attr("text-anchor", "end")
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotXXaccessor);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "white",
|
||||
paddingBottom: 20
|
||||
}}
|
||||
id="scatterplot_wrapper"
|
||||
>
|
||||
<div
|
||||
className={styles.scatterplot}
|
||||
id="scatterplot"
|
||||
style={{
|
||||
width: width + margin.left + margin.right + "px",
|
||||
height: height + margin.top + margin.bottom + "px"
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
marginLeft: margin.left - 7,
|
||||
marginTop: margin.top
|
||||
}}
|
||||
ref={canvas => {
|
||||
this.reglCanvas = canvas;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Scatterplot;
|
||||
|
||||
// <SectionHeader text="Continuous Metadata"/>
|
||||
@@ -0,0 +1,23 @@
|
||||
// jshint esversion: 6
|
||||
/*****************************************
|
||||
******************************************
|
||||
Setup SVG & Canvas elements
|
||||
******************************************
|
||||
******************************************/
|
||||
|
||||
const setupScatterplot = (width, height, margin) => {
|
||||
var container = d3.select("#scatterplot");
|
||||
|
||||
var svg = container
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
return {
|
||||
svg
|
||||
};
|
||||
};
|
||||
|
||||
export default setupScatterplot;
|
||||
@@ -0,0 +1,12 @@
|
||||
// jshint esversion: 6
|
||||
import _ from "lodash";
|
||||
|
||||
const paddingRight = 120;
|
||||
const continuousChartWidth = 340;
|
||||
|
||||
export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
|
||||
export const width = 340;
|
||||
export const height = 340 - margin.top - margin.bottom;
|
||||
export const innerHeight = height - 2;
|
||||
|
||||
export const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
Reference in New Issue
Block a user