Files
cellxgene/client/src/annoMatrix/normalize.js
T
eaae6df5e3 TS Revert (1) (#2402)
* revert all commits to before Typescript migration

* update compat workflow to match latest deps (#2335)

* update compat workflow to match latest deps

* attempt to debug

* attempt to debug

* remove debugging code

* typo

* update deps to match desktop (#2340)

* fix: don't run lint with `--fix` on push tests (#2273)

* fix: don't run lint with `--fix` on push tests

* npx

Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>

* rename X_approx_distribution to X_approximate_distribution (#2337)

* Correctly handle non-finite numbers in heuristic determination of X distribution (#2342)

* handle non-finites explicitly

* improve and test edge case handling for distribution estimation

* revert debugging changes

* code readability

* clean up type inferencing (#2332)

* unit tests for 64 bit conversion

* clean up type handling

* type inference tests

* more type inference fixes

* use schema to determine user intent for data typing

* stop using deprecated API

* fbs type encoding test

* add missing test

* add more tests

* correctly infer X type for CXG adaptor

* lint

* fix typo

* ts migration

* cleanup from PR review

* lint

* PR review changes

* remove unused packages from client (#2359)

* remove unused packages from client

* add missing peer dep

* fix: disable FE auth testing on compatibility tests (#2377)

* update: release process (#2277)

Co-authored-by: maniarathi <mani.arathi@gmail.com>

* fix: remove spaces in param setup (#2380)

* delete deploy workflow (#2396)

* undo reformatting which now does not pass lint

* fix snapshots which changed due to npm dep changes

* add missing quoting to snapshot

* another snapshot typo fix

* TS Revert (2) - replay PR #2347 and #2354 (#2403)

* replay edits from PR 2347

* TS Revert (3) - replay edits in PR #2327 (#2404)

* replay edits in PR 2327

* TS Revert (4) - replay PR #2355 (#2405)

* replay edits in PR 2355

* add additional babel config

* reformat with new prettier config

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2021-08-23 15:01:36 -07:00

151 lines
5.4 KiB
JavaScript

import { _getColumnSchema, _isIndex } from "./schema";
import catLabelSort from "../util/catLabelSort";
import {
unassignedCategoryLabel,
overflowCategoryLabel,
globalConfig,
} from "../globals";
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, label) {
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, col) {
/*
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, colLabel, colSchema) {
/*
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;
}