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,
differential: state.differential,
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
}))
class HistogramBrush extends React.Component {
+1 -1
View File
@@ -8,7 +8,7 @@ import * as globals from "../../globals";
@connect(state => ({
categoricalSelectionState: state.controls.categoricalSelectionState,
colorScale: state.controls.colorScale,
colorScale: state.controls.colors.scale,
colorAccessor: state.controls.colorAccessor,
schema: _.get(state.controls.world, "schema", null),
world: state.controls.world
@@ -11,7 +11,7 @@ import HistogramBrush from "../brushableHistogram";
@connect(state => ({
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null),
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
colorScale: state.controls.colors.scale,
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
schema: _.get(state.controls.world, "schema", null)
}))
@@ -99,7 +99,7 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
@connect(state => ({
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
colorScale: state.controls.colors.scale,
responsive: state.responsive
}))
class ContinuousLegend extends React.Component {
+2 -6
View File
@@ -30,7 +30,7 @@ import { World } from "../../util/stateManager";
universe: state.controls.universe,
crossfilter: state.controls.crossfilter,
responsive: state.responsive,
colorRGB: _.get(state.controls, "colorRGB", null),
colorRGB: _.get(state.controls, "colors.rgb", null),
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
resettingInterface: state.controls.resettingInterface,
@@ -159,11 +159,7 @@ class Graph extends React.Component {
}
// Colors for each point - a cached value that only changes when
// the cell metadata changes (done by updateCellColors middleware).
// 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.
// the cell metadata changes.
if (!renderCache.colors || colorRGB !== prevProps.colorRGB) {
const rgb = colorRGB;
if (!renderCache.colors) {
@@ -44,9 +44,9 @@ import finiteExtent from "../../util/finiteExtent";
return {
world,
colorRGB: state.controls.colorRGB,
colorRGB: _.get(state.controls, "colors.rgb", null),
colorScale: _.get(state.controls, "colors.scale", null),
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
// Accessors are var/gene names (strings)
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 { World, WorldUtil, ControlsHelper } from "../util/stateManager";
import parseRGB from "../util/parseRGB";
import {
World,
WorldUtil,
ControlsHelpers,
createColors
} from "../util/stateManager";
import Crossfilter from "../util/typedCrossfilter";
import * as globals from "../globals";
import {
@@ -29,7 +33,6 @@ const Controls = (
// all of the data + selection state
world: null,
colorRGB: null,
categoricalSelectionState: null,
crossfilter: null,
dimensionMap: null,
@@ -37,8 +40,11 @@ const Controls = (
userDefinedGenesLoading: false,
diffexpGenes: [],
// graph color-by
colorMode: null,
colorAccessor: null,
colorScale: null,
colors: {},
resettingInterface: false,
opacityForDeselectedCells: 0.2,
@@ -83,10 +89,9 @@ const Controls = (
/* first light - create world & other data-driven defaults */
const { universe } = action;
const world = World.createWorldFromEntireUniverse(universe);
const colorRGB = new Array(universe.nObs).fill(
parseRGB(globals.defaultCellColor)
);
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
const colorMode = null;
const colors = createColors(world, colorMode);
const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
state,
world
);
@@ -101,28 +106,23 @@ const Controls = (
universe,
fullUniverseCache: { world, crossfilter, dimensionMap },
world,
colorRGB,
categoricalSelectionState,
crossfilter,
dimensionMap,
colorMode,
colorAccessor: null,
colors,
resettingInterface: false
};
}
case "reset World to eq Universe": {
const {
userDefinedGenes,
diffexpGenes,
universe,
fullUniverseCache
} = state;
const { userDefinedGenes, diffexpGenes, fullUniverseCache } = state;
const { world, crossfilter } = fullUniverseCache;
// reset all crossfilter dimensions
_.forEach(fullUniverseCache.dimensionMap, dim => dim.filterAll());
const colorRGB = new Array(universe.nObs).fill(
parseRGB(globals.defaultCellColor)
);
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
const colorMode = null;
const colors = createColors(world, colorMode);
const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
state,
world
);
@@ -135,7 +135,7 @@ const Controls = (
});
const dimensionMap = {
...fullUniverseCache.dimensionMap,
...ControlsHelper.createGenesDimMap(
...ControlsHelpers.createGenesDimMap(
userDefinedGenes,
diffexpGenes,
world,
@@ -147,16 +147,22 @@ const Controls = (
return {
...state,
world,
colorRGB,
categoricalSelectionState,
crossfilter,
dimensionMap,
colorMode,
colorAccessor: null,
colors,
resettingInterface: false
};
}
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 */
const world = World.createWorldFromCurrentSelection(
@@ -164,17 +170,15 @@ const Controls = (
action.world,
action.crossfilter
);
const colorRGB = new Array(world.nObs).fill(
parseRGB(globals.defaultCellColor)
);
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
const colors = createColors(world, colorMode, colorAccessor);
const categoricalSelectionState = ControlsHelpers.createCategoricalSelectionState(
state,
world
);
const crossfilter = Crossfilter(world.obsAnnotations);
const dimensionMap = {
...World.createObsDimensionMap(crossfilter, world),
...ControlsHelper.createGenesDimMap(
...ControlsHelpers.createGenesDimMap(
userDefinedGenes,
diffexpGenes,
world,
@@ -188,11 +192,10 @@ const Controls = (
loading: false,
error: null,
world,
colorRGB,
colors,
categoricalSelectionState,
crossfilter,
dimensionMap,
colorAccessor: null
dimensionMap
};
}
case "expression load success": {
@@ -239,11 +242,11 @@ const Controls = (
Object.keys(action.expressionData)
)
);
universeVarData = ControlsHelper.pruneVarDataCache(
universeVarData = ControlsHelpers.pruneVarDataCache(
universeVarData,
allTheGenesWeNeed
);
worldVarData = ControlsHelper.pruneVarDataCache(
worldVarData = ControlsHelpers.pruneVarDataCache(
worldVarData,
allTheGenesWeNeed
);
@@ -372,13 +375,11 @@ const Controls = (
}
case "reset colorscale": {
const { world } = state;
const colorRGB = new Array(world.nObs).fill(
parseRGB(globals.defaultCellColor)
);
return {
...state,
colorRGB,
colorAccessor: null
colorMode: null,
colorAccessor: null,
colors: createColors(world)
};
}
case "expression load error":
@@ -476,7 +477,7 @@ const Controls = (
// update the filter to match all selected options
const cat = newCategoricalSelectionState[action.metadataField];
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
ControlsHelper.selectedValuesForCategory(cat)
ControlsHelpers.selectedValuesForCategory(cat)
);
return {
@@ -500,7 +501,7 @@ const Controls = (
// update the filter to match all selected options
const cat = newCategoricalSelectionState[action.metadataField];
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
ControlsHelper.selectedValuesForCategory(cat)
ControlsHelpers.selectedValuesForCategory(cat)
);
return {
@@ -552,19 +553,21 @@ const Controls = (
*******************************/
case "color by categorical metadata":
case "color by continuous metadata": {
const { world } = state;
return {
...state,
colorRGB: action.colors.rgb,
colorMode: action.type,
colorAccessor: action.colorAccessor,
colorScale: action.colorScale
colors: createColors(world, action.type, action.colorAccessor)
};
}
case "color by expression": {
const { world } = state;
return {
...state,
colorRGB: action.colors.rgb,
colorMode: action.type,
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 thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import updateCellColors from "../middleware/updateCellColors";
import config from "./config";
import differential from "./differential";
@@ -16,9 +15,6 @@ const Reducer = combineReducers({
differential
});
const store = createStore(
Reducer,
composeWithDevTools(applyMiddleware(thunk, updateCellColors))
);
const store = createStore(Reducer, composeWithDevTools(applyMiddleware(thunk)));
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.
*/
export { default as createColors } from "./colorHelpers";
export * as Universe from "./universe";
export * as World from "./world";
export * as WorldUtil from "./worldUtil";
export * as ControlsHelper from "./controlsHelpers";
export * as ControlsHelpers from "./controlsHelpers";