categorical metadata performance work (#356)

* do not create unecessary option state for categories we will not display

* performance improvements for categorical metadata handling
This commit is contained in:
Bruce Martin
2018-10-22 09:56:41 -07:00
committed by GitHub
parent efb55a6332
commit 0c271bc815
10 changed files with 184 additions and 96 deletions
@@ -64,10 +64,15 @@ describe("createWorldFromEntireUniverse", () => {
summary: expect.objectContaining({ summary: expect.objectContaining({
obs: _(REST.schema.schema.annotations.obs) obs: _(REST.schema.schema.annotations.obs)
.filter(v => v.name !== "name")
.keyBy("name") .keyBy("name")
.mapValues(() => expect.any(Object)) .mapValues(() => expect.any(Object))
.value(), .value(),
var: {} // TODO: currently unimplemneted var: _(REST.schema.schema.annotations.var)
.filter(v => v.name !== "name")
.keyBy("name")
.mapValues(() => expect.any(Object))
.value()
}), }),
varDataCache: expect.any(Object), varDataCache: expect.any(Object),
+13 -2
View File
@@ -30,10 +30,21 @@ const doInitialDataLoad = () =>
.map(url => doJsonRequest(url)) .map(url => doJsonRequest(url))
.value(); .value();
const results = await Promise.all(requests); const results = await Promise.all(requests);
const universe = Universe.createUniverseFromRestV02Response(...results);
/* set config defaults */
const config = { ...globals.configDefaults, ...results[0].config };
const [, schema, obsAnno, varAnno, obsLayout] = [...results];
const universe = Universe.createUniverseFromRestV02Response(
config,
schema,
obsAnno,
varAnno,
obsLayout
);
dispatch({ dispatch({
type: "configuration load complete", type: "configuration load complete",
config: results[0].config config
}); });
dispatch({ dispatch({
type: "initial data load complete (universe exists)", type: "initial data load complete (universe exists)",
@@ -23,11 +23,16 @@ const truncateCategories = options => {
}; };
@connect(state => ({ @connect(state => ({
ranges: _.get(state.controls.world, "summary.obs", null) ranges: _.get(state.controls.world, "summary.obs", null),
categorySelectionLimit: _.get(
state.config,
"parameters.category-selection-limit",
globals.configDefaults.parameters["category-selection-limit"]
)
})) }))
class Categories extends React.Component { class Categories extends React.Component {
render() { render() {
const { ranges } = this.props; const { ranges, categorySelectionLimit } = this.props;
if (!ranges) return null; if (!ranges) return null;
return ( return (
@@ -42,7 +47,13 @@ class Categories extends React.Component {
<p> Categorical Metadata </p> <p> Categorical Metadata </p>
{_.map(ranges, (value, key) => { {_.map(ranges, (value, key) => {
const isColorField = key.includes("color") || key.includes("Color"); const isColorField = key.includes("color") || key.includes("Color");
if (value.options && !isColorField && key !== "name") { const isSelectableCategory =
value.options &&
!isColorField &&
key !== "name" &&
value.numOptions < categorySelectionLimit;
if (isSelectableCategory) {
const categoryOptions = truncateCategories(value.options); const categoryOptions = truncateCategories(value.options);
return ( return (
<Category <Category
+25 -9
View File
@@ -2,11 +2,25 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { FaChevronRight, FaChevronDown, FaPaintBrush } from "react-icons/fa"; import { FaChevronRight, FaChevronDown, FaPaintBrush } from "react-icons/fa";
import memoize from "memoize-one";
import * as globals from "../../globals"; import * as globals from "../../globals";
import Value from "./value"; import Value from "./value";
import alphabeticallySortedValues from "./util"; import alphabeticallySortedValues from "./util";
const countCategories = (values, optsAsBools) =>
_.reduce(
values,
(r, v, k) => {
r.total += 1;
if (optsAsBools[k]) {
r.on += 1;
}
return r;
},
{ total: 0, on: 0 }
);
@connect(state => ({ @connect(state => ({
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap
@@ -18,22 +32,24 @@ class Category extends React.Component {
isChecked: true, isChecked: true,
isExpanded: false isExpanded: false
}; };
this.countCategories = memoize((values, optsAsBools) =>
countCategories(values, optsAsBools)
);
} }
componentDidUpdate() { componentDidUpdate() {
const { categoricalAsBooleansMap, metadataField } = this.props; const { categoricalAsBooleansMap, metadataField, values } = this.props;
const categoryCount = this.countCategories(
const valuesAsBool = _.values(categoricalAsBooleansMap[metadataField]); values,
/* count categories toggled on by counting true values */ categoricalAsBooleansMap[metadataField]
const categoriesToggledOn = _.values(valuesAsBool).filter(v => v).length; );
if (categoryCount.on === categoryCount.total) {
if (categoriesToggledOn === valuesAsBool.length) {
/* everything is on, so not indeterminate */ /* everything is on, so not indeterminate */
this.checkbox.indeterminate = false; this.checkbox.indeterminate = false;
} else if (categoriesToggledOn === 0) { } else if (categoryCount.on === 0) {
/* nothing is on, so no */ /* nothing is on, so no */
this.checkbox.indeterminate = false; this.checkbox.indeterminate = false;
} else if (categoriesToggledOn < valuesAsBool.length) { } else if (categoryCount.on < categoryCount.total) {
/* to be explicit... */ /* to be explicit... */
this.checkbox.indeterminate = true; this.checkbox.indeterminate = true;
} }
+13
View File
@@ -28,8 +28,21 @@ export const continuous = [
"Unmapped_short" "Unmapped_short"
]; ];
/* if a categorical metadata field has more options than this, truncate */
export const maxCategoricalOptionsToDisplay = 100; export const maxCategoricalOptionsToDisplay = 100;
/*
these are default values for configuration the CLI may supply.
See the REST API and CLI specs for more info.
*/
export const configDefaults = {
features: {},
displayNames: {},
parameters: {
"category-selection-limit": 1000
}
};
/* colors */ /* colors */
export const blue = "#4a90e2"; export const blue = "#4a90e2";
export const hcaBlue = "#1c7cc7"; export const hcaBlue = "#1c7cc7";
+2 -1
View File
@@ -2,7 +2,8 @@
const Config = ( const Config = (
state = { state = {
displayNames: null, displayNames: null,
features: null features: null,
parameters: null
}, },
action action
) => { ) => {
+1 -1
View File
@@ -16,7 +16,7 @@ import {
function createCategoricalAsBooleansMap(world) { function createCategoricalAsBooleansMap(world) {
const res = {}; const res = {};
_.each(world.summary.obs, (value, key) => { _.each(world.summary.obs, (value, key) => {
if (value.options) { if (value.options && key !== "name") {
const optionsAsBooleans = {}; const optionsAsBooleans = {};
_.each(value.options, (_value, _key) => { _.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true; optionsAsBooleans[_key] = true;
@@ -0,0 +1,94 @@
import _ from "lodash";
/*
Build and return obs/var summary using any annotation in the schema
Summary information for each annotation, keyed by annotation name.
Value will be an object, containing summary information.
For continuous annotations (int, float, etc):
<annotation_name>: {
range {
min: <number>,
max: <number>
}
}
For categorical annotations (boolean, string, category):
<annotatoin_name>: {
options: {
<option1>: <number>,
...
},
numOptions: <number>
}
Summarize will be returned for BOTH obs and var annotations.
Example:
{
"Splice_sites_Annotated": {
"range": {
"min": 26,
"max": 1075869
}
},
"Selection": {
numOptions, 6,
"options": {
"Astrocytes(HEPACAM)": 714,
"Endothelial(BSC)": 123,
"Oligodendrocytes(GC)": 294,
"Neurons(Thy1)": 685,
"Microglia(CD45)": 1108,
"Unpanned": 665
}
}
}
NOTE: will not summarize the required 'name' annotation, as that is
specified as unique per element.
*/
function summarizeDimension(schema, annotations) {
return _(schema)
.filter(v => v.name !== "name")
.keyBy("name")
.mapValues(anno => {
const { name, type } = anno;
const continuous = type === "int32" || type === "float32";
if (!continuous) {
const options = _.countBy(annotations, name);
const numOptions = _.size(options);
return {
numOptions,
options
};
}
if (continuous) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
_.forEach(annotations, obs => {
const val = Number(obs[name]);
min = val < min ? val : min;
max = val > max ? val : max;
});
return { range: { min, max } };
}
throw new Error("incomprehensible schema");
})
.value();
}
export default function summarizeAnnotations(
schema,
obsAnnotations,
varAnnotations
) {
return {
obs: summarizeDimension(schema.annotations.obs, obsAnnotations),
var: summarizeDimension(schema.annotations.var, varAnnotations)
};
}
+8
View File
@@ -2,6 +2,7 @@
import _ from "lodash"; import _ from "lodash";
import * as kvCache from "./keyvalcache"; import * as kvCache from "./keyvalcache";
import summarizeAnnotations from "./summarizeAnnotations";
/* /*
Private helper function - create and return a template Universe Private helper function - create and return a template Universe
@@ -28,6 +29,7 @@ function templateUniverse() {
varAnnotations: [] /* all var annotations, by var index */, varAnnotations: [] /* all var annotations, by var index */,
obsNameToIndexMap: {} /* reverse map 'name' to index */, obsNameToIndexMap: {} /* reverse map 'name' to index */,
varNameToIndexMap: {} /* reverse map 'name' to index */, varNameToIndexMap: {} /* reverse map 'name' to index */,
summary: null /* derived data summaries XXX: consider exploding in place */,
obsLayout: { X: [], Y: [] } /* xy layout */, obsLayout: { X: [], Y: [] } /* xy layout */,
@@ -191,6 +193,12 @@ export function createUniverseFromRestV02Response(
/* layout */ /* layout */
universe.obsLayout = RESTv02LayoutResponseToInternal(layoutObsResponse); universe.obsLayout = RESTv02LayoutResponseToInternal(layoutObsResponse);
universe.summary = summarizeAnnotations(
universe.schema,
universe.obsAnnotations,
universe.varAnnotations
);
return finalize(universe); return finalize(universe);
} }
+8 -79
View File
@@ -2,6 +2,7 @@
import _ from "lodash"; import _ from "lodash";
import * as kvCache from "./keyvalcache"; import * as kvCache from "./keyvalcache";
import summarizeAnnotations from "./summarizeAnnotations";
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators"; import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
/* /*
@@ -51,83 +52,6 @@ obs/cell.
const VarDataCacheLowWatermark = 32; // cache element count const VarDataCacheLowWatermark = 32; // cache element count
const VarDataCacheTTLMs = 1000; // min cache time in MS const VarDataCacheTTLMs = 1000; // min cache time in MS
function summarizeAnnotations(schema, obsAnnotations) {
/*
Build and return obs/var summary using any annotation in the schema
Summary information for each annotation, keyed by annotation name.
Value will be an object, containing either 'range' or 'options' object,
depending on the annotation schema type (categorical or continuous).
Summarize for BOTH obs and var annotations. Result format:
{
obs: {
annotation_name: { ... },
...
},
var: {
annotation_name: { ... },
...
}
}
Example:
{
"Splice_sites_Annotated": {
"range": {
"min": 26,
"max": 1075869
}
},
"Selection": {
"options": {
"Astrocytes(HEPACAM)": 714,
"Endothelial(BSC)": 123,
"Oligodendrocytes(GC)": 294,
"Neurons(Thy1)": 685,
"Microglia(CD45)": 1108,
"Unpanned": 665
}
}
}
*/
const obsSummary = _(schema.annotations.obs)
.keyBy("name")
.mapValues(anno => {
const { name, type } = anno;
const continuous = type === "int32" || type === "float32";
if (!continuous) {
return {
options: _.countBy(obsAnnotations, name)
};
}
if (continuous) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
_.forEach(obsAnnotations, obs => {
const val = Number(obs[name]);
min = val < min ? val : min;
max = val > max ? val : max;
});
return { range: { min, max } };
}
throw new Error("incomprehensible schema");
})
.value();
// TODO XXX - not currently used, so skip it
const varSummary = {};
return {
obs: obsSummary,
var: varSummary
};
}
function templateWorld() { function templateWorld() {
return { return {
// map from universe obsIndex to world offset. // map from universe obsIndex to world offset.
@@ -186,7 +110,11 @@ export function createWorldFromEntireUniverse(universe) {
world.obsLayout = universe.obsLayout; world.obsLayout = universe.obsLayout;
/* derived data & summaries */ /* derived data & summaries */
world.summary = summarizeAnnotations(world.schema, world.obsAnnotations); world.summary = summarizeAnnotations(
world.schema,
world.obsAnnotations,
world.varAnnotations
);
/* build the varDataCache */ /* build the varDataCache */
world.varDataCache = kvCache.map( world.varDataCache = kvCache.map(
@@ -242,7 +170,8 @@ export function createWorldFromCurrentSelection(universe, world, crossfilter) {
/* derived data & summaries */ /* derived data & summaries */
newWorld.summary = summarizeAnnotations( newWorld.summary = summarizeAnnotations(
newWorld.schema, newWorld.schema,
newWorld.obsAnnotations newWorld.obsAnnotations,
newWorld.varAnnotations
); );
/* build the varDataCache */ /* build the varDataCache */