mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-04 15:18:11 +08:00
Clean up max-category front-end limit (#2347)
* remove topN category truncation from component rendering layer * clean up category item limit implementation * name change for clarity * fix snapshot * comments
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
|
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
|
||||||
|
|
||||||
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover2-target\\"><span data-testid=\\"louvain:category-label\\" tabindex=\\"-1\\" aria-label=\\"louvain\\" class=\\"\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
|
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover2-target\\"><span data-testid=\\"louvain:category-label\\" tabindex=\\"-1\\" aria-label=\\"louvain\\" class=\\"\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div>"`;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
test controls helpers
|
test controls helpers
|
||||||
*/
|
*/
|
||||||
// TODO #2227 test: improve test coverage on control helper functions
|
// TODO #2227 test: improve test coverage on control helper functions
|
||||||
// (`topNCategories()`, `isSelectableCategoryName()`, `selectableCategoryNames()`, `createCategorySummaryFromDfCol()`, `createCategoricalSelection()`, )
|
// (`isSelectableCategoryName()`, `selectableCategoryNames()`, `createCategorySummaryFromDfCol()`, `createCategoricalSelection()`, )
|
||||||
|
|
||||||
describe("controls helpers", () => {
|
describe("controls helpers", () => {
|
||||||
test("placeholder", () => {});
|
test("placeholder", () => {});
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ import * as viewActions from "./viewStack";
|
|||||||
import * as embActions from "./embedding";
|
import * as embActions from "./embedding";
|
||||||
import * as genesetActions from "./geneset";
|
import * as genesetActions from "./geneset";
|
||||||
|
|
||||||
|
function setGlobalConfig(config: any) {
|
||||||
|
/**
|
||||||
|
* Set any global run-time config not _exclusively_ managed by the config reducer.
|
||||||
|
* This should only set fields defined in globals.globalConfig.
|
||||||
|
*/
|
||||||
|
globals.globalConfig.maxCategoricalOptionsToDisplay =
|
||||||
|
config?.parameters?.["max-category-items"] ??
|
||||||
|
globals.globalConfig.maxCategoricalOptionsToDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
return promise fetching user-configured colors
|
return promise fetching user-configured colors
|
||||||
*/
|
*/
|
||||||
@@ -33,6 +43,9 @@ async function schemaFetch() {
|
|||||||
async function configFetch(dispatch: any) {
|
async function configFetch(dispatch: any) {
|
||||||
return fetchJson("config").then((response) => {
|
return fetchJson("config").then((response) => {
|
||||||
const config = { ...globals.configDefaults, ...response.config };
|
const config = { ...globals.configDefaults, ...response.config };
|
||||||
|
|
||||||
|
setGlobalConfig(config);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "configuration load complete",
|
type: "configuration load complete",
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { doBinaryRequest, doFetch } from "./fetchHelpers";
|
import { doBinaryRequest, doFetch } from "./fetchHelpers";
|
||||||
import { matrixFBSToDataframe } from "../util/stateManager/matrix";
|
import { matrixFBSToDataframe } from "../util/stateManager/matrix";
|
||||||
import { _getColumnSchema, _normalizeCategoricalSchema } from "./schema";
|
import { _getColumnSchema } from "./schema";
|
||||||
import {
|
import {
|
||||||
addObsAnnoColumn,
|
addObsAnnoColumn,
|
||||||
removeObsAnnoColumn,
|
removeObsAnnoColumn,
|
||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
_urlEncodeComplexQuery,
|
_urlEncodeComplexQuery,
|
||||||
_hashStringValues,
|
_hashStringValues,
|
||||||
} from "./query";
|
} from "./query";
|
||||||
|
import {
|
||||||
|
normalizeResponse,
|
||||||
|
normalizeWritableCategoricalSchema,
|
||||||
|
} from "./normalize";
|
||||||
|
|
||||||
const promiseThrottle = new PromiseLimit(5);
|
const promiseThrottle = new PromiseLimit(5);
|
||||||
|
|
||||||
@@ -141,7 +145,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
newAnnoMatrix._cache.obs = (this as any)._cache.obs.withCol(colName, data);
|
newAnnoMatrix._cache.obs = (this as any)._cache.obs.withCol(colName, data);
|
||||||
_normalizeCategoricalSchema(
|
normalizeWritableCategoricalSchema(
|
||||||
colSchema,
|
colSchema,
|
||||||
newAnnoMatrix._cache.obs.col(colName)
|
newAnnoMatrix._cache.obs.col(colName)
|
||||||
);
|
);
|
||||||
@@ -306,61 +310,12 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
result.colIndex.labels()
|
result.colIndex.labels()
|
||||||
);
|
);
|
||||||
|
|
||||||
result = _responseTypeNormalization(field, query, this.schema, result);
|
result = normalizeResponse(field, query, this.schema, result);
|
||||||
|
|
||||||
return [whereCacheUpdate, result];
|
return [whereCacheUpdate, result];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(7006)
|
|
||||||
function _responseTypeNormalization(field, query, schema, response) {
|
|
||||||
/*
|
|
||||||
Schema-driven type normalization - there are a number of assumptions the front-end
|
|
||||||
makes about data typing, eg, that the schema will contain all categories in a categorical
|
|
||||||
column, that booleans are a JS array of true/false, etc.
|
|
||||||
|
|
||||||
The OTA format does not follow precisely the same conventions. This routine implements
|
|
||||||
the front-end conventions given the schema and an OTA data column.
|
|
||||||
*/
|
|
||||||
if (field === "obs" || field === "var") {
|
|
||||||
response = _booleanCast(field, query, schema, response);
|
|
||||||
}
|
|
||||||
if (field === "obs") {
|
|
||||||
/*
|
|
||||||
Note: this must be performed after any possible changes to string, boolean or categorical
|
|
||||||
columns. This routine relies on having access to any casts or other data transformations
|
|
||||||
made in this routine, above, in order to correctly determine schema updates.
|
|
||||||
*/
|
|
||||||
_normalizeCategoricalSchema(
|
|
||||||
schema.annotations.obsByName[query],
|
|
||||||
response.col(query)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(7006)
|
|
||||||
function _booleanCast(field, query, schema, response) {
|
|
||||||
/*
|
|
||||||
Boolean columns may be transmitted as an int [0/1] or bool [true/false].
|
|
||||||
Force to JS Array of bool.
|
|
||||||
*/
|
|
||||||
if (field === "obs" || field === "var") {
|
|
||||||
// @ts-expect-error ts-migrate(7006)
|
|
||||||
response = response.mapColumns((colData, colIdx) => {
|
|
||||||
const colLabel = response.colIndex.getLabel(colIdx);
|
|
||||||
const colSchema = _getColumnSchema(schema, field, colLabel);
|
|
||||||
if (colSchema?.type === "boolean") {
|
|
||||||
const nColData = new Array(colData.length);
|
|
||||||
for (let i = 0; i < colData.length; i += 1) nColData[i] = !!colData[i];
|
|
||||||
return nColData;
|
|
||||||
}
|
|
||||||
return colData;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Utility functions below
|
Utility functions below
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { _getColumnSchema, _isIndex } from "./schema";
|
||||||
|
import catLabelSort from "../util/catLabelSort";
|
||||||
|
import {
|
||||||
|
unassignedCategoryLabel,
|
||||||
|
overflowCategoryLabel,
|
||||||
|
globalConfig,
|
||||||
|
} from "../globals";
|
||||||
|
import { Dataframe } from "../util/dataframe";
|
||||||
|
|
||||||
|
// @ts-expect-error ts-migrate(7006)
|
||||||
|
export function normalizeResponse(field, query, schema, response) {
|
||||||
|
/**
|
||||||
|
* There are a number of assumptions in the front-end about data typing and data
|
||||||
|
* characteristics. This routine will normalize a server response dataframe
|
||||||
|
* to match front-end expectations and UI conventions. This includes cast/transform
|
||||||
|
* of the data and schema updates.
|
||||||
|
*
|
||||||
|
* This consolidates all assumptions into one location, for ease of update.
|
||||||
|
*
|
||||||
|
* Currently, this includes normalization for obs/var columns only:
|
||||||
|
*
|
||||||
|
* - Dataframe columns in var/obs that are declared type: boolean may be sent by
|
||||||
|
* the server in a variety of formats (eg uint8, etc). Cast to JS Array[boolean]
|
||||||
|
*
|
||||||
|
* - "Categorical" columns may not have all categories represented in the server-provided
|
||||||
|
* schema (for valid reasons, eg, floating point rounding differences). For all
|
||||||
|
* types we treat as categorical in the UI (string, boolean, categorical), update
|
||||||
|
* the schema to contain all categories as a convenience.
|
||||||
|
*
|
||||||
|
* - "Categorical" columns (ie, string, boolean, categorical) may contain an excess
|
||||||
|
* of category values (aka labels). Consolidate any excess into an "all other"
|
||||||
|
* category.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// currently no data or schema normalization necessary for X or emb
|
||||||
|
if (field !== "obs" && field !== "var") return response;
|
||||||
|
|
||||||
|
const colLabels = response.colIndex.labels();
|
||||||
|
for (const colLabel of colLabels) {
|
||||||
|
const colSchema = _getColumnSchema(schema, field, colLabel);
|
||||||
|
const isIndex = _isIndex(schema, field, colLabel);
|
||||||
|
const { type, writable } = colSchema;
|
||||||
|
|
||||||
|
// Boolean data -- cast entire array to Array[bool]
|
||||||
|
if (type === "boolean") {
|
||||||
|
response = castColumnToBoolean(response, colLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types that are categorical in UI (string, boolean, categorical) OR are writable
|
||||||
|
// are introspected to ensure the schema `categories` field and data values match,
|
||||||
|
// and that we do not have an excess of category values (for non-writable columns)
|
||||||
|
const isEnumType =
|
||||||
|
type === "boolean" ||
|
||||||
|
type === "string" ||
|
||||||
|
type === "categorical" ||
|
||||||
|
writable;
|
||||||
|
if (!isIndex && isEnumType) {
|
||||||
|
response = normalizeCategorical(response, colLabel, colSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function castColumnToBoolean(df: Dataframe, label: any): Dataframe {
|
||||||
|
const colData = df.col(label).asArray();
|
||||||
|
const newColData = new Array(colData.length);
|
||||||
|
for (let i = 0; i < colData.length; i += 1) newColData[i] = !!colData[i];
|
||||||
|
df = df.replaceColData(label, newColData);
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeWritableCategoricalSchema(colSchema: any, col: any) {
|
||||||
|
/*
|
||||||
|
Ensure all enum writable / categorical schema have a categories array, that
|
||||||
|
the categories array contains all unique values in the data array, AND that
|
||||||
|
the array is UI sorted.
|
||||||
|
*/
|
||||||
|
const categorySet = new Set(
|
||||||
|
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
|
||||||
|
);
|
||||||
|
if (!categorySet.has(unassignedCategoryLabel)) {
|
||||||
|
categorySet.add(unassignedCategoryLabel);
|
||||||
|
}
|
||||||
|
colSchema.categories = catLabelSort(true, Array.from(categorySet));
|
||||||
|
return colSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCategorical(
|
||||||
|
df: Dataframe,
|
||||||
|
colLabel: any,
|
||||||
|
colSchema: any
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
If writable, ensure schema matches data and we have an unassigned label
|
||||||
|
|
||||||
|
If not writable, ensure schema matches data and that we consolidate labels in excess
|
||||||
|
of "top N" into an overflow labels.
|
||||||
|
*/
|
||||||
|
const { writable } = colSchema;
|
||||||
|
const col = df.col(colLabel);
|
||||||
|
|
||||||
|
if (writable) {
|
||||||
|
// writable (aka user) annotations
|
||||||
|
normalizeWritableCategoricalSchema(colSchema, col);
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else read-only, categorical columns
|
||||||
|
const TopN = globalConfig.maxCategoricalOptionsToDisplay;
|
||||||
|
|
||||||
|
// consolidate all categories from data and schema into a single list
|
||||||
|
const colDataSummary = col.summarizeCategorical();
|
||||||
|
const allCategories = new Set(
|
||||||
|
colDataSummary.categories.concat(colSchema.categories ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
|
// if no overflow, just UI sort schema categories and return
|
||||||
|
if (allCategories.size <= TopN) {
|
||||||
|
colSchema.categories = catLabelSort(writable, [...allCategories.keys()]);
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, pick top N categories by count and rewrite data
|
||||||
|
|
||||||
|
// choose unique overflow category label
|
||||||
|
let overflowCatName = `${colLabel}${overflowCategoryLabel}`;
|
||||||
|
while (allCategories.has(overflowCatName)) {
|
||||||
|
overflowCatName += "_";
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick top N category labels and add overflow label
|
||||||
|
const topNCategories = new Set(
|
||||||
|
[...colDataSummary.categoryCounts.keys()].slice(0, TopN)
|
||||||
|
);
|
||||||
|
topNCategories.add(overflowCatName);
|
||||||
|
|
||||||
|
// rewrite data - consolidate all excess labels into overflow label
|
||||||
|
const newColData = Array.from(col.asArray());
|
||||||
|
for (let i = 0; i < newColData.length; i += 1) {
|
||||||
|
if (!topNCategories.has(newColData[i])) {
|
||||||
|
newColData[i] = overflowCatName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace data in dataframe
|
||||||
|
df = df.replaceColData(colLabel, newColData);
|
||||||
|
|
||||||
|
// Update schema with categories, in UI sort order. Ensure overflow label is at end
|
||||||
|
// of list for display purposes.
|
||||||
|
const revisedCategories = df.col(colLabel).summarizeCategorical().categories;
|
||||||
|
revisedCategories.push(
|
||||||
|
revisedCategories.splice(revisedCategories.indexOf(overflowCatName), 1)[0]
|
||||||
|
);
|
||||||
|
colSchema.categories = catLabelSort(writable, revisedCategories);
|
||||||
|
|
||||||
|
return df;
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Private helper functions related to schema
|
Private helper functions related to schema
|
||||||
*/
|
*/
|
||||||
import catLabelSort from "../util/catLabelSort";
|
|
||||||
import { unassignedCategoryLabel } from "../globals";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function _getColumnSchema(schema: any, field: any, col: any) {
|
export function _getColumnSchema(schema: any, field: any, col: any) {
|
||||||
/* look up the column definition */
|
/* look up the column definition */
|
||||||
switch (field) {
|
switch (field) {
|
||||||
@@ -27,7 +23,11 @@ export function _getColumnSchema(schema: any, field: any, col: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export function _isIndex(schema: any, field: any, col: any): bool {
|
||||||
|
const index = schema.annotations?.[field].index;
|
||||||
|
return index && index === col;
|
||||||
|
}
|
||||||
|
|
||||||
export function _getColumnDimensionNames(schema: any, field: any, col: any) {
|
export function _getColumnDimensionNames(schema: any, field: any, col: any) {
|
||||||
/*
|
/*
|
||||||
field/col may be an alias for multiple columns. Currently used to map ND
|
field/col may be an alias for multiple columns. Currently used to map ND
|
||||||
@@ -75,36 +75,3 @@ export function _isContinuousType(schema) {
|
|||||||
const { type } = schema;
|
const { type } = schema;
|
||||||
return !(type === "string" || type === "boolean" || type === "categorical");
|
return !(type === "string" || type === "boolean" || type === "categorical");
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'colSchema' implicitly has an 'any' type... Remove this comment to see the full error message
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function _normalizeCategoricalSchema(colSchema, col) {
|
|
||||||
/*
|
|
||||||
Ensure all enum schema types have a categories array, that
|
|
||||||
the categories array contains all unique values in the data
|
|
||||||
array, AND that the array is sorted.
|
|
||||||
|
|
||||||
Note that the back-end will not always set this hint, so we
|
|
||||||
must assume it may be incorrect and/or missing.
|
|
||||||
*/
|
|
||||||
const { type, writable } = colSchema;
|
|
||||||
if (
|
|
||||||
type === "string" ||
|
|
||||||
type === "boolean" ||
|
|
||||||
type === "categorical" ||
|
|
||||||
writable
|
|
||||||
) {
|
|
||||||
const categorySet = new Set(
|
|
||||||
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
|
|
||||||
);
|
|
||||||
if (writable && !categorySet.has(unassignedCategoryLabel)) {
|
|
||||||
categorySet.add(unassignedCategoryLabel);
|
|
||||||
}
|
|
||||||
colSchema.categories = Array.from(categorySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colSchema.categories) {
|
|
||||||
colSchema.categories = catLabelSort(writable, colSchema.categories);
|
|
||||||
}
|
|
||||||
return colSchema;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -310,7 +310,6 @@ class Category extends React.PureComponent {
|
|||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'handleCategoryToggleAllClick' does not e... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'handleCategoryToggleAllClick' does not e... Remove this comment to see the full error message
|
||||||
handleCategoryToggleAllClick,
|
handleCategoryToggleAllClick,
|
||||||
} = asyncProps;
|
} = asyncProps;
|
||||||
const isTruncated = !!categorySummary?.isTruncated;
|
|
||||||
const selectionState = this.getSelectionState(categorySummary);
|
const selectionState = this.getSelectionState(categorySummary);
|
||||||
return (
|
return (
|
||||||
<CategoryRender
|
<CategoryRender
|
||||||
@@ -318,7 +317,6 @@ class Category extends React.PureComponent {
|
|||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
checkboxID={checkboxID}
|
checkboxID={checkboxID}
|
||||||
isUserAnno={isUserAnno}
|
isUserAnno={isUserAnno}
|
||||||
isTruncated={isTruncated}
|
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
isColorAccessor={isColorAccessor}
|
isColorAccessor={isColorAccessor}
|
||||||
selectionState={selectionState}
|
selectionState={selectionState}
|
||||||
@@ -422,8 +420,6 @@ const CategoryHeader = React.memo(
|
|||||||
checkboxID,
|
checkboxID,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserAnno' does not exist on type '{ ch... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserAnno' does not exist on type '{ ch... Remove this comment to see the full error message
|
||||||
isUserAnno,
|
isUserAnno,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isTruncated' does not exist on type '{ c... Remove this comment to see the full error message
|
|
||||||
isTruncated,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
||||||
isColorAccessor,
|
isColorAccessor,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isExpanded' does not exist on type '{ ch... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isExpanded' does not exist on type '{ ch... Remove this comment to see the full error message
|
||||||
@@ -525,11 +521,7 @@ const CategoryHeader = React.memo(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content="Use as color scale"
|
||||||
isTruncated
|
|
||||||
? `Coloring by ${metadataField} is disabled, as it exceeds the limit of ${globals.maxCategoricalOptionsToDisplay} labels`
|
|
||||||
: "Use as color scale"
|
|
||||||
}
|
|
||||||
position={Position.LEFT}
|
position={Position.LEFT}
|
||||||
usePortal
|
usePortal
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
@@ -544,7 +536,6 @@ const CategoryHeader = React.memo(
|
|||||||
onClick={onColorChangeClick}
|
onClick={onColorChangeClick}
|
||||||
active={isColorAccessor}
|
active={isColorAccessor}
|
||||||
intent={isColorAccessor ? "primary" : "none"}
|
intent={isColorAccessor ? "primary" : "none"}
|
||||||
disabled={isTruncated}
|
|
||||||
icon="tint"
|
icon="tint"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -562,8 +553,6 @@ const CategoryRender = React.memo(
|
|||||||
checkboxID,
|
checkboxID,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserAnno' does not exist on type '{ ch... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserAnno' does not exist on type '{ ch... Remove this comment to see the full error message
|
||||||
isUserAnno,
|
isUserAnno,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isTruncated' does not exist on type '{ c... Remove this comment to see the full error message
|
|
||||||
isTruncated,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
||||||
isColorAccessor,
|
isColorAccessor,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isExpanded' does not exist on type '{ ch... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isExpanded' does not exist on type '{ ch... Remove this comment to see the full error message
|
||||||
@@ -625,7 +614,6 @@ const CategoryRender = React.memo(
|
|||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
checkboxID={checkboxID}
|
checkboxID={checkboxID}
|
||||||
isUserAnno={isUserAnno}
|
isUserAnno={isUserAnno}
|
||||||
isTruncated={isTruncated}
|
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
isColorAccessor={isColorAccessor}
|
isColorAccessor={isColorAccessor}
|
||||||
selectionState={selectionState}
|
selectionState={selectionState}
|
||||||
@@ -652,11 +640,6 @@ const CategoryRender = React.memo(
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{isExpanded && isTruncated ? (
|
|
||||||
<p style={{ paddingLeft: 15 }}>... truncated list ...</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -2,14 +2,14 @@ import { Colors } from "@blueprintjs/core";
|
|||||||
import { dispatchNetworkErrorMessageToUser } from "./util/actionHelpers";
|
import { dispatchNetworkErrorMessageToUser } from "./util/actionHelpers";
|
||||||
import ENV_DEFAULT from "../../environment.default.json";
|
import ENV_DEFAULT from "../../environment.default.json";
|
||||||
|
|
||||||
/* if a categorical metadata field has more options than this, truncate */
|
/* overflow category values are created using this string */
|
||||||
export const maxCategoricalOptionsToDisplay = 200;
|
export const overflowCategoryLabel = ": all other labels";
|
||||||
|
|
||||||
/* default "unassigned" value for user-created categorical metadata */
|
/* default "unassigned" value for user-created categorical metadata */
|
||||||
export const unassignedCategoryLabel = "unassigned";
|
export const unassignedCategoryLabel = "unassigned";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
these are default values for configuration the CLI may supply.
|
these are default values for configuration the CLI may supply.
|
||||||
See the REST API and CLI specs for more info.
|
See the REST API and CLI specs for more info.
|
||||||
*/
|
*/
|
||||||
export const configDefaults = {
|
export const configDefaults = {
|
||||||
@@ -22,6 +22,18 @@ export const configDefaults = {
|
|||||||
links: {},
|
links: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Most configuration is stored in the reducer. A handful of values
|
||||||
|
are global and stored here. They are typically set by the config
|
||||||
|
action handler, which pull the information from the backend/CLI.
|
||||||
|
|
||||||
|
All should be set here to their default value.
|
||||||
|
*/
|
||||||
|
export const globalConfig = {
|
||||||
|
/* if a categorical metadata field has more options than this, truncate */
|
||||||
|
maxCategoricalOptionsToDisplay: 200,
|
||||||
|
};
|
||||||
|
|
||||||
/* colors */
|
/* colors */
|
||||||
export const blue = Colors.BLUE3;
|
export const blue = Colors.BLUE3;
|
||||||
export const linkBlue = Colors.BLUE5;
|
export const linkBlue = Colors.BLUE5;
|
||||||
|
|||||||
@@ -18,15 +18,11 @@ function caseInsensitiveCompare(a: any, b: any) {
|
|||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
const catLabelSort = (isUserAnno: boolean, values: any[]): any[] => {
|
||||||
const catLabelSort = (isUserAnno: any, values: any) => {
|
|
||||||
/* this sort could be memoized for perf */
|
/* this sort could be memoized for perf */
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const strings: string[] = [];
|
||||||
const strings: any = [];
|
const ints: number[] = [];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const ints: any = [];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const unassignedOrNaN: any = [];
|
const unassignedOrNaN: any = [];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
@@ -43,11 +39,10 @@ const catLabelSort = (isUserAnno: any, values: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
strings.sort(caseInsensitiveCompare);
|
strings.sort(caseInsensitiveCompare);
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'a' implicitly has an 'any' type.
|
|
||||||
ints.sort((a, b) => +a - +b);
|
ints.sort((a, b) => +a - +b);
|
||||||
unassignedOrNaN.sort(caseInsensitiveCompare);
|
unassignedOrNaN.sort(caseInsensitiveCompare);
|
||||||
|
|
||||||
return ints.concat(strings, unassignedOrNaN);
|
return (<any>ints).concat(strings, unassignedOrNaN);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default catLabelSort;
|
export default catLabelSort;
|
||||||
|
|||||||
@@ -76,10 +76,13 @@ export function summarizeCategorical(col: any) {
|
|||||||
categoryCounts.set(val, curCount + 1);
|
categoryCounts.set(val, curCount + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const sortedCategoryByCounts = new Map(
|
||||||
|
[...categoryCounts.entries()].sort((a, b) => b[1] - a[1])
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: [...categoryCounts.keys()],
|
categories: [...sortedCategoryByCounts.keys()],
|
||||||
categoryCounts,
|
categoryCounts: sortedCategoryByCounts,
|
||||||
numCategories: categoryCounts.size,
|
numCategories: sortedCategoryByCounts.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ Helper functions for the controls reducer
|
|||||||
|
|
||||||
import difference from "lodash.difference";
|
import difference from "lodash.difference";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import { rangeFill as fillRange } from "../range";
|
|
||||||
import fromEntries from "../fromEntries";
|
import fromEntries from "../fromEntries";
|
||||||
import { isCategoricalAnnotation } from "./annotationsHelpers";
|
import { isCategoricalAnnotation } from "./annotationsHelpers";
|
||||||
|
|
||||||
@@ -29,41 +27,9 @@ Remember that option values can be ANY js type, except undefined/null.
|
|||||||
|
|
||||||
// number of options
|
// number of options
|
||||||
numCategoryValues: number,
|
numCategoryValues: number,
|
||||||
|
|
||||||
// isTruncated - true if the options for selection has
|
|
||||||
// been truncated (ie, was too large to implement)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function topNCategories(colSchema: any, summary: any, N: any) {
|
|
||||||
/* return top N categories by occurrences in the data */
|
|
||||||
const { categories: allCategories } = colSchema;
|
|
||||||
const counts = allCategories.map(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(cat: any) => summary.categoryCounts.get(cat) ?? 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allCategories.length <= N) {
|
|
||||||
return [allCategories, allCategories, counts];
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortIndex = fillRange(new Array(allCategories.length)).sort(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(a: any, b: any) => counts[b] - counts[a]
|
|
||||||
);
|
|
||||||
const topNindices = new Set(sortIndex.slice(0, N));
|
|
||||||
|
|
||||||
const _topNCategories = [];
|
|
||||||
const topNCounts = [];
|
|
||||||
for (let i = 0; i < allCategories.length; i += 1) {
|
|
||||||
if (topNindices.has(i)) {
|
|
||||||
_topNCategories.push(allCategories[i]);
|
|
||||||
topNCounts.push(counts[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [allCategories, _topNCategories, topNCounts];
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||||
export function isSelectableCategoryName(schema: any, name: any) {
|
export function isSelectableCategoryName(schema: any, name: any) {
|
||||||
@@ -93,7 +59,6 @@ export function selectableCategoryNames(schema: any, names: any) {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||||
export function createCategorySummaryFromDfCol(dfCol: any, colSchema: any) {
|
export function createCategorySummaryFromDfCol(dfCol: any, colSchema: any) {
|
||||||
const N = globals.maxCategoricalOptionsToDisplay;
|
|
||||||
const { writable: isUserAnno } = colSchema;
|
const { writable: isUserAnno } = colSchema;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -102,24 +67,22 @@ export function createCategorySummaryFromDfCol(dfCol: any, colSchema: any) {
|
|||||||
if they are not actively used in the current annoMatrix view.
|
if they are not actively used in the current annoMatrix view.
|
||||||
*/
|
*/
|
||||||
const summary = dfCol.summarizeCategorical();
|
const summary = dfCol.summarizeCategorical();
|
||||||
const [
|
const { categories: allCategoryValues } = colSchema;
|
||||||
allCategoryValues,
|
const categoryValues = allCategoryValues;
|
||||||
categoryValues,
|
const categoryValueCounts = allCategoryValues.map(
|
||||||
categoryValueCounts,
|
(cat: any) => summary.categoryCounts.get(cat) ?? 0
|
||||||
] = topNCategories(colSchema, summary, N);
|
);
|
||||||
const categoryValueIndices = new Map(
|
const categoryValueIndices = new Map(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
categoryValues.map((v: any, i: any) => [v, i])
|
categoryValues.map((v: any, i: any) => [v, i])
|
||||||
);
|
);
|
||||||
const numCategoryValues = categoryValueIndices.size;
|
const numCategoryValues = categoryValueIndices.size;
|
||||||
const isTruncated = categoryValues.length < summary.numCategories;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allCategoryValues, // array: of natively typed category values (all of them)
|
allCategoryValues, // array: of natively typed category values (all of them)
|
||||||
categoryValues, // array: of natively typed category values (top N only)
|
categoryValues, // array: of natively typed category values (top N only)
|
||||||
categoryValueIndices, // map: category value (native type) -> category index (top N only)
|
categoryValueIndices, // map: category value (native type) -> category index (top N only)
|
||||||
numCategoryValues, // number: of values in the category (top N)
|
numCategoryValues, // number: of values in the category (top N)
|
||||||
isTruncated, // bool: true if list was truncated (ie, if topN != all)
|
|
||||||
categoryValueCounts, // array: cardinality of each category, (top N)
|
categoryValueCounts, // array: cardinality of each category, (top N)
|
||||||
isUserAnno, // bool
|
isUserAnno, // bool
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user