mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-19 19:08:11 +08:00
* mocks for redux refactor - for discussion * more API design on redux refactor * add new reuqired dependencies for build * change babel target to use modern browser * remove dead code * remove dead code - joy plots * checkpoint on redux refactoring * checkpoint on redux refactoring * fix mistaken rebase conflict resolution * dead code removal; add name to dataframe backmap * rename dataframe to universe * update eslint config to more closely match prettier * lint * more eslint updates to match prettier * additional config to make eslint match prettier * add expression data to Universe/World * remove obsolete reducers * lint fixes * more eslint cleanup * lint * lint * fix but in countAllOnes when dimensions gt 1 * lint; do not display name metadata field * lint; colors refactor * lint; colors refactor * update comments * first cut at regraph and reset * enable object-curly-braces consistent mode * lint, handle regraph with no selection * fix expression scatterplot bugs * fix regression legend display * add expression data cache * remove console logging * reset cell color on regraph/reset * remove obsolete server URLs * rename UniverseV01 to Universe_REST_API_v01 * add additional comments on the varDataCache * merge universe reducer into controls reducer; simplify initialization-related actions * use spread operator * fix erroneous comment * convert universe and world state to plain objects, and functionalize supporting code (remove ES6 classes) * use spread operator * lint * improve variable names * rename obsCrossfilter to crossfilter and obsDimensionMap to dimensionMap * rename controls2 to controls
This commit is contained in:
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
|
||||
// import PulseLoader from "halogen/PulseLoader";
|
||||
|
||||
import LeftSideBar from "./leftsidebar";
|
||||
import Parallel from "./continuous/parallel";
|
||||
import Legend from "./continuousLegend";
|
||||
// import Joy from "./joy/joy";
|
||||
import Graph from "./graph/graph";
|
||||
import * as globals from "../globals";
|
||||
import actions from "../actions";
|
||||
@@ -18,8 +16,8 @@ import SectionHeader from "./framework/sectionHeader";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cells: state.cells,
|
||||
initialize: state.initialize
|
||||
loading: state.controls.loading,
|
||||
error: state.controls.error
|
||||
};
|
||||
})
|
||||
class App extends React.Component {
|
||||
@@ -27,20 +25,18 @@ class App extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
_onURLChanged() {
|
||||
this.props.dispatch({ type: "url changed", url: document.location.href });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
/* listen for url changes, fire one when we start the app up */
|
||||
window.addEventListener("popstate", this._onURLChanged);
|
||||
this._onURLChanged();
|
||||
|
||||
this.props.dispatch(actions.initialize());
|
||||
this.props.dispatch(actions.doInitialDataLoad(window.location.search));
|
||||
|
||||
/*
|
||||
first request includes query straight off the url bar for now
|
||||
*/
|
||||
this.props.dispatch(actions.requestCells(window.location.search));
|
||||
/* listen for resize events */
|
||||
window.addEventListener("resize", () => {
|
||||
this.props.dispatch({
|
||||
@@ -61,10 +57,11 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error } = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
{this.props.cells.loading || this.props.initialize.loading ? (
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
@@ -76,12 +73,9 @@ class App extends React.Component {
|
||||
loading cellxgene
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.cells.error ? "Error loading cells" : null}
|
||||
|
||||
{error ? "Error loading cells" : null}
|
||||
<div>
|
||||
{this.props.cells.loading || this.props.initialize.loading ? null : (
|
||||
<LeftSideBar />
|
||||
)}
|
||||
{loading ? null : <LeftSideBar />}
|
||||
<div
|
||||
style={{
|
||||
padding: 15,
|
||||
@@ -89,13 +83,10 @@ class App extends React.Component {
|
||||
marginLeft: 350 /* but responsive */
|
||||
}}
|
||||
>
|
||||
{this.props.cells.loading ||
|
||||
this.props.initialize.loading ? null : (
|
||||
<Graph />
|
||||
)}
|
||||
{loading ? null : <Graph />}
|
||||
|
||||
<Legend />
|
||||
{/*<Parallel/>*/}
|
||||
{}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -104,5 +95,3 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
// <Joy data={this.state.expressions && this.state.expressions.data} />
|
||||
|
||||
@@ -3,16 +3,14 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./categorical.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
import FaArrowRight from "react-icons/lib/fa/angle-right";
|
||||
import FaArrowDown from "react-icons/lib/fa/angle-down";
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
@@ -27,6 +25,7 @@ class Category extends React.Component {
|
||||
isExpanded: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const valuesAsBool = _.values(
|
||||
this.props.categoricalAsBooleansMap[this.props.metadataField]
|
||||
@@ -45,12 +44,14 @@ class Category extends React.Component {
|
||||
this.checkbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleColorChange() {
|
||||
this.props.dispatch({
|
||||
type: "color by categorical metadata",
|
||||
colorAccessor: this.props.metadataField
|
||||
});
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter all of these",
|
||||
@@ -58,6 +59,7 @@ class Category extends React.Component {
|
||||
});
|
||||
this.setState({ isChecked: true });
|
||||
}
|
||||
|
||||
toggleNone() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter none of these",
|
||||
@@ -66,6 +68,7 @@ class Category extends React.Component {
|
||||
});
|
||||
this.setState({ isChecked: false });
|
||||
}
|
||||
|
||||
renderCategoryItems() {
|
||||
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
|
||||
return (
|
||||
@@ -79,6 +82,7 @@ class Category extends React.Component {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleToggleAllClick() {
|
||||
// || this.checkbox.indeterminate === false
|
||||
if (this.state.isChecked) {
|
||||
@@ -89,6 +93,7 @@ class Category extends React.Component {
|
||||
this.toggleAll();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
@@ -164,8 +169,7 @@ class Category extends React.Component {
|
||||
}
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
|
||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
||||
return {
|
||||
ranges
|
||||
};
|
||||
@@ -192,7 +196,7 @@ class Categories extends React.Component {
|
||||
>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.options && key !== "CellName" && !isColorField) {
|
||||
if (value.options && !isColorField && key !== "name") {
|
||||
return (
|
||||
<Category key={key} metadataField={key} values={value.options} />
|
||||
);
|
||||
|
||||
@@ -17,18 +17,15 @@ import HistogramBrush from "./histogramBrush";
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
const metadata = _.get(state.controls.world, "obsAnnotations", null);
|
||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Continuous extends React.Component {
|
||||
@@ -41,17 +38,19 @@ class Continuous extends React.Component {
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
rangeMaxForColorAccessor: this.props.ranges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ class Continuous extends React.Component {
|
||||
<div>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.range && key !== "CellName" && !isColorField) {
|
||||
if (value.range && key !== "name" && !isColorField) {
|
||||
return (
|
||||
<HistogramBrush
|
||||
key={key}
|
||||
|
||||
@@ -8,87 +8,90 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
import * as globals from "../../globals";
|
||||
import * as d3 from "d3";
|
||||
import memoize from "memoize-one";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
@connect(state => {
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
|
||||
return {
|
||||
initializeRanges,
|
||||
initializeRanges: _.get(state.controls.world, "summary.obs"),
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
cellsMetadata: state.controls.cellsMetadata
|
||||
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
|
||||
};
|
||||
})
|
||||
class HistogramBrush extends React.Component {
|
||||
calcHistogramCache = memoize((obsAnnotations, metadataField, ranges) => {
|
||||
// recalculate expensive stuff
|
||||
const allValuesForContinuousFieldAsArray = _.map(
|
||||
obsAnnotations,
|
||||
metadataField
|
||||
);
|
||||
const histogramCache = {};
|
||||
|
||||
histogramCache.x = d3
|
||||
.scaleLinear()
|
||||
.domain([ranges.min, ranges.max])
|
||||
.range([0, this.width]);
|
||||
|
||||
histogramCache.y = d3
|
||||
.scaleLinear()
|
||||
.range([this.height - this.marginBottom, 0]);
|
||||
// .range([height - margin.bottom, margin.top]);
|
||||
|
||||
histogramCache.bins = d3
|
||||
.histogram()
|
||||
.domain(histogramCache.x.domain())
|
||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||
|
||||
histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||
|
||||
return histogramCache;
|
||||
});
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = 300;
|
||||
this.height = 100;
|
||||
this.marginBottom = 20;
|
||||
this.histogramCache = {};
|
||||
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
brush: null
|
||||
};
|
||||
}
|
||||
calcHistogramCache(nextProps) {
|
||||
// recalculate expensive stuff
|
||||
const allValuesForContinuousFieldAsArray = _.map(
|
||||
nextProps.cellsMetadata,
|
||||
nextProps.metadataField
|
||||
);
|
||||
|
||||
this.histogramCache.x = d3
|
||||
.scaleLinear()
|
||||
.domain([nextProps.ranges.min, nextProps.ranges.max])
|
||||
.range([0, this.width]);
|
||||
|
||||
this.histogramCache.y = d3
|
||||
.scaleLinear()
|
||||
.range([this.height - this.marginBottom, 0]);
|
||||
// .range([height - margin.bottom, margin.top]);
|
||||
|
||||
this.histogramCache.bins = d3
|
||||
.histogram()
|
||||
.domain(this.histogramCache.x.domain())
|
||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||
|
||||
this.histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.calcHistogramCache(this.props);
|
||||
}
|
||||
|
||||
onBrush(selection, x) {
|
||||
return () => {
|
||||
const { dispatch, metadataField } = this.props;
|
||||
if (d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
selection: metadataField,
|
||||
range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
selection: metadataField,
|
||||
range: null
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
drawHistogram(svgRef) {
|
||||
const x = this.histogramCache.x;
|
||||
const y = this.histogramCache.y;
|
||||
const bins = this.histogramCache.bins;
|
||||
const numValues = this.histogramCache.numValues;
|
||||
const { obsAnnotations, metadataField, ranges } = this.props;
|
||||
const histogramCache = this.calcHistogramCache(
|
||||
obsAnnotations,
|
||||
metadataField,
|
||||
ranges
|
||||
);
|
||||
|
||||
const { x, y, bins, numValues } = histogramCache;
|
||||
d3.select(svgRef)
|
||||
.selectAll(".bar")
|
||||
.remove();
|
||||
|
||||
d3.select(svgRef)
|
||||
.insert("g", "*")
|
||||
@@ -97,6 +100,7 @@ class HistogramBrush extends React.Component {
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("class", "bar")
|
||||
.attr("x", function(d) {
|
||||
return x(d.x0) + 1;
|
||||
})
|
||||
@@ -144,6 +148,7 @@ class HistogramBrush extends React.Component {
|
||||
this.setState({ brush, xAxis });
|
||||
}
|
||||
}
|
||||
|
||||
handleColorAction() {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
@@ -153,6 +158,7 @@ class HistogramBrush extends React.Component {
|
||||
].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
||||
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import styles from "./parallelCoordinates.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
|
||||
import setupParallelCoordinates from "./setupParallelCoordinates";
|
||||
import drawAxes from "./drawAxes";
|
||||
import drawLinesCanvas from "./drawLinesCanvas";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
};
|
||||
})
|
||||
class Parallel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
|
||||
this.setState({ svg, ctx });
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeDrawAxes(nextProps);
|
||||
this.maybeDrawLines(nextProps);
|
||||
}
|
||||
maybeDrawAxes(nextProps) {
|
||||
if (
|
||||
!this.state.axes &&
|
||||
nextProps.initializeRanges /* axes are created on full range of data */
|
||||
) {
|
||||
const dimensions = createDimensions(nextProps.initializeRanges);
|
||||
|
||||
const xscale = d3
|
||||
.scalePoint()
|
||||
.domain(d3.range(dimensions.length))
|
||||
.range([0, width]);
|
||||
|
||||
const axes = drawAxes(
|
||||
this.state.svg,
|
||||
this.state.ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
height,
|
||||
width,
|
||||
this.handleBrushAction.bind(this),
|
||||
this.handleColorAction.bind(this)
|
||||
);
|
||||
|
||||
this.setState({
|
||||
axes,
|
||||
xscale,
|
||||
dimensions
|
||||
});
|
||||
|
||||
this.props.dispatch({
|
||||
type: "parallel coordinates axes have been drawn"
|
||||
});
|
||||
}
|
||||
}
|
||||
maybeDrawLines = _.debounce(nextProps => {
|
||||
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
|
||||
if (
|
||||
nextProps.ranges &&
|
||||
nextProps.cellsMetadata &&
|
||||
nextProps.axesHaveBeenDrawn
|
||||
) {
|
||||
if (this.state._drawLinesCanvas) {
|
||||
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
|
||||
}
|
||||
|
||||
this.state.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const _drawLinesCanvas = drawLinesCanvas(
|
||||
nextProps.cellsMetadata,
|
||||
this.state.dimensions,
|
||||
this.state.xscale,
|
||||
this.state.ctx,
|
||||
nextProps.colorAccessor,
|
||||
nextProps.colorScale
|
||||
);
|
||||
|
||||
this.setState({
|
||||
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="parcoords_wrapper">
|
||||
<div
|
||||
className={styles.parcoords}
|
||||
id="parcoords"
|
||||
style={{
|
||||
width: width + margin.left + margin.right + "px",
|
||||
height: height + margin.top + margin.bottom + "px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Parallel;
|
||||
|
||||
// <SectionHeader text="Continuous Metadata"/>
|
||||
@@ -1,19 +1,17 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import * as d3 from "d3";
|
||||
import { interpolateViridis } from "d3-scale-chromatic";
|
||||
|
||||
// create continuous color legend
|
||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
||||
const continuous = (selector_id, colorscale) => {
|
||||
var legendheight = 200,
|
||||
legendwidth = 80,
|
||||
margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
const legendheight = 200;
|
||||
const legendwidth = 80;
|
||||
const margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
|
||||
var canvas = d3
|
||||
const canvas = d3
|
||||
.select(selector_id)
|
||||
.style("height", legendheight + "px")
|
||||
.style("width", legendwidth + "px")
|
||||
@@ -104,6 +102,7 @@ class ContinuousLegend extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.colorAccessor !== this.props.colorAccessor ||
|
||||
@@ -122,13 +121,15 @@ class ContinuousLegend extends React.Component {
|
||||
continuous(
|
||||
"#continuous_legend",
|
||||
d3
|
||||
.scaleSequential(d3.interpolateViridis)
|
||||
.scaleSequential(interpolateViridis)
|
||||
.domain(this.props.colorScale.domain())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawScale() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -9,7 +9,7 @@ import actions from "../../actions";
|
||||
@connect()
|
||||
class CellSetButton extends React.Component {
|
||||
set() {
|
||||
const set = _.map(this.props.crossfilter.cells.allFiltered(), "CellName");
|
||||
const set = _.map(this.props.crossfilter.allFiltered(), "name");
|
||||
|
||||
this.props.dispatch({
|
||||
type:
|
||||
@@ -18,6 +18,7 @@ class CellSetButton extends React.Component {
|
||||
data: set
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span style={{ marginRight: 10 }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import memoize from "memoize-one";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./expression.css";
|
||||
@@ -19,6 +20,7 @@ class HeatmapSquare extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const contrastColor = getContrast(
|
||||
this.props.backgroundColor
|
||||
@@ -66,6 +68,7 @@ class HeatmapRow extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
handleGeneColorScaleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch(
|
||||
@@ -75,6 +78,7 @@ class HeatmapRow extends React.Component {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
handleSetGeneAsScatterplotX(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -83,6 +87,7 @@ class HeatmapRow extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleSetGeneAsScatterplotY(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -91,6 +96,7 @@ class HeatmapRow extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
@@ -211,7 +217,7 @@ class HeatmapRow extends React.Component {
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
allGeneNames: state.controls.allGeneNames
|
||||
world: state.controls.world
|
||||
};
|
||||
})
|
||||
class Heatmap extends React.Component {
|
||||
@@ -221,12 +227,18 @@ class Heatmap extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
getAllGeneNames = memoize(world =>
|
||||
_.map(this.props.world.varAnnotations, "name")
|
||||
);
|
||||
|
||||
render() {
|
||||
if (!this.props.differential.diffExp)
|
||||
return <p>Select cells & compute differential to see heatmap</p>;
|
||||
|
||||
const topGenesForCellSet1 = this.props.differential.diffExp.data.celllist1;
|
||||
const topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2;
|
||||
// const allGeneNames = this.getAllGeneNames(this.props.world);
|
||||
|
||||
const extent = d3.extent(
|
||||
_.union(
|
||||
|
||||
@@ -9,7 +9,9 @@ import CellSetButton from "./cellSetButtons";
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
crossfilter: state.controls.crossfilter
|
||||
world: state.controls.world,
|
||||
crossfilter: _.get(state.controls, "crossfilter", null),
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Expression extends React.Component {
|
||||
@@ -17,6 +19,7 @@ class Expression extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -25,6 +28,7 @@ class Expression extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
computeDiffExp() {
|
||||
this.props.dispatch(
|
||||
actions.requestDifferentialExpression(
|
||||
@@ -33,6 +37,7 @@ class Expression extends React.Component {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.differential) {
|
||||
return null;
|
||||
|
||||
@@ -24,10 +24,12 @@ import FaSave from "react-icons/lib/fa/download";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
world: state.controls.world,
|
||||
crossfilter: state.controls.crossfilter,
|
||||
responsive: state.responsive,
|
||||
crossfilter: state.controls.crossfilter
|
||||
colorRGB: _.get(state.controls, "colorRGB", null),
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Graph extends React.Component {
|
||||
@@ -43,13 +45,12 @@ class Graph extends React.Component {
|
||||
colors: null
|
||||
};
|
||||
this.state = {
|
||||
drawn: false,
|
||||
svg: null,
|
||||
ctx: null,
|
||||
brush: null,
|
||||
mode: "brush"
|
||||
};
|
||||
}
|
||||
|
||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
||||
regl.clear({
|
||||
depth: 1,
|
||||
@@ -64,6 +65,7 @@ class Graph extends React.Component {
|
||||
view: camera.view()
|
||||
});
|
||||
}
|
||||
|
||||
restartReglLoop() {
|
||||
const reglRender = this.state.regl.frame(() => {
|
||||
this.reglDraw(
|
||||
@@ -83,6 +85,7 @@ class Graph extends React.Component {
|
||||
reglRender
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// setup canvas and camera
|
||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
||||
@@ -120,7 +123,16 @@ class Graph extends React.Component {
|
||||
reglRender
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
selectionUpdate,
|
||||
colorRGB,
|
||||
responsive
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.state.reglRender &&
|
||||
this.reglRenderState === "rendering" &&
|
||||
@@ -130,25 +142,22 @@ class Graph extends React.Component {
|
||||
this.reglRenderState = "paused";
|
||||
}
|
||||
|
||||
if (this.state.regl && this.props.crossfilter) {
|
||||
if (this.state.regl && world) {
|
||||
/* update the regl state */
|
||||
const crossfilter = this.props.crossfilter.cells;
|
||||
const cells = crossfilter.all();
|
||||
const cellCount = cells.length;
|
||||
const obsLayout = world.obsLayout;
|
||||
const cellCount = crossfilter.size();
|
||||
|
||||
// X/Y positions for each point - a cached value that only
|
||||
// changes if we have loaded entirely new cell data
|
||||
//
|
||||
if (
|
||||
!this.renderCache.positions ||
|
||||
this.props.crossfilter.cells != prevProps.crossfilter.cells
|
||||
selectionUpdate != prevProps.selectionUpdate
|
||||
) {
|
||||
if (!this.renderCache.positions)
|
||||
this.renderCache.positions = new Float32Array(2 * cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||
// d3.scaleLinear().domain([0,1]).range([1,-1])
|
||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||
|
||||
for (
|
||||
@@ -156,8 +165,8 @@ class Graph extends React.Component {
|
||||
i < cellCount;
|
||||
i++
|
||||
) {
|
||||
positions[2 * i] = glScaleX(cells[i].__x__);
|
||||
positions[2 * i + 1] = glScaleY(cells[i].__y__);
|
||||
positions[2 * i] = glScaleX(obsLayout.X[i]);
|
||||
positions[2 * i + 1] = glScaleY(obsLayout.Y[i]);
|
||||
}
|
||||
this.state.pointBuffer({
|
||||
data: this.renderCache.positions,
|
||||
@@ -171,14 +180,12 @@ class Graph extends React.Component {
|
||||
// could have changed for some other reason, but for now color is
|
||||
// the only metadata that changes client-side. If this is problematic,
|
||||
// we could add some sort of color-specific indicator to the app state.
|
||||
if (
|
||||
!this.renderCache.colors ||
|
||||
this.props.cellsMetadata != prevProps.cellsMetadata
|
||||
) {
|
||||
if (!this.renderCache.colors || colorRGB != prevProps.colorRGB) {
|
||||
const rgb = colorRGB;
|
||||
if (!this.renderCache.colors)
|
||||
this.renderCache.colors = new Float32Array(3 * cellCount);
|
||||
for (let i = 0, colors = this.renderCache.colors; i < cellCount; i++) {
|
||||
colors.set(cells[i].__colorRGB__, 3 * i);
|
||||
this.renderCache.colors = new Float32Array(3 * rgb.length);
|
||||
for (let i = 0, colors = this.renderCache.colors; i < rgb.length; i++) {
|
||||
colors.set(rgb[i], 3 * i);
|
||||
}
|
||||
this.state.colorBuffer({ data: this.renderCache.colors, dimension: 3 });
|
||||
}
|
||||
@@ -188,12 +195,8 @@ class Graph extends React.Component {
|
||||
// most property upates are due to changes driving a crossfilter
|
||||
// selection set change.
|
||||
//
|
||||
if (
|
||||
!this.renderCache.sizes ||
|
||||
this.props.crossfilter.cells != prevProps.crossfilter.cells
|
||||
) {
|
||||
if (!this.renderCache.sizes)
|
||||
this.renderCache.sizes = new Float32Array(cellCount);
|
||||
}
|
||||
crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2);
|
||||
this.state.sizeBuffer({ data: this.renderCache.sizes, dimension: 1 });
|
||||
|
||||
@@ -211,12 +214,10 @@ class Graph extends React.Component {
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.responsive.height !== this.props.responsive.height ||
|
||||
prevProps.responsive.width !== this.props.responsive.width ||
|
||||
prevProps.responsive.height !== responsive.height ||
|
||||
prevProps.responsive.width !== responsive.width ||
|
||||
/* first time */
|
||||
(this.props.responsive.height &&
|
||||
this.props.responsive.width &&
|
||||
!this.state.svg)
|
||||
(responsive.height && responsive.width && !this.state.svg)
|
||||
) {
|
||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
||||
d3.select("#graphAttachPoint")
|
||||
@@ -225,12 +226,13 @@ class Graph extends React.Component {
|
||||
const { svg, brush, brushContainer } = setupSVGandBrushElements(
|
||||
this.handleBrushSelectAction.bind(this),
|
||||
this.handleBrushDeselectAction.bind(this),
|
||||
this.props.responsive,
|
||||
responsive,
|
||||
this.graphPaddingTop
|
||||
);
|
||||
this.setState({ svg, brush, brushContainer });
|
||||
}
|
||||
}
|
||||
|
||||
handleBrushSelectAction() {
|
||||
/* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */
|
||||
if (d3.event.sourceEvent !== null) {
|
||||
@@ -280,6 +282,7 @@ class Graph extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleBrushDeselectAction() {
|
||||
if (d3.event && !d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
@@ -295,6 +298,7 @@ class Graph extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOpacityRangeChange(e) {
|
||||
this.props.dispatch({
|
||||
type: "change opacity deselected cells in 2d graph background",
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import styles from "./joy.css";
|
||||
|
||||
/*
|
||||
via https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3
|
||||
*/
|
||||
|
||||
var margin = { top: 30, right: 10, bottom: 30, left: 100 },
|
||||
width = 400 - margin.left - margin.right,
|
||||
height = 600 - margin.top - margin.bottom;
|
||||
|
||||
// Percent two area charts can overlap
|
||||
var overlap = 0.4;
|
||||
|
||||
var formatTime = d3.timeFormat("%I %p");
|
||||
|
||||
var x = function(d) {
|
||||
return d.time;
|
||||
},
|
||||
xScale = d3.scaleTime().range([0, width]),
|
||||
xValue = function(d) {
|
||||
return xScale(x(d));
|
||||
},
|
||||
xAxis = d3.axisBottom(xScale).tickFormat(formatTime);
|
||||
|
||||
var y = function(d) {
|
||||
return d.value;
|
||||
},
|
||||
yScale = d3.scaleLinear(),
|
||||
yValue = function(d) {
|
||||
return yScale(y(d));
|
||||
};
|
||||
|
||||
var activity = function(d) {
|
||||
return d.key;
|
||||
},
|
||||
activityScale = d3.scaleBand().range([0, height]),
|
||||
activityValue = function(d) {
|
||||
return activityScale(activity(d));
|
||||
},
|
||||
activityAxis = d3.axisLeft(activityScale);
|
||||
|
||||
var area = d3
|
||||
.area()
|
||||
.x(xValue)
|
||||
.y1(yValue);
|
||||
|
||||
var line = area.lineY1();
|
||||
|
||||
function parseTime(offset) {
|
||||
var date = new Date(2017, 0, 1); // chose an arbitrary day
|
||||
return d3.timeMinute.offset(date, offset);
|
||||
}
|
||||
|
||||
function row(d) {
|
||||
return {
|
||||
activity: d.activity,
|
||||
time: parseTime(d.time),
|
||||
value: +d.p_smooth
|
||||
};
|
||||
}
|
||||
|
||||
const drawJoy = data => {
|
||||
console.log("drawJoy: ", data);
|
||||
|
||||
var svg = d3
|
||||
.select("#joyplot")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
d3.tsv(
|
||||
"https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv",
|
||||
row,
|
||||
function(error, dataFlat) {
|
||||
// Sort by time
|
||||
dataFlat.sort(function(a, b) {
|
||||
return a.time - b.time;
|
||||
});
|
||||
|
||||
var data = d3
|
||||
.nest()
|
||||
.key(function(d) {
|
||||
return d.activity;
|
||||
})
|
||||
.entries(dataFlat);
|
||||
|
||||
// Sort activities by peak activity time
|
||||
function peakTime(d) {
|
||||
var i = d3.scan(d.values, function(a, b) {
|
||||
return y(b) - y(a);
|
||||
});
|
||||
return d.values[i].time;
|
||||
}
|
||||
data.sort(function(a, b) {
|
||||
return peakTime(b) - peakTime(a);
|
||||
});
|
||||
|
||||
console.log("sorted", data);
|
||||
|
||||
xScale.domain(d3.extent(dataFlat, x));
|
||||
|
||||
activityScale.domain(
|
||||
data.map(function(d) {
|
||||
return d.key;
|
||||
})
|
||||
);
|
||||
|
||||
var areaChartHeight =
|
||||
(1 + overlap) * (height / activityScale.domain().length);
|
||||
|
||||
yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
|
||||
|
||||
area.y0(yScale(0));
|
||||
|
||||
var gActivity = svg
|
||||
.append("g")
|
||||
.attr("class", "activities")
|
||||
.selectAll(".activity")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", function(d) {
|
||||
return `${styles.activity} ${styles.activity["--" + d.key]}`;
|
||||
})
|
||||
.attr("transform", function(d) {
|
||||
var ty = activityValue(d) - activityScale.bandwidth() + 5;
|
||||
return "translate(0," + ty + ")";
|
||||
});
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.area)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", area);
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.line)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", line);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--x"]}`)
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
|
||||
.call(activityAxis);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default drawJoy;
|
||||
@@ -1,42 +0,0 @@
|
||||
svg {
|
||||
display: block;
|
||||
/*margin: 0 auto;*/
|
||||
}
|
||||
|
||||
.axis .domain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--x text {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.axis--x line {
|
||||
stroke: #aaa;
|
||||
}
|
||||
|
||||
.axis--activity .tick line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--activity text {
|
||||
font-size: 12px;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
/*.axis--activity .tick:nth-child(odd) text {
|
||||
fill: #222;
|
||||
}*/
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.area {
|
||||
fill: #448cab;
|
||||
}
|
||||
|
||||
.activity:nth-child(odd) .area {
|
||||
fill: #5ca3c1;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import styles from "./joy.css";
|
||||
import drawJoy from "./drawJoy";
|
||||
import joyParser from "./joyParser";
|
||||
|
||||
class Joy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data) {
|
||||
console.log("joyplot data 44", nextProps.data);
|
||||
drawJoy(joyParser(nextProps.data));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="joyplot_wrapper" style={{ marginTop: 50 }}>
|
||||
<h3> Joy </h3>
|
||||
<p>
|
||||
{" "}
|
||||
Cell expression distribution per gene & if differential expression,
|
||||
Ie., cells for cluster 5, top genes expressed by cluster 8
|
||||
</p>
|
||||
<div id="joyplot"> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Joy;
|
||||
@@ -1,29 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
const joyParser = (data, count = 20) => {
|
||||
const genes = [];
|
||||
|
||||
/* setup */
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const gene = {
|
||||
key:
|
||||
data.genes[
|
||||
i
|
||||
] /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */,
|
||||
values: []
|
||||
};
|
||||
|
||||
data.cells.forEach(cell => {
|
||||
gene.values.push({
|
||||
value: cell["e"][i]
|
||||
});
|
||||
});
|
||||
|
||||
genes.push(gene);
|
||||
}
|
||||
|
||||
return genes;
|
||||
};
|
||||
|
||||
export default joyParser;
|
||||
@@ -5,38 +5,55 @@
|
||||
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 _regl from "regl";
|
||||
import * as d3 from "d3";
|
||||
|
||||
import mat4 from "gl-mat4";
|
||||
import fit from "canvas-fit";
|
||||
import _camera from "../../util/camera.js";
|
||||
import _regl from "regl";
|
||||
import _camera from "../../util/camera";
|
||||
|
||||
import setupScatterplot from "./setupScatterplot";
|
||||
import styles from "./scatterplot.css";
|
||||
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { scaleLinear } from "../../util/scaleLinear";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
import { margin, width, height } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor
|
||||
} = state.controls;
|
||||
const expressionX =
|
||||
world && scatterplotXXaccessor
|
||||
? state.controls.world.varDataCache[scatterplotXXaccessor]
|
||||
: null;
|
||||
const expressionY =
|
||||
world && scatterplotYYaccessor
|
||||
? state.controls.world.varDataCache[scatterplotYYaccessor]
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
world,
|
||||
|
||||
colorRGB: state.controls.colorRGB,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
|
||||
// Accessors are var/gene names (strings)
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
crossfilter: state.controls.crossfilter,
|
||||
|
||||
differential: state.differential,
|
||||
expression: state.expression
|
||||
|
||||
expressionX,
|
||||
expressionY,
|
||||
|
||||
crossfilter,
|
||||
// updated whenever the crossfilter selection is updated
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Scatterplot extends React.Component {
|
||||
@@ -46,9 +63,6 @@ class Scatterplot extends React.Component {
|
||||
this.axes = false;
|
||||
this.state = {
|
||||
svg: null,
|
||||
// ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
xScale: null,
|
||||
yScale: null
|
||||
};
|
||||
@@ -57,19 +71,10 @@ class Scatterplot extends React.Component {
|
||||
componentDidMount() {
|
||||
const { svg } = setupScatterplot(width, height, margin);
|
||||
let scales;
|
||||
const { expressionX, expressionY } = this.props;
|
||||
|
||||
/* if we've already got the data, user clicked back and forth between tabs, so render the scatterplot */
|
||||
if (
|
||||
this.props.expression &&
|
||||
this.props.expression.data &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor
|
||||
) {
|
||||
scales = this.setupScales(
|
||||
this.props.expression,
|
||||
this.props.scatterplotXXaccessor,
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
if (svg && expressionX && expressionY) {
|
||||
scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
||||
}
|
||||
|
||||
@@ -115,115 +120,101 @@ class Scatterplot extends React.Component {
|
||||
colorBuffer
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
svg,
|
||||
xScale,
|
||||
yScale,
|
||||
regl,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
sizeBuffer
|
||||
} = this.state;
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
expressionX,
|
||||
expressionY,
|
||||
colorRGB
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.state.svg &&
|
||||
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 || // was CLU now FTH1 etc
|
||||
world &&
|
||||
svg &&
|
||||
xScale &&
|
||||
yScale &&
|
||||
scatterplotXXaccessor &&
|
||||
scatterplotYYaccessor &&
|
||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
||||
!this.axes) // clicked off the tab and back again, rerender
|
||||
) {
|
||||
this.drawAxesSVG(this.state.xScale, this.state.yScale, this.state.svg);
|
||||
this.drawAxesSVG(xScale, yScale, svg);
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.metadata &&
|
||||
this.state.regl &&
|
||||
this.state.pointBuffer &&
|
||||
this.state.colorBuffer &&
|
||||
this.state.sizeBuffer &&
|
||||
this.props.expression.data &&
|
||||
this.props.expression.data.genes &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
this.state.xScale &&
|
||||
this.state.yScale
|
||||
world &&
|
||||
regl &&
|
||||
pointBuffer &&
|
||||
colorBuffer &&
|
||||
sizeBuffer &&
|
||||
expressionX &&
|
||||
expressionY &&
|
||||
scatterplotXXaccessor &&
|
||||
scatterplotYYaccessor &&
|
||||
xScale &&
|
||||
yScale
|
||||
) {
|
||||
const crossfilter = this.props.crossfilter.cells;
|
||||
const data = this.props.expression.data;
|
||||
const cells = data.cells;
|
||||
const genes = data.genes;
|
||||
const cellCount = cells.length;
|
||||
const positions = new Float32Array(2 * cellCount);
|
||||
const colors = new Float32Array(3 * cellCount);
|
||||
const sizes = new Float32Array(cellCount);
|
||||
const cellCount = expressionX.length;
|
||||
const positionsBuf = new Float32Array(2 * cellCount);
|
||||
const colorsBuf = new Float32Array(3 * cellCount);
|
||||
const sizesBuf = new Float32Array(cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0, width]).range([-0.95, 0.95])
|
||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
||||
|
||||
// d3.scaleLinear().domain([0, height]).range([-1, 1])
|
||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
||||
|
||||
const geneXXaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotXXaccessor
|
||||
);
|
||||
const geneYYaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
|
||||
/*
|
||||
Construct Vectors
|
||||
*/
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const cell = cells[i];
|
||||
|
||||
positions[2 * i] = glScaleX(
|
||||
this.state.xScale(cell.e[geneXXaccessorIndex])
|
||||
); /* scale each point first to the window as we calculate extents separately below, so no need to repeat */
|
||||
positions[2 * i + 1] = glScaleY(
|
||||
this.state.yScale(cell.e[geneYYaccessorIndex])
|
||||
);
|
||||
for (let i = 0; i < cellCount; i += 1) {
|
||||
positionsBuf[2 * i] = glScaleX(xScale(expressionX[i]));
|
||||
positionsBuf[2 * i + 1] = glScaleY(yScale(expressionY[i]));
|
||||
}
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const metadata = this.props.metadata[i];
|
||||
colors.set(metadata.__colorRGB__, 3 * i);
|
||||
for (let i = 0; i < cellCount; i += 1) {
|
||||
colorsBuf.set(colorRGB[i], 3 * i);
|
||||
}
|
||||
|
||||
crossfilter.fillByIsFiltered(sizes, 4, 0.2);
|
||||
crossfilter.fillByIsFiltered(sizesBuf, 4, 0.2);
|
||||
|
||||
this.state.pointBuffer({ data: positions, dimension: 2 });
|
||||
this.state.colorBuffer({ data: colors, dimension: 3 });
|
||||
this.state.sizeBuffer({ data: sizes, dimension: 1 });
|
||||
pointBuffer({ data: positionsBuf, dimension: 2 });
|
||||
colorBuffer({ data: colorsBuf, dimension: 3 });
|
||||
sizeBuffer({ data: sizesBuf, dimension: 1 });
|
||||
this.count = cellCount;
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.expression &&
|
||||
this.props.expression.data &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
||||
expressionX &&
|
||||
expressionY &&
|
||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
||||
) {
|
||||
const scales = this.setupScales(
|
||||
this.props.expression,
|
||||
this.props.scatterplotXXaccessor,
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||
this.setState(scales);
|
||||
}
|
||||
}
|
||||
setupScales(expression, scatterplotXXaccessor, scatterplotYYaccessor) {
|
||||
|
||||
static setupScales(expressionX, expressionY) {
|
||||
const xScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(expression.data.cells, (cell, i) => {
|
||||
return cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)];
|
||||
})
|
||||
)
|
||||
.domain(d3.extent(expressionX))
|
||||
.range([0, width]);
|
||||
|
||||
const yScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(expression.data.cells, cell => {
|
||||
return cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)];
|
||||
})
|
||||
)
|
||||
.domain(d3.extent(expressionY))
|
||||
.range([height, 0]);
|
||||
|
||||
return {
|
||||
@@ -231,15 +222,19 @@ class Scatterplot extends React.Component {
|
||||
yScale
|
||||
};
|
||||
}
|
||||
|
||||
drawAxesSVG(xScale, yScale, svg) {
|
||||
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
||||
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);
|
||||
// the axes are much cleaner and easier now. No need to rotate and orient
|
||||
// the axis, just call axisBottom, axisLeft etc.
|
||||
const xAxis = d3.axisBottom().scale(xScale);
|
||||
|
||||
var yAxis = d3.axisLeft().scale(yScale);
|
||||
const 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.
|
||||
// adding axes is also simpler now, just translate x-axis to (0,height)
|
||||
// and it's alread defined to be a bottom axis.
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
@@ -259,7 +254,7 @@ class Scatterplot extends React.Component {
|
||||
.attr("x", 10)
|
||||
.attr("y", 10)
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotYYaccessor);
|
||||
.text(scatterplotYYaccessor);
|
||||
|
||||
svg
|
||||
.append("text")
|
||||
@@ -267,7 +262,7 @@ class Scatterplot extends React.Component {
|
||||
.attr("y", height - 10)
|
||||
.attr("text-anchor", "end")
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotXXaccessor);
|
||||
.text(scatterplotXXaccessor);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
Reference in New Issue
Block a user