mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 02:28:11 +08:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
Vendored
+46
-43
@@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user