refactor regraph functionality to use crossfilter; add reset graph

This commit is contained in:
bkmartinjr
2018-05-27 11:49:50 -07:00
parent 2c79e98809
commit cfd13cad41
7 changed files with 160 additions and 109 deletions
+7
View File
@@ -65,6 +65,12 @@ const regraph = () => {
}; };
}; };
const resetGraph = () => {
return (dispatch, getState) => {
dispatch({ type: "reset graph" });
};
};
const initialize = () => { const initialize = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: "initialize started" }); dispatch({ type: "initialize started" });
@@ -218,6 +224,7 @@ export default {
initialize, initialize,
requestCells, requestCells,
regraph, regraph,
resetGraph,
requestGeneExpressionCounts, requestGeneExpressionCounts,
requestGeneExpressionCountsPOST, requestGeneExpressionCountsPOST,
requestSingleGeneExpressionCountsForColoringPOST, requestSingleGeneExpressionCountsForColoringPOST,
+2 -2
View File
@@ -26,7 +26,7 @@ import { connect } from "react-redux";
return { return {
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
allCellsMetadata: state.controls.allCellsMetadata cellsMetadata: state.controls.cellsMetadata
}; };
}) })
class HistogramBrush extends React.Component { class HistogramBrush extends React.Component {
@@ -52,7 +52,7 @@ class HistogramBrush extends React.Component {
calcHistogramCache(nextProps) { calcHistogramCache(nextProps) {
// recalculate expensive stuff // recalculate expensive stuff
const allValuesForContinuousFieldAsArray = _.map( const allValuesForContinuousFieldAsArray = _.map(
nextProps.allCellsMetadata, nextProps.cellsMetadata,
nextProps.metadataField nextProps.metadataField
); );
+3 -3
View File
@@ -36,7 +36,7 @@ import { margin, width, height, createDimensions } from "./util";
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection, graphBrushSelection: state.controls.graphBrushSelection,
allCellsMetadata: state.controls.allCellsMetadata, cellsMetadata: state.controls.cellsMetadata,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
}; };
}) })
@@ -96,7 +96,7 @@ class Parallel extends React.Component {
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */ /* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
if ( if (
nextProps.ranges && nextProps.ranges &&
nextProps.allCellsMetadata && nextProps.cellsMetadata &&
nextProps.axesHaveBeenDrawn nextProps.axesHaveBeenDrawn
) { ) {
if (this.state._drawLinesCanvas) { if (this.state._drawLinesCanvas) {
@@ -106,7 +106,7 @@ class Parallel extends React.Component {
this.state.ctx.clearRect(0, 0, width, height); this.state.ctx.clearRect(0, 0, width, height);
const _drawLinesCanvas = drawLinesCanvas( const _drawLinesCanvas = drawLinesCanvas(
nextProps.allCellsMetadata, nextProps.cellsMetadata,
this.state.dimensions, this.state.dimensions,
this.state.xscale, this.state.xscale,
this.state.ctx, this.state.ctx,
+20 -2
View File
@@ -23,7 +23,7 @@ import FaSave from "react-icons/lib/fa/download";
@connect(state => { @connect(state => {
return { return {
allCellsMetadata: state.controls.allCellsMetadata, cellsMetadata: state.controls.cellsMetadata,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
responsive: state.responsive, responsive: state.responsive,
crossfilter: state.controls.crossfilter crossfilter: state.controls.crossfilter
@@ -131,7 +131,7 @@ class Graph extends React.Component {
// we could add some sort of color-specific indicator to the app state. // we could add some sort of color-specific indicator to the app state.
if ( if (
!this.renderCache.colors || !this.renderCache.colors ||
this.props.allCellsMetadata != nextProps.allCellsMetadata this.props.cellsMetadata != nextProps.cellsMetadata
) { ) {
if (!this.renderCache.colors) if (!this.renderCache.colors)
this.renderCache.colors = new Float32Array(3 * cellCount); this.renderCache.colors = new Float32Array(3 * cellCount);
@@ -244,6 +244,24 @@ class Graph extends React.Component {
alignItems: "baseline" alignItems: "baseline"
}} }}
> >
<button
onClick={() => {
this.props.dispatch(actions.resetGraph());
}}
style={{
fontSize: 14,
fontWeight: 700,
color: "white",
padding: "10px 20px",
marginRight: 10,
borderRadius: 2,
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer"
}}
>
reset graph
</button>
<button <button
onClick={() => { onClick={() => {
this.props.dispatch(actions.regraph()); this.props.dispatch(actions.regraph());
+12 -14
View File
@@ -31,22 +31,20 @@ const updateCellColorsMiddleware = store => {
action.type === "color by continuous metadata" || action.type === "color by continuous metadata" ||
action.type === "color by categorical metadata"; action.type === "color by categorical metadata";
if (!filterJustChanged || !s.controls.allCellsOnClient) { if (!filterJustChanged || !s.controls.cellsMetadata) {
return next( return next(
action action
); /* if the cells haven't loaded or the action wasn't a color change, bail */ ); /* if the cells haven't loaded or the action wasn't a color change, bail */
} }
let allCellsMetadataWithUpdatedColors = s.controls.allCellsMetadata.slice( let cellsMetadataWithUpdatedColors = s.controls.cellsMetadata.slice(0);
0
);
let colorScale; let colorScale;
/* /*
in plain language... in plain language...
(a) once the cells have loaded. (a) once the cells have loaded.
(b) each time a user changes a color control we need to update allCellsMetadata colors (b) each time a user changes a color control we need to update cellsMetadata colors
This is available to all the draw functions as cell["__color__"] and cell["__colorRGB__"] This is available to all the draw functions as cell["__color__"] and cell["__colorRGB__"]
*/ */
@@ -54,8 +52,8 @@ const updateCellColorsMiddleware = store => {
if (action.type === "color by categorical metadata") { if (action.type === "color by categorical metadata") {
colorScale = d3.scaleOrdinal().range(globals.ordinalColors); colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
for (let i = 0; i < allCellsMetadataWithUpdatedColors.length; i++) { for (let i = 0; i < cellsMetadataWithUpdatedColors.length; i++) {
const cell = allCellsMetadataWithUpdatedColors[i]; const cell = cellsMetadataWithUpdatedColors[i];
let c = colorScale(cell[action.colorAccessor]); let c = colorScale(cell[action.colorAccessor]);
cell.__color__ = c; cell.__color__ = c;
cell.__colorRGB__ = parseRGB(c); cell.__colorRGB__ = parseRGB(c);
@@ -68,10 +66,10 @@ const updateCellColorsMiddleware = store => {
.domain([0, action.rangeMaxForColorAccessor]) .domain([0, action.rangeMaxForColorAccessor])
.range([1, 0]); .range([1, 0]);
_.each(allCellsMetadataWithUpdatedColors, (cell, i) => { _.each(cellsMetadataWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis(colorScale(cell[action.colorAccessor])); let c = d3.interpolateViridis(colorScale(cell[action.colorAccessor]));
allCellsMetadataWithUpdatedColors[i]["__color__"] = c; cellsMetadataWithUpdatedColors[i]["__color__"] = c;
allCellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c); cellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
}); });
} }
@@ -113,12 +111,12 @@ const updateCellColorsMiddleware = store => {
0 0
]); /* invert viridis... probably pass this scale through to others */ ]); /* invert viridis... probably pass this scale through to others */
_.each(allCellsMetadataWithUpdatedColors, (cell, i) => { _.each(cellsMetadataWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis( let c = d3.interpolateViridis(
colorScale(expressionMap[cell.CellName][indexOfGene]) colorScale(expressionMap[cell.CellName][indexOfGene])
); );
allCellsMetadataWithUpdatedColors[i]["__color__"] = c; cellsMetadataWithUpdatedColors[i]["__color__"] = c;
allCellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c); cellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
}); });
} }
@@ -126,7 +124,7 @@ const updateCellColorsMiddleware = store => {
append the result of all the filters to the action the user just triggered append the result of all the filters to the action the user just triggered
*/ */
let modifiedAction = Object.assign({}, action, { let modifiedAction = Object.assign({}, action, {
allCellsMetadataWithUpdatedColors, cellsMetadataWithUpdatedColors,
colorScale colorScale
}); });
@@ -2,6 +2,13 @@
import uri from "urijs"; import uri from "urijs";
import * as globals from "../globals"; import * as globals from "../globals";
/*
XXX: this file should be obsolete. We just need to complete the refactoring
of parallel.js and it can be removed entirely.
It is currently not in use - the middleware constructor does not include include it
*/
/* /*
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6 https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
@@ -34,11 +41,7 @@ const updateCellSelectionMiddleware = store => {
action.type === "categorical metadata filter none of these" || action.type === "categorical metadata filter none of these" ||
action.type === "categorical metadata filter all of these"; action.type === "categorical metadata filter all of these";
if ( if (!filterJustChanged || !s.controls.cellsMetadata) {
!filterJustChanged ||
!s.controls.allCellsOnClient
/* graph is set at the same time as allCells, so we assume it exists */
) {
return next( return next(
action action
); /* if the cells haven't loaded or the action wasn't a filter, bail */ ); /* if the cells haven't loaded or the action wasn't a filter, bail */
@@ -48,7 +51,7 @@ const updateCellSelectionMiddleware = store => {
- make a FRESH copy of all of the cells - make a FRESH copy of all of the cells
- metadata has cellname and index, and that's all we ever need to reference cell info - metadata has cellname and index, and that's all we ever need to reference cell info
*/ */
let newSelection = s.controls.allCellsMetadata.slice(0); let newSelection = s.controls.cellsMetadata.slice(0);
// _.forEach(newSelection, cell => (cell.__selected__ = true)); // _.forEach(newSelection, cell => (cell.__selected__ = true));
for (let i = 0; i < newSelection.length; i++) { for (let i = 0; i < newSelection.length; i++) {
newSelection[i].__selected__ = true; newSelection[i].__selected__ = true;
@@ -58,7 +61,7 @@ const updateCellSelectionMiddleware = store => {
in plain language... in plain language...
(a) once the cells have loaded. (a) once the cells have loaded.
(b) each time a user changes ANY control we need to update allCellsMetadata (b) each time a user changes ANY control we need to update cellsMetadata
there are two states: there are two states:
1. control state we already know about (state.foo) 1. control state we already know about (state.foo)
+106 -81
View File
@@ -65,14 +65,89 @@ function deduceDimensionType(attributes, fieldName) {
return dimensionType; return dimensionType;
} }
// Create view state from /cells data response. Used both during a data
// load and during a graph reset.
//
function createViewState(schema, data) {
const cellsMetadata = data.metadata.slice(0);
/*
construct a copy of the ranges object that only has categorical
replace all counts with bool flags
ie., everything starts out checked
we mutate this map in the actions below
*/
const categoricalAsBooleansMap = {};
_.each(data.ranges, (value, key) => {
if (
key !== "CellName" &&
value.options /* it's categorical, it has options instead of ranges */
) {
const optionsAsBooleans = {};
_.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true;
});
categoricalAsBooleansMap[key] = optionsAsBooleans;
}
});
const graph = data.graph;
_.each(cellsMetadata, (cell, idx) => {
cell.__cellIndex__ = idx;
cell.__color__ =
"rgba(0,0,0,1)"; /* initial color for all cells in all charts */
cell.__colorRGB__ = parseRGB(cell.__color__);
cell.__x__ = graph[idx][1];
cell.__y__ = graph[idx][2];
});
// Build the selection crossfilter.
//
let cellsCrossfilter = crossfilter(cellsMetadata);
let cellsDimensionsMap = {};
cellsDimensionsMap.x = cellsCrossfilter.dimension(r => r.__x__, Float32Array);
cellsDimensionsMap.y = cellsCrossfilter.dimension(r => r.__y__, Float32Array);
// Now walk the schema and make an appropriate dimension for each
// metadata field. This is a simplistic mapping, and could be
// optmized to use smaller scalars (to save memory) or larger
// floating point where precision is needed.
//
_.forEach(schema, (attributes, key) => {
if (key !== "CellName") {
const dimensionType = deduceDimensionType(attributes, key);
if (dimensionType) {
cellsDimensionsMap[key] = cellsCrossfilter.dimension(
r => r[key],
dimensionType
);
}
}
});
return {
cellsMetadata,
crossfilter: {
cells: cellsCrossfilter,
dimensionMap: cellsDimensionsMap
},
categoricalAsBooleansMap
};
}
const Controls = ( const Controls = (
state = { state = {
/* Universe - all cells known to us. Set once, during initial load */
_ranges: null /* this comes from initialize, this is universe */, _ranges: null /* this comes from initialize, this is universe */,
allGeneNames: null, allGeneNames: null,
allCellsOnClient: null /* this comes from cells endpoint, this is world */, allCells: null /* this comes from cells endpoint, this is universe */,
allCellsMetadata: null /* this comes from user actions, all draw components use this, it is created by middleware */, allCellsMetadata: null /* this comes from cells endpoint, and is just the metadata for universe */,
/* View / World - all cells currently being displayed. May be a subset of Universe. */
cellsMetadata: null,
crossfilter: null /* the current user selection state */, crossfilter: null /* the current user selection state */,
categoricalAsBooleansMap: null, categoricalAsBooleansMap: null,
colorAccessor: null, colorAccessor: null,
colorScale: null, colorScale: null,
opacityForDeselectedCells: 0.2, opacityForDeselectedCells: 0.2,
@@ -101,91 +176,41 @@ const Controls = (
}); });
} }
case "request cells success": { case "request cells success": {
const allCellsMetadata = action.data.data.metadata.slice(0);
const allCellsMetadataMap = _.keyBy(allCellsMetadata, "CellName");
/*
construct a copy of the ranges object that only has categorical
replace all counts with bool flags
ie., everything starts out checked
we mutate this map in the actions below
*/
const categoricalAsBooleansMap = {};
_.each(action.data.data.ranges, (value, key) => {
if (
key !== "CellName" &&
value.options /* it's categorical, it has options instead of ranges */
) {
const optionsAsBooleans = {};
_.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true;
});
categoricalAsBooleansMap[key] = optionsAsBooleans;
}
});
const graph = action.data.data.graph;
_.each(allCellsMetadata, (cell, idx) => {
cell.__cellIndex__ = idx;
cell.__color__ =
"rgba(0,0,0,1)"; /* initial color for all cells in all charts */
cell.__colorRGB__ = parseRGB(cell.__color__);
cell.__x__ = graph[idx][1];
cell.__y__ = graph[idx][2];
});
// Build the selection crossfilter.
//
let cellsCrossfilter = crossfilter(allCellsMetadata);
let cellsDimensionsMap = {};
cellsDimensionsMap.x = cellsCrossfilter.dimension(
r => r.__x__,
Float32Array
);
cellsDimensionsMap.y = cellsCrossfilter.dimension(
r => r.__y__,
Float32Array
);
// Now walk the schema and make an appropriate dimension for each
// metadata field. This is a simplistic mapping, and could be
// optmized to use smaller scalars (to save memory) or larger
// floating point where precision is needed.
//
// If we don't have a schema (bad server!), fake it by inferring // If we don't have a schema (bad server!), fake it by inferring
// important fields from the ranges element. // important fields from the ranges element.
// //
if (!state.schema) { if (!state.schema) {
state.schema = createSchemaByDataSniffing(action.data.data.ranges); state.schema = createSchemaByDataSniffing(data.ranges);
} }
_.forEach(state.schema, (attributes, key) => {
if (key !== "CellName") { /* Set viewable world to the provided cell data */
const dimensionType = deduceDimensionType(attributes, key); const viewState = createViewState(state.schema, action.data.data);
if (dimensionType) {
cellsDimensionsMap[key] = cellsCrossfilter.dimension(
r => r[key],
dimensionType
);
}
}
});
return Object.assign({}, state, { return Object.assign({}, state, {
// this is only used as a flag that data has loaded. Could be /* Universe - initialize once */
// removed (other variables would suffice for the same test). allCells: state.allCells ? state.allCells : action.data,
allCellsOnClient: action.data.data, allCellsMetadata: state.allCellsMetadata
allCellsMetadata, ? state.allCellsMetadata
allCellsMetadataMap, : viewState.cellsMetadata,
categoricalAsBooleansMap, allCellsMetadataMap: state.allCellsMetadataMap
crossfilter: { ? state.allCellsMetadataMap
cells: cellsCrossfilter, : _.keyBy(viewState.cellsMetadata, "CellName"),
dimensionMap: cellsDimensionsMap
}, /* World */
...viewState,
graphBrushSelection: null /* if we are getting new cells from the server, the layout (probably? definitely?) just changed, so this is now irrelevant, and we WILL need to call a function to reset state of this kind when cells success happens */ graphBrushSelection: null /* if we are getting new cells from the server, the layout (probably? definitely?) just changed, so this is now irrelevant, and we WILL need to call a function to reset state of this kind when cells success happens */
}); });
} }
/* * * * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * * * *
User events User events
* * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * */
case "reset graph": {
/* Reset viewable world to the entire Universe */
const viewState = createViewState(state.schema, state.allCells.data);
return Object.assign({}, state, {
...viewState
});
}
case "parallel coordinates axes have been drawn": { case "parallel coordinates axes have been drawn": {
return Object.assign({}, state, { return Object.assign({}, state, {
axesHaveBeenDrawn: true axesHaveBeenDrawn: true
@@ -338,23 +363,23 @@ const Controls = (
case "color by continuous metadata": case "color by continuous metadata":
return Object.assign({}, state, { return Object.assign({}, state, {
colorAccessor: action.colorAccessor, colorAccessor: action.colorAccessor,
allCellsMetadata: cellsMetadata:
action.allCellsMetadataWithUpdatedColors /* this comes from middleware */, action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale colorScale: action.colorScale
}); });
case "color by expression": case "color by expression":
return Object.assign({}, state, { return Object.assign({}, state, {
colorAccessor: action.gene, colorAccessor: action.gene,
allCellsMetadata: cellsMetadata:
action.allCellsMetadataWithUpdatedColors /* this comes from middleware */, action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale colorScale: action.colorScale
}); });
case "color by categorical metadata": case "color by categorical metadata":
return Object.assign({}, state, { return Object.assign({}, state, {
colorAccessor: colorAccessor:
action.colorAccessor /* pass the scale through additionally, and it's a legend! */, action.colorAccessor /* pass the scale through additionally, and it's a legend! */,
allCellsMetadata: cellsMetadata:
action.allCellsMetadataWithUpdatedColors /* this comes from middleware */, action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale colorScale: action.colorScale
}); });
case "store current cell selection as differential set 1": case "store current cell selection as differential set 1":