#130 redux refactor (#208)

* 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:
Bruce Martin
2018-09-17 20:43:39 -07:00
committed by GitHub
parent 7b00fea44d
commit d4e850d18f
40 changed files with 2008 additions and 1584 deletions

View File

@@ -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} />

View File

@@ -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} />
);

View File

@@ -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}

View File

@@ -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

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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 }}>

View File

@@ -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(

View File

@@ -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;

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {