diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index 022a2860..749cc41b 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -26,7 +26,7 @@ import actions from "../../actions"; obsAnnotations: _.get(state.controls.world, "obsAnnotations", null) })) class HistogramBrush extends React.Component { - calcHistogramCache = memoize((obsAnnotations, field, ranges) => { + calcHistogramCache = memoize((obsAnnotations, field, rangeMin, rangeMax) => { const { world } = this.props; const histogramCache = {}; @@ -40,7 +40,7 @@ class HistogramBrush extends React.Component { histogramCache.x = d3 .scaleLinear() - .domain([ranges.min, ranges.max]) + .domain([rangeMin, rangeMax]) .range([0, this.width]); histogramCache.bins = d3 @@ -130,7 +130,8 @@ class HistogramBrush extends React.Component { const histogramCache = this.calcHistogramCache( obsAnnotations, field, - ranges + ranges.min, + ranges.max ); const { x, y, bins, numValues } = histogramCache; diff --git a/client/src/middleware/updateCellColors.js b/client/src/middleware/updateCellColors.js index 29056c27..376895b5 100644 --- a/client/src/middleware/updateCellColors.js +++ b/client/src/middleware/updateCellColors.js @@ -1,14 +1,7 @@ // jshint esversion: 6 import _ from "lodash"; import * as d3 from "d3"; -import { - interpolateViridis, - interpolateSpectral, - interpolateRainbow, - interpolateBlues, - interpolateCool -} from "d3-scale-chromatic"; -import * as globals from "../globals"; +import { interpolateRainbow, interpolateCool } from "d3-scale-chromatic"; import parseRGB from "../util/parseRGB"; /* @@ -54,62 +47,79 @@ const updateCellColorsMiddleware = store => next => action => { const { obsAnnotations } = s.controls.world; let colorScale; - const colorsByName = new Array(obsAnnotations.length); 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 world.colorName[index] or world.colorRGB[index] + 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, { + const { categories } = _.filter(s.controls.world.schema.annotations.obs, { name: action.colorAccessor - })[0].categories; + })[0]; colorScale = d3 .scaleSequential(interpolateRainbow) .domain([0, categories.length]); - for (let i = 0; i < obsAnnotations.length; i += 1) { + /* 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; + for (let i = 0, len = obsAnnotations.length; i < len; i += 1) { const obs = obsAnnotations[i]; - const c = colorScale(categories.indexOf(obs[action.colorAccessor])); - colorsByName[i] = c; - colorsByRGB[i] = parseRGB(c); + const cat = obs[key]; + colorsByRGB[i] = colors[cat]; } } if (action.type === "color by continuous metadata") { + const colorBins = 100; + const [min, max] = [0, action.rangeMaxForColorAccessor]; colorScale = d3 - .scaleLinear() - .domain([0, action.rangeMaxForColorAccessor]) - .range([1, 0]); + .scaleQuantile() + .domain([min, max]) + .range(_.range(colorBins - 1, -1, -1)); - for (let i = 0; i < obsAnnotations.length; i += 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; + for (let i = 0, len = obsAnnotations.length; i < len; i += 1) { const obs = obsAnnotations[i]; - const c = interpolateCool(colorScale(obs[action.colorAccessor])); - colorsByName[i] = c; - colorsByRGB[i] = parseRGB(c); + const c = colorScale(obs[key]); + colorsByRGB[i] = colors[c]; } } if (action.type === "color by expression") { const { gene, data } = action; const expression = data[gene]; // Float32Array + const colorBins = 100; + // XXX TODO - replace _.min/_.max with the much faster finiteExtent + const [min, max] = [_.min(expression), _.max(expression)]; colorScale = d3 - .scaleLinear() - .domain([_.min(expression), _.max(expression)]) - .range([ - 1, - 0 - ]); /* invert viridis... probably pass this scale through to others */ + .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)); + } for (let i = 0, len = expression.length; i < len; i += 1) { - const c = interpolateCool(colorScale(expression[i])); - colorsByName[i] = c; - colorsByRGB[i] = parseRGB(c); + const c = colorScale(expression[i]); + colorsByRGB[i] = colors[c]; } } @@ -117,7 +127,7 @@ const updateCellColorsMiddleware = store => next => action => { append the result of all the filters to the action the user just triggered */ const modifiedAction = Object.assign({}, action, { - colors: { name: colorsByName, rgb: colorsByRGB }, + colors: { rgb: colorsByRGB }, colorScale }); diff --git a/client/src/reducers/controls.js b/client/src/reducers/controls.js index b0c84bf2..1af0e903 100644 --- a/client/src/reducers/controls.js +++ b/client/src/reducers/controls.js @@ -112,7 +112,6 @@ const Controls = ( // all of the data + selection state world: null, - colorName: null, colorRGB: null, categoricalSelectionState: null, crossfilter: null, @@ -167,8 +166,9 @@ const Controls = ( /* first light - create world & other data-driven defaults */ const { universe } = action; const world = World.createWorldFromEntireUniverse(universe); - const colorName = new Array(universe.nObs).fill(globals.defaultCellColor); - const colorRGB = _.map(colorName, c => parseRGB(c)); + const colorRGB = new Array(universe.nObs).fill( + parseRGB(globals.defaultCellColor) + ); const categoricalSelectionState = createCategoricalSelectionState( state, world @@ -223,7 +223,6 @@ const Controls = ( error: null, universe, world, - colorName, colorRGB, categoricalSelectionState, crossfilter, @@ -240,8 +239,9 @@ const Controls = ( action.world, action.crossfilter ); - const colorName = new Array(world.nObs).fill(globals.defaultCellColor); - const colorRGB = _.map(colorName, c => parseRGB(c)); + const colorRGB = new Array(world.nObs).fill( + parseRGB(globals.defaultCellColor) + ); const categoricalSelectionState = createCategoricalSelectionState( state, world @@ -289,7 +289,6 @@ const Controls = ( loading: false, error: null, world, - colorName, colorRGB, categoricalSelectionState, crossfilter, @@ -442,11 +441,11 @@ const Controls = ( } case "reset colorscale": { const { world } = state; - const colorName = new Array(world.nObs).fill(globals.defaultCellColor); - const colorRGB = _.map(colorName, c => parseRGB(c)); + const colorRGB = new Array(world.nObs).fill( + parseRGB(globals.defaultCellColor) + ); return { ...state, - colorName, colorRGB, colorAccessor: null }; @@ -609,7 +608,6 @@ const Controls = ( case "color by continuous metadata": { return { ...state, - colorName: action.colors.name, colorRGB: action.colors.rgb, colorAccessor: action.colorAccessor, colorScale: action.colorScale @@ -618,7 +616,6 @@ const Controls = ( case "color by expression": { return { ...state, - colorName: action.colors.name, colorRGB: action.colors.rgb, colorAccessor: action.gene, colorScale: action.colorScale