mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 18:48:11 +08:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ba3ff870 | ||
|
|
3fdf5cac9d | ||
|
|
3c3a794986 | ||
|
|
4b417cb5a5 | ||
|
|
925b785b1f | ||
|
|
660dff256c | ||
|
|
59c475b821 | ||
|
|
b553da0264 | ||
|
|
b67142e98f | ||
|
|
22a0921147 | ||
|
|
020e562f5c | ||
|
|
60d89b9478 | ||
|
|
c2b12abe2b | ||
|
|
2462d4afb1 | ||
|
|
02e79d502f | ||
|
|
9c2323b7bc | ||
|
|
e7200ce6c3 | ||
|
|
b7ffb2748d | ||
|
|
7ab8a8894d | ||
|
|
7b01e9e67b | ||
|
|
72ee670620 | ||
|
|
e21cac65bf | ||
|
|
bb5bbaac8a | ||
|
|
e29a6f72c2 | ||
|
|
ed97013277 | ||
|
|
2b29a152b9 | ||
|
|
f0e9b1ab91 | ||
|
|
c489221296 | ||
|
|
0a69af98c5 | ||
|
|
11570273e0 | ||
|
|
5f9d0a6b34 |
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
env:
|
||||
JEST_ENV: prod
|
||||
CXG_AUTH_TYPE: none
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
|
||||
@@ -108,7 +108,7 @@ server:
|
||||
# false or null: this returns a 404 code
|
||||
# true: loads a test index page, which links to the datasets that are available in the dataroot
|
||||
# string/URL: redirect to this URL: flask.redirect(config.multi_dataset__index)
|
||||
index: true
|
||||
index: false
|
||||
|
||||
# A list of allowed matrix types. If an empty list, then all matrix types are allowed
|
||||
allowed_matrix_types: []
|
||||
|
||||
@@ -38,7 +38,7 @@ class BaseConfigTest(ConfigTests):
|
||||
self.assertIsNotNone(mapping["dataset__presentation__max_categories"])
|
||||
self.assertIsNotNone(mapping["server__multi_dataset__allowed_matrix_types"])
|
||||
|
||||
def xtest_changes_from_default_returns_list_of_nondefault_config_values(self):
|
||||
def test_changes_from_default_returns_list_of_nondefault_config_values(self):
|
||||
config = self.get_config(verbose="true", lfc_cutoff=0.05)
|
||||
server_changes = config.server_config.changes_from_default()
|
||||
dataset_changes = config.default_dataset_config.changes_from_default()
|
||||
|
||||
+5
-3
@@ -1,11 +1,13 @@
|
||||
include ../common.mk
|
||||
|
||||
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../backend/test/fixtures/pbmc3k-annotations.csv)
|
||||
GENE_SETS := $(if $(GENE_SETS),$(GENE_SETS),../backend/test/fixtures/pbmc3k-genesets.csv)
|
||||
GENE_SETS := $(if $(GENE_SETS),$(GENE_SETS),../backend/test/fixtures/pbmc3k-genesets.csv)
|
||||
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
||||
GENE_SETS_FILENAME := $(shell basename $(GENE_SETS))
|
||||
|
||||
CXG_CONFIG := $(if $(CXG_CONFIG), $(CXG_CONFIG), ./__tests__/e2e/test_config.yaml)
|
||||
CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml)
|
||||
|
||||
CXG_AUTH_TYPE := $(if $(CXG_AUTH_TYPE),$(CXG_AUTH_TYPE),"test")
|
||||
|
||||
|
||||
# Packaging
|
||||
@@ -38,7 +40,7 @@ smoke-test:
|
||||
start_server_and_test \
|
||||
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
||||
$(CXG_SERVER_PORT) \
|
||||
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE="test" npm run e2e -- --verbose false'
|
||||
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE=$(CXG_AUTH_TYPE) npm run e2e -- --verbose false'
|
||||
|
||||
# start an instance of cellxgene and run the end-to-end annotations tests
|
||||
.PHONY: smoke-test-annotations
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Reducer } from "redux";
|
||||
import undoable from "../../src/reducers/undoable";
|
||||
|
||||
describe("create", () => {
|
||||
test("no keys", () => {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2-3 arguments, but got 1.
|
||||
expect(() => undoable(() => {})).toThrow();
|
||||
expect(() => undoable(() => {}, null)).toThrow();
|
||||
expect(() =>
|
||||
undoable(() => {}, undefined as unknown as string[])
|
||||
).toThrow();
|
||||
expect(() => undoable(() => {}, null as unknown as string[])).toThrow();
|
||||
expect(() => undoable(() => {}, [])).toThrow();
|
||||
expect(() => undoable(() => {}, [], {})).toThrow();
|
||||
});
|
||||
@@ -24,8 +26,7 @@ describe("create", () => {
|
||||
describe("undo", () => {
|
||||
test("expected state modifications", () => {
|
||||
const initialState = { a: 0, b: 1000 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
||||
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||
const undoableReducer = undoable(reducer, ["a"]);
|
||||
|
||||
const s1 = undoableReducer(initialState, { type: "test" });
|
||||
@@ -43,10 +44,8 @@ describe("undo", () => {
|
||||
|
||||
describe("redo", () => {
|
||||
const initialState = { a: 0, b: 1000 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
let UR: any;
|
||||
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||
let UR: Reducer;
|
||||
|
||||
beforeEach(() => {
|
||||
UR = undoable(reducer, ["a"]);
|
||||
|
||||
@@ -34,7 +34,7 @@ function makeMockColumn(s: any, length: any) {
|
||||
return new Array(length).fill(s.categories[0]);
|
||||
|
||||
default:
|
||||
throw new Error("unkonwn type");
|
||||
throw new Error("unknown type");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RawSchema } from "../../../../src/common/types/entities";
|
||||
import { RawSchema } from "../../../../src/common/types/schema";
|
||||
|
||||
export const schema: { schema: RawSchema } = {
|
||||
schema: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import zip from "lodash.zip";
|
||||
import _ from "lodash";
|
||||
import { flatbuffers } from "flatbuffers";
|
||||
import { NetEncoding } from "../../../src/util/stateManager/matrix_generated";
|
||||
import { RawSchema } from "../../../src/common/types/entities";
|
||||
import { RawSchema } from "../../../src/common/types/schema";
|
||||
|
||||
/*
|
||||
test data mocking REST 0.2 API responses. Used in several tests.
|
||||
|
||||
@@ -25,7 +25,7 @@ const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const devConfig = {
|
||||
mode: "development",
|
||||
devtool: "source-map",
|
||||
devtool: "eval",
|
||||
output: {
|
||||
pathinfo: true,
|
||||
filename: "static/js/bundle.js",
|
||||
|
||||
+64
-180
@@ -1,12 +1,5 @@
|
||||
import * as globals from "../globals";
|
||||
import { AnnoMatrixLoader, AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||
import { postExplainNewTab } from "../components/framework/toasters";
|
||||
import {
|
||||
KEYS,
|
||||
storageGet,
|
||||
storageSet,
|
||||
WORK_IN_PROGRESS_WARN_STATE,
|
||||
} from "../components/util/localStorage";
|
||||
import {
|
||||
catchErrorsWrap,
|
||||
doJsonRequest,
|
||||
@@ -17,11 +10,6 @@ import * as selnActions from "./selection";
|
||||
import * as annoActions from "./annotation";
|
||||
import * as viewActions from "./viewStack";
|
||||
import * as embActions from "./embedding";
|
||||
import {
|
||||
createDatasetUrl,
|
||||
createExplorerUrl,
|
||||
createAPIPrefix,
|
||||
} from "../util/stateManager/collectionsHelpers";
|
||||
import * as genesetActions from "./geneset";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -67,22 +55,6 @@ async function configFetch(dispatch: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
async function collectionFetchAndLoad(dispatch: any) {
|
||||
/*
|
||||
Fetch dataset meta for the current visualization then fetch the corresponding collection.
|
||||
*/
|
||||
const datasetMeta = await datasetMetaFetch();
|
||||
const { collection_id: collectionId, dataset_id: selectedDatasetId } =
|
||||
datasetMeta;
|
||||
const collection = await collectionFetch(collectionId);
|
||||
dispatch({
|
||||
type: "collection load complete",
|
||||
collection,
|
||||
selectedDatasetId,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
async function userInfoFetch(dispatch: any) {
|
||||
return fetchJson("userinfo").then((response) => {
|
||||
@@ -117,23 +89,6 @@ async function genesetsFetch(dispatch: any, config: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function datasetMetaFetch() {
|
||||
/*
|
||||
Fetch dataset meta for the current dataset.
|
||||
TODO(cc) revisit swap of explorer URL origin for environments without a corresponding Portal instance (eg local, canary)
|
||||
*/
|
||||
const explorerUrl = createExplorerUrl();
|
||||
const explorerUrlParam = encodeURIComponent(explorerUrl);
|
||||
return fetchPortalJson(`datasets/meta?url=${explorerUrlParam}`);
|
||||
}
|
||||
|
||||
async function collectionFetch(collectionId: string) {
|
||||
/*
|
||||
Fetch collection with the given ID.
|
||||
*/
|
||||
return fetchPortalJson(`collections/${collectionId}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function prefetchEmbeddings(annoMatrix: any) {
|
||||
/*
|
||||
@@ -162,7 +117,6 @@ const doInitialDataLoad = () =>
|
||||
schemaFetch(dispatch),
|
||||
userColorsFetchAndLoad(dispatch),
|
||||
userInfoFetch(dispatch),
|
||||
collectionFetchAndLoad(dispatch),
|
||||
]);
|
||||
|
||||
genesetsFetch(dispatch, config);
|
||||
@@ -234,141 +188,82 @@ const dispatchDiffExpErrors = (dispatch: any, response: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const requestDifferentialExpression =
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set1: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set2: any,
|
||||
num_genes = 50
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
try {
|
||||
/*
|
||||
const requestDifferentialExpression = (
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set1: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
set2: any,
|
||||
num_genes = 50
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
) => async (dispatch: any, getState: any) => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
try {
|
||||
/*
|
||||
Steps:
|
||||
1. get the most differentially expressed genes
|
||||
2. get expression data for each
|
||||
*/
|
||||
const { annoMatrix } = getState();
|
||||
const varIndexName = annoMatrix.schema.annotations.var.index;
|
||||
const { annoMatrix } = getState();
|
||||
const varIndexName = annoMatrix.schema.annotations.var.index;
|
||||
|
||||
// Legal values are null, Array or TypedArray. Null is initial state.
|
||||
if (!set1) set1 = [];
|
||||
if (!set2) set2 = [];
|
||||
// Legal values are null, Array or TypedArray. Null is initial state.
|
||||
if (!set1) set1 = [];
|
||||
if (!set2) set2 = [];
|
||||
|
||||
// These lines ensure that we convert any TypedArray to an Array.
|
||||
// This is necessary because JSON.stringify() does some very strange
|
||||
// things with TypedArrays (they are marshalled to JSON objects, rather
|
||||
// than being marshalled as a JSON array).
|
||||
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
|
||||
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
|
||||
// These lines ensure that we convert any TypedArray to an Array.
|
||||
// This is necessary because JSON.stringify() does some very strange
|
||||
// things with TypedArrays (they are marshalled to JSON objects, rather
|
||||
// than being marshalled as a JSON array).
|
||||
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
|
||||
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
|
||||
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
mode: "topN",
|
||||
count: num_genes,
|
||||
set1: { filter: { obs: { index: set1 } } },
|
||||
set2: { filter: { obs: { index: set2 } } },
|
||||
}),
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||
return dispatchDiffExpErrors(dispatch, res);
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
mode: "topN",
|
||||
count: num_genes,
|
||||
set1: { filter: { obs: { index: set1 } } },
|
||||
set2: { filter: { obs: { index: set2 } } },
|
||||
}),
|
||||
credentials: "include",
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||
const diffexpLists = { negative: [], positive: [] };
|
||||
for (const polarity of Object.keys(diffexpLists)) {
|
||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
diffexpLists[polarity] = response[polarity].map((v: any) => [
|
||||
varIndex.at(v[0], varIndexName),
|
||||
...v.slice(1),
|
||||
]);
|
||||
}
|
||||
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data: diffexpLists,
|
||||
});
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: "request differential expression error",
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const checkExplainNewTab = () => (dispatch: any) => {
|
||||
/*
|
||||
Opens toast "work in progress" warning.
|
||||
*/
|
||||
if (
|
||||
storageGet(KEYS.WORK_IN_PROGRESS_WARN) === WORK_IN_PROGRESS_WARN_STATE.ON
|
||||
) {
|
||||
dispatch({ type: "work in progress warning displayed" });
|
||||
postExplainNewTab(
|
||||
"To maintain your in-progress work on the previous dataset, we opened this dataset in a new tab."
|
||||
);
|
||||
storageSet(KEYS.WORK_IN_PROGRESS_WARN, WORK_IN_PROGRESS_WARN_STATE.OFF);
|
||||
|
||||
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
||||
return dispatchDiffExpErrors(dispatch, res);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||
const diffexpLists = { negative: [], positive: [] };
|
||||
for (const polarity of Object.keys(diffexpLists)) {
|
||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
diffexpLists[polarity] = response[polarity].map((v: any) => [
|
||||
varIndex.at(v[0], varIndexName),
|
||||
...v.slice(1),
|
||||
]);
|
||||
}
|
||||
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data: diffexpLists,
|
||||
});
|
||||
} catch (error) {
|
||||
return dispatch({
|
||||
type: "request differential expression error",
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const openDataset = (dataset: any) => (dispatch: any) => {
|
||||
/*
|
||||
Update in a new tab the browser location to dataset's deployment URL, kick off data load.
|
||||
*/
|
||||
|
||||
const deploymentUrl = dataset.dataset_deployments?.[0].url ?? "";
|
||||
const datasetUrl = createDatasetUrl(deploymentUrl);
|
||||
|
||||
dispatch({ type: "dataset opened" });
|
||||
storageSet(KEYS.WORK_IN_PROGRESS_WARN, WORK_IN_PROGRESS_WARN_STATE.ON);
|
||||
window.open(datasetUrl, "_blank");
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export const switchDataset = (dataset: any) => (dispatch: any) => {
|
||||
/*
|
||||
Update browser location to dataset's deployment URL, kick off data load.
|
||||
TODO(cc) revisit:
|
||||
- origin (and data root) switch for environments without corresponding Portal instance (eg local, canary)
|
||||
- globals update: move to server-side, split from initial doc returned from server?
|
||||
*/
|
||||
dispatch({ type: "dataset switch" });
|
||||
|
||||
const deploymentUrl = dataset.dataset_deployments?.[0].url ?? "";
|
||||
const datasetUrl = createDatasetUrl(deploymentUrl);
|
||||
dispatch(updateLocation(datasetUrl));
|
||||
|
||||
globals.API.prefix = createAPIPrefix(globals.API.prefix, datasetUrl);
|
||||
dispatch(doInitialDataLoad());
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
const updateLocation = (url: string) => (dispatch: any) => {
|
||||
/*
|
||||
Add entry to the session's history stack.
|
||||
*/
|
||||
dispatch({ type: "location update" });
|
||||
window.history.pushState(null, "", url);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function fetchJson(pathAndQuery: any) {
|
||||
return doJsonRequest(
|
||||
@@ -376,22 +271,11 @@ function fetchJson(pathAndQuery: any) {
|
||||
);
|
||||
}
|
||||
|
||||
function fetchPortalJson(url: string) {
|
||||
/*
|
||||
Fetch JSON from Portal API.
|
||||
TODO(cc) revisit - required for dataset meta and collection requests from Portal
|
||||
*/
|
||||
return doJsonRequest(`${globals.API.portalPrefix}${url}`);
|
||||
}
|
||||
|
||||
export default {
|
||||
doInitialDataLoad,
|
||||
requestDifferentialExpression,
|
||||
requestSingleGeneExpressionCountsForColoringPOST,
|
||||
requestUserDefinedGene,
|
||||
checkExplainNewTab,
|
||||
openDataset,
|
||||
switchDataset,
|
||||
selectContinuousMetadataAction: selnActions.selectContinuousMetadataAction,
|
||||
selectCategoricalMetadataAction: selnActions.selectCategoricalMetadataAction,
|
||||
selectCategoricalAllMetadataAction:
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
removeObsAnnoCategory,
|
||||
addObsLayout,
|
||||
} from "../util/stateManager/schemaHelpers";
|
||||
import { isArrayOrTypedArray } from "../util/typeHelpers";
|
||||
import { isAnyArray } from "../common/types/arraytypes";
|
||||
import { _whereCacheCreate } from "./whereCache";
|
||||
import AnnoMatrix from "./annoMatrix";
|
||||
import PromiseLimit from "../util/promiseLimit";
|
||||
@@ -134,7 +134,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
|
||||
const newAnnoMatrix = this._clone();
|
||||
let data;
|
||||
if (isArrayOrTypedArray(value)) {
|
||||
if (isAnyArray(value)) {
|
||||
if (value.constructor !== Ctor)
|
||||
throw new Error("Mismatched value array type");
|
||||
if (value.length !== this.nObs)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Utility type and interface definitions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TypedArrays that can be assigned to a number.
|
||||
*/
|
||||
export type TypedArray =
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| Float32Array
|
||||
| Float64Array;
|
||||
|
||||
export type UnsignedTypedArray = Uint8Array | Uint16Array | Uint32Array;
|
||||
export type FloatTypedArray = Float32Array | Float64Array;
|
||||
|
||||
export type TypedArrayConstructor =
|
||||
| Int8ArrayConstructor
|
||||
| Uint8ArrayConstructor
|
||||
| Int16ArrayConstructor
|
||||
| Uint16ArrayConstructor
|
||||
| Int32ArrayConstructor
|
||||
| Uint32ArrayConstructor
|
||||
| Float32ArrayConstructor
|
||||
| Float64ArrayConstructor;
|
||||
|
||||
export type AnyArray = Array<unknown> | TypedArray;
|
||||
|
||||
export type NumberArray = Array<number> | TypedArray;
|
||||
|
||||
export type Int8 = Int8Array[0];
|
||||
export type Uint8 = Uint8Array[0];
|
||||
export type Int16 = Int16Array[0];
|
||||
export type Uint16 = Uint16Array[0];
|
||||
export type Int32 = Int32Array[0];
|
||||
export type Uint32 = Uint32Array[0];
|
||||
export type Float32 = Float32Array[0];
|
||||
export type Float64 = Float64Array[0];
|
||||
|
||||
/**
|
||||
* Test if the parameter is a TypedArray.
|
||||
* @param tbd - value to be tested
|
||||
* @returns true if `tbd` is a TypedArray, false if not.
|
||||
*/
|
||||
export function isTypedArray(tbd: unknown): tbd is TypedArray {
|
||||
return (
|
||||
ArrayBuffer.isView(tbd) &&
|
||||
Object.prototype.toString.call(tbd) !== "[object DataView]"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the paramter is a float TypedArray
|
||||
* @param tbd - value to be tested
|
||||
* @returns - true if `tbd` is a float typed array.
|
||||
*/
|
||||
export function isFloatTypedArray(tbd: unknown): tbd is FloatTypedArray {
|
||||
return tbd instanceof Float32Array || tbd instanceof Float64Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the paramter is a float TypedArray
|
||||
* @param tbd - value to be tested
|
||||
* @returns - true if `tbd` is a float typed array.
|
||||
*/
|
||||
export function isUnsignedTypedArray(tbd: unknown): tbd is UnsignedTypedArray {
|
||||
return (
|
||||
tbd instanceof Uint8Array ||
|
||||
tbd instanceof Uint16Array ||
|
||||
tbd instanceof Uint32Array
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the parameter is a TypedArray or Array
|
||||
* @param tbd - value to be tested
|
||||
* @returns - true if `tbd` is a TypedArray or Array
|
||||
*/
|
||||
export function isAnyArray(tbd: unknown): tbd is AnyArray {
|
||||
return Array.isArray(tbd) || isTypedArray(tbd);
|
||||
}
|
||||
@@ -1,60 +1,3 @@
|
||||
// If a globally shared type or interface doesn't have a clear owner, put it here
|
||||
|
||||
export type Category = number | string | boolean;
|
||||
|
||||
export interface AnnotationColumn {
|
||||
categories?: Category[];
|
||||
name: string;
|
||||
type: "string" | "float32" | "int32" | "categorical" | "boolean";
|
||||
writable: boolean;
|
||||
}
|
||||
|
||||
interface DataFrame {
|
||||
nObs: number;
|
||||
nVar: number;
|
||||
// TODO(thuang): Not sure what other types are available
|
||||
type: "float32";
|
||||
}
|
||||
|
||||
export interface LayoutColumn {
|
||||
dims: string[];
|
||||
name: string;
|
||||
// TODO(thuang): Not sure what other types are available
|
||||
type: "float32";
|
||||
}
|
||||
interface RawLayout {
|
||||
obs: LayoutColumn[];
|
||||
var?: LayoutColumn[];
|
||||
}
|
||||
|
||||
interface RawAnnotations {
|
||||
obs: {
|
||||
columns: AnnotationColumn[];
|
||||
index: string;
|
||||
};
|
||||
var: {
|
||||
columns: AnnotationColumn[];
|
||||
index: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RawSchema {
|
||||
annotations: RawAnnotations;
|
||||
dataframe: DataFrame;
|
||||
layout: RawLayout;
|
||||
}
|
||||
|
||||
interface Annotations extends RawAnnotations {
|
||||
obsByName: { [name: string]: AnnotationColumn };
|
||||
varByName: { [name: string]: AnnotationColumn };
|
||||
}
|
||||
|
||||
interface Layout extends RawLayout {
|
||||
obsByName: { [name: string]: LayoutColumn };
|
||||
varByName: { [name: string]: LayoutColumn };
|
||||
}
|
||||
|
||||
export interface Schema extends RawSchema {
|
||||
annotations: Annotations;
|
||||
layout: Layout;
|
||||
}
|
||||
export {};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
type Category = number | string | boolean;
|
||||
|
||||
export interface AnnotationColumnSchema {
|
||||
categories?: Category[];
|
||||
name: string;
|
||||
type: "string" | "float32" | "int32" | "categorical" | "boolean";
|
||||
writable: boolean;
|
||||
}
|
||||
|
||||
interface XMatrixSchema {
|
||||
nObs: number;
|
||||
nVar: number;
|
||||
// TODO(thuang): Not sure what other types are available
|
||||
type: "float32";
|
||||
}
|
||||
|
||||
export interface EmbeddingSchema {
|
||||
dims: string[];
|
||||
name: string;
|
||||
// TODO(thuang): Not sure what other types are available
|
||||
type: "float32";
|
||||
}
|
||||
interface RawLayoutSchema {
|
||||
obs: EmbeddingSchema[];
|
||||
var?: EmbeddingSchema[];
|
||||
}
|
||||
|
||||
interface RawAnnotationsSchema {
|
||||
obs: {
|
||||
columns: AnnotationColumnSchema[];
|
||||
index: string;
|
||||
};
|
||||
var: {
|
||||
columns: AnnotationColumnSchema[];
|
||||
index: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RawSchema {
|
||||
annotations: RawAnnotationsSchema;
|
||||
dataframe: XMatrixSchema;
|
||||
layout: RawLayoutSchema;
|
||||
}
|
||||
|
||||
interface AnnotationsSchema extends RawAnnotationsSchema {
|
||||
obsByName: { [name: string]: AnnotationColumnSchema };
|
||||
varByName: { [name: string]: AnnotationColumnSchema };
|
||||
}
|
||||
|
||||
interface LayoutSchema extends RawLayoutSchema {
|
||||
obsByName: { [name: string]: EmbeddingSchema };
|
||||
varByName: { [name: string]: EmbeddingSchema };
|
||||
}
|
||||
|
||||
export interface Schema extends RawSchema {
|
||||
annotations: AnnotationsSchema;
|
||||
layout: LayoutSchema;
|
||||
}
|
||||
@@ -2,20 +2,19 @@ import React from "react";
|
||||
import Helmet from "react-helmet";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import DatasetSelector from "./datasetSelector/datasetSelector";
|
||||
import Container from "./framework/container";
|
||||
import Layout from "./framework/layout";
|
||||
import Skeleton from "./framework/skeleton";
|
||||
import LeftSideBar from "./leftSidebar";
|
||||
import RightSideBar from "./rightSidebar";
|
||||
import Legend from "./continuousLegend";
|
||||
import Graph from "./graph/graph";
|
||||
import Dotplot from "./dotplot";
|
||||
import MenuBar from "./menubar";
|
||||
import Autosave from "./autosave";
|
||||
import Embedding from "./embedding";
|
||||
import TermsOfServicePrompt from "./termsPrompt";
|
||||
|
||||
import actions, { checkExplainNewTab } from "../actions";
|
||||
import actions from "../actions";
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => ({
|
||||
@@ -25,6 +24,7 @@ import actions, { checkExplainNewTab } from "../actions";
|
||||
error: (state as any).controls.error,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
graphRenderCounter: (state as any).controls.graphRenderCounter,
|
||||
layoutChoice: (state as any).layoutChoice,
|
||||
}))
|
||||
class App extends React.Component {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -36,7 +36,6 @@ class App extends React.Component {
|
||||
this._onURLChanged();
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
||||
dispatch(actions.doInitialDataLoad(window.location.search));
|
||||
dispatch(checkExplainNewTab());
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -50,11 +49,22 @@ class App extends React.Component {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'loading' does not exist on type 'Readonl... Remove this comment to see the full error message
|
||||
const { loading, error, graphRenderCounter } = this.props;
|
||||
const { loading, error, graphRenderCounter, layoutChoice } = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
{loading ? <Skeleton /> : null}
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
fontWeight: 500,
|
||||
top: window.innerHeight / 2,
|
||||
left: window.innerWidth / 2 - 50,
|
||||
}}
|
||||
>
|
||||
loading cellxgene
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -73,27 +83,20 @@ class App extends React.Component {
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{(viewportRef: any) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
left: 8,
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 0,
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<DatasetSelector />
|
||||
<MenuBar />
|
||||
</div>
|
||||
<MenuBar />
|
||||
<Embedding />
|
||||
<Autosave />
|
||||
<TermsOfServicePrompt />
|
||||
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
|
||||
<Legend viewportRef={viewportRef} />
|
||||
{layoutChoice.dotplot && <Dotplot viewportRef={viewportRef} />}
|
||||
|
||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ key: any; viewportRef: any; }' is not assi... Remove this comment to see the full error message */}
|
||||
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
|
||||
<Graph
|
||||
key={graphRenderCounter}
|
||||
dotplotMode={layoutChoice.dotplot}
|
||||
viewportRef={viewportRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<RightSideBar />
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error --- TODO fix import
|
||||
import styles from "./datasetSelector.css";
|
||||
|
||||
// @ts-expect-error --- TODO add typing for datasets
|
||||
const buildDatasetMenuItems = (datasets) =>
|
||||
/*
|
||||
map dataset to menu item
|
||||
*/
|
||||
// @ts-expect-error --- TODO add typing for dataset
|
||||
datasets.map((dataset) => (
|
||||
<MenuItem key={dataset.id} onClick={dataset.onClick} text={dataset.name} />
|
||||
));
|
||||
|
||||
/*
|
||||
dataset menu, toggled from dataset name in app-level breadcrumbs
|
||||
*/
|
||||
// @ts-expect-error --- TODO add typing for props
|
||||
const DatasetMenu = React.memo(({ children, datasets }) => (
|
||||
<Popover
|
||||
boundary="viewport"
|
||||
content={
|
||||
<Menu
|
||||
style={{
|
||||
maxHeight:
|
||||
"290px" /* show 9.5 datasets at 30px height each, plus top padding of 5px */,
|
||||
maxWidth: "680px" /* TODO(cc) revisit max-width versus width */,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{buildDatasetMenuItems(datasets)}
|
||||
</Menu>
|
||||
}
|
||||
hasBackdrop
|
||||
minimal
|
||||
modifiers={{ offset: { offset: "0, 10" } }}
|
||||
popoverClassName={styles.datasetPopover}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
targetClassName={styles.datasetPopoverTarget}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
));
|
||||
export default DatasetMenu;
|
||||
@@ -1,17 +0,0 @@
|
||||
:local(.datasetBreadcrumb),
|
||||
:local(.datasetDisabledBreadcrumb) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:local(.datasetBreadcrumb):hover {
|
||||
color: #10161a; /* Colors.BLACK */ /* TODO(cc) revisit variable specification */
|
||||
}
|
||||
|
||||
:local(.datasetPopoverTarget) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:local(.datasetPopover) {
|
||||
box-shadow: 0 8px 24px 0 rgba(16, 22, 26, 0.2),
|
||||
0 2px 4px 0 rgba(16, 22, 26, 0.2), 0 0 0 0 rgba(16, 22, 26, 0.1);
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Breadcrumb, Icon } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import React, { PureComponent } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
/* app dependencies */
|
||||
import { openDataset, switchDataset } from "../../actions";
|
||||
import DatasetMenu from "./datasetMenu";
|
||||
import * as globals from "../../globals";
|
||||
import TruncatingBreadcrumbs from "./truncatingBreadcrumbs";
|
||||
import { sortDatasets } from "../../util/stateManager/collectionsHelpers";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error --- TODO revisit
|
||||
import styles from "./datasetSelector.css";
|
||||
|
||||
/*
|
||||
app-level collection and dataset breadcrumbs.
|
||||
*/
|
||||
// @ts-expect-error ts-migrate(1238) TODO revisit
|
||||
@connect((state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
const genesetsInProgress = (state as any).genesets?.genesets?.size > 0;
|
||||
const individualGenesInProgress =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
(state as any).controls?.userDefinedGenes?.length > 0;
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
collection: (state as any).collections?.collection,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit
|
||||
selectedDatasetId: (state as any).collections?.selectedDatasetId,
|
||||
workInProgress: genesetsInProgress || individualGenesInProgress,
|
||||
};
|
||||
})
|
||||
class DatasetSelector extends PureComponent {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
buildBreadcrumbProp = (breadcrumbProp) =>
|
||||
/*
|
||||
Return base breadcrumb object.
|
||||
*/
|
||||
({ ...breadcrumbProp, className: styles.datasetBreadcrumb });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
buildBreadcrumbProps = (
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
dispatch,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
collection,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
selectedDatasetId,
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
workInProgress
|
||||
) => {
|
||||
/*
|
||||
Create the set of breadcrumbs elements, home > collection name > dataset name, where dataset name reveals the
|
||||
dataset menu.
|
||||
*/
|
||||
const { origin } = globals.API;
|
||||
const homeProp = this.buildBreadcrumbProp({
|
||||
href: origin,
|
||||
shortText: "Home",
|
||||
text: "Home",
|
||||
});
|
||||
const collectionProp = this.buildBreadcrumbProp({
|
||||
href: `${origin}collections/${collection.id}`,
|
||||
shortText: "Collection",
|
||||
text: collection.name,
|
||||
});
|
||||
const selectedDataset = this.findDatasetById(
|
||||
selectedDatasetId,
|
||||
collection.datasets
|
||||
);
|
||||
const datasets = [...collection.datasets]
|
||||
.sort(sortDatasets)
|
||||
.map((dataset) => {
|
||||
const dispatchAction = workInProgress
|
||||
? openDataset(dataset)
|
||||
: switchDataset(dataset);
|
||||
return {
|
||||
...dataset,
|
||||
onClick: () => {
|
||||
dispatch(dispatchAction);
|
||||
},
|
||||
};
|
||||
});
|
||||
const datasetProp = this.buildBreadcrumbProp({
|
||||
shortText: "Dataset",
|
||||
text: selectedDataset.name,
|
||||
datasets,
|
||||
selectedDatasetId,
|
||||
});
|
||||
return [homeProp, collectionProp, datasetProp];
|
||||
};
|
||||
|
||||
/*
|
||||
Returns the dataset with the given ID.
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
findDatasetById = (selectedDatasetId, datasets) =>
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
datasets.find((dataset) => dataset.id === selectedDatasetId);
|
||||
|
||||
/*
|
||||
Returns the set of datasets excluding the given selected dataset.
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
listSiblingDatasets = (datasets, selectedDataset) =>
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
datasets.filter((dataset) => dataset !== selectedDataset);
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumb = (item, disabled, renderAsMenu?) => {
|
||||
/*
|
||||
Render BP Breadcrumb, adding menu-specific styles if necessary.
|
||||
TODO(cc) split and simplify breadcrumb versus menu breadcrumb functionality.
|
||||
*/
|
||||
const className = disabled
|
||||
? styles.datasetDisabledBreadcrumb /* no sibling datasets */
|
||||
: styles.datasetBreadcrumb;
|
||||
return (
|
||||
<Breadcrumb href={item.href} className={className}>
|
||||
{item.displayText}
|
||||
{this.renderBreadcrumbMenuIcon(renderAsMenu)}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumbMenu = (item, datasetsExceptSelected) =>
|
||||
/*
|
||||
Clicking on dataset name opens menu containing all dataset names except the current dataset name for the current
|
||||
collection.
|
||||
*/
|
||||
(
|
||||
// @ts-expect-error --- TODO revisit
|
||||
<DatasetMenu datasets={datasetsExceptSelected}>
|
||||
{this.renderBreadcrumb(item, false, true)}
|
||||
</DatasetMenu>
|
||||
)
|
||||
;
|
||||
|
||||
/*
|
||||
Render breadcrumb menu icon "chevron down".
|
||||
*/
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderBreadcrumbMenuIcon = (renderAsMenu) =>
|
||||
renderAsMenu ? (
|
||||
<Icon
|
||||
icon={IconNames.CHEVRON_DOWN}
|
||||
style={{ marginLeft: "5px", marginRight: 0 }}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
renderDatasetBreadcrumb = (item) => {
|
||||
/*
|
||||
Renders the final dataset breadcrumb where sibling datasets are selectable by a breadcrumb menu.
|
||||
*/
|
||||
const { datasets, selectedDatasetId } = item;
|
||||
const selectedDataset = this.findDatasetById(selectedDatasetId, datasets);
|
||||
const siblingDatasets = this.listSiblingDatasets(datasets, selectedDataset);
|
||||
const renderMenu = siblingDatasets.length > 0;
|
||||
return renderMenu
|
||||
? this.renderBreadcrumbMenu(item, siblingDatasets)
|
||||
: this.renderBreadcrumb(item, true);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO add return value
|
||||
render() {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
const { collection, dispatch, selectedDatasetId, workInProgress } =
|
||||
this.props;
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px", // Match margin on sibling menu buttons
|
||||
flexGrow: 1,
|
||||
overflow: "scroll",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<TruncatingBreadcrumbs
|
||||
// @ts-expect-error --- TODO revisit
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- TODO revisit
|
||||
breadcrumbRenderer={this.renderBreadcrumb}
|
||||
currentBreadcrumbRenderer={this.renderDatasetBreadcrumb}
|
||||
items={this.buildBreadcrumbProps(
|
||||
dispatch,
|
||||
collection,
|
||||
selectedDatasetId,
|
||||
workInProgress
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DatasetSelector;
|
||||
@@ -1,280 +0,0 @@
|
||||
// Core dependencies
|
||||
import { Classes, ResizeSensor } from "@blueprintjs/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// Characters to be used to indicate display text has been truncated
|
||||
const CHAR_ELLIPSIS = "...";
|
||||
|
||||
// Minimum number of characters to be displayed before transitioning to a smaller state of the breadcrumbs
|
||||
const MIN_VISIBLE_CHARS = 11;
|
||||
|
||||
// Approximate padding in pixels for each breadcrumb.
|
||||
// TODO(cc) revisit - remove if we calculate actual DOM sizes rather than estimate
|
||||
const ITEM_PADDING = 26;
|
||||
|
||||
// Approximate pixel to character ratio
|
||||
const PIXELS_PER_CHAR = 6;
|
||||
|
||||
/*
|
||||
Individual Breadcrumb States
|
||||
----------------------------
|
||||
F - full text
|
||||
T - truncated text
|
||||
S - indicates use of short text (eg "Collection" for collection name or "Dataset" for dataset name)
|
||||
H - hidden
|
||||
*/
|
||||
const STATE_FULL = "F"; // eg "Tabula Muris Senis"
|
||||
const STATE_TRUNCATED = "T"; // eg "Tabula...Senis"
|
||||
const STATE_SHORT_TEXT = "S"; // eg "Collection"
|
||||
const STATE_HIDDEN = "H"; // --
|
||||
|
||||
/*
|
||||
Breadcrumbs States
|
||||
------------------
|
||||
FFF
|
||||
FTF
|
||||
HSF
|
||||
HST
|
||||
HHS
|
||||
*/
|
||||
const STATES_FFF = `${STATE_FULL}${STATE_FULL}${STATE_FULL}`;
|
||||
const STATES_FTF = `${STATE_FULL}${STATE_TRUNCATED}${STATE_FULL}`;
|
||||
const STATES_HSF = `${STATE_HIDDEN}${STATE_SHORT_TEXT}${STATE_FULL}`;
|
||||
const STATES_HST = `${STATE_HIDDEN}${STATE_SHORT_TEXT}${STATE_TRUNCATED}`;
|
||||
const STATES_HHS = `${STATE_HIDDEN}${STATE_HIDDEN}${STATE_SHORT_TEXT}`;
|
||||
|
||||
/*
|
||||
Breadcrumb Transitions
|
||||
----------------------
|
||||
Breadcrumbs can transition bidirectionally through states in the following order, and can also repeat individual states.
|
||||
*/
|
||||
const STATES = [STATES_FFF, STATES_FTF, STATES_HSF, STATES_HST, STATES_HHS];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const calculateRequiredWidth = (itemsState: any, items: any) =>
|
||||
/*
|
||||
Return the total width required to display the given items with the given states.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.reduce((accum: number, item: any, i: number) => {
|
||||
// Grab the state for this item. For example, given the state HTF, the state of the first item is H, the state of
|
||||
// the second item is T and the state of the third item is F.
|
||||
const itemState = itemsState[i];
|
||||
// Add the width (of text) corresponding to the item's state.
|
||||
if (isItemShortText(itemState)) {
|
||||
accum += item.shortTextWidth;
|
||||
} else if (isItemTruncated(itemState)) {
|
||||
accum += item.minTextWidth;
|
||||
} else if (isItemFull(itemState)) {
|
||||
accum += item.textWidth;
|
||||
}
|
||||
return accum;
|
||||
}, 0);
|
||||
|
||||
const calculateAvailableTruncatedWidth = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items: any,
|
||||
truncatedIndex: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
itemsState: any,
|
||||
availableWidth: number
|
||||
) => {
|
||||
/*
|
||||
Return the width that the truncated item has available for display. That is, the available width minus the widths
|
||||
required by the other, non-truncated, items.
|
||||
*/
|
||||
// Grab the items other than the truncated item.
|
||||
const otherItems = [...items];
|
||||
otherItems.splice(truncatedIndex, 1);
|
||||
|
||||
// Grab the states of the the items, other than the truncated item.
|
||||
const otherItemsState = itemsState.split("");
|
||||
otherItemsState.splice(truncatedIndex, 1);
|
||||
|
||||
// Calculate the width of the other items in their corresponding states.
|
||||
const otherItemsRequiredWidth = calculateRequiredWidth(
|
||||
otherItemsState.join(""),
|
||||
otherItems
|
||||
);
|
||||
|
||||
return availableWidth - otherItemsRequiredWidth;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const getItemsStateForAvailableWidth = (items: any, availableWidth: number) => {
|
||||
/*
|
||||
Determine the current items state (eg FFF, FTF etc) for the given available width and set of items.
|
||||
*/
|
||||
for (let i = 0; i < STATES.length; i += 1) {
|
||||
const itemsState = STATES[i];
|
||||
const requiredWidth = calculateRequiredWidth(itemsState, items);
|
||||
if (availableWidth >= requiredWidth) {
|
||||
return itemsState;
|
||||
}
|
||||
}
|
||||
return STATES[STATES.length - 1]; // There's a problem, default to smallest state. TODO(cc) revisit error case here.
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const initItems = (items: any) =>
|
||||
/*
|
||||
Build initial state of items, including the calculation of short text, truncated text and full text dimensions. Use
|
||||
approximation of six pixels per char. TODO(cc) revisit use of actual widths if approximation is too loose.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.map((item: any) => ({
|
||||
...item,
|
||||
displayText: item.text, // Default display to full breadcrumb text
|
||||
minTextWidth: MIN_VISIBLE_CHARS * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
shortTextWidth: item.shortText.length * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
textWidth: item.text.length * PIXELS_PER_CHAR + ITEM_PADDING,
|
||||
}));
|
||||
|
||||
const isItemFull = (stateName: string) => stateName === STATE_FULL;
|
||||
|
||||
const isItemHidden = (stateName: string) => stateName === STATE_HIDDEN;
|
||||
|
||||
const isItemShortText = (stateName: string) => stateName === STATE_SHORT_TEXT;
|
||||
|
||||
const isItemTruncated = (stateName: string) => stateName === STATE_TRUNCATED;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const buildResizedItems = (items: any, availableWidth: number) => {
|
||||
/*
|
||||
Resize the items, either the set of visible items, or the individual item display text, to fit the given available
|
||||
width.
|
||||
*/
|
||||
const itemsState = getItemsStateForAvailableWidth(items, availableWidth);
|
||||
// TODO(cc) if same state as previous and state does not contain T (eg FFF or FSF or HHS) then don't recalc here
|
||||
return updateItems(itemsState, items, availableWidth);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const truncate = (availableWidth: number, text: string) => {
|
||||
/*
|
||||
Return truncated text with characters removed to reduce text width to the available width.
|
||||
*/
|
||||
const visibleLength = Math.floor(availableWidth / PIXELS_PER_CHAR);
|
||||
// Determine the break indices for the "before" and "after" ellipsis text tokens
|
||||
const tokenBeforeEndIndex = Math.ceil(visibleLength / 2);
|
||||
const tokenAfterStartIndex = Math.floor(visibleLength / 2);
|
||||
// Split text at break indices and join with ellipsis
|
||||
const tokenBefore = text.substr(0, tokenBeforeEndIndex).trim();
|
||||
const tokenAfter = text.substr(text.length - tokenAfterStartIndex).trim();
|
||||
return `${tokenBefore}${CHAR_ELLIPSIS}${tokenAfter}`;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const updateItems = (itemsState: any, items: any, availableWidth: number) =>
|
||||
/*
|
||||
Update each item to match its display format to the state being transitioned to.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
items.map((item: any, i: number) => {
|
||||
const itemState = itemsState[i];
|
||||
if (isItemHidden(itemState)) {
|
||||
return {
|
||||
...item,
|
||||
hidden: true,
|
||||
};
|
||||
}
|
||||
if (isItemShortText(itemState)) {
|
||||
return {
|
||||
...item,
|
||||
displayText: item.shortText,
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
if (isItemTruncated(itemState)) {
|
||||
const truncatedAvailableWidth = calculateAvailableTruncatedWidth(
|
||||
items,
|
||||
i,
|
||||
itemsState,
|
||||
availableWidth
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
displayText: truncate(truncatedAvailableWidth, item.text),
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
displayText: item.text,
|
||||
hidden: false,
|
||||
};
|
||||
});
|
||||
|
||||
const TruncatingBreadcrumbs = React.memo(
|
||||
// @ts-expect-error --- TODO revisit
|
||||
({ breadcrumbRenderer, currentBreadcrumbRenderer, items: originalItems }) => {
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const onResize = (entries: any) => {
|
||||
/*
|
||||
On resize callback from ResizeSensor, save the current width of the breadcrumbs.
|
||||
*/
|
||||
const availableWidth = Math.floor(entries[0].contentRect.width);
|
||||
setItems(buildResizedItems(items, availableWidth));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderBreadcrumb = (item: any, currentProp: any) => {
|
||||
/*
|
||||
Invoke the render callback to render the given breadcrumb.
|
||||
*/
|
||||
if (currentProp) {
|
||||
return currentBreadcrumbRenderer(item);
|
||||
}
|
||||
return breadcrumbRenderer(item);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderBreadcrumbs = (bcItems: any) =>
|
||||
/*
|
||||
Return list element/breadcrumb for each item.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
bcItems.map((item: any, i: number) => {
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
// TODO(cc) possibly "clean" each item back to the format expected by BP so we can spread the Breadcrumb-specific
|
||||
// props in our render method, and so that knowledge of "displayText" vs "text" for example, is not required by
|
||||
// parent components.
|
||||
// See datasetSelector.renderBreadcrumb for our usage, and also the following for
|
||||
// an example pattern:
|
||||
// https://github.com/palantir/blueprint/blob/826cbdf95b577c43d5fe95b99c67ee2761c853e0/packages/core/src/components/breadcrumbs/breadcrumbs.tsx#L151
|
||||
// Could possibly also have an explicit breadcrumbsProps props to neatly encapsulate and spread
|
||||
// breadcrumb-specific props, resulting in this component being a relatively transparent wrapper around
|
||||
// BP's Breadcrumbs component. For an example pattern, see `overflowListProps` on BP Breadcrumbs component.
|
||||
const currentItem = i === bcItems.length - 1;
|
||||
return <li key={item.text}>{renderBreadcrumb(item, currentItem)}</li>;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
init/update truncating breadcrumb items
|
||||
*/
|
||||
setItems(initItems(originalItems));
|
||||
}, [originalItems]);
|
||||
|
||||
return (
|
||||
<ResizeSensor onResize={onResize}>
|
||||
<ul
|
||||
className={Classes.BREADCRUMBS}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{renderBreadcrumbs(items)}
|
||||
</ul>
|
||||
</ResizeSensor>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default TruncatingBreadcrumbs;
|
||||
@@ -0,0 +1,237 @@
|
||||
import React from "react";
|
||||
import { connect, shallowEqual } from "react-redux";
|
||||
|
||||
import * as d3 from "d3";
|
||||
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import Async from "react-async";
|
||||
import ErrorLoading from "./err";
|
||||
import StillLoading from "./load";
|
||||
import Dot from "./dot";
|
||||
|
||||
import { createCategorySummaryFromDfCol } from "../../util/stateManager/controlsHelpers";
|
||||
|
||||
import { createColorQuery } from "../../util/stateManager/colorHelpers";
|
||||
|
||||
@connect((state) => ({
|
||||
annoMatrix: state.annoMatrix,
|
||||
colors: state.colors,
|
||||
genesets: state.genesets.genesets,
|
||||
pointDilation: state.pointDilation,
|
||||
differential: state.differential,
|
||||
dotplot: state.dotplot,
|
||||
}))
|
||||
class Column extends React.Component {
|
||||
static watchAsync(props, prevProps) {
|
||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||
}
|
||||
|
||||
createCategorySummaryFromDfCol = memoize(createCategorySummaryFromDfCol);
|
||||
|
||||
fetchAsyncProps = async (props) => {
|
||||
const {
|
||||
annoMatrix,
|
||||
colors,
|
||||
_geneSymbol,
|
||||
_geneIndex,
|
||||
metadataField,
|
||||
} = props.watchProps;
|
||||
|
||||
const [categoryData, categorySummary, colorData] = await this.fetchData(
|
||||
annoMatrix,
|
||||
metadataField,
|
||||
colors,
|
||||
_geneSymbol,
|
||||
_geneIndex
|
||||
);
|
||||
|
||||
return {
|
||||
categoryData,
|
||||
categorySummary,
|
||||
colorData,
|
||||
};
|
||||
};
|
||||
|
||||
async fetchData(annoMatrix, metadataField, colors, _geneSymbol) {
|
||||
/*
|
||||
fetch our data and the color-by data if appropriate, and then build a summary
|
||||
of our category and a color table for the color-by annotation.
|
||||
*/
|
||||
const { schema } = annoMatrix;
|
||||
const { colorMode } = colors;
|
||||
const { genesets, differential } = this.props;
|
||||
let colorDataPromise = Promise.resolve(null);
|
||||
|
||||
const query = createColorQuery(
|
||||
colorMode,
|
||||
_geneSymbol,
|
||||
schema,
|
||||
genesets,
|
||||
differential.diffExp
|
||||
);
|
||||
if (query) colorDataPromise = annoMatrix.fetch(...query);
|
||||
|
||||
const [categoryData, colorData] = await Promise.all([
|
||||
annoMatrix.fetch("obs", metadataField),
|
||||
colorDataPromise,
|
||||
]);
|
||||
|
||||
// our data
|
||||
const column = categoryData.icol(0);
|
||||
const colSchema = schema.annotations.obsByName[metadataField];
|
||||
|
||||
const categorySummary = this.createCategorySummaryFromDfCol(
|
||||
column,
|
||||
colSchema
|
||||
);
|
||||
|
||||
return [categoryData, categorySummary, colorData];
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
annoMatrix,
|
||||
pointDilation,
|
||||
colors,
|
||||
viewport,
|
||||
_geneSymbol,
|
||||
_geneIndex,
|
||||
rowColumnSize,
|
||||
metadataField,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<g key={_geneSymbol}>
|
||||
<Async
|
||||
watchFn={Column.watchAsync}
|
||||
promiseFn={this.fetchAsyncProps}
|
||||
watchProps={{
|
||||
annoMatrix,
|
||||
pointDilation,
|
||||
colors,
|
||||
viewport,
|
||||
_geneSymbol,
|
||||
_geneIndex,
|
||||
metadataField,
|
||||
}}
|
||||
>
|
||||
<Async.Pending initial>
|
||||
<StillLoading width={viewport.width} height={viewport.height} />
|
||||
</Async.Pending>
|
||||
<Async.Rejected>
|
||||
{(error) => (
|
||||
<ErrorLoading
|
||||
width={viewport.width}
|
||||
height={viewport.height}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
</Async.Rejected>
|
||||
<Async.Fulfilled persist>
|
||||
{(asyncProps) => {
|
||||
const { categoryData, categorySummary, colorData } = asyncProps;
|
||||
|
||||
if (!_geneSymbol || !colorData) return null;
|
||||
|
||||
/* TODO(colinmegill) #632 wire to dotplot */
|
||||
|
||||
const groupBy = categoryData.col(metadataField);
|
||||
const col = colorData.icol(0);
|
||||
const range = col.summarize();
|
||||
|
||||
const histogramMap = col.histogram(
|
||||
100,
|
||||
[range.min, range.max],
|
||||
groupBy
|
||||
);
|
||||
|
||||
const categories =
|
||||
annoMatrix?.schema?.annotations?.obsByName[metadataField]
|
||||
?.categories;
|
||||
const cellCategories = groupBy.asArray();
|
||||
const geneExpressions = col.asArray();
|
||||
let mean;
|
||||
const meanGeneExpressions = {};
|
||||
for (const c of categories) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < geneExpressions.length; i += 1) {
|
||||
if (cellCategories[i] === c) {
|
||||
arr.push(geneExpressions[i]);
|
||||
}
|
||||
}
|
||||
mean = arr.reduce((a, b) => a + b) / arr.length;
|
||||
meanGeneExpressions[c] = mean;
|
||||
}
|
||||
|
||||
const columnColorScale = d3
|
||||
.scaleLinear()
|
||||
.domain(d3.extent(Object.values(meanGeneExpressions)))
|
||||
.range([1, 0]);
|
||||
|
||||
return categorySummary.categoryValues.map(
|
||||
(val, _categoryValueIndex) => {
|
||||
return (
|
||||
<Dot
|
||||
key={val}
|
||||
categoryValue={val}
|
||||
_categoryValueIndex={_categoryValueIndex}
|
||||
histogramMap={histogramMap}
|
||||
_geneSymbol={_geneSymbol}
|
||||
_geneIndex={_geneIndex}
|
||||
colorData={colorData}
|
||||
rowColumnSize={rowColumnSize}
|
||||
columnColorScale={columnColorScale}
|
||||
meanGeneExpression={meanGeneExpressions[val]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}}
|
||||
</Async.Fulfilled>
|
||||
</Async>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Column;
|
||||
|
||||
// updateColorTable(colors, colorDf) {
|
||||
// const { annoMatrix } = this.props;
|
||||
// const { schema } = annoMatrix;
|
||||
|
||||
// /* update color table state */
|
||||
// if (!colors || !colorDf) {
|
||||
// return createColorTable(
|
||||
// null, // default mode
|
||||
// null,
|
||||
// null,
|
||||
// schema,
|
||||
// null
|
||||
// );
|
||||
// }
|
||||
|
||||
// const { colorAccessor, userColors, colorMode } = colors;
|
||||
// return createColorTable(
|
||||
// colorMode,
|
||||
// colorAccessor /* TODO(colinmegill) #632 dotplot wiring */,
|
||||
// colorDf,
|
||||
// schema,
|
||||
// userColors
|
||||
// );
|
||||
// }
|
||||
|
||||
// createColorByQuery(colors) {
|
||||
// const { annoMatrix, genesets, differential } = this.props;
|
||||
// const { schema } = annoMatrix;
|
||||
// const { colorMode, colorAccessor } = colors;
|
||||
|
||||
// return createColorQuery(
|
||||
// colorMode,
|
||||
// colorAccessor /* TODO(colinmegill) #632 dotplot wiring */,
|
||||
// schema,
|
||||
// genesets,
|
||||
// differential.diffExp
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { interpolateCool } from "d3-scale-chromatic";
|
||||
import * as d3 from "d3";
|
||||
|
||||
const Dot = (props) => {
|
||||
const {
|
||||
categoryValue,
|
||||
_categoryValueIndex,
|
||||
histogramMap,
|
||||
_geneSymbol,
|
||||
_geneIndex,
|
||||
rowColumnSize,
|
||||
columnColorScale,
|
||||
meanGeneExpression,
|
||||
} = props;
|
||||
|
||||
const bins = histogramMap.has(categoryValue)
|
||||
? histogramMap.get(categoryValue)
|
||||
: new Array(100).fill(0);
|
||||
|
||||
const totalCells = bins.reduce(
|
||||
(acc, current) => acc + current
|
||||
); /* SUM REMAINING ELEMENTS */
|
||||
/*
|
||||
TODO(colinmegill) #632 this is a heuristic —
|
||||
we need to figure out what non expressing means
|
||||
and use it, rather than shifting off the first bin
|
||||
for prototyping
|
||||
*/
|
||||
bins.shift(); /* MUTATES, REMOVES FIRST ELEMENT */
|
||||
|
||||
const expressing = bins.reduce(
|
||||
(acc, current) => acc + current
|
||||
); /* SUM REMAINING ELEMENTS */
|
||||
|
||||
/* TODO(colinmegill) #632 scale between correct dimensions */
|
||||
const paddingEquivalentToRowColumnIndexOffset = 8;
|
||||
/* domain is some fraction of the cells expressing, percent as decimal */
|
||||
const dotscale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, 1])
|
||||
.range([0, rowColumnSize - paddingEquivalentToRowColumnIndexOffset]);
|
||||
|
||||
const _radius = dotscale(expressing / totalCells);
|
||||
|
||||
return (
|
||||
<g
|
||||
id={`row_${categoryValue}_${_geneSymbol}`}
|
||||
key={`${_categoryValueIndex}_${categoryValue}`}
|
||||
transform={`translate(${_geneIndex * rowColumnSize}, ${
|
||||
_categoryValueIndex * rowColumnSize
|
||||
})`}
|
||||
>
|
||||
{_geneIndex === 0 && (
|
||||
<text
|
||||
textAnchor="end"
|
||||
style={{ fill: "black", font: "12px Roboto Condensed" }}
|
||||
>
|
||||
{categoryValue}
|
||||
</text>
|
||||
)}
|
||||
<circle
|
||||
r={_radius}
|
||||
cx="11"
|
||||
cy="-3.5"
|
||||
style={{
|
||||
fill: interpolateCool(columnColorScale(meanGeneExpression)),
|
||||
fillOpacity: 1,
|
||||
stroke: "none",
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dot;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
const ErrorLoading = ({ error, width, height }) => {
|
||||
console.log(error); // log to console as this is an unepected error
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
fontWeight: 500,
|
||||
top: height / 2,
|
||||
left: globals.leftSidebarWidth + width / 2 - 50,
|
||||
}}
|
||||
>
|
||||
<span>Failure loading dotplot</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorLoading;
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import Column from "./column";
|
||||
|
||||
@connect((state) => ({
|
||||
layoutChoice: state.layoutChoice,
|
||||
genesets: state.genesets.genesets,
|
||||
dotplot: state.dotplot,
|
||||
}))
|
||||
class Dotplot extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const viewport = this.getViewportDimensions();
|
||||
this.dotplotTopPadding = 120;
|
||||
this.dotplotLeftPadding = 170;
|
||||
this.rowColumnSize = 15;
|
||||
this.dotplotBrowserScalingFactor = 0.65;
|
||||
|
||||
this.state = {
|
||||
viewport,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
/* chrome only, which is support matrix as of 2021 */
|
||||
document.body.style.zoom = `${this.dotplotBrowserScalingFactor * 100}%`;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
document.body.style.zoom = "100%";
|
||||
}
|
||||
|
||||
getViewportDimensions = () => {
|
||||
const { viewportRef } = this.props;
|
||||
return {
|
||||
height: viewportRef.clientHeight,
|
||||
width: viewportRef.clientWidth,
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { viewport } = this.state;
|
||||
const { genesets, dotplot } = this.props;
|
||||
|
||||
let _geneset = null;
|
||||
let _genes = null;
|
||||
|
||||
if (dotplot.column) {
|
||||
_geneset = genesets.get(dotplot.column);
|
||||
_genes = Array.from(_geneset.genes.keys());
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id="dotplot-wrapper"
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: -9999,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
id="dotplot"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
width={viewport.width * (1 + this.dotplotBrowserScalingFactor)}
|
||||
height={viewport.height * (1 + this.dotplotBrowserScalingFactor)}
|
||||
>
|
||||
<g
|
||||
id="dotplot_help_text"
|
||||
transform={`translate(${this.dotplotLeftPadding},${this.dotplotTopPadding})`}
|
||||
>
|
||||
<text>
|
||||
{!dotplot.row && "Select a row"}{" "}
|
||||
{!dotplot.column && "Select a column"}
|
||||
</text>
|
||||
</g>
|
||||
{/* ASYNC HERE */}
|
||||
{dotplot.row && dotplot.column && (
|
||||
<g
|
||||
id="dotplot_interface_margin"
|
||||
transform={`translate(${this.dotplotLeftPadding},${this.dotplotTopPadding})`}
|
||||
>
|
||||
{/* Acaa1b, Mal, Foxq1 ... across the top of the dotplot */}
|
||||
<g id="dotplot_column_labels" transform="translate(14,-13)">
|
||||
{_genes.map((_geneSymbol, _geneIndexInGeneset) => (
|
||||
<text
|
||||
key={_geneSymbol}
|
||||
x={0}
|
||||
y={0}
|
||||
transform={`translate(${
|
||||
_geneIndexInGeneset * this.rowColumnSize
|
||||
}) rotate(270)`}
|
||||
style={{ fill: "black", font: "12px Roboto Condensed" }}
|
||||
>
|
||||
{_geneSymbol}
|
||||
</text>
|
||||
))}
|
||||
</g>
|
||||
{/* loop over genes in the geneset, */}
|
||||
<g id="dotplot_columns">
|
||||
{_genes.map((_geneSymbol, _geneIndexInGeneset) => (
|
||||
<Column
|
||||
key={_geneSymbol}
|
||||
_geneSymbol={_geneSymbol}
|
||||
_geneIndex={_geneIndexInGeneset}
|
||||
viewport={viewport}
|
||||
rowColumnSize={this.rowColumnSize}
|
||||
metadataField={dotplot.row}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dotplot;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
const StillLoading = ({ width, height }) => {
|
||||
/*
|
||||
Render a busy/loading indicator
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
fontWeight: 500,
|
||||
top: height / 2,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
justifyItems: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontStyle: "italic" }}>Loading dotplot</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StillLoading;
|
||||
@@ -26,6 +26,7 @@ type EmbeddingState = any;
|
||||
schema: (state as any).annoMatrix?.schema,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
crossfilter: (state as any).obsCrossfilter,
|
||||
dotplot: (state as any).layoutChoice.dotplot,
|
||||
}))
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
class Embedding extends React.PureComponent<{}, EmbeddingState> {
|
||||
@@ -45,13 +46,13 @@ class Embedding extends React.PureComponent<{}, EmbeddingState> {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'layoutChoice' does not exist on type 'Re... Remove this comment to see the full error message
|
||||
const { layoutChoice, schema, crossfilter } = this.props;
|
||||
const { layoutChoice, schema, crossfilter, dotplot } = this.props;
|
||||
const { annoMatrix } = crossfilter;
|
||||
return (
|
||||
<ButtonGroup
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "inherit",
|
||||
display: dotplot ? "none" : "inherit",
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
zIndex: 9999,
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import LeftSidebarSkeleton from "../leftSidebar/leftSidebarSkeleton";
|
||||
import Layout from "./layout";
|
||||
import RightSidebarSkeleton from "../rightSidebar/rightSidebarSkeleton";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function Skeleton() {
|
||||
/*
|
||||
Skeleton layout component displayed when in loading state.
|
||||
TODO(cc)
|
||||
- Remove dupe of "graph" area inline styles
|
||||
*/
|
||||
return (
|
||||
<Layout>
|
||||
<LeftSidebarSkeleton />
|
||||
{() => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
left: 8,
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 8,
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ height: "30px", width: "calc(100% - 482px - 10px)" }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
<div
|
||||
style={{ height: "30px", width: "482px" }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<RightSidebarSkeleton />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Skeleton;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from "react";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
const Title = () => (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 24,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
cell
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
);
|
||||
|
||||
export default Title;
|
||||
@@ -1,4 +0,0 @@
|
||||
:local(.newTabToast) {
|
||||
max-width: fit-content;
|
||||
top: 42px;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
||||
|
||||
/* styles */
|
||||
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message
|
||||
import styles from "./toasters.css";
|
||||
|
||||
/** Singleton toaster instance. Create separate instances for different options. */
|
||||
|
||||
const ToastTopCenter = Toaster.create({
|
||||
@@ -59,15 +55,3 @@ export const postAsyncFailureToast = (message: any) =>
|
||||
timeout: 10000,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
|
||||
/*
|
||||
Dataset opened in new tab
|
||||
*/
|
||||
export const postExplainNewTab = (message: string) => {
|
||||
ToastTopCenter.show({
|
||||
className: styles.newTabToast,
|
||||
message,
|
||||
timeout: 5000,
|
||||
intent: Intent.PRIMARY,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -509,10 +509,6 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
setReglCanvas = (canvas: any) => {
|
||||
// Ignore null canvas on unmount
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
this.reglCanvas = canvas;
|
||||
this.setState({
|
||||
...Graph.createReglState(canvas),
|
||||
@@ -562,11 +558,7 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
handleEnd = this.handleLassoEnd.bind(this);
|
||||
handleCancel = this.handleLassoCancel.bind(this);
|
||||
}
|
||||
const {
|
||||
svg: newToolSVG,
|
||||
tool,
|
||||
container,
|
||||
} = setupSVGandBrushElements(
|
||||
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
|
||||
selectionTool,
|
||||
handleStart,
|
||||
handleDrag,
|
||||
@@ -922,6 +914,7 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
pointDilation,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message
|
||||
crossfilter,
|
||||
dotplotMode,
|
||||
} = this.props;
|
||||
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
|
||||
const cameraTF = camera?.view()?.slice();
|
||||
@@ -932,6 +925,7 @@ class Graph extends React.Component<{}, GraphState> {
|
||||
position: "relative",
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: dotplotMode ? "none" : "inherit",
|
||||
}}
|
||||
>
|
||||
<GraphOverlayLayer
|
||||
|
||||
@@ -7,8 +7,6 @@ import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
collection: ((state as any).collections as any)?.collection,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
schema: (state as any).annoMatrix.schema,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -32,8 +30,6 @@ class InfoDrawer extends PureComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
const {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
collection,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'position' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
position,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'aboutURL' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
@@ -62,10 +58,15 @@ class InfoDrawer extends PureComponent {
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer onClose={this.handleClose} size={480} {...{ isOpen, position }}>
|
||||
<Drawer
|
||||
title="Dataset Overview"
|
||||
onClose={this.handleClose}
|
||||
{...{ isOpen, position }}
|
||||
>
|
||||
<InfoFormat
|
||||
{...{
|
||||
collection,
|
||||
datasetTitle,
|
||||
aboutURL,
|
||||
singleValueCategories,
|
||||
dataPortalProps: dataPortalProps ?? {},
|
||||
}}
|
||||
|
||||
@@ -1,244 +1,189 @@
|
||||
import { Classes, H3, HTMLTable, Position, Tooltip } from "@blueprintjs/core";
|
||||
import { H3, H1, UL, HTMLTable, Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
const ONTOLOGY_KEY = "ontology_term_id";
|
||||
const COLLECTION_LINK_ORDER_BY = [
|
||||
"DOI",
|
||||
"DATA_SOURCE",
|
||||
"RAW_DATA",
|
||||
"PROTOCOL",
|
||||
"LAB_WEBSITE",
|
||||
"OTHER",
|
||||
];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderContributors = (contributors: any, affiliations: any) => {
|
||||
// eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII
|
||||
if (!contributors || contributors.length === 0 || true) return null;
|
||||
return (
|
||||
<>
|
||||
<H3>Contributors</H3>
|
||||
<p>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{contributors.map((contributor: any) => {
|
||||
const { email, name, institution } = contributor;
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildCollectionLinks = (links) => {
|
||||
/*
|
||||
sort links by custom sort order, create view-friendly model of link types.
|
||||
*/
|
||||
const sortedLinks = [...links].sort(sortCollectionLinks);
|
||||
return sortedLinks.map((link) => {
|
||||
const { link_name: name, link_type: type, link_url: url } = link;
|
||||
return {
|
||||
name: buildLinkName(name, type, url),
|
||||
type: transformLinkTypeToDisplay(type),
|
||||
url,
|
||||
};
|
||||
return (
|
||||
<span key={name}>
|
||||
{name}
|
||||
{email && `(${email})`}
|
||||
<sup>{affiliations.indexOf(institution) + 1}</sup>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
{renderAffiliations(affiliations)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// generates a list of unique institutions by order of appearance in contributors
|
||||
const buildAffiliations = (contributors = []) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const affiliations: any = [];
|
||||
contributors.forEach((contributor) => {
|
||||
const { institution } = contributor;
|
||||
if (affiliations.indexOf(institution) === -1) {
|
||||
affiliations.push(institution);
|
||||
}
|
||||
});
|
||||
return affiliations;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildDatasetMetadata = (singleValueCategories, corporaMetadata) => {
|
||||
/*
|
||||
transform Corpora metadata and single value categories into sort and render-friendly format.
|
||||
@returns [{key, value, tip}]
|
||||
*/
|
||||
const metadata = [
|
||||
...transformCorporaMetadata(corporaMetadata),
|
||||
...transformSingleValueCategoriesMetadata(singleValueCategories),
|
||||
];
|
||||
metadata.sort(sortDatasetMetadata);
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const getTableStyles = () => ({ tableLayout: "fixed", width: "100%" });
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const sortCollectionLinks = (l0, l1) =>
|
||||
/*
|
||||
sort collection links by custom order.
|
||||
TODO(cc) revisit - improve readability here
|
||||
*/
|
||||
COLLECTION_LINK_ORDER_BY.indexOf(l1.type) -
|
||||
COLLECTION_LINK_ORDER_BY.indexOf(l0.type);
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const buildLinkName = (name, type, url) => {
|
||||
/*
|
||||
determine name to display for collection link.
|
||||
TODO(cc) error handling
|
||||
*/
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (type === "DOI") {
|
||||
return new URL(url).pathname.substring(1);
|
||||
}
|
||||
return new URL(url).host;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const sortDatasetMetadata = (m0, m1) => {
|
||||
/*
|
||||
sort metadata key value pairs by key - alpha, ascending
|
||||
*/
|
||||
if (m0.key < m1.key) {
|
||||
return -1;
|
||||
}
|
||||
if (m0.key > m1.key) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformCorporaMetadata = (corporaMetadata) =>
|
||||
/*
|
||||
build array of view model objects from given Corpora metadata object.
|
||||
@returns [{key, value}]
|
||||
*/
|
||||
Object.entries(corporaMetadata)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}));
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformSingleValueCategoriesMetadata = (singleValueCategories) =>
|
||||
/*
|
||||
build array of view model objects from given single value categories map, ignoring ontology terms or metadata
|
||||
without values. add ontology terms as tooltips of their corresponding values.
|
||||
@returns [{key, value, tip}] where tip is an optional ontology term for the category
|
||||
*/
|
||||
Array.from(singleValueCategories.entries())
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.filter(([key, value]) => {
|
||||
if (key.indexOf(ONTOLOGY_KEY) >= 0) {
|
||||
// skip ontology terms
|
||||
return false;
|
||||
}
|
||||
// skip metadata without values
|
||||
return value;
|
||||
})
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.map(([key, value]) => {
|
||||
const viewModel = { key, value: String(value) };
|
||||
// add ontology term as tool tip if specified
|
||||
const tip = singleValueCategories.get(`${key}_${ONTOLOGY_KEY}`);
|
||||
if (tip) {
|
||||
// @ts-expect-error --- TODO revisit
|
||||
viewModel.tip = tip;
|
||||
}
|
||||
return viewModel;
|
||||
});
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const transformLinkTypeToDisplay = (type) => {
|
||||
/*
|
||||
convert link type from upper snake case to title case
|
||||
TODO(cc) revisit approach here, maybe create enum-type mapping to avoid string concat inside loop?
|
||||
*/
|
||||
const tokens = type.split("_");
|
||||
return (
|
||||
tokens
|
||||
// @ts-expect-error --- TODO revisit
|
||||
.map((token) => token.charAt(0) + token.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderCollectionLinks = (collection) => {
|
||||
/*
|
||||
render collection contact and links.
|
||||
TODO(cc) handle case where there is no contact and no links?
|
||||
*/
|
||||
const links = buildCollectionLinks(collection.links);
|
||||
const { contact_name: contactName, contact_email: contactEmail } = collection;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderAffiliations = (affiliations: any) => {
|
||||
if (affiliations.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
{renderSectionTitle("Collection")}
|
||||
{/* @ts-expect-error --- TODO revisit */}
|
||||
<HTMLTable style={getTableStyles()}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Contact</td>
|
||||
<td>{renderCollectionContactLink(contactName, contactEmail)}</td>
|
||||
</tr>
|
||||
{links.map(({ name, type, url }, i) => (
|
||||
<tr {...{ key: i }}>
|
||||
<td>{type}</td>
|
||||
<td>
|
||||
<a href={url} rel="noopener" target="_blank">
|
||||
{name}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
<H3>Affiliations</H3>
|
||||
<UL>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{affiliations.map((item: any, index: any) => (
|
||||
<div key={item}>
|
||||
<sup>{index + 1}</sup>
|
||||
{" "}
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderCollectionContactLink = (name, email) => {
|
||||
/*
|
||||
display collection contact's name with a link to their associated email.
|
||||
*/
|
||||
if (!name && !email) {
|
||||
return null;
|
||||
}
|
||||
if (email) {
|
||||
return <a href={`mailto:${email}`}>{name}</a>;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderDatasetMetadata = (singleValueCategories, corporaMetadata) => {
|
||||
/*
|
||||
render dataset metadata, mix of meta from Corpora and attributes found in categorical field.
|
||||
*/
|
||||
if (
|
||||
singleValueCategories.size === 0 &&
|
||||
Object.entries(corporaMetadata).length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const metadata = buildDatasetMetadata(singleValueCategories, corporaMetadata);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderDOILink = (type: any, doi: any) => {
|
||||
if (!doi) return null;
|
||||
return (
|
||||
<>
|
||||
{renderSectionTitle("Dataset")}
|
||||
<H3>{type}</H3>
|
||||
<p>
|
||||
<a href={doi} target="_blank" rel="noopener">
|
||||
{doi}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ONTOLOGY_KEY = "ontology_term_id";
|
||||
// Render list of metadata attributes found in categorical field
|
||||
const renderDatasetMetadata = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
singleValueCategories: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
corporaMetadata: any
|
||||
) => {
|
||||
if (singleValueCategories.size === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3>Dataset Metadata</H3>
|
||||
<HTMLTable
|
||||
// @ts-expect-error --- TODO revisit
|
||||
style={getTableStyles()}
|
||||
striped
|
||||
condensed
|
||||
style={{ display: "block", width: "100%", overflowX: "auto" }}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Label</th>
|
||||
<th>Ontology ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* @ts-expect-error --- TODO revisit */}
|
||||
{metadata.map(({ key, value, tip }) => (
|
||||
{Object.entries(corporaMetadata).map(([key, value]) => (
|
||||
<tr {...{ key }}>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
<Tooltip
|
||||
content={tip}
|
||||
disabled={!tip}
|
||||
minimal
|
||||
modifiers={{ flip: { enabled: false } }}
|
||||
position={Position.TOP}
|
||||
>
|
||||
{value}
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>{`${key}:`}</td>
|
||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'unknown' is not assignable to type 'ReactNod... Remove this comment to see the full error message */}
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
</tr>
|
||||
))}
|
||||
{Array.from(singleValueCategories).reduce((elems, pair) => {
|
||||
// @ts-expect-error ts-migrate(2488) FIXME: Type 'unknown' must have a '[Symbol.iterator]()' m... Remove this comment to see the full error message
|
||||
const [category, value] = pair;
|
||||
// If the value is empty skip it
|
||||
if (!value) return elems;
|
||||
|
||||
// If this category is a ontology term, let's add its value to the previous node
|
||||
if (String(category).includes(ONTOLOGY_KEY)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const prevElem = (elems as any).pop();
|
||||
const newChildren = [...prevElem.props.children];
|
||||
newChildren.splice(2, 1, [<td key="ontology">{value}</td>]);
|
||||
// Props aren't extensible so we must clone and alter the component to append the new child
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
(elems as any).push(
|
||||
React.cloneElement(prevElem, prevElem.props, newChildren)
|
||||
);
|
||||
} else {
|
||||
// Create the list item
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
(elems as any).push(
|
||||
<tr key={category}>
|
||||
<td>{`${category}:`}</td>
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return elems;
|
||||
}, [])}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error --- TODO revisit
|
||||
const renderSectionTitle = (title) => (
|
||||
<p style={{ margin: "24px 0 8px" }}>
|
||||
<strong>{title}</strong>
|
||||
</p>
|
||||
);
|
||||
// Renders any links found in the config where link_type is not "SUMMARY"
|
||||
// If there are no links in the config, render the aboutURL
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const renderLinks = (projectLinks: any, aboutURL: any) => {
|
||||
if (!projectLinks && !aboutURL) return null;
|
||||
if (projectLinks)
|
||||
return (
|
||||
<>
|
||||
<H3>Project Links</H3>
|
||||
<UL>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
||||
{projectLinks.map((link: any) => {
|
||||
if (link.link_type === "SUMMARY") return null;
|
||||
return (
|
||||
<li key={link.link_name}>
|
||||
<a href={link.link_url} target="_blank" rel="noopener">
|
||||
{link.link_name}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H3>More Info</H3>
|
||||
<p>
|
||||
<a href={aboutURL} target="_blank" rel="noopener">
|
||||
{aboutURL}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InfoFormat = React.memo(
|
||||
// @ts-expect-error --- TODO revisit
|
||||
({ collection, singleValueCategories, dataPortalProps = {} }) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'datasetTitle' does not exist on type '{ ... Remove this comment to see the full error message
|
||||
({ datasetTitle, singleValueCategories, aboutURL, dataPortalProps = {} }) => {
|
||||
if (
|
||||
["1.0.0", "1.1.0"].indexOf(
|
||||
dataPortalProps.version?.corpora_schema_version
|
||||
@@ -246,15 +191,26 @@ const InfoFormat = React.memo(
|
||||
) {
|
||||
dataPortalProps = {};
|
||||
}
|
||||
const { organism } = dataPortalProps;
|
||||
const {
|
||||
title,
|
||||
publication_doi: doi,
|
||||
preprint_doi: preprintDOI,
|
||||
organism,
|
||||
contributors,
|
||||
project_links: projectLinks,
|
||||
} = dataPortalProps;
|
||||
|
||||
const affiliations = buildAffiliations(contributors);
|
||||
|
||||
return (
|
||||
<div className={Classes.DRAWER_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<H3>{collection.name}</H3>
|
||||
<p>{collection.description}</p>
|
||||
{renderCollectionLinks(collection)}
|
||||
<H1>{title ?? datasetTitle}</H1>
|
||||
{renderContributors(contributors, affiliations)}
|
||||
{renderDatasetMetadata(singleValueCategories, { organism })}
|
||||
{renderLinks(projectLinks, aboutURL)}
|
||||
{renderDOILink("DOI", doi)}
|
||||
{renderDOILink("Preprint DOI", preprintDOI)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconAbout() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.99999 2.00002C4.68628 2.00002 1.99999 4.68631 1.99999 8.00002C1.99999 11.3137 4.68628 14 7.99999 14C11.3137 14 14 11.3137 14 8.00002C14 4.68631 11.3137 2.00002 7.99999 2.00002ZM0.666656 8.00002C0.666656 3.94993 3.9499 0.666687 7.99999 0.666687C12.0501 0.666687 15.3333 3.94993 15.3333 8.00002C15.3333 12.0501 12.0501 15.3334 7.99999 15.3334C3.9499 15.3334 0.666656 12.0501 0.666656 8.00002ZM7.33332 5.33335C7.33332 4.96516 7.6318 4.66669 7.99999 4.66669H8.00666C8.37485 4.66669 8.67332 4.96516 8.67332 5.33335C8.67332 5.70154 8.37485 6.00002 8.00666 6.00002H7.99999C7.6318 6.00002 7.33332 5.70154 7.33332 5.33335ZM7.99999 7.33335C8.36818 7.33335 8.66666 7.63183 8.66666 8.00002V10.6667C8.66666 11.0349 8.36818 11.3334 7.99999 11.3334C7.6318 11.3334 7.33332 11.0349 7.33332 10.6667V8.00002C7.33332 7.63183 7.6318 7.33335 7.99999 7.33335Z"
|
||||
fill="#5C7080"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconAbout;
|
||||
@@ -1,28 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconDocument() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.58579 1.25244C2.96086 0.87737 3.46957 0.666656 4 0.666656H8.66667C8.84348 0.666656 9.01305 0.736894 9.13807 0.861919L13.8047 5.52858C13.9298 5.65361 14 5.82318 14 5.99999V13.3333C14 13.8638 13.7893 14.3725 13.4142 14.7475C13.0391 15.1226 12.5304 15.3333 12 15.3333H4C3.46957 15.3333 2.96086 15.1226 2.58579 14.7475C2.21071 14.3725 2 13.8638 2 13.3333V2.66666C2 2.13622 2.21071 1.62752 2.58579 1.25244ZM4 1.99999C3.82319 1.99999 3.65362 2.07023 3.5286 2.19525C3.40357 2.32028 3.33333 2.48985 3.33333 2.66666V13.3333C3.33333 13.5101 3.40357 13.6797 3.5286 13.8047C3.65362 13.9298 3.82319 14 4 14H12C12.1768 14 12.3464 13.9298 12.4714 13.8047C12.5964 13.6797 12.6667 13.5101 12.6667 13.3333V6.66666H8.66667C8.29848 6.66666 8 6.36818 8 5.99999V1.99999H4ZM9.33333 2.9428L11.7239 5.33332H9.33333V2.9428Z"
|
||||
fill="#5C7080"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconDocument;
|
||||
@@ -1,32 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconGitHub() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.00004 0.637878C3.83166 0.637878 0.451538 4.01725 0.451538 8.18638C0.451538 11.5216 2.61441 14.351 5.61366 15.3493C5.99079 15.4193 6.12929 15.1855 6.12929 14.9861C6.12929 14.8061 6.12229 14.2115 6.11904 13.5808C4.01904 14.0374 3.57591 12.6901 3.57591 12.6901C3.23254 11.8176 2.73779 11.5856 2.73779 11.5856C2.05279 11.1171 2.78941 11.1269 2.78941 11.1269C3.54729 11.18 3.94654 11.9048 3.94654 11.9048C4.61979 13.0585 5.71241 12.725 6.14316 12.5323C6.21091 12.0444 6.40654 11.7113 6.62241 11.5228C4.94579 11.3321 3.18316 10.6848 3.18316 7.79238C3.18316 6.96825 3.47816 6.29488 3.96104 5.76613C3.88254 5.57613 3.62416 4.80838 4.03404 3.76863C4.03404 3.76863 4.66779 3.56575 6.11029 4.54238C6.71254 4.375 7.35841 4.29088 8.00004 4.288C8.64129 4.29088 9.28754 4.37475 9.89091 4.54213C11.3317 3.5655 11.9647 3.76838 11.9647 3.76838C12.3755 4.808 12.1172 5.57588 12.0388 5.76588C12.5228 6.29463 12.8157 6.968 12.8157 7.79213C12.8157 10.6914 11.0498 11.3296 9.36891 11.5166C9.63979 11.7509 9.88104 12.2104 9.88104 12.9145C9.88104 13.9245 9.87229 14.7374 9.87229 14.986C9.87229 15.1869 10.0083 15.4223 10.3908 15.3481C13.3883 14.3489 15.5487 11.5204 15.5487 8.18638C15.5485 4.0175 12.1688 0.638003 8.00004 0.638003V0.637878Z"
|
||||
fill="#181616"
|
||||
/>
|
||||
<path
|
||||
d="M3.31057 11.4758C3.29395 11.5133 3.23495 11.5245 3.1812 11.4989C3.1262 11.4744 3.09557 11.4233 3.11332 11.3856C3.12957 11.3469 3.1887 11.3363 3.24332 11.3621C3.29832 11.3868 3.32957 11.4384 3.31045 11.4759L3.31057 11.4758ZM3.61632 11.8169C3.58045 11.8503 3.51007 11.8348 3.46232 11.7819C3.41282 11.7294 3.40357 11.659 3.4402 11.6251C3.47745 11.5919 3.5457 11.6076 3.5952 11.6601C3.64445 11.7134 3.6542 11.7831 3.61645 11.817L3.61632 11.8169ZM3.91407 12.2515C3.86782 12.2838 3.79207 12.2536 3.74532 12.1865C3.69907 12.1193 3.69907 12.0386 3.74657 12.0065C3.7932 11.9743 3.86782 12.0034 3.91532 12.0699C3.96132 12.138 3.96132 12.2186 3.91407 12.2514V12.2515ZM4.3217 12.6716C4.28045 12.7173 4.19219 12.705 4.12769 12.6429C4.06182 12.582 4.04345 12.4954 4.08482 12.4499C4.12682 12.4041 4.21544 12.4169 4.28032 12.4786C4.34619 12.5394 4.36607 12.6261 4.32194 12.6715L4.3217 12.6716ZM4.88419 12.9155C4.86582 12.9746 4.78107 13.0015 4.69544 12.9763C4.61007 12.9504 4.55419 12.8813 4.57169 12.8215C4.58919 12.7619 4.67457 12.734 4.76069 12.7609C4.84607 12.7866 4.90194 12.8554 4.88419 12.9155ZM5.50207 12.9606C5.50419 13.0229 5.43169 13.0744 5.34207 13.0756C5.25169 13.0778 5.17857 13.0273 5.17769 12.966C5.17769 12.9031 5.24869 12.8523 5.33894 12.8505C5.42857 12.8489 5.50219 12.8989 5.50219 12.9605L5.50207 12.9606ZM6.07682 12.8629C6.08757 12.9235 6.02519 12.9859 5.93607 13.0025C5.84857 13.0188 5.76732 12.981 5.75607 12.9209C5.74532 12.8586 5.80882 12.7963 5.89632 12.7801C5.98557 12.7648 6.06557 12.8014 6.07682 12.863V12.8629Z"
|
||||
fill="#181616"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconGitHub;
|
||||
@@ -1,54 +0,0 @@
|
||||
/* core dependencies */
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function IconSlack() {
|
||||
/*
|
||||
TODO(cc) Generalize iconography into single component with icon prop.
|
||||
*/
|
||||
return (
|
||||
<svg
|
||||
className={Classes.ICON}
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.60123 8.44999C1.32765 8.45681 1.05913 8.37541 0.835376 8.21784C0.611621 8.06026 0.444506 7.83488 0.358733 7.57499C0.351233 7.55374 0.346233 7.53374 0.339983 7.51374C0.246912 7.18295 0.284862 6.82901 0.445947 6.52546C0.607032 6.22191 0.878869 5.99209 1.20498 5.88375L12.125 2.225C12.2546 2.18729 12.3888 2.16752 12.5237 2.16625C12.804 2.15869 13.0793 2.24133 13.3091 2.40201C13.5388 2.56268 13.7109 2.7929 13.8 3.05875L13.8162 3.11125C14.02 3.825 13.5125 4.4625 12.905 4.66625L2.04498 8.37499C1.90203 8.42355 1.75221 8.44887 1.60123 8.44999Z"
|
||||
fill="#80CADE"
|
||||
/>
|
||||
<path
|
||||
d="M3.4212 13.8188C3.14802 13.8283 2.87899 13.7498 2.65389 13.5947C2.4288 13.4396 2.25956 13.2162 2.1712 12.9575C2.16495 12.9375 2.1587 12.9175 2.15245 12.8963C2.05699 12.563 2.09393 12.2056 2.25556 11.8989C2.4172 11.5922 2.69105 11.3597 3.01995 11.25L13.9375 7.55751C14.0773 7.51073 14.2237 7.48626 14.3712 7.48501C14.6502 7.47966 14.9237 7.56293 15.1523 7.72286C15.381 7.88279 15.553 8.11112 15.6437 8.37502L15.66 8.43002C15.7117 8.61519 15.7241 8.80913 15.6966 8.9994C15.669 9.18968 15.6021 9.37211 15.5 9.53502C15.3437 9.77877 14.8512 9.99252 14.8512 9.99252L3.8912 13.7425C3.74172 13.793 3.58522 13.8196 3.42745 13.8213L3.4212 13.8188Z"
|
||||
fill="#DB015C"
|
||||
/>
|
||||
<path
|
||||
d="M12.51 13.8425C12.2269 13.8453 11.9502 13.7584 11.7196 13.5943C11.489 13.4301 11.3162 13.1971 11.2262 12.9287L7.58247 2.10499L7.56372 2.04374C7.47896 1.71028 7.52546 1.357 7.69363 1.05684C7.8618 0.756674 8.13879 0.532532 8.46744 0.430683C8.79608 0.328834 9.15128 0.357052 9.45973 0.509513C9.76817 0.661973 10.0063 0.927038 10.125 1.24999L13.7687 12.0725L13.7787 12.1075C13.8403 12.3073 13.8543 12.5187 13.8197 12.7248C13.785 12.931 13.7026 13.1262 13.5791 13.2949C13.4556 13.4636 13.2944 13.601 13.1084 13.6963C12.9223 13.7916 12.7165 13.8421 12.5075 13.8437L12.51 13.8425Z"
|
||||
fill="#E8A900"
|
||||
/>
|
||||
<path
|
||||
d="M7.09375 15.6663C6.81042 15.6687 6.53359 15.5814 6.30297 15.4168C6.07235 15.2522 5.89977 15.0188 5.81 14.75L2.16625 3.92876L2.1475 3.86876C2.05234 3.53693 2.08892 3.18118 2.2496 2.87565C2.41028 2.57012 2.68267 2.33839 3.01 2.22876C3.14466 2.1838 3.28553 2.16018 3.4275 2.15876C3.71057 2.15594 3.98725 2.24284 4.21788 2.407C4.4485 2.57115 4.62122 2.80413 4.71125 3.07251L8.35375 13.8963C8.36 13.915 8.36625 13.9363 8.3725 13.9563C8.42934 14.155 8.43947 14.3642 8.4021 14.5675C8.36474 14.7708 8.28088 14.9627 8.15708 15.1283C8.03328 15.2938 7.8729 15.4285 7.68844 15.5219C7.50399 15.6152 7.30046 15.6646 7.09375 15.6663Z"
|
||||
fill="#41B088"
|
||||
/>
|
||||
<path
|
||||
d="M10.7137 11.405L13.25 10.5363L12.4237 8.07001L9.875 8.93001L10.7137 11.405Z"
|
||||
fill="#CA161A"
|
||||
/>
|
||||
<path
|
||||
d="M5.30619 13.25L7.84619 12.3812L7.00994 9.89999L4.46619 10.76L5.30619 13.25Z"
|
||||
fill="#3B1D37"
|
||||
/>
|
||||
<path
|
||||
d="M8.90496 6.03251L11.4462 5.16501L10.625 2.72751L8.07996 3.58001L8.90496 6.03251Z"
|
||||
fill="#69852C"
|
||||
/>
|
||||
<path
|
||||
d="M3.49988 7.87498L6.03738 7.01123L5.20613 4.54248L2.66113 5.39498L3.49988 7.87498Z"
|
||||
fill="#118F79"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export default IconSlack;
|
||||
@@ -2,12 +2,6 @@ import React from "react";
|
||||
import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
/* app dependencies */
|
||||
import IconAbout from "./iconAbout";
|
||||
import IconDocument from "./iconDocument";
|
||||
import IconGitHub from "./iconGitHub";
|
||||
import IconSlack from "./iconSlack";
|
||||
|
||||
const InformationMenu = React.memo((props) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'libraryVersions' does not exist on type ... Remove this comment to see the full error message
|
||||
const { libraryVersions, tosURL, privacyURL } = props;
|
||||
@@ -17,58 +11,60 @@ const InformationMenu = React.memo((props) => {
|
||||
<Menu>
|
||||
<MenuItem
|
||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||
icon={<IconDocument />}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
icon="book"
|
||||
text="Documentation"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://join-cellxgene-users.herokuapp.com/"
|
||||
icon={<IconSlack />}
|
||||
target="_blank"
|
||||
icon="chat"
|
||||
text="Chat"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://github.com/chanzuckerberg/cellxgene"
|
||||
icon={<IconGitHub />}
|
||||
target="_blank"
|
||||
icon="git-branch"
|
||||
text="Github"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<IconAbout />}
|
||||
popoverProps={{ openOnTargetFocus: false }}
|
||||
text="About cellxgene"
|
||||
>
|
||||
<MenuItem text={libraryVersions?.cellxgene || null} />
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL && (
|
||||
<MenuItem
|
||||
href={tosURL}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
text="Terms of Service"
|
||||
/>
|
||||
)}
|
||||
{privacyURL && (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem target="_blank" text={libraryVersions?.cellxgene || null} />
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL && (
|
||||
<MenuItem
|
||||
href={tosURL}
|
||||
target="_blank"
|
||||
text="Terms of Service"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
{privacyURL && (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<Button data-testid="menu" icon={IconNames.MENU} minimal type="button" />
|
||||
<Button
|
||||
data-testid="menu"
|
||||
type="button"
|
||||
icon={IconNames.INFO_SIGN}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import Logo from "../framework/logo";
|
||||
import Title from "../framework/title";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function LeftSidebarSkeleton() {
|
||||
/*
|
||||
Skeleton of left side bar, to be displayed during data load.
|
||||
TODO(cc)
|
||||
- Remove dupe of LeftSidebar inline styles
|
||||
- Remove dupe of TopLeftLogoAndTitle styles
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRight: `1px solid ${globals.lightGrey}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* TopLeftLogoAndTitle */}
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 5,
|
||||
paddingTop: 8,
|
||||
width: globals.leftSidebarWidth,
|
||||
zIndex: 1,
|
||||
borderBottom: `1px solid ${globals.lighterGrey}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Logo size={28} />
|
||||
<Title />
|
||||
</div>
|
||||
{/* Hamburger */}
|
||||
<div style={{ height: 30, width: 30 }} className={SKELETON} />
|
||||
</div>
|
||||
{/* Categorical */}
|
||||
<div style={{ padding: 8 }}>
|
||||
{[...Array(10).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{ height: 30, marginBottom: 4 }}
|
||||
className={SKELETON}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Continuous */}
|
||||
{[...Array(2).keys()].map((i) => (
|
||||
<div key={i} style={{ height: 211 }} className={SKELETON} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeftSidebarSkeleton;
|
||||
@@ -1,26 +1,49 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Button } from "@blueprintjs/core";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Logo from "../framework/logo";
|
||||
import Title from "../framework/title";
|
||||
import Truncate from "../util/truncate";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
import InformationMenu from "./infoMenu";
|
||||
|
||||
const DATASET_TITLE_FONT_SIZE = 14;
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => ({
|
||||
@connect((state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
libraryVersions: (state as any).config?.library_versions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
aboutLink: (state as any).config?.links?.["about-dataset"],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
tosURL: (state as any).config?.parameters?.about_legal_tos,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
privacyURL: (state as any).config?.parameters?.about_legal_privacy,
|
||||
}))
|
||||
const { corpora_props: corporaProps } = (state as any).config;
|
||||
const correctVersion =
|
||||
["1.0.0", "1.1.0"].indexOf(corporaProps?.version?.corpora_schema_version) >
|
||||
-1;
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
datasetTitle: (state as any).config?.displayNames?.dataset ?? "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
libraryVersions: (state as any).config?.library_versions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
aboutLink: (state as any).config?.links?.["about-dataset"],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
tosURL: (state as any).config?.parameters?.about_legal_tos,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
privacyURL: (state as any).config?.parameters?.about_legal_privacy,
|
||||
title: correctVersion ? corporaProps?.title : undefined,
|
||||
};
|
||||
})
|
||||
class LeftSideBar extends React.Component {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
handleClick = () => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
const { dispatch } = this.props;
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
render() {
|
||||
const {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'datasetTitle' does not exist on type 'Re... Remove this comment to see the full error message
|
||||
datasetTitle,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'libraryVersions' does not exist on type ... Remove this comment to see the full error message
|
||||
libraryVersions,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'aboutLink' does not exist on type 'Reado... Remove this comment to see the full error message
|
||||
@@ -31,6 +54,8 @@ class LeftSideBar extends React.Component {
|
||||
tosURL,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
dispatch,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -48,9 +73,48 @@ class LeftSideBar extends React.Component {
|
||||
>
|
||||
<div>
|
||||
<Logo size={28} />
|
||||
<Title />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 24,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
cell
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginRight: 5, height: "100%" }}>
|
||||
<Button
|
||||
minimal
|
||||
style={{
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "relative",
|
||||
top: -1,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||
{title ?? datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
</Button>
|
||||
<InfoDrawer />
|
||||
<InformationMenu
|
||||
{...{
|
||||
libraryVersions,
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
|
||||
import AuthButtons from "./authButtons";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
|
||||
|
||||
@connect((state) => {
|
||||
const { annoMatrix } = state;
|
||||
const crossfilter = state.obsCrossfilter;
|
||||
const selectedCount = crossfilter.countSelected();
|
||||
|
||||
const subsetPossible =
|
||||
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all and not none are selected
|
||||
const embSubsetView = getEmbSubsetView(annoMatrix);
|
||||
const subsetResetPossible = !embSubsetView
|
||||
? annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs
|
||||
: annoMatrix.nObs !== embSubsetView.nObs;
|
||||
|
||||
return {
|
||||
subsetPossible,
|
||||
subsetResetPossible,
|
||||
graphInteractionMode: state.controls.graphInteractionMode,
|
||||
clipPercentileMin: Math.round(100 * (annoMatrix?.clipRange?.[0] ?? 0)),
|
||||
clipPercentileMax: Math.round(100 * (annoMatrix?.clipRange?.[1] ?? 1)),
|
||||
userDefinedGenes: state.controls.userDefinedGenes,
|
||||
colorAccessor: state.colors.colorAccessor,
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
libraryVersions: state.config?.library_versions,
|
||||
auth: state.config?.authentication,
|
||||
userInfo: state.userInfo,
|
||||
undoDisabled: state["@@undoable/past"].length === 0,
|
||||
redoDisabled: state["@@undoable/future"].length === 0,
|
||||
aboutLink: state.config?.links?.["about-dataset"],
|
||||
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
|
||||
diffexpMayBeSlow:
|
||||
state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
|
||||
showCentroidLabels: state.centroidLabels.showLabels,
|
||||
tosURL: state.config?.parameters?.about_legal_tos,
|
||||
privacyURL: state.config?.parameters?.about_legal_privacy,
|
||||
categoricalSelection: state.categoricalSelection,
|
||||
dotplotEnabled: state.layoutChoice.dotplot,
|
||||
};
|
||||
})
|
||||
class MenuBar extends React.PureComponent {
|
||||
static isValidDigitKeyEvent(e) {
|
||||
/*
|
||||
Return true if this event is necessary to enter a percent number input.
|
||||
Return false if not.
|
||||
|
||||
Returns true for events with keys: backspace, control, alt, meta, [0-9],
|
||||
or events that don't have a key.
|
||||
*/
|
||||
if (e.key === null) return true;
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return true;
|
||||
|
||||
// concept borrowed from blueprint's numericInputUtils:
|
||||
// keys that print a single character when pressed have a `key` name of
|
||||
// length 1. every other key has a longer `key` name (e.g. "Backspace",
|
||||
// "ArrowUp", "Shift"). since none of those keys can print a character
|
||||
// to the field--and since they may have important native behaviors
|
||||
// beyond printing a character--we don't want to disable their effects.
|
||||
const isSingleCharKey = e.key.length === 1;
|
||||
if (!isSingleCharKey) return true;
|
||||
|
||||
const key = e.key.charCodeAt(0) - 48; /* "0" */
|
||||
return key >= 0 && key <= 9;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pendingClipPercentiles: null,
|
||||
};
|
||||
}
|
||||
|
||||
isClipDisabled = () => {
|
||||
/*
|
||||
return true if clip button should be disabled.
|
||||
*/
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
|
||||
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
|
||||
const {
|
||||
clipPercentileMin: currentClipMin,
|
||||
clipPercentileMax: currentClipMax,
|
||||
} = this.props;
|
||||
|
||||
// if you change this test, be careful with logic around
|
||||
// comparisons between undefined / NaN handling.
|
||||
const isDisabled =
|
||||
!(clipPercentileMin < clipPercentileMax) ||
|
||||
(clipPercentileMin === currentClipMin &&
|
||||
clipPercentileMax === currentClipMax);
|
||||
|
||||
return isDisabled;
|
||||
};
|
||||
|
||||
handleClipOnKeyPress = (e) => {
|
||||
/*
|
||||
allow only numbers, plus other critical keys which
|
||||
may be required to make a number
|
||||
*/
|
||||
if (!MenuBar.isValidDigitKeyEvent(e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
handleClipPercentileMinValueChange = (v) => {
|
||||
/*
|
||||
Ignore anything that isn't a legit number
|
||||
*/
|
||||
if (!Number.isFinite(v)) return;
|
||||
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
|
||||
|
||||
/*
|
||||
clamp to [0, currentClipPercentileMax]
|
||||
*/
|
||||
if (v <= 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
const clipPercentileMin = Math.round(v); // paranoia
|
||||
this.setState({
|
||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
|
||||
});
|
||||
};
|
||||
|
||||
handleClipPercentileMaxValueChange = (v) => {
|
||||
/*
|
||||
Ignore anything that isn't a legit number
|
||||
*/
|
||||
if (!Number.isFinite(v)) return;
|
||||
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
|
||||
|
||||
/*
|
||||
clamp to [0, 100]
|
||||
*/
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
const clipPercentileMax = Math.round(v); // paranoia
|
||||
|
||||
this.setState({
|
||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
|
||||
});
|
||||
};
|
||||
|
||||
handleClipCommit = () => {
|
||||
const { dispatch } = this.props;
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles;
|
||||
const min = clipPercentileMin / 100;
|
||||
const max = clipPercentileMax / 100;
|
||||
dispatch(actions.clipAction(min, max));
|
||||
};
|
||||
|
||||
handleClipOpening = () => {
|
||||
const { clipPercentileMin, clipPercentileMax } = this.props;
|
||||
this.setState({
|
||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
|
||||
});
|
||||
};
|
||||
|
||||
handleClipClosing = () => {
|
||||
this.setState({ pendingClipPercentiles: null });
|
||||
};
|
||||
|
||||
handleCentroidChange = () => {
|
||||
const { dispatch, showCentroidLabels } = this.props;
|
||||
|
||||
dispatch({
|
||||
type: "show centroid labels for category",
|
||||
showLabels: !showCentroidLabels,
|
||||
});
|
||||
};
|
||||
|
||||
handleSubset = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.subsetAction());
|
||||
};
|
||||
|
||||
handleSubsetReset = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.resetSubsetAction());
|
||||
};
|
||||
|
||||
handleDotplotToggle = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({ type: "toggle dotplot" });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatch,
|
||||
disableDiffexp,
|
||||
undoDisabled,
|
||||
redoDisabled,
|
||||
selectionTool,
|
||||
clipPercentileMin,
|
||||
clipPercentileMax,
|
||||
graphInteractionMode,
|
||||
showCentroidLabels,
|
||||
categoricalSelection,
|
||||
colorAccessor,
|
||||
subsetPossible,
|
||||
subsetResetPossible,
|
||||
userInfo,
|
||||
auth,
|
||||
dotplotEnabled,
|
||||
} = this.props;
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
|
||||
const isColoredByCategorical = !!categoricalSelection?.[colorAccessor];
|
||||
|
||||
// constants used to create selection tool button
|
||||
const [selectionTooltip, selectionButtonIcon] =
|
||||
selectionTool === "brush"
|
||||
? ["Brush selection", "Lasso selection"]
|
||||
: ["select", "polygon-filter"];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-start",
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons {...{ auth, userInfo }} />
|
||||
<UndoRedoReset
|
||||
dispatch={dispatch}
|
||||
undoDisabled={undoDisabled}
|
||||
redoDisabled={redoDisabled}
|
||||
/>
|
||||
<Clip
|
||||
pendingClipPercentiles={pendingClipPercentiles}
|
||||
clipPercentileMin={clipPercentileMin}
|
||||
clipPercentileMax={clipPercentileMax}
|
||||
handleClipOpening={this.handleClipOpening}
|
||||
handleClipClosing={this.handleClipClosing}
|
||||
handleClipCommit={this.handleClipCommit}
|
||||
isClipDisabled={this.isClipDisabled}
|
||||
handleClipOnKeyPress={this.handleClipOnKeyPress}
|
||||
handleClipPercentileMaxValueChange={
|
||||
this.handleClipPercentileMaxValueChange
|
||||
}
|
||||
handleClipPercentileMinValueChange={
|
||||
this.handleClipPercentileMinValueChange
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
content="Enable dotplot mode (hides embedding)"
|
||||
position="bottom"
|
||||
>
|
||||
<AnchorButton
|
||||
className={styles.menubarButton}
|
||||
type="button"
|
||||
data-testid="dotplot-toggle"
|
||||
icon="layout-grid"
|
||||
onClick={this.handleDotplotToggle}
|
||||
active={dotplotEnabled}
|
||||
intent={dotplotEnabled ? "success" : "none"}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content="When a category is colored by, show labels on the graph"
|
||||
position="bottom"
|
||||
disabled={graphInteractionMode === "zoom"}
|
||||
>
|
||||
<AnchorButton
|
||||
className={styles.menubarButton}
|
||||
type="button"
|
||||
data-testid="centroid-label-toggle"
|
||||
icon="property"
|
||||
onClick={this.handleCentroidChange}
|
||||
active={showCentroidLabels}
|
||||
intent={showCentroidLabels ? "primary" : "none"}
|
||||
disabled={!isColoredByCategorical}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<Tooltip
|
||||
content={selectionTooltip}
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="mode-lasso"
|
||||
icon={selectionButtonIcon}
|
||||
active={graphInteractionMode === "select"}
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: "change graph interaction mode",
|
||||
data: "select",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content="Drag to pan, scroll to zoom"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="mode-pan-zoom"
|
||||
icon="zoom-in"
|
||||
active={graphInteractionMode === "zoom"}
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: "change graph interaction mode",
|
||||
data: "zoom",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
<Subset
|
||||
subsetPossible={subsetPossible}
|
||||
subsetResetPossible={subsetResetPossible}
|
||||
handleSubset={this.handleSubset}
|
||||
handleSubsetReset={this.handleSubsetReset}
|
||||
/>
|
||||
{disableDiffexp ? null : <DiffexpButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MenuBar;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message
|
||||
@@ -10,7 +9,6 @@ import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
|
||||
import AuthButtons from "./authButtons";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
@@ -288,27 +286,18 @@ class MenuBar extends React.PureComponent<{}, State> {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 0,
|
||||
display: "flex",
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "flex-start",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-start",
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons {...{ auth, userInfo }} />
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
icon={IconNames.INFO_SIGN}
|
||||
onClick={() => {
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
data-testid="drawer"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<UndoRedoReset
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ dispatch: any; undoDisabled: any; redoDisa... Remove this comment to see the full error message
|
||||
dispatch={dispatch}
|
||||
@@ -395,7 +384,6 @@ class MenuBar extends React.PureComponent<{}, State> {
|
||||
handleSubsetReset={this.handleSubsetReset}
|
||||
/>
|
||||
{disableDiffexp ? null : <DiffexpButtons />}
|
||||
<InfoDrawer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ export default class MiniHistogram extends React.PureComponent {
|
||||
height,
|
||||
borderBottom: "solid rgb(230, 230, 230) 0.25px",
|
||||
}}
|
||||
className="mini-histo"
|
||||
width={width}
|
||||
height={height}
|
||||
ref={this.canvasRef}
|
||||
|
||||
@@ -81,6 +81,7 @@ export default class MiniStackedBar extends React.PureComponent {
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
className="mini-stacked-bar-canvas"
|
||||
width={width}
|
||||
height={height}
|
||||
ref={this.canvasRef}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// Core dependencies
|
||||
import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes";
|
||||
import React from "react";
|
||||
|
||||
// App dependencies
|
||||
import * as globals from "../../globals";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
function RightSidebarSkeleton() {
|
||||
/*
|
||||
Skeleton of left side bar, to be displayed during data load.
|
||||
TODO(cc)
|
||||
- Remove dupe of RightSidebar inline styles
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `1px solid ${globals.lightGrey}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
overflowY: "inherit",
|
||||
height: "inherit",
|
||||
width: "inherit",
|
||||
padding: globals.leftSidebarSectionPadding,
|
||||
}}
|
||||
>
|
||||
{/* Create new gene set button */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 10,
|
||||
position: "relative",
|
||||
top: -2,
|
||||
height: "30px",
|
||||
width: "133px",
|
||||
}}
|
||||
className={SKELETON}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RightSidebarSkeleton;
|
||||
@@ -566,6 +566,7 @@ class Scatterplot extends React.PureComponent<{}, State> {
|
||||
width={width}
|
||||
height={height}
|
||||
data-testid="scatterplot"
|
||||
className="scatterplot-canvas"
|
||||
style={{
|
||||
marginLeft: margin.left,
|
||||
marginTop: margin.top,
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
export const KEYS = {
|
||||
COOKIE_DECISION: "cxg.cookieDecision",
|
||||
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
|
||||
WORK_IN_PROGRESS_WARN: "cxg.WORK_IN_PROGRESS_WARN",
|
||||
};
|
||||
|
||||
// TODO(cc) review location
|
||||
export const WORK_IN_PROGRESS_WARN_STATE = {
|
||||
OFF: "off",
|
||||
ON: "on",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
|
||||
@@ -123,22 +123,4 @@ if ((window as any).CELLXGENE && (window as any).CELLXGENE.API) {
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
TODO(cc) temp set local flag to handle differences between local and deployed environments
|
||||
*/
|
||||
_API.local = window.location.hostname === "localhost";
|
||||
|
||||
/*
|
||||
TODO(cc) temp set Portal staging prefix to handle meta and collection API requests
|
||||
*/
|
||||
_API.portalPrefix =
|
||||
"https://api.cellxgene.staging.single-cell.czi.technology/dp/v1/";
|
||||
|
||||
/*
|
||||
TODO(cc) temp set of Portal/Explorer origin, required for breadcrumb links as well as generating explore URL param for
|
||||
meta endpoint in environments where hosted origin does not match Portal/dataset deployment URL origin (eg local and
|
||||
canary).
|
||||
*/
|
||||
_API.origin = "https://cellxgene.staging.single-cell.czi.technology/";
|
||||
|
||||
export const API = _API;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- TODO fix return value
|
||||
const Collections = (
|
||||
/*
|
||||
collections reducer, modifies portal collections-related state.
|
||||
*/
|
||||
state = {
|
||||
// data loading flag
|
||||
loading: true,
|
||||
error: null,
|
||||
|
||||
collection: null,
|
||||
selectedDatasetId: null,
|
||||
},
|
||||
// @ts-expect-error --- TODO fix typings
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- TODO fix return value
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "initial data load start":
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
case "collection load complete":
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: null,
|
||||
collection: action.collection,
|
||||
selectedDatasetId: action.selectedDatasetId,
|
||||
};
|
||||
case "initial data load error":
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Collections;
|
||||
@@ -5,6 +5,7 @@ Color By UI state
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
const ColorsReducer = (
|
||||
state = {
|
||||
/* TODO(colinmegill) #632 remove hardcode for dev */
|
||||
colorMode: null /* by continuous, by expression */,
|
||||
colorAccessor: null /* tissue, Apod */,
|
||||
},
|
||||
@@ -53,6 +54,17 @@ const ColorsReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case "toggle dotplot": {
|
||||
return {
|
||||
...state,
|
||||
colorMode:
|
||||
state.colorMode === "color by dotplot columns"
|
||||
? null
|
||||
: "color by dotplot columns",
|
||||
colorAccessor: null,
|
||||
};
|
||||
}
|
||||
|
||||
case "color by categorical metadata":
|
||||
case "color by continuous metadata": {
|
||||
/* toggle between this mode and reset */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const Dotplot = (
|
||||
state = {
|
||||
row: null,
|
||||
column: null,
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "set dotplot row":
|
||||
return {
|
||||
...state,
|
||||
row: action.data,
|
||||
};
|
||||
case "set dotplot column":
|
||||
return {
|
||||
...state,
|
||||
column: action.data,
|
||||
};
|
||||
case "toggle dotplot":
|
||||
return {
|
||||
...state,
|
||||
row: null,
|
||||
column: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Dotplot;
|
||||
@@ -3,7 +3,6 @@ import thunk from "redux-thunk";
|
||||
|
||||
import cascadeReducers from "./cascade";
|
||||
import undoable from "./undoable";
|
||||
import collections from "./collections";
|
||||
import config from "./config";
|
||||
import userInfo from "./userInfo";
|
||||
import annoMatrix from "./annoMatrix";
|
||||
@@ -18,6 +17,7 @@ import controls from "./controls";
|
||||
import annotations from "./annotations";
|
||||
import genesets from "./genesets";
|
||||
import genesetsUI from "./genesetsUI";
|
||||
import dotplot from "./dotplot";
|
||||
import autosave from "./autosave";
|
||||
import centroidLabels from "./centroidLabels";
|
||||
import pointDialation from "./pointDilation";
|
||||
@@ -33,6 +33,7 @@ const Reducer = undoable(
|
||||
["annotations", annotations],
|
||||
["genesets", genesets],
|
||||
["genesetsUI", genesetsUI],
|
||||
["dotplot", dotplot],
|
||||
["layoutChoice", layoutChoice],
|
||||
["categoricalSelection", categoricalSelection],
|
||||
["continuousSelection", continuousSelection],
|
||||
@@ -44,7 +45,6 @@ const Reducer = undoable(
|
||||
["pointDilation", pointDialation],
|
||||
["autosave", autosave],
|
||||
["userInfo", userInfo],
|
||||
["collections", collections],
|
||||
]),
|
||||
[
|
||||
"annoMatrix",
|
||||
|
||||
@@ -28,6 +28,7 @@ function setToDefaultLayout(schema: any) {
|
||||
const LayoutChoice = (
|
||||
state = {
|
||||
available: [], // all available choices
|
||||
dotplot: false, // is the dotplot toggled on, or not
|
||||
current: undefined, // name of the current layout, eg, 'umap'
|
||||
currentDimNames: [], // dimension name
|
||||
},
|
||||
@@ -46,6 +47,13 @@ const LayoutChoice = (
|
||||
};
|
||||
}
|
||||
|
||||
case "toggle dotplot": {
|
||||
return {
|
||||
...state,
|
||||
dotplot: !state.dotplot,
|
||||
};
|
||||
}
|
||||
|
||||
case "set layout choice": {
|
||||
const { schema } = nextSharedState.annoMatrix;
|
||||
const current = action.layoutChoice;
|
||||
|
||||
@@ -51,27 +51,55 @@ history state processing. The undoable action object contents, by key:
|
||||
filter state are entirely at the discretion of the action filter.
|
||||
|
||||
*/
|
||||
import { Reducer, AnyAction } from "redux";
|
||||
import fromEntries from "../util/fromEntries";
|
||||
|
||||
const historyKeyPrefix = "@@undoable/";
|
||||
const pastKey = `${historyKeyPrefix}past`;
|
||||
const futureKey = `${historyKeyPrefix}future`;
|
||||
const filterStateKey = `${historyKeyPrefix}filterState`;
|
||||
const filterActionKey = `${historyKeyPrefix}filterAction`;
|
||||
const pendingKey = `${historyKeyPrefix}pending`;
|
||||
export const pastKey = "@@undoable/past";
|
||||
export const futureKey = "@@undoable/future";
|
||||
export const filterStateKey = "@@undoable/filterState";
|
||||
export const filterActionKey = "@@undoable/filterAction";
|
||||
export const pendingKey = "@@undoable/pending";
|
||||
const defaultHistoryLimit = -100;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'debug' does not exist on type '{}'.
|
||||
const { debug } = options;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'historyLimit' does not exist on type '{}... Remove this comment to see the full error message
|
||||
export interface UndoableFilterState {
|
||||
[name: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UndoableConfig<FilterStateType extends UndoableFilterState> {
|
||||
debug?: boolean | number;
|
||||
historyLimit?: number;
|
||||
actionFilter?: ActionFilterFn<FilterStateType>;
|
||||
}
|
||||
|
||||
export interface UndoableAction<FilterStateType extends UndoableFilterState> {
|
||||
[filterActionKey]: string;
|
||||
[filterStateKey]?: FilterStateType;
|
||||
}
|
||||
|
||||
export type ActionFilterFn<FilterStateType extends UndoableFilterState> = (
|
||||
undoableState: UndoableState<FilterStateType>,
|
||||
action: AnyAction,
|
||||
filterState?: FilterStateType
|
||||
) => UndoableAction<FilterStateType>;
|
||||
|
||||
export interface UndoableState<FilterStateType extends UndoableFilterState> {
|
||||
[pastKey]: [string, unknown][][];
|
||||
[futureKey]: [string, unknown][][];
|
||||
[pendingKey]: [string, unknown][] | null;
|
||||
[filterStateKey]: FilterStateType | undefined;
|
||||
}
|
||||
|
||||
const Undoable = <FilterStateType extends UndoableFilterState>(
|
||||
reducer: Reducer,
|
||||
undoableKeys: string[],
|
||||
options: UndoableConfig<FilterStateType> = {}
|
||||
): Reducer => {
|
||||
const debug = options?.debug ?? false;
|
||||
let { historyLimit } = options;
|
||||
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
||||
if (historyLimit > 0) historyLimit = -historyLimit;
|
||||
const actionFilter =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
(options as any).actionFilter || (() => ({ [filterActionKey]: "save" }));
|
||||
const actionFilter: ActionFilterFn<FilterStateType> =
|
||||
options?.actionFilter ?? (() => ({ [filterActionKey]: "save" }));
|
||||
|
||||
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
||||
throw new Error("undoable keys array must be specified");
|
||||
@@ -80,8 +108,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Undo the current to previous history
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function undo(currentState: any) {
|
||||
function undo(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
const past = currentState[pastKey];
|
||||
const future = currentState[futureKey];
|
||||
if (past.length === 0) return currentState;
|
||||
@@ -89,7 +118,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
undoableKeysSet.has(kv[0])
|
||||
);
|
||||
const newPast = [...past];
|
||||
const newState = newPast.pop();
|
||||
const newState = newPast.pop() || [];
|
||||
const newFuture = push(future, currentUndoableState);
|
||||
const nextState = {
|
||||
...currentState,
|
||||
@@ -104,8 +133,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Replay future, previously undone.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function redo(currentState: any) {
|
||||
function redo(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
const past = currentState[pastKey] || [];
|
||||
const future = currentState[futureKey] || [];
|
||||
if (future.length === 0) return currentState;
|
||||
@@ -113,7 +143,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
undoableKeysSet.has(kv[0])
|
||||
);
|
||||
const newFuture = [...future];
|
||||
const newState = newFuture.pop();
|
||||
const newState = newFuture.pop() || [];
|
||||
const newPast = push(past, currentUndoableState);
|
||||
const nextState = {
|
||||
...currentState,
|
||||
@@ -128,13 +158,14 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Clear the history state. No side-effects on current state.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function clear(currentState: any) {
|
||||
function clear(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
return {
|
||||
...currentState,
|
||||
[pastKey]: [],
|
||||
[futureKey]: [],
|
||||
[filterStateKey]: {},
|
||||
[filterStateKey]: undefined,
|
||||
[pendingKey]: null,
|
||||
};
|
||||
}
|
||||
@@ -142,8 +173,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Reduce current action, with no history side-effects
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function skip(currentState: any, action: any, filterState: any) {
|
||||
function skip(
|
||||
currentState: UndoableState<FilterStateType>,
|
||||
action: AnyAction,
|
||||
filterState: UndoableFilterState
|
||||
): UndoableState<FilterStateType> {
|
||||
const past = currentState[pastKey] || [];
|
||||
const future = currentState[futureKey] || [];
|
||||
const pending = currentState[pendingKey];
|
||||
@@ -160,8 +194,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Save current state in the history, then reduce action.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function save(currentState: any, action: any, filterState: any) {
|
||||
function save(
|
||||
currentState: UndoableState<FilterStateType>,
|
||||
action: AnyAction,
|
||||
filterState: UndoableFilterState
|
||||
): UndoableState<FilterStateType> {
|
||||
const past = currentState[pastKey] || [];
|
||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||
undoableKeysSet.has(kv[0])
|
||||
@@ -181,8 +218,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Save current state as pending history change. No other side effects.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function stashPending(currentState: any) {
|
||||
function stashPending(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||
undoableKeysSet.has(kv[0])
|
||||
);
|
||||
@@ -195,8 +233,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Cancel pending history state change. No other side effects.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function cancelPending(currentState: any) {
|
||||
function cancelPending(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
return {
|
||||
...currentState,
|
||||
[pendingKey]: null,
|
||||
@@ -206,10 +245,12 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
/*
|
||||
Push pending state onto the history stack
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function applyPending(currentState: any) {
|
||||
const past = currentState[pastKey] || [];
|
||||
function applyPending(
|
||||
currentState: UndoableState<FilterStateType>
|
||||
): UndoableState<FilterStateType> {
|
||||
const past = currentState[pastKey];
|
||||
const pendingState = currentState[pendingKey];
|
||||
if (pendingState === null) return currentState;
|
||||
const newPast = push(past, pendingState, historyLimit);
|
||||
const nextState = {
|
||||
...currentState,
|
||||
@@ -221,14 +262,13 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
currentState = {
|
||||
currentState: UndoableState<FilterStateType> = {
|
||||
[pastKey]: [],
|
||||
[futureKey]: [],
|
||||
[filterStateKey]: {},
|
||||
[filterStateKey]: undefined,
|
||||
[pendingKey]: null,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: any
|
||||
action: AnyAction
|
||||
) => {
|
||||
if (debug > 1) console.log("---- ACTION", action.type);
|
||||
const aType = action.type;
|
||||
@@ -288,8 +328,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function push(arr: any, val: any, limit = undefined) {
|
||||
function push<T = unknown>(arr: T[], val: T, limit?: number) {
|
||||
/*
|
||||
functional array push, with a max length limit to the new array.
|
||||
Like Array.push, except it returns new array and discards as needed
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import StateMachine from "../util/statemachine";
|
||||
import { AnyAction } from "redux";
|
||||
import { StateMachine, FsmActionFn, FsmErrorFn } from "../util/statemachine";
|
||||
import {
|
||||
UndoableConfig,
|
||||
UndoableState,
|
||||
UndoableFilterState,
|
||||
UndoableAction,
|
||||
filterActionKey,
|
||||
filterStateKey,
|
||||
} from "./undoable";
|
||||
import createFsmTransitions from "./undoableFsm";
|
||||
|
||||
const actionKey = "@@undoable/filterAction";
|
||||
const stateKey = "@@undoable/filterState";
|
||||
|
||||
/*
|
||||
these actions will not affect history
|
||||
*/
|
||||
const skipOnActions = new Set([
|
||||
const skipOnActions = new Set<string>([
|
||||
"annoMatrix: init complete",
|
||||
"url changed",
|
||||
"initial data load start",
|
||||
@@ -52,21 +58,18 @@ const skipOnActions = new Set([
|
||||
"geneset: disable add new genes mode",
|
||||
"geneset: activate rename geneset mode",
|
||||
"geneset: disable rename geneset mode",
|
||||
|
||||
/* collections */
|
||||
"collection load complete",
|
||||
]);
|
||||
|
||||
/*
|
||||
identical, repeated occurances of these action types will be debounced.
|
||||
Entire action must be identical (all keys).
|
||||
*/
|
||||
const debounceOnActions = new Set([]);
|
||||
const debounceOnActions = new Set<string>([]);
|
||||
|
||||
/*
|
||||
history will be cleared when these actions occur
|
||||
*/
|
||||
const clearOnActions = new Set([
|
||||
const clearOnActions = new Set<string>([
|
||||
"initial data load complete",
|
||||
"initial data load error",
|
||||
]);
|
||||
@@ -74,7 +77,7 @@ const clearOnActions = new Set([
|
||||
/*
|
||||
An immediate history save will be done for these
|
||||
*/
|
||||
const saveOnActions = new Set([
|
||||
const saveOnActions = new Set<string>([
|
||||
"categorical metadata filter select",
|
||||
"categorical metadata filter deselect",
|
||||
"categorical metadata filter all of these",
|
||||
@@ -85,6 +88,10 @@ const saveOnActions = new Set([
|
||||
"color by expression",
|
||||
"color by geneset mean expression",
|
||||
|
||||
"set dotplot row",
|
||||
"set dotplot column",
|
||||
"toggle dotplot",
|
||||
|
||||
"show centroid labels for category",
|
||||
|
||||
"set scatterplot x",
|
||||
@@ -115,9 +122,6 @@ const saveOnActions = new Set([
|
||||
"geneset: add genes",
|
||||
"geneset: delete genes",
|
||||
"geneset: set gene description",
|
||||
|
||||
/* collections */
|
||||
"collection load complete",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -125,35 +129,43 @@ StateMachine - processing complex action handling - see FSM graph for
|
||||
actual structure, in undoableFsm.js
|
||||
**/
|
||||
|
||||
interface MyFilterState extends UndoableFilterState {
|
||||
prevAction?: AnyAction;
|
||||
fsm: StateMachine<MyUndoableAction> | null;
|
||||
}
|
||||
type MyUndoableAction = UndoableAction<MyFilterState>;
|
||||
|
||||
/*
|
||||
Default FSM actions. Used to side-effect transitions in the graph.
|
||||
See graph definition for the transitions that use each.
|
||||
|
||||
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const stashPending = (fsm: any) => ({
|
||||
[actionKey]: "stashPending",
|
||||
[stateKey]: { fsm },
|
||||
const stashPending: FsmActionFn<MyUndoableAction> = (
|
||||
fsm: StateMachine<MyUndoableAction>
|
||||
) => ({
|
||||
[filterActionKey]: "stashPending",
|
||||
[filterStateKey]: { fsm },
|
||||
});
|
||||
const cancelPending = () => ({
|
||||
[actionKey]: "cancelPending",
|
||||
[stateKey]: { fsm: null },
|
||||
const cancelPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||
[filterActionKey]: "cancelPending",
|
||||
[filterStateKey]: { fsm: null },
|
||||
});
|
||||
const applyPending = () => ({
|
||||
[actionKey]: "applyPending",
|
||||
[stateKey]: { fsm: null },
|
||||
const applyPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||
[filterActionKey]: "applyPending",
|
||||
[filterStateKey]: { fsm: null },
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
||||
const skip = (fsm, transition) => ({
|
||||
[actionKey]: "skip",
|
||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||
const skip: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||
[filterActionKey]: "skip",
|
||||
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||
});
|
||||
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } });
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
||||
const save = (fsm, transition) => ({
|
||||
[actionKey]: "save",
|
||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||
const clear: FsmActionFn<MyUndoableAction> = () => ({
|
||||
[filterActionKey]: "clear",
|
||||
[filterStateKey]: { fsm: null },
|
||||
});
|
||||
const save: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||
[filterActionKey]: "save",
|
||||
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -162,11 +174,13 @@ StateMachine when it doesn't know what to do.
|
||||
|
||||
Signature: (fsm, event, from) => undoableAction
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const onFsmError = (fsm: any, event: any, from: any) => {
|
||||
const onFsmError: FsmErrorFn<MyUndoableAction> = (fsm, event, from) => {
|
||||
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
||||
// In production, try to recover gracefully if we have unexpected state
|
||||
return clear();
|
||||
return {
|
||||
[filterActionKey]: "clear",
|
||||
[filterStateKey]: { fsm: null },
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -181,7 +195,11 @@ const fsmTransitions = createFsmTransitions(
|
||||
save
|
||||
);
|
||||
/* State machine we clone whenever we need to run it */
|
||||
const seedFsm = new StateMachine("init", fsmTransitions, onFsmError);
|
||||
const seedFsm = new StateMachine<MyUndoableAction>(
|
||||
"init",
|
||||
fsmTransitions,
|
||||
onFsmError
|
||||
);
|
||||
|
||||
/*
|
||||
See undoable.js for description action filter interface description.
|
||||
@@ -191,37 +209,34 @@ Basic approach:
|
||||
* only implement complex state machines where absolutely required (eg,
|
||||
multi-event selection and the like)
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
const actionFilter =
|
||||
(debug: any) =>
|
||||
(debug: boolean) =>
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
state: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
prevFilterState: any
|
||||
) => {
|
||||
state: UndoableState<MyFilterState>,
|
||||
action: AnyAction,
|
||||
prevFilterState: MyFilterState | undefined
|
||||
): UndoableAction<MyFilterState> => {
|
||||
const actionType = action.type;
|
||||
const filterState = {
|
||||
prevFilterState = prevFilterState || { fsm: null };
|
||||
const filterState: MyFilterState = {
|
||||
...prevFilterState,
|
||||
prevAction: action,
|
||||
};
|
||||
if (skipOnActions.has(actionType)) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||
}
|
||||
if (
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
debounceOnActions.has(actionType) &&
|
||||
prevFilterState.prevAction &&
|
||||
shallowObjectEq(action, prevFilterState.prevAction)
|
||||
) {
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||
}
|
||||
if (clearOnActions.has(actionType)) {
|
||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
||||
return { [filterActionKey]: "clear", [filterStateKey]: filterState };
|
||||
}
|
||||
if (saveOnActions.has(actionType)) {
|
||||
return { [actionKey]: "save", [stateKey]: filterState };
|
||||
return { [filterActionKey]: "save", [filterStateKey]: filterState };
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -238,7 +253,7 @@ const actionFilter =
|
||||
|
||||
/* else, we have no idea what this is - skip it */
|
||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
||||
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -247,8 +262,10 @@ return true if objA and objB are ===, OR if:
|
||||
- have same own properties
|
||||
- all values are strict equal (===)
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function shallowObjectEq(objA: any, objB: any) {
|
||||
function shallowObjectEq(
|
||||
objA: Record<string | number | symbol, unknown>,
|
||||
objB: Record<string | number | symbol, unknown>
|
||||
) {
|
||||
if (objA === objB) return true;
|
||||
if (!objA || !objB) return false;
|
||||
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
||||
@@ -260,8 +277,7 @@ function shallowObjectEq(objA: any, objB: any) {
|
||||
return true if arrA and arrB contain the same strict-equal values,
|
||||
in the same order.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function shallowArrayEq(arrA: any, arrB: any) {
|
||||
function shallowArrayEq(arrA: unknown[], arrB: unknown[]) {
|
||||
if (arrA.length !== arrB.length) return false;
|
||||
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
||||
if (arrA[i] !== arrB[i]) return false;
|
||||
@@ -276,7 +292,7 @@ Set to true or 1 for base logging, high number for more verbosity (currently onl
|
||||
or 2).
|
||||
*/
|
||||
const debug = false;
|
||||
const undoableConfig = {
|
||||
const undoableConfig: UndoableConfig<MyFilterState> = {
|
||||
debug,
|
||||
historyLimit: 50, // maximum history size
|
||||
actionFilter: actionFilter(debug),
|
||||
@@ -315,7 +331,7 @@ if (debug) {
|
||||
);
|
||||
if (trivialOverlapWithFsm.size > 0) {
|
||||
console.error(
|
||||
"Undoable misconfiguration - trivival action filter blocking FSM filter",
|
||||
"Undoable misconfiguration - trivial action filter blocking FSM filter",
|
||||
[...trivialOverlapWithFsm]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,22 +18,16 @@ b) compound actions that should be collapsed into a single history change.
|
||||
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||
const createFsmTransitions = (
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
stashPending: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
cancelPending: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
applyPending: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
skip: any,
|
||||
// @ts-expect-error ts-migrate(6133) FIXME: 'clear' is declared but its value is never read.
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
clear: any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
save: any
|
||||
) => [
|
||||
import { StateMachine, FsmTransition, FsmActionFn } from "../util/statemachine";
|
||||
|
||||
const createFsmTransitions = <ActionReturnType>(
|
||||
stashPending: FsmActionFn<ActionReturnType>,
|
||||
cancelPending: FsmActionFn<ActionReturnType>,
|
||||
applyPending: FsmActionFn<ActionReturnType>,
|
||||
skip: FsmActionFn<ActionReturnType>,
|
||||
_clear: FsmActionFn<ActionReturnType>,
|
||||
save: FsmActionFn<ActionReturnType>
|
||||
): FsmTransition<ActionReturnType>[] => [
|
||||
/* graph selection brushing */
|
||||
{
|
||||
event: "graph brush start",
|
||||
@@ -52,12 +46,14 @@ const createFsmTransitions = (
|
||||
from: "graph brush in progress",
|
||||
to: "done",
|
||||
/* if current selection is all, cancelPending. Else, applyPending */
|
||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: (fsm: any, transition: any, data: any) =>
|
||||
action: (
|
||||
fsm: StateMachine<ActionReturnType>,
|
||||
transition: FsmTransition<ActionReturnType>,
|
||||
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
|
||||
) =>
|
||||
data.state.graphSelection.selection.mode === "all"
|
||||
? cancelPending()
|
||||
: applyPending(),
|
||||
? cancelPending(fsm, transition, data)
|
||||
: applyPending(fsm, transition, data),
|
||||
},
|
||||
{
|
||||
event: "graph brush end",
|
||||
@@ -84,12 +80,14 @@ const createFsmTransitions = (
|
||||
from: "graph lasso in progress",
|
||||
to: "done",
|
||||
/* if current selection is all, cancelPending. Else, applyPending */
|
||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
action: (fsm: any, transition: any, data: any) =>
|
||||
action: (
|
||||
fsm: StateMachine<ActionReturnType>,
|
||||
transition: FsmTransition<ActionReturnType>,
|
||||
data: any // eslint-disable-line @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. Requires state typing.
|
||||
) =>
|
||||
data.state.graphSelection.selection.mode === "all"
|
||||
? cancelPending()
|
||||
: applyPending(),
|
||||
? cancelPending(fsm, transition, data)
|
||||
: applyPending(fsm, transition, data),
|
||||
},
|
||||
{
|
||||
event: "graph lasso end",
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
|
||||
// weird cross-dependency that we should clean up someday...
|
||||
import {
|
||||
isTypedArray,
|
||||
isArrayOrTypedArray,
|
||||
callOnceLazy,
|
||||
memoize,
|
||||
__getMemoId,
|
||||
} from "./util";
|
||||
import { callOnceLazy, memoize, __getMemoId } from "./util";
|
||||
import { isTypedArray, isAnyArray } from "../../common/types/arraytypes";
|
||||
import {
|
||||
summarizeContinuous,
|
||||
summarizeCategorical as _summarizeCategorical,
|
||||
@@ -163,7 +158,7 @@ class Dataframe {
|
||||
if (!Array.isArray(columnarData)) {
|
||||
throw new TypeError("Dataframe constructor requires array of columns");
|
||||
}
|
||||
if (!columnarData.every((c) => isArrayOrTypedArray(c))) {
|
||||
if (!columnarData.every((c) => isAnyArray(c))) {
|
||||
throw new TypeError("Dataframe columns must all be Array or TypedArray");
|
||||
}
|
||||
if (!isLabelIndex(rowIndex)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Dataframe histogram
|
||||
*/
|
||||
import { isTypedArray } from "./util";
|
||||
import { isTypedArray } from "../../common/types/arraytypes";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function _histogramContinuous(column: any, bins: any, min: any, max: any) {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
Private utility code for dataframe
|
||||
*/
|
||||
|
||||
export { isTypedArray, isArrayOrTypedArray } from "../typeHelpers";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function callOnceLazy(f: any) {
|
||||
/*
|
||||
|
||||
@@ -3,7 +3,7 @@ Helper functions for user-editable annotations state management.
|
||||
See also reducers/annotations.js
|
||||
*/
|
||||
|
||||
import { Schema } from "../../common/types/entities";
|
||||
import { Schema } from "../../common/types/schema";
|
||||
|
||||
/*
|
||||
There are a number of state constraints assumed throughout the
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
Helper functions for querying, binding and sorting Portal dataset meta and collections.
|
||||
*/
|
||||
|
||||
/* app dependencies */
|
||||
import * as globals from "../../globals";
|
||||
|
||||
export function createAPIPrefix(
|
||||
existingPrefix: string,
|
||||
replaceWithPrefix: string
|
||||
): string {
|
||||
/*
|
||||
When selecting a dataset from the dataset selector, the globals API prefix value must be updated to match the selected
|
||||
dataset's deployment URL. For example, update protocol://origin/dataRoot/current-dataset.cxg/api/v2 to
|
||||
protocol://origin/dataRoot/selected-dataset.cxg/api/v2.
|
||||
TODO(cc) revisit updating globals API prefix locally, possibly move to back end?
|
||||
*/
|
||||
const newDataRootAndDeploymentId =
|
||||
bindDataRootAndDeploymentId(replaceWithPrefix);
|
||||
return existingPrefix.replace(
|
||||
/([a-z0-9_.-]+\/[a-z0-9_.-]+\.cxg)/i,
|
||||
newDataRootAndDeploymentId
|
||||
);
|
||||
}
|
||||
|
||||
export function createDatasetUrl(deploymentUrl: string): string {
|
||||
/*
|
||||
Switch out dataset URL origin to the current location's origin. For local environments, also replace the
|
||||
data root of the given URL to /d/. For example, update protocol://origin/dataRoot/dataset.cxg to
|
||||
protocol://origin/d/dataset.cxg.
|
||||
TODO(cc) revisit special handling for canary and local environments.
|
||||
*/
|
||||
const dataRoot = globals.API.local ? "d" : bindDataRoot(deploymentUrl);
|
||||
const deploymentId = bindDeploymentId(deploymentUrl);
|
||||
return `${window.location.origin}/${dataRoot}/${deploymentId}/`;
|
||||
}
|
||||
|
||||
export function createExplorerUrl(): string {
|
||||
/*
|
||||
The current URL is passed as an "explorer URL" query string parameter to the dataset meta API. For environments where
|
||||
the current origin does not match the origin specified in the dataset deployment URLs (eg local, canary), update the
|
||||
origin to be the origin specified in the globals. Also update the data root for local environments.
|
||||
TODO(cc) revisit special handling for local and canary environments
|
||||
*/
|
||||
const url = window.location.href;
|
||||
if (url.indexOf(globals.API.origin) === 0) {
|
||||
return url;
|
||||
}
|
||||
const { origin } = globals.API;
|
||||
if (globals.API.local) {
|
||||
const dataRoot = "e";
|
||||
const deploymentId = bindDeploymentId(url);
|
||||
return `${origin}${dataRoot}/${deploymentId}/`;
|
||||
}
|
||||
const dataRootAndDeploymentId = bindDataRootAndDeploymentId(url);
|
||||
return `${origin}${dataRootAndDeploymentId}/`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function sortDatasets(vm0: any, vm1: any): number {
|
||||
/*
|
||||
Sort datasets by cell count, descending.
|
||||
*/
|
||||
return (vm1.cell_count ?? 0) - (vm0.cell_count ?? 0);
|
||||
}
|
||||
|
||||
function bindDataRoot(pathOrUrl: string): string {
|
||||
/*
|
||||
read dataroot from path. given "protocol://hostname/x/any-alpha-numeric.cxg/", match on "/x/",
|
||||
read data root from path. given "protocol://origin/x/any-alpha-numeric.cxg/", match on "/x/",
|
||||
group on "x".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]+)\/[a-z0-9_.-]+\.cxg\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(`Unable to bind data root from "${pathOrUrl}"`);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
function bindDataRootAndDeploymentId(pathOrUrl: string): string {
|
||||
/*
|
||||
Read data root and deployment ID from path. Given "protocol://origin/x/any-alpha-numeric.cxg/", match on
|
||||
"/x/any-alpha-numeric.cxg/", group on "x/any-alpha-numeric.cxg".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]+\/[a-z0-9_.-]+\.cxg)\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
if (globals.API.local) {
|
||||
return "e/792d29b8-83d4-4e6e-b3ce-cad060d1a23b.cxg"; // TODO(cc) default data root and deployment ID for local (single mode)
|
||||
}
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(
|
||||
`Unable to bind data root and deployment ID from "${pathOrUrl}"`
|
||||
);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
function bindDeploymentId(pathOrUrl: string): string {
|
||||
/*
|
||||
read name of cxg from path. given "protocol://origin/x/any-alpha-numeric.cxg/", match on "/any-alpha-numeric.cxg/",
|
||||
group on "any-alpha-numeric.cxg".
|
||||
*/
|
||||
const matches = pathOrUrl.match(/\/([a-z0-9_.-]*\.cxg)\//i);
|
||||
if (!matches || matches.length < 2) {
|
||||
if (globals.API.local) {
|
||||
return "792d29b8-83d4-4e6e-b3ce-cad060d1a23b.cxg"; // TODO(cc) default dataset for local (single mode)
|
||||
}
|
||||
// Expecting at least match and one capturing group
|
||||
throw new Error(`Unable to bind deployment ID from "${pathOrUrl}"`);
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
@@ -65,6 +65,28 @@ export function createColorQuery(
|
||||
},
|
||||
];
|
||||
}
|
||||
case "color by dotplot columns": {
|
||||
/*
|
||||
Color by COLUMNS is a mode at the UI level,
|
||||
as we are going to be keeping track of many color scales —
|
||||
one per column in the dotplot. The query is for
|
||||
one gene at a time.
|
||||
*/
|
||||
const varIndex = schema?.annotations?.var?.index;
|
||||
|
||||
if (!varIndex) return null;
|
||||
|
||||
return [
|
||||
"X",
|
||||
{
|
||||
where: {
|
||||
field: "var",
|
||||
column: varIndex,
|
||||
value: colorByAccessor,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { flatbuffers } from "flatbuffers";
|
||||
import { NetEncoding } from "./matrix_generated";
|
||||
import { isTypedArray, isFpTypedArray } from "../typeHelpers";
|
||||
import { isTypedArray, isFloatTypedArray } from "../../common/types/arraytypes";
|
||||
import {
|
||||
Dataframe,
|
||||
IdentityInt32Index,
|
||||
@@ -188,7 +188,7 @@ function promoteTypedArray(o: any) {
|
||||
TODO - future optimization: not all int32/uint32 data series require
|
||||
promotion to float64. We COULD simply look at the data to decide.
|
||||
*/
|
||||
if (isFpTypedArray(o) || Array.isArray(o)) return o;
|
||||
if (isFloatTypedArray(o) || Array.isArray(o)) return o;
|
||||
|
||||
let TypedArrayCtor;
|
||||
switch (o.constructor) {
|
||||
@@ -246,7 +246,7 @@ export function matrixFBSToDataframe(arrayBuffers: any) {
|
||||
.map((fb: any) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
fb.columns.map((c: any) => {
|
||||
if (isFpTypedArray(c) || Array.isArray(c)) return c;
|
||||
if (isFloatTypedArray(c) || Array.isArray(c)) return c;
|
||||
return promoteTypedArray(c);
|
||||
})
|
||||
)
|
||||
|
||||
@@ -11,9 +11,9 @@ import catLabelSort from "../catLabelSort";
|
||||
import {
|
||||
RawSchema,
|
||||
Schema,
|
||||
LayoutColumn,
|
||||
AnnotationColumn,
|
||||
} from "../../common/types/entities";
|
||||
EmbeddingSchema,
|
||||
AnnotationColumnSchema,
|
||||
} from "../../common/types/schema";
|
||||
|
||||
/*
|
||||
System wide schema assumptions:
|
||||
@@ -86,7 +86,7 @@ export function removeObsAnnoColumn(schema: Schema, name: string): Schema {
|
||||
export function addObsAnnoColumn(
|
||||
schema: Schema,
|
||||
_: string,
|
||||
defn: AnnotationColumn
|
||||
defn: AnnotationColumnSchema
|
||||
): Schema {
|
||||
const newSchema = _copyObsAnno(schema);
|
||||
|
||||
@@ -142,7 +142,7 @@ export function addObsAnnoCategory(schema: any, name: any, category: any) {
|
||||
return newSchema;
|
||||
}
|
||||
|
||||
export function addObsLayout(schema: Schema, layout: LayoutColumn): Schema {
|
||||
export function addObsLayout(schema: Schema, layout: EmbeddingSchema): Schema {
|
||||
/* add or replace a layout */
|
||||
const newSchema = _copyObsLayout(schema);
|
||||
newSchema.layout.obs.push(layout);
|
||||
|
||||
@@ -13,18 +13,17 @@ Where:
|
||||
to: state_name_transitioning_to,
|
||||
from: state_name_transitioning_from,
|
||||
event: value_that_will_cause_transition,
|
||||
action: optional_callback_upon_transition
|
||||
action: callback_upon_transition
|
||||
}
|
||||
The transition will be provided to the action callback, so other data
|
||||
may be stored in the transition object for use by the action callback.
|
||||
* onErrorCallback - a callback function called if the FSM receives an event
|
||||
for which it has no defined transition.
|
||||
|
||||
|
||||
Interface:
|
||||
* states - property containing the state names. A Set(), contianing the
|
||||
* states - property containing the state names. A Set(), containing the
|
||||
union of to: and from: values.
|
||||
* events - property containing all of the accepted event values. Set().
|
||||
* events - property containing all of the accepted event values. Set().
|
||||
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
|
||||
* clone() - clone the entire statemachine.
|
||||
* next(eventValue) - drive the FSM to the next state. If the event
|
||||
@@ -40,47 +39,69 @@ Example:
|
||||
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
|
||||
fsm.next("yo"); // returns 42
|
||||
|
||||
|
||||
*/
|
||||
export default class StateMachine {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
events: any;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
graph: any;
|
||||
export type FsmState = number | string;
|
||||
export type FsmEvent = string; // by convention, we assume Events are redux action types, aka strings
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
onError: any;
|
||||
export type FsmActionFn<ActionReturnType> = (
|
||||
fsm: StateMachine<ActionReturnType>,
|
||||
transition: FsmTransition<ActionReturnType>,
|
||||
data: unknown
|
||||
) => ActionReturnType;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
state: any;
|
||||
export interface FsmTransition<ActionReturnType> {
|
||||
from: FsmState;
|
||||
to: FsmState;
|
||||
event: FsmEvent;
|
||||
action: FsmActionFn<ActionReturnType>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
states: any;
|
||||
export type FsmErrorFn<ActionReturnType> = (
|
||||
fsm: StateMachine<ActionReturnType>,
|
||||
event: FsmEvent,
|
||||
state: FsmState
|
||||
) => ActionReturnType;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
constructor(initState: any, transitions: any, onError: any) {
|
||||
this.onError = onError || (() => undefined);
|
||||
export class StateMachine<ActionReturnType> {
|
||||
events: Set<FsmEvent>;
|
||||
|
||||
graph: Map<FsmEvent, Map<FsmState, FsmTransition<ActionReturnType>>>;
|
||||
|
||||
onError: FsmErrorFn<ActionReturnType>;
|
||||
|
||||
state: FsmState;
|
||||
|
||||
states: Set<FsmState>;
|
||||
|
||||
constructor(
|
||||
initState: FsmState,
|
||||
transitions: FsmTransition<ActionReturnType>[],
|
||||
onError: FsmErrorFn<ActionReturnType>
|
||||
) {
|
||||
this.onError = onError;
|
||||
this.state = initState;
|
||||
|
||||
// all states
|
||||
this.states = new Set(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
transitions.reduce((names: any, tsn: any) => {
|
||||
names.push(tsn.from);
|
||||
names.push(tsn.to);
|
||||
return names;
|
||||
}, [])
|
||||
transitions.reduce(
|
||||
(names: Array<FsmState>, tsn: FsmTransition<ActionReturnType>) => {
|
||||
names.push(tsn.from);
|
||||
names.push(tsn.to);
|
||||
return names;
|
||||
},
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
// all transition names (aka events)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
this.events = new Set(transitions.map((tsn: any) => tsn.event));
|
||||
this.events = new Set(
|
||||
transitions.map((tsn: FsmTransition<ActionReturnType>) => tsn.event)
|
||||
);
|
||||
|
||||
// the transition graph.
|
||||
// graph[event][from] -> transition
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
this.graph = transitions.reduce((graph: any, tsn: any) => {
|
||||
this.graph = transitions.reduce((graph, tsn) => {
|
||||
const { event, from } = tsn;
|
||||
if (!graph.has(event)) graph.set(event, new Map());
|
||||
const tsnMap = graph.get(event);
|
||||
@@ -89,29 +110,23 @@ export default class StateMachine {
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
clone(initState: any) {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
||||
const fsm = new StateMachine(initState, []);
|
||||
fsm.onError = this.onError;
|
||||
clone(initState: FsmState): StateMachine<ActionReturnType> {
|
||||
const fsm = new StateMachine<ActionReturnType>(initState, [], this.onError);
|
||||
fsm.states = this.states;
|
||||
fsm.events = this.events;
|
||||
fsm.graph = this.graph;
|
||||
return fsm;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
next(event: any, data: any) {
|
||||
next(event: FsmEvent, data: unknown): ActionReturnType {
|
||||
const { graph, state } = this;
|
||||
const tsnMap = graph.get(event);
|
||||
if (!tsnMap) return this.onError(this, event, state, undefined);
|
||||
if (!tsnMap) return this.onError(this, event, state);
|
||||
|
||||
const transition = tsnMap.get(state);
|
||||
if (!transition) return this.onError(this, event, state, undefined);
|
||||
if (!transition) return this.onError(this, event, state);
|
||||
|
||||
this.state = transition.to;
|
||||
return transition.action
|
||||
? transition.action(this, transition, data)
|
||||
: undefined;
|
||||
return transition.action(this, transition, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
Various type and schema related helper functions.
|
||||
*/
|
||||
|
||||
/*
|
||||
Utility function to test for a typed array
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function isTypedArray(x: any) {
|
||||
return (
|
||||
ArrayBuffer.isView(x) &&
|
||||
Object.prototype.toString.call(x) !== "[object DataView]"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Test for float typed array, ie, Float32TypedArray or Float64TypedArray
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function isFpTypedArray(x: any) {
|
||||
let constructor;
|
||||
const isFloatArray =
|
||||
x &&
|
||||
({ constructor } = x) &&
|
||||
(constructor === Float32Array || constructor === Float64Array);
|
||||
return isFloatArray;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
export function isArrayOrTypedArray(x: any) {
|
||||
return Array.isArray(x) || isTypedArray(x);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
upperBoundIndirect,
|
||||
} from "./sort";
|
||||
import { makeSortIndex } from "./util";
|
||||
import { isAnyArray } from "../../common/types/arraytypes";
|
||||
|
||||
class NotImplementedError extends Error {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
@@ -450,7 +451,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
||||
value,
|
||||
new ValueArrayType(data.length)
|
||||
);
|
||||
} else if (isArrayOrTypedArray(value)) {
|
||||
} else if (isAnyArray(value)) {
|
||||
// Create value array from user-provided array. Typically used
|
||||
// only by enumerated dimensions
|
||||
array = this._createValueArray(
|
||||
@@ -729,15 +730,6 @@ export const DimTypes = {
|
||||
spatial: ImmutableSpatialDimension,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function isArrayOrTypedArray(x: any) {
|
||||
return (
|
||||
Array.isArray(x) ||
|
||||
(ArrayBuffer.isView(x) &&
|
||||
Object.prototype.toString.call(x) !== "[object DataView]")
|
||||
);
|
||||
}
|
||||
|
||||
/* return bounding box of the polygon */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||
function polygonBoundingBox(polygon: any) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isTypedArray, isFpTypedArray } from "../typeHelpers";
|
||||
import { isTypedArray, isFloatTypedArray } from "../../common/types/arraytypes";
|
||||
|
||||
/* eslint-disable no-bitwise -- code relies on bitwise ops */
|
||||
|
||||
@@ -219,7 +219,7 @@ export function sortArray(arr: any) {
|
||||
return quicksort(arr, 0, arr.length - 1);
|
||||
}
|
||||
if (isTypedArray(arr)) {
|
||||
if (isFpTypedArray(arr)) {
|
||||
if (isFloatTypedArray(arr)) {
|
||||
return quicksortFloats(arr, 0, arr.length - 1);
|
||||
}
|
||||
return quicksort(arr, 0, arr.length - 1);
|
||||
@@ -230,7 +230,7 @@ export function sortArray(arr: 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 sortIndex(index: any, source: any) {
|
||||
if (isFpTypedArray(source))
|
||||
if (isFloatTypedArray(source))
|
||||
return quicksortFloatsIndirect(index, source, 0, index.length - 1);
|
||||
return quicksortIndirect(index, source, 0, index.length - 1);
|
||||
}
|
||||
@@ -293,7 +293,7 @@ function lowerBoundFloat(valueArray: any, value: any, first: any, last: 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 lowerBound(valueArray: any, value: any, first: any, last: any) {
|
||||
if (isFpTypedArray(valueArray)) {
|
||||
if (isFloatTypedArray(valueArray)) {
|
||||
return lowerBoundFloat(valueArray, value, first, last);
|
||||
}
|
||||
return lowerBoundNonFloat(valueArray, value, first, last);
|
||||
@@ -366,7 +366,7 @@ export function lowerBoundIndirect(
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
last: any
|
||||
) {
|
||||
if (isFpTypedArray(valueArray)) {
|
||||
if (isFloatTypedArray(valueArray)) {
|
||||
return lowerBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
||||
}
|
||||
return lowerBoundNonFloatIndirect(valueArray, indexArray, value, first, last);
|
||||
@@ -425,7 +425,7 @@ function upperBoundFloat(valueArray: any, value: any, first: any, last: 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 upperBound(valueArray: any, value: any, first: any, last: any) {
|
||||
if (isFpTypedArray(valueArray)) {
|
||||
if (isFloatTypedArray(valueArray)) {
|
||||
return upperBoundFloat(valueArray, value, first, last);
|
||||
}
|
||||
return upperBoundNonFloat(valueArray, value, first, last);
|
||||
@@ -498,7 +498,7 @@ export function upperBoundIndirect(
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||
last: any
|
||||
) {
|
||||
if (isFpTypedArray(valueArray)) {
|
||||
if (isFloatTypedArray(valueArray)) {
|
||||
return upperBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
||||
}
|
||||
return upperBoundNonFloatIndirect(valueArray, indexArray, value, first, last);
|
||||
|
||||
@@ -26,7 +26,7 @@ Please scroll down the section below for how to release a patch version. Follow
|
||||
- Write the release title and release notes and add to [release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit)
|
||||
2. Create a release branch, eg, `release-version-0.16.0`
|
||||
3. In the release branch, run `make create-release-candidate PART=[major | minor | patch]` where you choose major/minor/patch depending on which part of the version is being bumped (e.g., `0.2.9` -> `0.3.0` is minor version bump). This will bump the version and create a release *candidate* version (i.e. `0.3.0-rc.0`).
|
||||
4. Commit and push the new branch. This will trigger tests to ensure that your branch isn't broken.
|
||||
4. Push the new branch to origin and open a `DO NOT MERGE` PR, this will run tests on your branch.
|
||||
5. Upload the release candidate to Test PyPI by running the command `make release-candidate-to-test-pypi`. (Make sure you are registered for PyPI and Test PyPI and you have write access to the cellxgene PyPI package for both).
|
||||
6. Verify the release candidate in a fresh virtual environment by running `make install-release-test` which installs the cellxgene build you just uploaded the Test PyPI.
|
||||
7. If you find errors with the release candidate, run `make recreate-release-candidate` to increment the release candidate version (i.e. `0.3.0-rc.0` -> `0.3.0-rc.1`). Then go back to Steps 5 and 6 to re-upload and re-test the new release candidate.
|
||||
|
||||
Reference in New Issue
Block a user