renaming backend, cellxgene to server, client respectively

This commit is contained in:
Charlotte Weaver
2018-06-26 11:41:32 -07:00
parent 59dcfe2bc4
commit dd5fa57259
97 changed files with 4 additions and 7 deletions
@@ -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
};
+45
View File
@@ -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
};
+105
View File
@@ -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;
});
};
+126
View File
@@ -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;
+57
View File
@@ -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;
}
+13
View File
@@ -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"
});
}
+34
View File
@@ -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;
}
+383
View File
@@ -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
};
};
+26
View File
@@ -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;
// }
+163
View File
@@ -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;
+42
View File
@@ -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;
}
+38
View File
@@ -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;
+29
View File
@@ -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;
+109
View File
@@ -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;
+12
View File
@@ -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;