This commit is contained in:
bkmartinjr
2018-05-05 13:15:10 -07:00
parent d91d42f2c4
commit 90c7daf571
62 changed files with 2398 additions and 1912 deletions
@@ -1,11 +1,15 @@
import React from 'react';
// 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';
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);
@@ -20,16 +24,16 @@ class Heatmap extends React.Component {
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]},
{ 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: '',
title: ""
},
viewport: {
lookAt: [0, 0, 0],
@@ -46,7 +50,7 @@ class Heatmap extends React.Component {
getColor(cluster) {
let color = [0, 0, 0];
switch(cluster){
switch (cluster) {
case 0:
color = [166, 206, 227];
break;
@@ -82,9 +86,8 @@ class Heatmap extends React.Component {
return color;
}
componentWillMount() {
window.addEventListener('resize', this.onResize);
window.addEventListener("resize", this.onResize);
this.onResize();
}
@@ -97,30 +100,31 @@ class Heatmap extends React.Component {
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
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 })
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)
console.log("d", d);
if (d.object) {
const object = d.object;
@@ -129,7 +133,7 @@ class Heatmap extends React.Component {
title: object.title,
displayed: true,
x: d.x,
y: d.y,
y: d.y
};
}
@@ -137,8 +141,8 @@ class Heatmap extends React.Component {
}
onResize() {
const {innerWidth: width, innerHeight: height} = window;
this.setState({width: width / 1.5, height: height / 1.5});
const { innerWidth: width, innerHeight: height } = window;
this.setState({ width: width / 1.5, height: height / 1.5 });
}
onInitialized(gl) {
@@ -150,27 +154,30 @@ class Heatmap extends React.Component {
onChangeViewport(viewport) {
this.setState({
rotating: !viewport.isDragging,
viewport: {...this.state.viewport, ...viewport}
viewport: { ...this.state.viewport, ...viewport }
});
}
onUpdate() {
const {viewport} = this.state;
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
});
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() {
@@ -182,7 +189,7 @@ class Heatmap extends React.Component {
* ]
*/
const screenGridLayer = new ScreenGridLayer({
id: 'screen-grid-layer',
id: "screen-grid-layer",
data: this.state.sampleExpressionMatrix,
projectionMode: COORDINATE_SYSTEM.IDENTITY,
pickable: true,
@@ -195,34 +202,42 @@ class Heatmap extends React.Component {
}
renderDeckGLCanvas() {
const {width, height, viewport} = this.state;
const canvasProps = {width, height, ...viewport};
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>
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;
const { width, height, popup } = this.state;
if (!width || !height) {
return null;
}
const renderedPopup = (popup.displayed)? <Popup {...popup} /> : null;
const renderedPopup = popup.displayed ? <Popup {...popup} /> : null;
return (
<div id="heatmap">
@@ -231,6 +246,6 @@ class Heatmap extends React.Component {
</div>
);
}
};
}
export default Heatmap;
@@ -1,7 +1,8 @@
// jshint esversion: 6
/* global window */
import React, {Component} from 'react';
import {PerspectiveViewport} from 'deck.gl';
import {vec3} from 'gl-matrix';
import React, { Component } from "react";
import { PerspectiveViewport } from "deck.gl";
import { vec3 } from "gl-matrix";
/* Utils */
// constrain number between bounds
@@ -15,15 +16,24 @@ function clamp(x, min, max) {
return x;
}
const ua = typeof window.navigator !== 'undefined' ?
window.navigator.userAgent.toLowerCase() : '';
const firefox = ua.indexOf('firefox') !== -1;
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}) {
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);
@@ -45,25 +55,29 @@ export default class OrbitController extends Component {
}
_onDragStart(evt) {
const {pageX, pageY} = evt;
const { pageX, pageY } = evt;
this._dragStartPos = [pageX, pageY];
this.props.onChangeViewport({isDragging: true});
this.props.onChangeViewport({ isDragging: true });
}
_onDrag(evt) {
if (this._dragStartPos) {
const {pageX, pageY} = evt;
const {width, height} = this.props;
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 { 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]);
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);
@@ -72,7 +86,7 @@ export default class OrbitController extends Component {
});
} else {
// rotate
const {rotationX, rotationY} = this.props;
const { rotationX, rotationY } = this.props;
const newRotationX = clamp(rotationX - dy * 180, -90, 90);
const newRotationY = (rotationY - dx * 180) % 360;
@@ -88,7 +102,7 @@ export default class OrbitController extends Component {
_onDragEnd() {
this._dragStartPos = null;
this.props.onChangeViewport({isDragging: false});
this.props.onChangeViewport({ isDragging: false });
}
_onWheel(evt) {
@@ -107,8 +121,12 @@ export default class OrbitController extends Component {
value = Math.floor(value / 4);
}
const {distance, minDistance, maxDistance} = this.props;
const newDistance = clamp(distance * Math.pow(1.01, value), minDistance, maxDistance);
const { distance, minDistance, maxDistance } = this.props;
const newDistance = clamp(
distance * Math.pow(1.01, value),
minDistance,
maxDistance
);
this.props.onChangeViewport({
distance: newDistance
@@ -117,7 +135,7 @@ export default class OrbitController extends Component {
// public API
fitBounds(min, max) {
const {fov} = this.props;
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;
@@ -128,16 +146,17 @@ export default class OrbitController extends Component {
render() {
return (
<div style={{position: 'relative', userSelect: 'none'}}
<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)} >
onWheel={this._onWheel.bind(this)}
>
{this.props.children}
</div>);
</div>
);
}
}
@@ -1,30 +1,31 @@
import React, {PureComponent} from 'react';
// jshint esversion: 6
import React, { PureComponent } from "react";
export class Popup extends PureComponent {
render() {
const { title, x, y } = this.props;
const style = {
position: 'absolute',
position: "absolute",
top: y,
left: x,
maxWidth: '200px',
padding: '10px',
color: 'white',
backgroundColor: 'black',
pointerEvents: 'none',
transform: 'translate(10px, -50%)',
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',
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 (
@@ -32,13 +33,13 @@ export class Popup extends PureComponent {
<div style={arrowStyle} />
{title}
</div>
)
};
);
}
}
Popup.defaultProps = {
id: 'id',
title: '',
id: "id",
title: "",
x: 0,
y: 0,
y: 0
};
+40 -32
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import Helmet from "react-helmet";
@@ -15,33 +16,30 @@ import actions from "../actions";
import SectionHeader from "./framework/sectionHeader";
@connect((state) => {
@connect(state => {
return {
cells: state.cells
}
};
})
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.state = {};
}
_onURLChanged() {
this.props.dispatch({ type: "url changed", url: document.location.href });
}
_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);
window.addEventListener("popstate", this._onURLChanged);
this._onURLChanged();
this.props.dispatch(actions.initialize())
this.props.dispatch(actions.initialize());
/*
first request includes query straight off the url bar for now
*/
this.props.dispatch(actions.requestCells(window.location.search))
this.props.dispatch(actions.requestCells(window.location.search));
}
render() {
@@ -50,32 +48,42 @@ class App extends React.Component {
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.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}/> : ""}
{false ? (
<Joy data={this.state.expressions && this.state.expressions.data} />
) : (
""
)}
<div>
<LeftSideBar/>
<div style={{
padding: 15,
backgroundColor: "#F7F7F7",
width: 1440 - 410 /* but responsive */,
marginLeft: 350 /* but responsive */
}}>
<Graph/>
<DynamicScatterplot/>
<LeftSideBar />
<div
style={{
padding: 15,
backgroundColor: "#F7F7F7",
width: 1440 - 410 /* but responsive */,
marginLeft: 350 /* but responsive */
}}
>
<Graph />
<DynamicScatterplot />
{/*<Parallel/>*/}
</div>
</div>
</Container>
)
);
}
};
}
export default App;
+90 -82
View File
@@ -1,22 +1,22 @@
// 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 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';
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) => {
@connect(state => {
return {
colorAccessor: state.controls.colorAccessor
}
};
})
class Category extends React.Component {
constructor(props) {
@@ -31,22 +31,22 @@ class Category extends React.Component {
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})
});
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})
});
this.setState({ isChecked: false });
}
renderCategoryItems() {
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
@@ -56,43 +56,60 @@ class Category extends React.Component {
metadataField={this.props.metadataField}
count={this.props.values[v]}
value={v}
i={i} />
)
})
i={i}
/>
);
});
}
render() {
return (
<div style={{
<div
style={{
// display: "flex",
// alignItems: "baseline",
maxWidth: globals.maxControlsWidth,
}}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline"
}}>
<p style={{
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",
}}>
margin: "3px 10px 3px 0px"
}}
>
<span
style={{cursor: "pointer", display: "inline-block", position: "relative", top: 2}}
style={{
cursor: "pointer",
display: "inline-block",
position: "relative",
top: 2
}}
onClick={() => {
this.setState({isExpanded: !this.state.isExpanded})
}}>
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight/> }
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"/>
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={{
@@ -100,77 +117,69 @@ class Category extends React.Component {
marginLeft: 4,
// padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto",
borderRadius: 3,
color: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "black",
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/>
cursor: "pointer"
}}
>
<FaPaintBrush />
</span>
</p>
</div>
<div>
{
this.state.isExpanded ? this.renderCategoryItems() : null
}
</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;
@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 = {
};
this.state = {};
}
render () {
if (!this.props.ranges) return null
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
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;
@@ -190,7 +199,6 @@ export default Categories;
*/
/*
Each category has a color associated with it - ie., color by location should show up on these buttons,
+5 -4
View File
@@ -1,7 +1,8 @@
export const alphabeticallySortedValues = (values) => {
// 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;
})
}
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
};
+34 -19
View File
@@ -1,17 +1,17 @@
// jshint esversion: 6
import { connect } from "react-redux";
import React from "react";
import * as globals from "../../globals";
import actions from "../../actions";
@connect((state) => {
@connect(state => {
return {
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
colorScale: state.controls.colorScale,
colorAccessor: state.controls.colorAccessor,
}
colorAccessor: state.controls.colorAccessor
};
})
class CategoryValue extends React.Component {
toggleOff() {
this.props.dispatch({
type: "categorical metadata filter deselect",
@@ -28,11 +28,16 @@ class CategoryValue extends React.Component {
});
}
render () {
if (!this.props.categoricalAsBooleansMap) return null
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 */
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
@@ -41,32 +46,42 @@ class CategoryValue extends React.Component {
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
fontWeight: selected ? 700 : 400,
}}>
<p style={{
fontWeight: selected ? 700 : 400
}}
>
<p
style={{
paddingLeft: 15,
width: 200,
flexShrink: 0,
margin: 0,
lineHeight: "1em"
}}>
}}
>
<input
onChange={selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)}
onChange={
selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)
}
checked={selected}
type="checkbox"/>
{this.props.value}
type="checkbox"
/>
{this.props.value}
</p>
<p style={{
<p
style={{
padding: "1px 10px",
backgroundColor: c ? this.props.colorScale(this.props.value) : "inherit",
backgroundColor: c
? this.props.colorScale(this.props.value)
: "inherit",
color: c ? "white" : "black",
margin: 0,
lineHeight: "1em"
}}>
}}
>
{this.props.count}
</p>
</div>
)
);
}
}
+1
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";
+51 -43
View File
@@ -1,9 +1,6 @@
import styles from './parallelCoordinates.css';
import {
yAxis,
brushstart,
} from "./util";
// jshint esversion: 6
import styles from "./parallelCoordinates.css";
import { yAxis, brushstart } from "./util";
const drawAxes = (
svg,
@@ -13,19 +10,19 @@ const drawAxes = (
height,
width,
handleBrushAction,
handleColorAction,
handleColorAction
) => {
/*****************************************
******************************************
Handles a brush event, toggling the display of foreground lines.
******************************************
******************************************/
function brush () {
function brush() {
var actives = [];
svg.selectAll(".parcoords_axis .parcoords_brush")
.filter(function (d) {
svg
.selectAll(".parcoords_axis .parcoords_brush")
.filter(function(d) {
return d3.brushSelection(this);
})
.each(function(d) {
@@ -35,46 +32,57 @@ const drawAxes = (
});
});
/* fire action, with selected dimensions & their values */
handleBrushAction(actives)
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) + ")"; });
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);
})
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 + " 🖌️"; });
.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)
)
})
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);
.attr("x", -8)
.attr("width", 16);
return axes;
}
};
export default drawAxes;
+24 -33
View File
@@ -1,7 +1,6 @@
// jshint esversion: 6
import _ from "lodash";
import {
project
} from "./util";
import { project } from "./util";
import renderQueue from "../../util/renderQueue";
@@ -16,16 +15,15 @@ const drawLinesCanvas = (
dimensions,
xscale,
colorAccessor,
colorScale,
colorScale
) => {
return (d) => {
ctx.globalAlpha = .1;
return d => {
ctx.globalAlpha = 0.1;
if (d["__selected__"]) {
ctx.strokeStyle = d["__color__"];
} else {
return
return;
}
ctx.beginPath();
@@ -36,31 +34,31 @@ const drawLinesCanvas = (
// 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];
var prev = coords[i - 1];
if (prev !== null) {
ctx.moveTo(prev[0],prev[1]);
ctx.lineTo(prev[0]+6,prev[1]);
ctx.moveTo(prev[0], prev[1]);
ctx.lineTo(prev[0] + 6, prev[1]);
}
}
if (i < coords.length-1) {
var next = coords[i+1];
if (i < coords.length - 1) {
var next = coords[i + 1];
if (next !== null) {
ctx.moveTo(next[0]-6,next[1]);
ctx.moveTo(next[0] - 6, next[1]);
}
}
return;
}
if (i == 0) {
ctx.moveTo(p[0],p[1]);
ctx.moveTo(p[0], p[1]);
return;
}
ctx.lineTo(p[0],p[1]);
ctx.lineTo(p[0], p[1]);
});
ctx.stroke();
}
}
};
};
const drawCellLinesUsingRenderQueue = (
metadata,
@@ -68,21 +66,14 @@ const drawCellLinesUsingRenderQueue = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _renderLinesWithQueue = renderQueue(
drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
drawLinesCanvas(ctx, dimensions, xscale, colorAccessor, colorScale)
).rate(50);
_renderLinesWithQueue(metadata);
return _renderLinesWithQueue;
}
};
const drawCellLinesSync = (
metadata,
@@ -90,17 +81,17 @@ const drawCellLinesSync = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _draw = drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
_.each(metadata, _draw)
}
colorScale
);
_.each(metadata, _draw);
};
export default drawCellLinesUsingRenderQueue;
// export default drawCellLinesUsingRenderQueue;
+87 -59
View File
@@ -3,22 +3,31 @@ https://bl.ocks.org/mbostock/4341954
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
*/
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@connect((state) => {
@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 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;
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,
currentCellSelection: state.controls.currentCellSelection,
}
currentCellSelection: state.controls.currentCellSelection
};
})
class HistogramBrush extends React.Component {
constructor(props) {
@@ -33,15 +42,11 @@ class HistogramBrush extends React.Component {
ctx: null,
axes: null,
dimensions: null,
brush: null,
brush: null
};
}
componentDidMount() {
}
componentDidUpdate() {
}
componentDidMount() {}
componentDidUpdate() {}
onBrush(selection, x) {
return () => {
if (d3.event.selection) {
@@ -57,81 +62,104 @@ class HistogramBrush extends React.Component {
range: null
});
}
}
};
}
drawHistogram(svgRef) {
const allValuesForContinuousFieldAsArray = _.map(
this.props.currentCellSelection,
this.props.metadataField
)
);
var x = d3.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, (d) => +d))
.range([0, this.width])
// .range([margin.left, width - margin.right]);
var x = d3
.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, d => +d))
.range([0, this.width]);
// .range([margin.left, width - margin.right]);
var y = d3.scaleLinear()
.range([this.height - this.marginBottom, 0])
// .range([height - margin.bottom, margin.top]);
var y = d3.scaleLinear().range([this.height - this.marginBottom, 0]);
// .range([height - margin.bottom, margin.top]);
const bins = d3.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray)
const bins = d3
.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray);
d3.select(svgRef)
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 / allValuesForContinuousFieldAsArray.length); })
.attr("width", function(d) { return Math.abs(x(d.x1) - x(d.x0) - 1); })
.attr("height", function(d) { return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length); });
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x0) + 1;
})
.attr("y", function(d) {
return y(d.length / allValuesForContinuousFieldAsArray.length);
})
.attr("width", function(d) {
return Math.abs(x(d.x1) - x(d.x0) - 1);
})
.attr("height", function(d) {
return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length);
});
if (!this.state.brush && !this.state.axis) {
const brush = d3.select(svgRef)
.append('g')
.attr('class', 'brush')
const brush = d3
.select(svgRef)
.append("g")
.attr("class", "brush")
.call(
d3.brushX()
.on('end', this.onBrush(this.props.metadataField, x.invert).bind(this))
)
d3
.brushX()
.on(
"end",
this.onBrush(this.props.metadataField, x.invert).bind(this)
)
);
const xAxis = d3.select(svgRef)
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)
.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)
.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})
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)}}>
<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;
export default HistogramBrush;
+42 -44
View File
@@ -1,29 +1,33 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from 'react';
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import styles from './parallelCoordinates.css';
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";
import { margin, width, height, createDimensions } from "./util";
@connect((state) => {
@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 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;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
ranges,
@@ -33,8 +37,8 @@ import {
colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection,
currentCellSelection: state.controls.currentCellSelection,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn,
}
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
};
})
class Parallel extends React.Component {
constructor(props) {
@@ -43,16 +47,12 @@ class Parallel extends React.Component {
svg: null,
ctx: null,
axes: null,
dimensions: null,
dimensions: null
};
}
componentDidMount() {
const {svg, ctx} = setupParallelCoordinates(
width,
height,
margin
);
this.setState({svg, ctx})
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
this.setState({ svg, ctx });
}
componentWillReceiveProps(nextProps) {
this.maybeDrawAxes(nextProps);
@@ -63,10 +63,10 @@ class Parallel extends React.Component {
!this.state.axes &&
nextProps.initializeRanges /* axes are created on full range of data */
) {
const dimensions = createDimensions(nextProps.initializeRanges);
const xscale = d3.scalePoint()
const xscale = d3
.scalePoint()
.domain(d3.range(dimensions.length))
.range([0, width]);
@@ -78,28 +78,27 @@ class Parallel extends React.Component {
height,
width,
this.handleBrushAction.bind(this),
this.handleColorAction.bind(this),
this.handleColorAction.bind(this)
);
this.setState({
axes,
xscale,
dimensions,
})
dimensions
});
this.props.dispatch({
type: "parallel coordinates axes have been drawn"
})
});
}
}
maybeDrawLines = _.debounce((nextProps) => { /* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
maybeDrawLines = _.debounce(nextProps => {
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
if (
nextProps.ranges &&
nextProps.currentCellSelection &&
nextProps.axesHaveBeenDrawn
) {
if (this.state._drawLinesCanvas) {
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
}
@@ -112,22 +111,22 @@ class Parallel extends React.Component {
this.state.xscale,
this.state.ctx,
nextProps.colorAccessor,
nextProps.colorScale,
nextProps.colorScale
);
this.setState({
_drawLinesCanvas, /* this will only exist if the internals of drawLinesCanvas are using the render queue */
})
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
});
}
}, 200)
}, 200);
handleBrushAction (selection) {
handleBrushAction(selection) {
this.props.dispatch({
type: "continuous selection using parallel coords brushing",
data: selection
})
});
}
handleColorAction (key) {
handleColorAction(key) {
this.props.dispatch({
type: "color by continuous metadata",
colorAccessor: key,
@@ -136,22 +135,21 @@ class Parallel extends React.Component {
}
render() {
return (
<div id="parcoords_wrapper">
<div
className={styles.parcoords}
id="parcoords"
style={{
width: width + margin.left + margin.right + "px",
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}></div>
}}
/>
</div>
)
);
}
};
}
export default Parallel;
// <SectionHeader text="Continuous Metadata"/>
@@ -3,22 +3,19 @@
Setup SVG & Canvas elements
******************************************
******************************************/
// jshint esversion: 6
const setupParallelCoordinates = (width, height, margin) => {
var container = d3.select("#parcoords");
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)
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 + ")");
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = container.append("canvas")
var canvas = container
.append("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
@@ -27,16 +24,15 @@ const setupParallelCoordinates = (
.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);
ctx.globalCompositeOperation = "darken";
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
return {
svg,
ctx,
}
ctx
};
};
}
export default setupParallelCoordinates
export default setupParallelCoordinates;
+25 -19
View File
@@ -1,51 +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 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 = []
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 */
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])
})
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 d3_functor = v => {
return typeof v === "function"
? v
: () => {
return v;
};
};
export const project = (d, dimensions, xscale) => {
return dimensions.map((p,i) => {
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;
if (!(p.key in d) || d[p.key] === null) return null;
return [xscale(i),p.scale(d[p.key])];
return [xscale(i), p.scale(d[p.key])];
});
};
+33 -16
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@@ -10,20 +11,22 @@ class CellSetButton extends React.Component {
set() {
const set = [];
_.each(this.props.currentCellSelection, (cell) => {
_.each(this.props.currentCellSelection, cell => {
if (cell["__selected__"]) {
set.push(cell.CellName)
set.push(cell.CellName);
}
})
});
this.props.dispatch({
type: "store current cell selection as differential set " + this.props.eitherCellSetOneOrTwo,
type:
"store current cell selection as differential set " +
this.props.eitherCellSetOneOrTwo,
data: set
})
});
}
render() {
return (
<span style={{marginRight: 10}}>
<span style={{ marginRight: 10 }}>
<button
style={{
color: "#FFF",
@@ -31,20 +34,34 @@ class CellSetButton extends React.Component {
height: 30,
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
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"
}
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>
)
);
}
}
+168 -111
View File
@@ -1,42 +1,45 @@
// 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 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';
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
class HeatmapSquare extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
render() {
const contrastColor = getContrast(
this.props.backgroundColor
.substring(4, this.props.backgroundColor.length-1)
.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,
}}>
<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>
)
);
}
}
@@ -47,72 +50,66 @@ class HeatmapSquare extends React.Component {
***********************************
***********************************
**********************************/
@connect((state) => {
@connect(state => {
return {
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
colorAccessor: state.controls.colorAccessor,
}
colorAccessor: state.controls.colorAccessor
};
})
class HeatmapRow extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
handleGeneColorScaleClick(gene) {
return () => {
this.props.dispatch(
actions.requestSingleGeneExpressionCountsForColoringPOST(this.props.gene)
)
}
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
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>
<div
style={{
width: 220,
display: "flex",
justifyContent: "flex-start",
alignItems: "baseline"
}}
>
<div style={{ width: 150, flexShrink: 0 }}>
<span
onClick={this.handleSetGeneAsScatterplotY(this.props.gene).bind(this)}
onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(
this
)}
style={{
fontSize: 16,
color: this.props.scatterplotYYaccessor === this.props.gene ? "white" : globals.brightBlue,
color:
this.props.scatterplotXXaccessor === this.props.gene
? "white"
: globals.brightBlue,
cursor: "pointer",
position: "relative",
top: 1,
@@ -120,8 +117,39 @@ class HeatmapRow extends React.Component {
marginRight: 4,
borderRadius: 3,
padding: "2px 3px",
backgroundColor: this.props.scatterplotYYaccessor === this.props.gene ? globals.brightBlue : "inherit",
}}>Y</span>
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={{
@@ -131,32 +159,45 @@ class HeatmapRow extends React.Component {
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>
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>
<span
style={{
fontSize: 14
}}>
}}
>
{this.props.gene}
</span>
</div>
<HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set1exp)}
text={this.props.set1exp}/>
text={this.props.set1exp}
/>
<HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set2exp)}
text={this.props.set2exp}/>
text={this.props.set2exp}
/>
<span
title={this.props.aveDiff}
style={{
fontSize: 14,
paddingLeft: 10
}}>
{this.props.aveDiff.toFixed(2)}
}}
>
{this.props.aveDiff.toFixed(2)}
</span>
</div>
)
);
}
}
@@ -168,21 +209,22 @@ class HeatmapRow extends React.Component {
***********************************
**********************************/
@connect((state) => {
@connect(state => {
return {
differential: state.differential,
allGeneNames: state.controls.allGeneNames
}
};
})
class Heatmap extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
render() {
if (!this.props.differential.diffExp) return <p>Select cells & compute differential to see heatmap</p>
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;
@@ -194,71 +236,86 @@ class Heatmap extends React.Component {
topGenesForCellSet2.mean_expression_cellset1,
topGenesForCellSet2.mean_expression_cellset2
)
)
);
const greyColorScale = d3.scaleSequential()
.domain(extent)
.interpolator(d3.interpolateGreys);
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}
shouldItemRender={(item, value) =>
item.toLowerCase().indexOf(value.toLowerCase()) > -1
}
getItemValue={item => item}
renderItem={(item, highlighted) =>
renderItem={(item, highlighted) => (
<div
key={item}
style={{ backgroundColor: highlighted ? '#eee' : 'transparent'}}
style={{ backgroundColor: highlighted ? "#eee" : "transparent" }}
>
{item}
</div>
}
)}
value={this.state.value}
onChange={e => this.setState({ value: e.target.value })}
onSelect={(value) => {
onSelect={value => {
this.setState({ value });
this.props.dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(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>
<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
{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
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])}
/>
})
}
set1exp={Math.floor(
topGenesForCellSet2.mean_expression_cellset1[i]
)}
set2exp={Math.floor(
topGenesForCellSet2.mean_expression_cellset2[i]
)}
/>
);
})}
</div>
)
);
}
}
+32 -40
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@@ -5,26 +6,24 @@ import * as globals from "../../globals";
import actions from "../../actions";
import CellSetButton from "./cellSetButtons";
@connect((state) => {
@connect(state => {
return {
currentCellSelection: state.controls.currentCellSelection,
differential: state.differential
}
};
})
class Expression extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.state = {};
}
handleClick(gene) {
return () => {
this.props.dispatch({
type: "color by expression",
gene: gene,
gene: gene
});
}
};
}
computeDiffExp() {
this.props.dispatch(
@@ -32,36 +31,28 @@ class Expression extends React.Component {
this.props.differential.celllist1,
this.props.differential.celllist2
)
)
);
}
render () {
if (
!this.props.differential
) {
return null
render() {
if (!this.props.differential) {
return null;
}
return (
<div>
<div style={{margin: 10}}>
<div style={{marginBottom: 10, width: 300}}>
There are currently
{
" " +
_.filter(this.props.currentCellSelection, "__selected__").length +
" "
}
cells selected, click a cell set button to store them.
</div>
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={1}/>
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={2}/>
<div style={{ margin: 10 }}>
<div style={{ marginBottom: 10, width: 300 }}>
There are currently
{" " +
_.filter(this.props.currentCellSelection, "__selected__").length +
" "}
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 ?
{this.props.differential.celllist1 &&
this.props.differential.celllist2 ? (
<button
style={{
fontSize: 18,
@@ -71,11 +62,13 @@ class Expression extends React.Component {
padding: "12px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
cursor: "pointer"
}}
onClick={this.computeDiffExp.bind(this)}>
onClick={this.computeDiffExp.bind(this)}
>
Compute differential expression
</button> :
</button>
) : (
<button
style={{
fontSize: 18,
@@ -84,17 +77,16 @@ class Expression extends React.Component {
color: "#FFF",
padding: "12px 20px",
backgroundColor: globals.mediumGrey,
border: "none",
border: "none"
}}
>
>
Compute differential expression
</button>
}
)}
</div>
</div>
)
);
}
};
}
export default Expression;
+4 -5
View File
@@ -1,11 +1,10 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
import styles from './container.css';
import styles from "./container.css";
const Container = props => (
<div className={styles.container}>
{props.children}
</div>
<div className={styles.container}>{props.children}</div>
);
export default Container;
+5 -7
View File
@@ -1,14 +1,12 @@
import React from 'react';
import Container from './container';
import styles from './header.css';
// jshint esversion: 6
import React from "react";
import Container from "./container";
import styles from "./header.css";
const Header = () => (
<header className={styles.header}>
<Container>
</Container>
<Container />
</header>
);
+9 -6
View File
@@ -1,10 +1,13 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
const SectionHeader = ({text}) => (
<p style={{
fontSize: 32,
fontWeight: 700
}}>
const SectionHeader = ({ text }) => (
<p
style={{
fontSize: 32,
fontWeight: 700
}}
>
{text}
</p>
);
+58 -59
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./graph.css";
import renderQueue from "../../util/renderQueue";
import _ from "lodash";
@@ -13,7 +14,6 @@ export const setupGraphElements = (
handleBrushSelectAction,
handleBrushDeselectAction
) => {
// var canvas = d3.select("#graphAttachPoint")
// .append("canvas")
// .attr("width", globals.graphWidth)
@@ -22,25 +22,22 @@ export const setupGraphElements = (
// var ctx = canvas.node().getContext("2d");
var svg = d3.select("#graphAttachPoint").append("svg")
var svg = d3
.select("#graphAttachPoint")
.append("svg")
.attr("width", globals.graphWidth)
.attr("height", globals.graphHeight)
.attr("class", `${styles.graphSVG}`)
// .append("g")
// .attr("transform", "translate(" + margin.left + " " + margin.top + ")");
.attr("class", `${styles.graphSVG}`);
// .append("g")
// .attr("transform", "translate(" + margin.left + " " + margin.top + ")");
setupGraphBrush(
svg,
handleBrushSelectAction,
handleBrushDeselectAction
);
setupGraphBrush(svg, handleBrushSelectAction, handleBrushDeselectAction);
return {
svg,
svg
// ctx
}
}
};
};
/******************************************
*******************************************
@@ -57,36 +54,34 @@ const drawGraph = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells,
_currentCellSelectionMap,
_currentCellSelectionMap
) => {
return (p) => {
return p => {
/* shuffle the data to overcome render order hiding cells, & filter first */
// data = d3.shuffle(data); /* make me a control */
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
globals.graphXScale(p[1]), /* x */
globals.graphYScale(p[2]), /* y */
_currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5, /* r */
0, /* sAngle */
2 * Math.PI /* eAngle */
);
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
globals.graphXScale(p[1]) /* x */,
globals.graphYScale(p[2]) /* y */,
_currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5 /* r */,
0 /* sAngle */,
2 * Math.PI /* eAngle */
);
context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"]
context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"];
if (_currentCellSelectionMap[p[0]]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
if (_currentCellSelectionMap[p[0]]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
context.fill();
}
}
context.fill();
};
};
const _drawGraphUsingRenderQueue = (
context,
@@ -97,22 +92,26 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
opacityForDeselectedCells,
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells
) => {
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */
const _currentCellSelectionMap = _.keyBy(
currentCellSelection,
"CellName"
); /* move me to the reducer */
const dataForGraph = [];
_.each(currentCellSelection, (cell, i) => {
if (graphMap[cell["CellName"]]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
if (graphMap[cell["CellName"]]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
dataForGraph.push([
cell["CellName"],
graphMap[cell["CellName"]][0],
graphMap[cell["CellName"]][1]
])
]);
}
})
});
/* clear canvas */
context.clearRect(0, 0, globals.graphWidth, globals.graphHeight);
@@ -127,30 +126,30 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells,
_currentCellSelectionMap,
_currentCellSelectionMap
)
)
);
_renderGraphWithFunctionReturnedByQueue(dataForGraph);
return _renderGraphWithFunctionReturnedByQueue;
}
};
export const drawGraphUsingRenderQueue = _.debounce(_drawGraphUsingRenderQueue, 100);
export const drawGraphUsingRenderQueue = _.debounce(
_drawGraphUsingRenderQueue,
100
);
const setupGraphBrush = (
svg,
handleBrushSelectAction,
handleBrushDeselectAction
) => {
svg.append("g")
.call(
d3.brush()
.extent([
[0, 0],
[globals.graphWidth, globals.graphHeight]
])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction)
);
}
svg.append("g").call(
d3
.brush()
.extent([[0, 0], [globals.graphWidth, globals.graphHeight]])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction)
);
};
+13 -12
View File
@@ -1,8 +1,9 @@
const mat4 = require('gl-mat4')
// jshint esversion: 6
const mat4 = require("gl-mat4");
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
export default function (regl) {
export default function(regl) {
return regl({
vert: `
precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`,
attributes: {
position: regl.prop('position'),
color: regl.prop('color'),
size: regl.prop('size')
position: regl.prop("position"),
color: regl.prop("color"),
size: regl.prop("size")
},
uniforms: {
distance: regl.prop('distance'),
view: regl.prop('view'),
distance: regl.prop("distance"),
view: regl.prop("view"),
projection: (context, props) => {
return mat4.perspective(
[],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight,
0.01,
1000
)
},
);
}
},
count: regl.prop('count'),
count: regl.prop("count"),
primitive: 'points'
})
primitive: "points"
});
}
+145 -104
View File
@@ -1,29 +1,38 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import * as globals from "../../globals";
import styles from "./graph.css";
import {setupGraphElements, drawGraphUsingRenderQueue} from "./drawGraph";
import { setupGraphElements, drawGraphUsingRenderQueue } from "./drawGraph";
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 mat4 from "gl-mat4";
import fit from "canvas-fit";
import _camera from "../../util/camera.js";
import _regl from "regl";
import _drawPoints from "./drawPointsRegl";
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';
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) => {
const vertices = state.cells.cells && state.cells.cells.data.graph ? state.cells.cells.data.graph : null;
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;
@connect(state => {
const vertices =
state.cells.cells && state.cells.cells.data.graph
? state.cells.cells.data.graph
: null;
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;
return {
ranges,
@@ -35,11 +44,10 @@ import FaSave from 'react-icons/lib/fa/download';
graphMap: state.controls.graphMap,
currentCellSelection: state.controls.currentCellSelection,
graphBrushSelection: state.controls.graphBrushSelection,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
}
opacityForDeselectedCells: state.controls.opacityForDeselectedCells
};
})
class Graph extends React.Component {
constructor(props) {
super(props);
this.count = 0;
@@ -48,35 +56,32 @@ class Graph extends React.Component {
svg: null,
ctx: null,
brush: null,
mode: "brush",
mode: "brush"
};
}
componentDidMount() {
const {
svg
} = setupGraphElements(
const { svg } = setupGraphElements(
this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this)
);
this.setState({svg});
this.setState({ svg });
// setup canvas and camera
const camera = _camera(this.reglCanvas, {scale: true, rotate: false});
const regl = _regl(this.reglCanvas)
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl)
const drawPoints = _drawPoints(regl);
// preallocate buffers
const pointBuffer = regl.buffer();
const colorBuffer = regl.buffer();
const sizeBuffer = regl.buffer();
regl.frame(({viewportWidth, viewportHeight}) => {
regl.frame(({ viewportWidth, viewportHeight }) => {
regl.clear({
depth: 1,
color: [1, 1, 1, 1]
})
});
drawPoints({
size: sizeBuffer,
@@ -86,18 +91,17 @@ class Graph extends React.Component {
count: this.count,
view: camera.view(),
scale: viewportHeight / viewportWidth
})
});
camera.tick()
})
camera.tick();
});
this.setState({
regl,
pointBuffer,
colorBuffer,
sizeBuffer
})
});
}
componentWillReceiveProps(nextProps) {
/* maybe should do a check here to confirm ref exists and pass it? */
@@ -120,12 +124,11 @@ class Graph extends React.Component {
// nextProps.opacityForDeselectedCells,
// )
// }
if (
this.state.regl &&
nextProps.vertices
) {
const _currentCellSelectionMap = _.keyBy(nextProps.currentCellSelection, "CellName"); /* move me to the reducer */
if (this.state.regl && nextProps.vertices) {
const _currentCellSelectionMap = _.keyBy(
nextProps.currentCellSelection,
"CellName"
); /* move me to the reducer */
const positions = [];
positions.length = nextProps.currentCellSelection.length;
@@ -134,33 +137,38 @@ class Graph extends React.Component {
const sizes = [];
sizes.length = nextProps.currentCellSelection.length;
const glScaleX = d3.scaleLinear()
.domain([0,1])
.range([-1, 1]) /* padding */
const glScaleY = d3.scaleLinear()
const glScaleX = d3
.scaleLinear()
.domain([0, 1])
.range([1, -1]) /* padding */
.range([-1, 1]); /* padding */
const glScaleY = d3
.scaleLinear()
.domain([0, 1])
.range([1, -1]); /* padding */
/*
Construct Vectors
*/
_.each(nextProps.currentCellSelection, (cell, i) => {
if (nextProps.graphMap[cell["CellName"]]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
if (nextProps.graphMap[cell["CellName"]]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
positions[i] = [
glScaleX(nextProps.graphMap[cell["CellName"]][0]),
glScaleY(nextProps.graphMap[cell["CellName"]][1])
]
];
colors[i] = cell.__colorRGB__;
sizes[i] = cell["__selected__"] ? 4 : .2 /* make this a function of the number of total cells, including regraph */
sizes[i] = cell["__selected__"]
? 4
: 0.2; /* make this a function of the number of total cells, including regraph */
}
})
});
this.state.pointBuffer(positions)
this.state.colorBuffer(colors)
this.state.sizeBuffer(sizes)
this.count = positions.length
this.state.pointBuffer(positions);
this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes);
this.count = positions.length;
}
}
handleBrushSelectAction() {
@@ -183,7 +191,7 @@ class Graph extends React.Component {
northwestY: s[0][1],
southeastX: s[1][0],
southeastY: s[1][1]
}
};
brushCoords.dx = brushCoords.southeastX - brushCoords.northwestX;
brushCoords.dy = brushCoords.southeastY - brushCoords.northwestY;
@@ -191,20 +199,20 @@ class Graph extends React.Component {
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() {
@@ -212,20 +220,24 @@ class Graph extends React.Component {
<div
id="graphWrapper"
style={{
height: 1050, /* move this to globals */
height: 1050 /* move this to globals */,
backgroundColor: "white",
borderRadius: 3,
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)",
}}>
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)"
}}
>
<div
style={{
padding: 10,
display: "flex",
justifyContent: "space-between",
alignItems: "baseline"
}}>
}}
>
<button
onClick={() => { this.props.dispatch(actions.regraph()) }}
onClick={() => {
this.props.dispatch(actions.regraph());
}}
style={{
fontSize: 12,
fontWeight: 400,
@@ -233,67 +245,96 @@ class Graph extends React.Component {
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
cursor: "pointer"
}}
>
Regraph present selection
Regraph present selection
</button>
<div>
<span style={{ marginRight: 10, fontSize: 12}}>
<span style={{ marginRight: 10, fontSize: 12 }}>
deselected opacity
</span>
<input
style={{position: "relative", top: 6, marginRight: 20}}
style={{ position: "relative", top: 6, marginRight: 20 }}
type="range"
onChange={this.handleOpacityRangeChange.bind(this)}
min={0}
max={1}
step="0.01"
/>
<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>
/>
<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: 12,
fontWeight: 400,
color: "white",
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
}}> <FaSave style={{display: "inline-block"}}/> csv url for present selection </button>
<button
style={{
fontSize: 12,
fontWeight: 400,
color: "white",
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer"
}}
>
{" "}
<FaSave style={{ display: "inline-block" }} /> csv url for present
selection{" "}
</button>
</div>
</div>
<div
style={{display: this.state.mode === "brush" ? "inherit" : "none"}}
style={{ display: this.state.mode === "brush" ? "inherit" : "none" }}
id="graphAttachPoint"
>
</div>
<div style={{padding: 0, margin: 0}}>
<canvas width={globals.graphWidth} height={globals.graphHeight} ref={(canvas) => { this.reglCanvas = canvas}}/>
/>
<div style={{ padding: 0, margin: 0 }}>
<canvas
width={globals.graphWidth}
height={globals.graphHeight}
ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div>
</div>
)
);
}
};
}
export default Graph;
+1
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
// createExpressionsCountsMap () {
//
// const CHANGE_ME_MAGIC_GENE_INDEX = 5;
+118 -71
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./joy.css";
/*
@@ -5,112 +6,158 @@ import styles from "./joy.css";
*/
var margin = { top: 30, right: 10, bottom: 30, left: 100 },
width = 400 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
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 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 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 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 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 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);
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
};
return {
activity: d.activity,
time: parseTime(d.time),
value: +d.p_smooth
};
}
const drawJoy = (data) => {
const drawJoy = data => {
console.log("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 + ")");
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;
});
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; })
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); });
// 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); });
}
data.sort(function(a, b) {
return peakTime(b) - peakTime(a);
});
console.log('sorted', data)
console.log("sorted", data);
xScale.domain(d3.extent(dataFlat, x));
xScale.domain(d3.extent(dataFlat, x));
activityScale.domain(data.map(function(d) { return d.key; }));
activityScale.domain(
data.map(function(d) {
return d.key;
})
);
var areaChartHeight = (1 + overlap) * (height / activityScale.domain().length);
var areaChartHeight =
(1 + overlap) * (height / activityScale.domain().length);
yScale
.domain(d3.extent(dataFlat, y))
.range([areaChartHeight, 0]);
yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
area.y0(yScale(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 + ')';
});
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.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);
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 + ')')
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"]}`)
svg
.append("g")
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
.call(activityAxis);
})
}
}
);
};
export default drawJoy;
+13 -13
View File
@@ -1,38 +1,38 @@
import React from 'react';
// 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 = {
};
this.state = {};
}
componentWillReceiveProps(nextProps) {
if (nextProps.data) {
console.log('joyplot data 44', nextProps.data)
console.log("joyplot data 44", nextProps.data);
drawJoy(joyParser(nextProps.data));
}
}
componentDidMount() {
}
componentDidMount() {}
render() {
return (
<div id="joyplot_wrapper" style={{marginTop: 50}}>
<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>
<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;
+11 -12
View File
@@ -1,6 +1,4 @@
// jshint esversion: 6
const joyParser = (data, count = 20) => {
const genes = [];
@@ -9,22 +7,23 @@ const joyParser = (data, count = 20) => {
for (let i = 0; i < count; i++) {
const gene = {
key: data.genes[i], /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */
key:
data.genes[
i
] /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */,
values: []
}
};
data.cells.forEach((cell) => {
data.cells.forEach(cell => {
gene.values.push({
value: cell["e"][i]
})
})
});
});
genes.push(gene)
genes.push(gene);
}
return genes;
}
};
export default joyParser;
+73 -36
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import Categorical from "./categorical/categorical";
@@ -7,10 +8,8 @@ import { connect } from "react-redux";
import Heatmap from "./expression/diffExpHeatmap";
import * as globals from "../globals";
@connect((state) => {
return {
}
@connect(state => {
return {};
})
class LeftSideBar extends React.Component {
constructor(props) {
@@ -21,24 +20,42 @@ class LeftSideBar extends React.Component {
}
render() {
return (
<div style={{position: "fixed"}}>
<p style={{margin: 10, fontSize: 16, fontWeight: 700, width: "100%"}}>CELLxGENE {globals.datasetTitle} </p>
<div style={{padding: 10}}>
<div style={{ position: "fixed" }}>
<p style={{ margin: 10, fontSize: 16, fontWeight: 700, width: "100%" }}>
CELLxGENE {globals.datasetTitle}{" "}
</p>
<div style={{ padding: 10 }}>
<button
style={{
padding: "10px 30px",
outline: 0,
fontSize: 18,
fontStyle: this.state.currentTab === "metadata" ? "inherit" : "italic",
fontStyle:
this.state.currentTab === "metadata" ? "inherit" : "italic",
cursor: "pointer",
border: "none",
backgroundColor: "#FFF",
borderTop: this.state.currentTab === "metadata" ? "4px solid " + globals.brightBlue : "none",
borderBottom: this.state.currentTab === "metadata" ? "none" : "1px solid " + globals.lightGrey,
borderRight: this.state.currentTab === "metadata" ? "1px solid " + globals.lightGrey : "none",
borderLeft: this.state.currentTab === "metadata" ? "1px solid " + globals.lightGrey : "none",
borderTop:
this.state.currentTab === "metadata"
? "4px solid " + globals.brightBlue
: "none",
borderBottom:
this.state.currentTab === "metadata"
? "none"
: "1px solid " + globals.lightGrey,
borderRight:
this.state.currentTab === "metadata"
? "1px solid " + globals.lightGrey
: "none",
borderLeft:
this.state.currentTab === "metadata"
? "1px solid " + globals.lightGrey
: "none"
}}
onClick={() => {this.setState({currentTab: "metadata"})}}>
onClick={() => {
this.setState({ currentTab: "metadata" });
}}
>
Metadata
</button>
<button
@@ -46,39 +63,59 @@ class LeftSideBar extends React.Component {
padding: "10px 30px",
outline: 0,
fontSize: 18,
fontStyle: this.state.currentTab === "expression" ? "inherit" : "italic",
fontStyle:
this.state.currentTab === "expression" ? "inherit" : "italic",
cursor: "pointer",
border: "none",
backgroundColor: "#FFF",
borderTop: this.state.currentTab === "expression" ? "4px solid " + globals.brightBlue : "none",
borderBottom: this.state.currentTab === "expression" ? "none" : "1px solid " + globals.lightGrey,
borderRight: this.state.currentTab === "expression" ? "1px solid " + globals.lightGrey : "none",
borderLeft: this.state.currentTab === "expression" ? "1px solid " + globals.lightGrey : "none",
borderTop:
this.state.currentTab === "expression"
? "4px solid " + globals.brightBlue
: "none",
borderBottom:
this.state.currentTab === "expression"
? "none"
: "1px solid " + globals.lightGrey,
borderRight:
this.state.currentTab === "expression"
? "1px solid " + globals.lightGrey
: "none",
borderLeft:
this.state.currentTab === "expression"
? "1px solid " + globals.lightGrey
: "none"
}}
onClick={() => {this.setState({currentTab: "expression"})}}>
onClick={() => {
this.setState({ currentTab: "expression" });
}}
>
Expression
</button>
</div>
<div style={{
height: 500,
width: 350,
padding: 10,
overflowY: "scroll",
overflowX: "hidden",
}}>
{this.state.currentTab === "metadata" ? <Categorical/> : null}
{this.state.currentTab === "metadata" ? <Continuous/> : null}
{this.state.currentTab === "expression" ? <Heatmap/> : null}
<div
style={{
height: 500,
width: 350,
padding: 10,
overflowY: "scroll",
overflowX: "hidden"
}}
>
{this.state.currentTab === "metadata" ? <Categorical /> : null}
{this.state.currentTab === "metadata" ? <Continuous /> : null}
{this.state.currentTab === "expression" ? <Heatmap /> : null}
</div>
<div style={{
boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)",
paddingTop: 10
}}>
<ExpressionButtons/>
<div
style={{
boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)",
paddingTop: 10
}}
>
<ExpressionButtons />
</div>
</div>
)
);
}
};
}
export default LeftSideBar;
+13 -12
View File
@@ -1,8 +1,9 @@
const mat4 = require('gl-mat4')
// jshint esversion: 6
const mat4 = require("gl-mat4");
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
export default function (regl) {
export default function(regl) {
return regl({
vert: `
precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`,
attributes: {
position: regl.prop('position'),
color: regl.prop('color'),
size: regl.prop('size')
position: regl.prop("position"),
color: regl.prop("color"),
size: regl.prop("size")
},
uniforms: {
distance: regl.prop('distance'),
view: regl.prop('view'),
distance: regl.prop("distance"),
view: regl.prop("view"),
projection: (context, props) => {
return mat4.perspective(
[],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight,
0.01,
1000
)
},
);
}
},
count: regl.prop('count'),
count: regl.prop("count"),
primitive: 'points'
})
primitive: "points"
});
}
@@ -1,12 +1,8 @@
// jshint esversion: 6
import _ from "lodash";
import renderQueue from "../../util/renderQueue";
import {
margin,
width,
height,
createDimensions,
} from "./util";
import { margin, width, height, createDimensions } from "./util";
const drawScatterplotCanvas = (
context,
@@ -17,38 +13,42 @@ const drawScatterplotCanvas = (
expression,
scatterplotXXaccessor,
scatterplotYYaccessor,
_currentCellSelectionMap,
_currentCellSelectionMap
) => {
return (cell) => {
/*
return cell => {
/*
this is necessary until we are no longer getting expression for all cells, but only for 'world'
...which will mean refetching when we regraph, or 'go back up to all cells'
*/
if (!_currentCellSelectionMap[cell.cellname]) { return }
if (!_currentCellSelectionMap[cell.cellname]) {
return;
}
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
xScale(cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]), /* x */
yScale(cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]), /* y */
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5, /* r */
0, /* sAngle */
2 * Math.PI /* eAngle */
);
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
xScale(
cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]
) /* x */,
yScale(
cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]
) /* y */,
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5 /* r */,
0 /* sAngle */,
2 * Math.PI /* eAngle */
);
context.fillStyle = _currentCellSelectionMap[cell.cellname]["__color__"]
context.fillStyle = _currentCellSelectionMap[cell.cellname]["__color__"];
if (_currentCellSelectionMap[cell.cellname]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
if (_currentCellSelectionMap[cell.cellname]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
context.fill();
}
}
context.fill();
};
};
export const drawScatterplotCanvasUsingRenderQueue = (
context,
@@ -63,7 +63,10 @@ export const drawScatterplotCanvasUsingRenderQueue = (
/* clear canvas */
context.clearRect(0, 0, width, height);
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */
const _currentCellSelectionMap = _.keyBy(
currentCellSelection,
"CellName"
); /* move me to the reducer */
const _renderScatterplotWithFunctionReturnedByQueue = renderQueue(
drawScatterplotCanvas(
@@ -75,13 +78,12 @@ export const drawScatterplotCanvasUsingRenderQueue = (
expression,
scatterplotXXaccessor,
scatterplotYYaccessor,
_currentCellSelectionMap,
_currentCellSelectionMap
)
)
);
_renderScatterplotWithFunctionReturnedByQueue(expression.data.cells)
_renderScatterplotWithFunctionReturnedByQueue(expression.data.cells);
return _renderScatterplotWithFunctionReturnedByQueue;
};
}
export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100)
export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100);
+142 -107
View File
@@ -1,33 +1,37 @@
// jshint esversion: 6
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
import React from 'react';
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 styles from "./scatterplot.css";
import drawScatterplotCanvas from "./drawScatterplotCanvas";
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 mat4 from "gl-mat4";
import fit from "canvas-fit";
import _camera from "../../util/camera.js";
import _regl from "regl";
import _drawPoints from "./drawPointsRegl";
import {
margin,
width,
height,
createDimensions,
} from "./util";
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;
@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,
@@ -40,8 +44,8 @@ import {
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
differential: state.differential,
expression: state.expression,
}
expression: state.expression
};
})
class Scatterplot extends React.Component {
constructor(props) {
@@ -53,39 +57,31 @@ class Scatterplot extends React.Component {
axes: null,
dimensions: null,
xScale: null,
yScale: null,
yScale: null
};
}
componentDidMount() {
const {
svg
} = setupScatterplot(
width,
height,
margin
);
const { svg } = setupScatterplot(width, height, margin);
this.setState({
svg
})
});
const camera = _camera(this.reglCanvas, {scale: true, rotate: false});
const regl = _regl(this.reglCanvas)
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl)
const drawPoints = _drawPoints(regl);
// preallocate buffers
const pointBuffer = regl.buffer()
const colorBuffer = regl.buffer()
const sizeBuffer = regl.buffer()
regl.frame(({viewportWidth, viewportHeight}) => {
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,
@@ -95,29 +91,28 @@ class Scatterplot extends React.Component {
count: this.count,
view: camera.view(),
scale: viewportHeight / viewportWidth
})
});
camera.tick()
})
camera.tick();
});
this.setState({
regl,
sizeBuffer,
pointBuffer,
colorBuffer,
})
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.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);
@@ -136,20 +131,24 @@ class Scatterplot extends React.Component {
this.state.xScale &&
this.state.yScale
) {
const _currentCellSelectionMap = _.keyBy(this.props.currentCellSelection, "CellName"); /* move me to the reducer */
const _currentCellSelectionMap = _.keyBy(
this.props.currentCellSelection,
"CellName"
); /* move me to the reducer */
const positions = [];
const colors = [];
const sizes = [];
const glScaleX = d3.scaleLinear()
const glScaleX = d3
.scaleLinear()
.domain([0, width])
.range([-.95, .95]) /* padding */
.range([-0.95, 0.95]); /* padding */
const glScaleY = d3.scaleLinear()
const glScaleY = d3
.scaleLinear()
.domain([0, height])
.range([-1, 1])
.range([-1, 1]);
/*
Construct Vectors
@@ -159,21 +158,40 @@ class Scatterplot extends React.Component {
this if is necessary until we are no longer getting expression for all cells, but only for 'world'
...which will mean refetching when we regraph, or 'go back up to all cells'
*/
if (_currentCellSelectionMap[cell.cellname]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
if (_currentCellSelectionMap[cell.cellname]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
positions.push([
glScaleX(this.state.xScale(cell.e[this.props.expression.data.genes.indexOf(this.props.scatterplotXXaccessor)])), /* scale each point first to the window as we calculate extents separately below, so no need to repeat */
glScaleY(this.state.yScale(cell.e[this.props.expression.data.genes.indexOf(this.props.scatterplotYYaccessor)]))
])
glScaleX(
this.state.xScale(
cell.e[
this.props.expression.data.genes.indexOf(
this.props.scatterplotXXaccessor
)
]
)
) /* scale each point first to the window as we calculate extents separately below, so no need to repeat */,
glScaleY(
this.state.yScale(
cell.e[
this.props.expression.data.genes.indexOf(
this.props.scatterplotYYaccessor
)
]
)
)
]);
colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"])
sizes.push(_currentCellSelectionMap[cell.cellname]["__selected__"] ? 4 : .2) /* make this a function of the number of total cells, including regraph */
colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"]);
sizes.push(
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 4 : 0.2
); /* make this a function of the number of total cells, including regraph */
}
})
});
this.state.pointBuffer(positions)
this.state.colorBuffer(colors)
this.state.sizeBuffer(sizes)
this.count = positions.length
this.state.pointBuffer(positions);
this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes);
this.count = positions.length;
}
}
maybeSetupScalesAndDrawAxes(nextProps) {
@@ -183,61 +201,75 @@ class Scatterplot extends React.Component {
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 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])
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 xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft()
.scale(yScale);
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')
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')
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')
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')
this.state.svg
.append("text")
.attr("x", width)
.attr("y", height - 10)
.attr("text-anchor", "end")
.attr("class", "label")
.text(this.props.scatterplotXXaccessor);
}
render() {
@@ -248,15 +280,16 @@ class Scatterplot extends React.Component {
borderRadius: 3,
marginTop: 15,
paddingBottom: 20,
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)",
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)"
}}
id="scatterplot_wrapper">
id="scatterplot_wrapper"
>
<div
className={styles.scatterplot}
id="scatterplot"
style={{
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px",
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}
>
<canvas
@@ -266,14 +299,16 @@ class Scatterplot extends React.Component {
marginLeft: margin.left - 7,
marginTop: margin.top
}}
ref={(canvas) => { this.reglCanvas = canvas}}/>
ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div>
</div>
)
);
}
};
}
export default Scatterplot;
// <SectionHeader text="Continuous Metadata"/>
+11 -16
View File
@@ -1,28 +1,23 @@
// jshint esversion: 6
/*****************************************
******************************************
Setup SVG & Canvas elements
******************************************
******************************************/
const setupScatterplot = (
width,
height,
margin
) => {
const setupScatterplot = (width, height, margin) => {
var container = d3.select("#scatterplot");
var container = d3.select("#scatterplot")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
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 + ")");
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
return {
svg,
}
}
svg
};
};
export default setupScatterplot;
+2 -1
View File
@@ -1,9 +1,10 @@
// 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 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;