mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-01 22:48:12 +08:00
refactor categorical controls state (#1549)
* refactor categorical controls state * lint * fix race condition in tests * fix typo * add missing update on subset * remove obsolete code * update jest and puppeteer major version; update all minors * update when label changes * remove lint from tests; increase timeouts in e2e tests * changes in response to PR review * lint * more PR comment changes * more PR comment fixes * lint * more PR comment resolutions
This commit is contained in:
+22
-49
@@ -1,6 +1,10 @@
|
||||
import quantile from "./quantile";
|
||||
import { memoize } from "./dataframe/util";
|
||||
import { unassignedCategoryLabel } from "../globals";
|
||||
import {
|
||||
createCategorySummary,
|
||||
isSelectableCategoryName,
|
||||
} from "./stateManager/controlsHelpers";
|
||||
|
||||
/*
|
||||
Centroid coordinate calculation
|
||||
@@ -18,31 +22,24 @@ label -> {
|
||||
yCoordinates: Float32Array
|
||||
}
|
||||
*/
|
||||
const getCoordinatesByLabel = (
|
||||
obsAnnotations,
|
||||
obsLayout,
|
||||
categoryName,
|
||||
layoutDimNames,
|
||||
categoricalSelection,
|
||||
schemaObsByName
|
||||
) => {
|
||||
const getCoordinatesByLabel = (world, categoryName, layoutDimNames) => {
|
||||
const coordsByCategoryLabel = new Map();
|
||||
|
||||
const categoryArray = obsAnnotations.col(categoryName).asArray();
|
||||
|
||||
const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray();
|
||||
const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray();
|
||||
|
||||
const { categoryValueIndices, categoryValueCounts } =
|
||||
categoricalSelection?.[categoryName] || {};
|
||||
|
||||
// If the coloredBy is not a categorical col
|
||||
if (categoryValueIndices === undefined) {
|
||||
if (!isSelectableCategoryName(world.schema, categoryName)) {
|
||||
return coordsByCategoryLabel;
|
||||
}
|
||||
|
||||
// Check to see if the current category is a user created annotation
|
||||
const isUserAnno = schemaObsByName[categoryName].writable;
|
||||
const { obsAnnotations, obsLayout } = world;
|
||||
const categoryArray = obsAnnotations.col(categoryName).asArray();
|
||||
const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray();
|
||||
const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray();
|
||||
|
||||
const categorySummary = createCategorySummary(world, categoryName);
|
||||
const {
|
||||
isUserAnno,
|
||||
categoryValueIndices,
|
||||
categoryValueCounts,
|
||||
} = categorySummary;
|
||||
|
||||
// Iterate over all cells
|
||||
for (let i = 0, len = categoryArray.length; i < len; i += 1) {
|
||||
@@ -96,23 +93,9 @@ const getCoordinatesByLabel = (
|
||||
label -> [x-Coordinate, y-Coordinate]
|
||||
*/
|
||||
|
||||
const calcMedianCentroid = (
|
||||
obsAnnotations,
|
||||
obsLayout,
|
||||
categoryName,
|
||||
layoutDimNames,
|
||||
categoricalSelection,
|
||||
schemaObsByName
|
||||
) => {
|
||||
const calcMedianCentroid = (world, categoryName, layoutDimNames) => {
|
||||
// generate a map describing the coordinates for each label within the given category
|
||||
const dataMap = getCoordinatesByLabel(
|
||||
obsAnnotations,
|
||||
obsLayout,
|
||||
categoryName,
|
||||
layoutDimNames,
|
||||
categoricalSelection,
|
||||
schemaObsByName
|
||||
);
|
||||
const dataMap = getCoordinatesByLabel(world, categoryName, layoutDimNames);
|
||||
|
||||
// label => [medianXCoordinate, medianYCoordinate]
|
||||
const coordinates = new Map();
|
||||
@@ -137,19 +120,9 @@ const calcMedianCentroid = (
|
||||
};
|
||||
|
||||
// A simple function to hash the parameters
|
||||
const hashMedianCentroid = (
|
||||
obsAnnotations,
|
||||
obsLayout,
|
||||
categoryName,
|
||||
layoutDimNames,
|
||||
categorySelection,
|
||||
schemaObsByName
|
||||
) => {
|
||||
return `${obsAnnotations.__id}+${
|
||||
obsLayout.__id
|
||||
}:${categoryName}:${layoutDimNames}:${Object.keys(
|
||||
categorySelection
|
||||
)}:${Object.keys(schemaObsByName)}`;
|
||||
const hashMedianCentroid = (world, categoryName, layoutDimNames) => {
|
||||
const { obsAnnotations, obsLayout } = world;
|
||||
return `${obsAnnotations.__id}+${obsLayout.__id}:${categoryName}:${layoutDimNames}`;
|
||||
};
|
||||
// export the memoized calculation function
|
||||
export default memoize(calcMedianCentroid, hashMedianCentroid);
|
||||
|
||||
@@ -6,17 +6,12 @@ import _ from "lodash";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import { rangeFill as fillRange } from "../range";
|
||||
import fromEntries from "../fromEntries";
|
||||
import {
|
||||
userDefinedDimensionName,
|
||||
diffexpDimensionName,
|
||||
} from "../nameCreators";
|
||||
|
||||
export function maxCategoryItems(config) {
|
||||
return (
|
||||
config.parameters?.["max-category-items"] ??
|
||||
globals.configDefaults.parameters["max-category-items"]
|
||||
);
|
||||
}
|
||||
import { isCategoricalAnnotation } from "./annotationsHelpers";
|
||||
|
||||
/*
|
||||
Selection state for categoricals are tracked in an Object that
|
||||
@@ -69,7 +64,12 @@ function topNCategories(colSchema, summary, N) {
|
||||
return [_topNCategories, topNCounts];
|
||||
}
|
||||
|
||||
export function selectableCategoryNames(schema, maxCatItems, names) {
|
||||
export function isSelectableCategoryName(schema, name) {
|
||||
const { index } = schema.annotations.obs;
|
||||
return name && name !== index && isCategoricalAnnotation(schema, name);
|
||||
}
|
||||
|
||||
export function selectableCategoryNames(schema, names) {
|
||||
/*
|
||||
return all obs annotation names that are categorical AND have a
|
||||
"reasonably" small number of categories AND are not the index column.
|
||||
@@ -77,56 +77,44 @@ export function selectableCategoryNames(schema, maxCatItems, names) {
|
||||
If the initial name list not provided, use everything in the schema.
|
||||
*/
|
||||
if (!schema) return [];
|
||||
const { index, columns } = schema.annotations.obs;
|
||||
|
||||
return columns
|
||||
.filter((colSchema) => !names || names.indexOf(colSchema.name) !== -1)
|
||||
.filter((colSchema) => {
|
||||
const { type, name } = colSchema;
|
||||
const isSelectableType =
|
||||
type === "string" || type === "boolean" || type === "categorical";
|
||||
return isSelectableType && name !== index;
|
||||
})
|
||||
.map((v) => v.name);
|
||||
if (!names) names = schema.annotations.obs.columns.map((c) => c.name);
|
||||
return names.filter((name) => isSelectableCategoryName(schema, name));
|
||||
}
|
||||
|
||||
export function createCategoricalSelection(world, names) {
|
||||
export function createCategorySummary(world, name) {
|
||||
const N = globals.maxCategoricalOptionsToDisplay;
|
||||
const { obsAnnotations, schema } = world;
|
||||
|
||||
const res = names.reduce((acc, name) => {
|
||||
const colSchema = schema.annotations.obsByName[name];
|
||||
const { writable: isUserAnno } = colSchema;
|
||||
const colSchema = schema.annotations.obsByName[name];
|
||||
const { writable: isUserAnno } = colSchema;
|
||||
|
||||
/*
|
||||
Summarize the annotation data currently in world. Must return categoryValues
|
||||
in sorted order, and must include all category values even if they are not
|
||||
actively used in the current world.
|
||||
*/
|
||||
const summary = obsAnnotations.col(name).summarizeCategorical();
|
||||
const [categoryValues, categoryValueCounts] = topNCategories(
|
||||
colSchema,
|
||||
summary,
|
||||
N
|
||||
);
|
||||
const categoryValueIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
||||
const numCategoryValues = categoryValueIndices.size;
|
||||
const categoryValueSelected = new Array(numCategoryValues).fill(true);
|
||||
const isTruncated = categoryValues.length < summary.numCategories;
|
||||
/*
|
||||
Summarize the annotation data currently in world. Must return categoryValues
|
||||
in sorted order, and must include all category values even if they are not
|
||||
actively used in the current world.
|
||||
*/
|
||||
const summary = obsAnnotations.col(name).summarizeCategorical();
|
||||
const [categoryValues, categoryValueCounts] = topNCategories(
|
||||
colSchema,
|
||||
summary,
|
||||
N
|
||||
);
|
||||
const categoryValueIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
||||
const numCategoryValues = categoryValueIndices.size;
|
||||
const isTruncated = categoryValues.length < summary.numCategories;
|
||||
|
||||
acc[name] = {
|
||||
categoryValues, // array: of natively typed category values
|
||||
categoryValueIndices, // map: category value (native type) -> category index
|
||||
categoryValueSelected, // array: t/f selection state
|
||||
numCategoryValues, // number: of values in the category
|
||||
isTruncated, // bool: true if list was truncated
|
||||
categoryValueCounts, // array: cardinality of each category,
|
||||
categorySelected: true, // bool - default state for entire category
|
||||
isUserAnno, // bool
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
return res;
|
||||
return {
|
||||
categoryValues, // array: of natively typed category values
|
||||
categoryValueIndices, // map: category value (native type) -> category index
|
||||
numCategoryValues, // number: of values in the category
|
||||
isTruncated, // bool: true if list was truncated
|
||||
categoryValueCounts, // array: cardinality of each category,
|
||||
isUserAnno, // bool
|
||||
};
|
||||
}
|
||||
|
||||
export function createCategoricalSelection(names) {
|
||||
return fromEntries(names.map((name) => [name, new Map()]));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user