do not reset color-by when subsetting world (#636)

* do not reset colors when subsetting to world

* revert diffexp state change
This commit is contained in:
Bruce Martin
2019-03-12 10:48:35 -07:00
committed by GitHub
parent f96fd36ecb
commit caaee7e9bf
11 changed files with 178 additions and 211 deletions
@@ -21,7 +21,6 @@ import finiteExtent from "../../util/finiteExtent";
crossfilter: state.controls.crossfilter, crossfilter: state.controls.crossfilter,
differential: state.differential, differential: state.differential,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null) obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
})) }))
class HistogramBrush extends React.Component { class HistogramBrush extends React.Component {
+1 -1
View File
@@ -8,7 +8,7 @@ import * as globals from "../../globals";
@connect(state => ({ @connect(state => ({
categoricalSelectionState: state.controls.categoricalSelectionState, categoricalSelectionState: state.controls.categoricalSelectionState,
colorScale: state.controls.colorScale, colorScale: state.controls.colors.scale,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
schema: _.get(state.controls.world, "schema", null), schema: _.get(state.controls.world, "schema", null),
world: state.controls.world world: state.controls.world
@@ -11,7 +11,7 @@ import HistogramBrush from "../brushableHistogram";
@connect(state => ({ @connect(state => ({
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null), obsAnnotations: _.get(state.controls.world, "obsAnnotations", null),
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colors.scale,
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null), selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
schema: _.get(state.controls.world, "schema", null) schema: _.get(state.controls.world, "schema", null)
})) }))
@@ -99,7 +99,7 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
@connect(state => ({ @connect(state => ({
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colors.scale,
responsive: state.responsive responsive: state.responsive
})) }))
class ContinuousLegend extends React.Component { class ContinuousLegend extends React.Component {
+2 -6
View File
@@ -30,7 +30,7 @@ import { World } from "../../util/stateManager";
universe: state.controls.universe, universe: state.controls.universe,
crossfilter: state.controls.crossfilter, crossfilter: state.controls.crossfilter,
responsive: state.responsive, responsive: state.responsive,
colorRGB: _.get(state.controls, "colorRGB", null), colorRGB: _.get(state.controls, "colors.rgb", null),
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null), selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
resettingInterface: state.controls.resettingInterface, resettingInterface: state.controls.resettingInterface,
@@ -159,11 +159,7 @@ class Graph extends React.Component {
} }
// Colors for each point - a cached value that only changes when // Colors for each point - a cached value that only changes when
// the cell metadata changes (done by updateCellColors middleware). // the cell metadata changes.
// NOTE: this is a slightly pessimistic assumption, as the metadata
// could have changed for some other reason, but for now color is
// the only metadata that changes client-side. If this is problematic,
// we could add some sort of color-specific indicator to the app state.
if (!renderCache.colors || colorRGB !== prevProps.colorRGB) { if (!renderCache.colors || colorRGB !== prevProps.colorRGB) {
const rgb = colorRGB; const rgb = colorRGB;
if (!renderCache.colors) { if (!renderCache.colors) {
@@ -44,9 +44,9 @@ import finiteExtent from "../../util/finiteExtent";
return { return {
world, world,
colorRGB: state.controls.colorRGB, colorRGB: _.get(state.controls, "colors.rgb", null),
colorScale: _.get(state.controls, "colors.scale", null),
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
// Accessors are var/gene names (strings) // Accessors are var/gene names (strings)
scatterplotXXaccessor, scatterplotXXaccessor,
-150
View File
@@ -1,150 +0,0 @@
// jshint esversion: 6
import _ from "lodash";
import * as d3 from "d3";
import { interpolateRainbow, interpolateCool } from "d3-scale-chromatic";
import * as globals from "../globals";
import parseRGB from "../util/parseRGB";
import finiteExtent from "../util/finiteExtent";
/*
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
storeInstance =>
functionToCallWithAnActionThatWillSendItToTheNextMiddleware =>
actionThatDispatchWasCalledWith =>
valueToUseAsTheReturnValueOfTheDispatchCall
*/
/*
What this file does:
1. fire a filter action anywhere in the app
2. ** this middleware checks to see the state of all the currently selected filters,
including the new one
3. ** create updated selection from a copy of all the cells presently on the client
(this may be a subset of 'all')
4. ** append that new selection to the action so that it magically appears in the reducer
just because the action was fired
This is nice because we keep a lot of filtering business logic centralized
(what it means in practice to be selected)
*/
const updateCellColorsMiddleware = store => next => action => {
const s = store.getState();
/*
this is a hardcoded map of the things we need to keep an eye on and update
global cell selection in response to
*/
const filterJustChanged =
action.type === "color by expression" ||
action.type === "color by continuous metadata" ||
action.type === "color by categorical metadata";
const obsAnnotations = _.get(s.controls, "world.obsAnnotations", null);
if (!filterJustChanged || !obsAnnotations) {
return next(
action
); /* if the cells haven't loaded or the action wasn't a color change, bail */
}
let colorScale;
const colorsByRGB = new Array(obsAnnotations.length);
/*
in plain language...
(a) once the cells have loaded.
(b) each time a user changes a color control we need to update cellsMetadata colors
This is available to all the draw functions as controls.colorRGB[index]
*/
if (action.type === "color by categorical metadata") {
const { categories } = _.filter(s.controls.world.schema.annotations.obs, {
name: action.colorAccessor
})[0];
colorScale = d3
.scaleSequential(interpolateRainbow)
.domain([0, categories.length]);
/* pre-create colors - much faster than doing it for each obs */
const colors = _.transform(categories, (acc, cat, idx) => {
acc[cat] = parseRGB(colorScale(idx));
});
const key = action.colorAccessor;
const col = obsAnnotations.col(key).asArray();
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
const cat = col[i];
colorsByRGB[i] = colors[cat];
}
}
if (action.type === "color by continuous metadata") {
const colorBins = 100;
const { min, max } = action.rangeForColorAccessor;
colorScale = d3
.scaleQuantile()
.domain([min, max])
.range(_.range(colorBins - 1, -1, -1));
/* pre-create colors - much faster than doing it for each obs */
const colors = new Array(colorBins);
for (let i = 0; i < colorBins; i += 1) {
colors[i] = parseRGB(interpolateCool(i / colorBins));
}
const key = action.colorAccessor;
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
const col = obsAnnotations.col(key).asArray();
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
const val = col[i];
if (Number.isFinite(val)) {
const c = colorScale(val);
colorsByRGB[i] = colors[c];
} else {
colorsByRGB[i] = nonFiniteColor;
}
}
}
if (action.type === "color by expression") {
const { gene, data } = action;
const expression = data[gene]; // Float32Array
const colorBins = 100;
const [min, max] = finiteExtent(expression);
colorScale = d3
.scaleQuantile()
.domain([min, max])
.range(_.range(colorBins - 1, -1, -1));
/* pre-create colors - much faster than doing it for each obs */
const colors = new Array(colorBins);
for (let i = 0; i < colorBins; i += 1) {
colors[i] = parseRGB(interpolateCool(i / colorBins));
}
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
for (let i = 0, len = expression.length; i < len; i += 1) {
const e = expression[i];
if (Number.isFinite(e)) {
const c = colorScale(e);
colorsByRGB[i] = colors[c];
} else {
colorsByRGB[i] = nonFiniteColor;
}
}
}
/*
append the result of all the filters to the action the user just triggered
*/
const modifiedAction = Object.assign({}, action, {
colors: { rgb: colorsByRGB },
colorScale
});
return next(modifiedAction);
};
export default updateCellColorsMiddleware;
+46 -43
View File
@@ -2,8 +2,12 @@
import _ from "lodash"; import _ from "lodash";
import { World, WorldUtil, ControlsHelper } from "../util/stateManager"; import {
import parseRGB from "../util/parseRGB"; World,
WorldUtil,
ControlsHelpers,
createColors
} from "../util/stateManager";
import Crossfilter from "../util/typedCrossfilter"; import Crossfilter from "../util/typedCrossfilter";
import * as globals from "../globals"; import * as globals from "../globals";
import { import {
@@ -29,7 +33,6 @@ const Controls = (
// all of the data + selection state // all of the data + selection state
world: null, world: null,
colorRGB: null,
categoricalSelectionState: null, categoricalSelectionState: null,
crossfilter: null, crossfilter: null,
dimensionMap: null, dimensionMap: null,
@@ -37,8 +40,11 @@ const Controls = (
userDefinedGenesLoading: false, userDefinedGenesLoading: false,
diffexpGenes: [], diffexpGenes: [],
// graph color-by
colorMode: null,
colorAccessor: null, colorAccessor: null,
colorScale: null, colors: {},
resettingInterface: false, resettingInterface: false,
opacityForDeselectedCells: 0.2, opacityForDeselectedCells: 0.2,
@@ -83,10 +89,9 @@ const Controls = (
/* first light - create world & other data-driven defaults */ /* first light - create world & other data-driven defaults */
const { universe } = action; const { universe } = action;
const world = World.createWorldFromEntireUniverse(universe); const world = World.createWorldFromEntireUniverse(universe);
const colorRGB = new Array(universe.nObs).fill( const colorMode = null;
parseRGB(globals.defaultCellColor) const colors = createColors(world, colorMode);
); const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
state, state,
world world
); );
@@ -101,28 +106,23 @@ const Controls = (
universe, universe,
fullUniverseCache: { world, crossfilter, dimensionMap }, fullUniverseCache: { world, crossfilter, dimensionMap },
world, world,
colorRGB,
categoricalSelectionState, categoricalSelectionState,
crossfilter, crossfilter,
dimensionMap, dimensionMap,
colorMode,
colorAccessor: null, colorAccessor: null,
colors,
resettingInterface: false resettingInterface: false
}; };
} }
case "reset World to eq Universe": { case "reset World to eq Universe": {
const { const { userDefinedGenes, diffexpGenes, fullUniverseCache } = state;
userDefinedGenes,
diffexpGenes,
universe,
fullUniverseCache
} = state;
const { world, crossfilter } = fullUniverseCache; const { world, crossfilter } = fullUniverseCache;
// reset all crossfilter dimensions // reset all crossfilter dimensions
_.forEach(fullUniverseCache.dimensionMap, dim => dim.filterAll()); _.forEach(fullUniverseCache.dimensionMap, dim => dim.filterAll());
const colorRGB = new Array(universe.nObs).fill( const colorMode = null;
parseRGB(globals.defaultCellColor) const colors = createColors(world, colorMode);
); const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
state, state,
world world
); );
@@ -135,7 +135,7 @@ const Controls = (
}); });
const dimensionMap = { const dimensionMap = {
...fullUniverseCache.dimensionMap, ...fullUniverseCache.dimensionMap,
...ControlsHelper.createGenesDimMap( ...ControlsHelpers.createGenesDimMap(
userDefinedGenes, userDefinedGenes,
diffexpGenes, diffexpGenes,
world, world,
@@ -147,16 +147,22 @@ const Controls = (
return { return {
...state, ...state,
world, world,
colorRGB,
categoricalSelectionState, categoricalSelectionState,
crossfilter, crossfilter,
dimensionMap, dimensionMap,
colorMode,
colorAccessor: null, colorAccessor: null,
colors,
resettingInterface: false resettingInterface: false
}; };
} }
case "set World to current selection": { case "set World to current selection": {
const { userDefinedGenes, diffexpGenes } = state; const {
userDefinedGenes,
diffexpGenes,
colorMode,
colorAccessor
} = state;
/* Set viewable world to be the currently selected data */ /* Set viewable world to be the currently selected data */
const world = World.createWorldFromCurrentSelection( const world = World.createWorldFromCurrentSelection(
@@ -164,17 +170,15 @@ const Controls = (
action.world, action.world,
action.crossfilter action.crossfilter
); );
const colorRGB = new Array(world.nObs).fill( const colors = createColors(world, colorMode, colorAccessor);
parseRGB(globals.defaultCellColor) const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
);
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
state, state,
world world
); );
const crossfilter = Crossfilter(world.obsAnnotations); const crossfilter = Crossfilter(world.obsAnnotations);
const dimensionMap = { const dimensionMap = {
...World.createObsDimensionMap(crossfilter, world), ...World.createObsDimensionMap(crossfilter, world),
...ControlsHelper.createGenesDimMap( ...ControlsHelpers.createGenesDimMap(
userDefinedGenes, userDefinedGenes,
diffexpGenes, diffexpGenes,
world, world,
@@ -188,11 +192,10 @@ const Controls = (
loading: false, loading: false,
error: null, error: null,
world, world,
colorRGB, colors,
categoricalSelectionState, categoricalSelectionState,
crossfilter, crossfilter,
dimensionMap, dimensionMap
colorAccessor: null
}; };
} }
case "expression load success": { case "expression load success": {
@@ -239,11 +242,11 @@ const Controls = (
Object.keys(action.expressionData) Object.keys(action.expressionData)
) )
); );
universeVarData = ControlsHelper.pruneVarDataCache( universeVarData = ControlsHelpers.pruneVarDataCache(
universeVarData, universeVarData,
allTheGenesWeNeed allTheGenesWeNeed
); );
worldVarData = ControlsHelper.pruneVarDataCache( worldVarData = ControlsHelpers.pruneVarDataCache(
worldVarData, worldVarData,
allTheGenesWeNeed allTheGenesWeNeed
); );
@@ -372,13 +375,11 @@ const Controls = (
} }
case "reset colorscale": { case "reset colorscale": {
const { world } = state; const { world } = state;
const colorRGB = new Array(world.nObs).fill(
parseRGB(globals.defaultCellColor)
);
return { return {
...state, ...state,
colorRGB, colorMode: null,
colorAccessor: null colorAccessor: null,
colors: createColors(world)
}; };
} }
case "expression load error": case "expression load error":
@@ -476,7 +477,7 @@ const Controls = (
// update the filter to match all selected options // update the filter to match all selected options
const cat = newCategoricalSelectionState[action.metadataField]; const cat = newCategoricalSelectionState[action.metadataField];
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum( state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
ControlsHelper.selectedValuesForCategory(cat) ControlsHelpers.selectedValuesForCategory(cat)
); );
return { return {
@@ -500,7 +501,7 @@ const Controls = (
// update the filter to match all selected options // update the filter to match all selected options
const cat = newCategoricalSelectionState[action.metadataField]; const cat = newCategoricalSelectionState[action.metadataField];
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum( state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
ControlsHelper.selectedValuesForCategory(cat) ControlsHelpers.selectedValuesForCategory(cat)
); );
return { return {
@@ -552,19 +553,21 @@ const Controls = (
*******************************/ *******************************/
case "color by categorical metadata": case "color by categorical metadata":
case "color by continuous metadata": { case "color by continuous metadata": {
const { world } = state;
return { return {
...state, ...state,
colorRGB: action.colors.rgb, colorMode: action.type,
colorAccessor: action.colorAccessor, colorAccessor: action.colorAccessor,
colorScale: action.colorScale colors: createColors(world, action.type, action.colorAccessor)
}; };
} }
case "color by expression": { case "color by expression": {
const { world } = state;
return { return {
...state, ...state,
colorRGB: action.colors.rgb, colorMode: action.type,
colorAccessor: action.gene, colorAccessor: action.gene,
colorScale: action.colorScale colors: createColors(world, action.type, action.gene)
}; };
} }
+1 -5
View File
@@ -2,7 +2,6 @@
import { combineReducers, createStore, applyMiddleware } from "redux"; import { combineReducers, createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension"; import { composeWithDevTools } from "redux-devtools-extension";
import updateCellColors from "../middleware/updateCellColors";
import config from "./config"; import config from "./config";
import differential from "./differential"; import differential from "./differential";
@@ -16,9 +15,6 @@ const Reducer = combineReducers({
differential differential
}); });
const store = createStore( const store = createStore(Reducer, composeWithDevTools(applyMiddleware(thunk)));
Reducer,
composeWithDevTools(applyMiddleware(thunk, updateCellColors))
);
export default store; export default store;
@@ -0,0 +1,122 @@
/*
Helper functions for the embedded graph colors
*/
import _ from "lodash";
import * as d3 from "d3";
import { interpolateRainbow, interpolateCool } from "d3-scale-chromatic";
import * as globals from "../../globals";
import parseRGB from "../parseRGB";
import finiteExtent from "../finiteExtent";
/*
create new colors state object. Paramters:
- world - current world object
- mode - color-by mode. One of: null, "color by expression",
"color by continuous metadata", "color by categorical metadata"
-
*/
function createColors(world, colorMode = null, colorAccessor = null) {
switch (colorMode) {
case "color by categorical metadata": {
return createColorsByCategoricalMetadata(world, colorAccessor);
}
case "color by continuous metadata": {
return createColorsByContinuousMetadata(world, colorAccessor);
}
case "color by expression": {
return createColorsByExpression(world, colorAccessor);
}
default: {
const defaultCellColor = parseRGB(globals.defaultCellColor);
return {
rgb: new Array(world.nObs).fill(defaultCellColor),
scale: undefined
};
}
}
}
function createColorsByCategoricalMetadata(world, accessor) {
const { categories } = _.filter(world.schema.annotations.obs, {
name: accessor
})[0];
const scale = d3
.scaleSequential(interpolateRainbow)
.domain([0, categories.length]);
/* pre-create colors - much faster than doing it for each obs */
const colors = categories.reduce((acc, cat, idx) => {
acc[cat] = parseRGB(scale(idx));
return acc;
}, {});
const rgb = new Array(world.nObs);
const data = world.obsAnnotations.col(accessor).asArray();
for (let i = 0, len = world.obsAnnotations.length; i < len; i += 1) {
const cat = data[i];
rgb[i] = colors[cat];
}
return { rgb, scale };
}
function createColorsByContinuousMetadata(world, accessor) {
const colorBins = 100;
const col = world.obsAnnotations.col(accessor);
const { min, max } = col.summarize();
const scale = d3
.scaleQuantile()
.domain([min, max])
.range(_.range(colorBins - 1, -1, -1));
/* pre-create colors - much faster than doing it for each obs */
const colors = new Array(colorBins);
for (let i = 0; i < colorBins; i += 1) {
colors[i] = parseRGB(interpolateCool(i / colorBins));
}
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
const rgb = new Array(world.nObs);
const data = col.asArray();
for (let i = 0, len = world.obsAnnotations.length; i < len; i += 1) {
const val = data[i];
if (Number.isFinite(val)) {
const c = scale(val);
rgb[i] = colors[c];
} else {
rgb[i] = nonFiniteColor;
}
}
return { rgb, scale };
}
function createColorsByExpression(world, accessor) {
const expression = world.varData.col(accessor).asArray();
const colorBins = 100;
const [min, max] = finiteExtent(expression);
const scale = d3
.scaleQuantile()
.domain([min, max])
.range(_.range(colorBins - 1, -1, -1));
/* pre-create colors - much faster than doing it for each obs */
const colors = new Array(colorBins);
for (let i = 0; i < colorBins; i += 1) {
colors[i] = parseRGB(interpolateCool(i / colorBins));
}
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
const rgb = new Array(world.nObs);
for (let i = 0, len = expression.length; i < len; i += 1) {
const e = expression[i];
if (Number.isFinite(e)) {
const c = scale(e);
rgb[i] = colors[c];
} else {
rgb[i] = nonFiniteColor;
}
}
return { rgb, scale };
}
export default createColors;
+2 -1
View File
@@ -14,7 +14,8 @@ This is all VERY tightly integrated with reducers and actions, and
exists to support those concepts. exists to support those concepts.
*/ */
export { default as createColors } from "./colorHelpers";
export * as Universe from "./universe"; export * as Universe from "./universe";
export * as World from "./world"; export * as World from "./world";
export * as WorldUtil from "./worldUtil"; export * as WorldUtil from "./worldUtil";
export * as ControlsHelper from "./controlsHelpers"; export * as ControlsHelpers from "./controlsHelpers";