mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 06:08:12 +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:
|
env:
|
||||||
JEST_ENV: prod
|
JEST_ENV: prod
|
||||||
|
CXG_AUTH_TYPE: none
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
|
|||||||
+4
-2
@@ -5,7 +5,9 @@ GENE_SETS := $(if $(GENE_SETS),$(GENE_SETS),../backend/test/fixtures/pbmc3k-gene
|
|||||||
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
||||||
GENE_SETS_FILENAME := $(shell basename $(GENE_SETS))
|
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
|
# Packaging
|
||||||
@@ -38,7 +40,7 @@ smoke-test:
|
|||||||
start_server_and_test \
|
start_server_and_test \
|
||||||
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
|
||||||
$(CXG_SERVER_PORT) \
|
$(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
|
# start an instance of cellxgene and run the end-to-end annotations tests
|
||||||
.PHONY: smoke-test-annotations
|
.PHONY: smoke-test-annotations
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { Reducer } from "redux";
|
||||||
import undoable from "../../src/reducers/undoable";
|
import undoable from "../../src/reducers/undoable";
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
test("no keys", () => {
|
test("no keys", () => {
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2-3 arguments, but got 1.
|
expect(() =>
|
||||||
expect(() => undoable(() => {})).toThrow();
|
undoable(() => {}, undefined as unknown as string[])
|
||||||
expect(() => undoable(() => {}, null)).toThrow();
|
).toThrow();
|
||||||
|
expect(() => undoable(() => {}, null as unknown as string[])).toThrow();
|
||||||
expect(() => undoable(() => {}, [])).toThrow();
|
expect(() => undoable(() => {}, [])).toThrow();
|
||||||
expect(() => undoable(() => {}, [], {})).toThrow();
|
expect(() => undoable(() => {}, [], {})).toThrow();
|
||||||
});
|
});
|
||||||
@@ -24,8 +26,7 @@ describe("create", () => {
|
|||||||
describe("undo", () => {
|
describe("undo", () => {
|
||||||
test("expected state modifications", () => {
|
test("expected state modifications", () => {
|
||||||
const initialState = { a: 0, b: 1000 };
|
const initialState = { a: 0, b: 1000 };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
|
||||||
const undoableReducer = undoable(reducer, ["a"]);
|
const undoableReducer = undoable(reducer, ["a"]);
|
||||||
|
|
||||||
const s1 = undoableReducer(initialState, { type: "test" });
|
const s1 = undoableReducer(initialState, { type: "test" });
|
||||||
@@ -43,10 +44,8 @@ describe("undo", () => {
|
|||||||
|
|
||||||
describe("redo", () => {
|
describe("redo", () => {
|
||||||
const initialState = { a: 0, b: 1000 };
|
const initialState = { a: 0, b: 1000 };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||||
const reducer = (state: any) => ({ a: state.a + 1, b: state.b + 1 });
|
let UR: Reducer;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
let UR: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UR = undoable(reducer, ["a"]);
|
UR = undoable(reducer, ["a"]);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function makeMockColumn(s: any, length: any) {
|
|||||||
return new Array(length).fill(s.categories[0]);
|
return new Array(length).fill(s.categories[0]);
|
||||||
|
|
||||||
default:
|
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 } = {
|
export const schema: { schema: RawSchema } = {
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import zip from "lodash.zip";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { flatbuffers } from "flatbuffers";
|
import { flatbuffers } from "flatbuffers";
|
||||||
import { NetEncoding } from "../../../src/util/stateManager/matrix_generated";
|
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.
|
test data mocking REST 0.2 API responses. Used in several tests.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
removeObsAnnoCategory,
|
removeObsAnnoCategory,
|
||||||
addObsLayout,
|
addObsLayout,
|
||||||
} from "../util/stateManager/schemaHelpers";
|
} from "../util/stateManager/schemaHelpers";
|
||||||
import { isArrayOrTypedArray } from "../util/typeHelpers";
|
import { isAnyArray } from "../common/types/arraytypes";
|
||||||
import { _whereCacheCreate } from "./whereCache";
|
import { _whereCacheCreate } from "./whereCache";
|
||||||
import AnnoMatrix from "./annoMatrix";
|
import AnnoMatrix from "./annoMatrix";
|
||||||
import PromiseLimit from "../util/promiseLimit";
|
import PromiseLimit from "../util/promiseLimit";
|
||||||
@@ -134,7 +134,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
let data;
|
let data;
|
||||||
if (isArrayOrTypedArray(value)) {
|
if (isAnyArray(value)) {
|
||||||
if (value.constructor !== Ctor)
|
if (value.constructor !== Ctor)
|
||||||
throw new Error("Mismatched value array type");
|
throw new Error("Mismatched value array type");
|
||||||
if (value.length !== this.nObs)
|
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
|
// If a globally shared type or interface doesn't have a clear owner, put it here
|
||||||
|
|
||||||
export type Category = number | string | boolean;
|
export {};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import LeftSideBar from "./leftSidebar";
|
|||||||
import RightSideBar from "./rightSidebar";
|
import RightSideBar from "./rightSidebar";
|
||||||
import Legend from "./continuousLegend";
|
import Legend from "./continuousLegend";
|
||||||
import Graph from "./graph/graph";
|
import Graph from "./graph/graph";
|
||||||
|
import Dotplot from "./dotplot";
|
||||||
import MenuBar from "./menubar";
|
import MenuBar from "./menubar";
|
||||||
import Autosave from "./autosave";
|
import Autosave from "./autosave";
|
||||||
import Embedding from "./embedding";
|
import Embedding from "./embedding";
|
||||||
@@ -23,6 +24,7 @@ import actions from "../actions";
|
|||||||
error: (state as any).controls.error,
|
error: (state as any).controls.error,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
graphRenderCounter: (state as any).controls.graphRenderCounter,
|
graphRenderCounter: (state as any).controls.graphRenderCounter,
|
||||||
|
layoutChoice: (state as any).layoutChoice,
|
||||||
}))
|
}))
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||||
@@ -47,7 +49,7 @@ class App extends React.Component {
|
|||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||||
render() {
|
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
|
// @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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Helmet title="cellxgene" />
|
<Helmet title="cellxgene" />
|
||||||
@@ -87,8 +89,14 @@ class App extends React.Component {
|
|||||||
<TermsOfServicePrompt />
|
<TermsOfServicePrompt />
|
||||||
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
|
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
|
||||||
<Legend viewportRef={viewportRef} />
|
<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 */}
|
{/* @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 />
|
<RightSideBar />
|
||||||
|
|||||||
@@ -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,
|
schema: (state as any).annoMatrix?.schema,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
crossfilter: (state as any).obsCrossfilter,
|
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.
|
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
||||||
class Embedding extends React.PureComponent<{}, EmbeddingState> {
|
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.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||||
render() {
|
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
|
// @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;
|
const { annoMatrix } = crossfilter;
|
||||||
return (
|
return (
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
display: "inherit",
|
display: dotplot ? "none" : "inherit",
|
||||||
left: 8,
|
left: 8,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
|
|||||||
@@ -914,6 +914,7 @@ class Graph extends React.Component<{}, GraphState> {
|
|||||||
pointDilation,
|
pointDilation,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message
|
||||||
crossfilter,
|
crossfilter,
|
||||||
|
dotplotMode,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
|
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
|
||||||
const cameraTF = camera?.view()?.slice();
|
const cameraTF = camera?.view()?.slice();
|
||||||
@@ -924,6 +925,7 @@ class Graph extends React.Component<{}, GraphState> {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
display: dotplotMode ? "none" : "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GraphOverlayLayer
|
<GraphOverlayLayer
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -92,6 +92,7 @@ export default class MiniHistogram extends React.PureComponent {
|
|||||||
height,
|
height,
|
||||||
borderBottom: "solid rgb(230, 230, 230) 0.25px",
|
borderBottom: "solid rgb(230, 230, 230) 0.25px",
|
||||||
}}
|
}}
|
||||||
|
className="mini-histo"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
ref={this.canvasRef}
|
ref={this.canvasRef}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default class MiniStackedBar extends React.PureComponent {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}}
|
}}
|
||||||
|
className="mini-stacked-bar-canvas"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
ref={this.canvasRef}
|
ref={this.canvasRef}
|
||||||
|
|||||||
@@ -566,6 +566,7 @@ class Scatterplot extends React.PureComponent<{}, State> {
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
data-testid="scatterplot"
|
data-testid="scatterplot"
|
||||||
|
className="scatterplot-canvas"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: margin.left,
|
marginLeft: margin.left,
|
||||||
marginTop: margin.top,
|
marginTop: margin.top,
|
||||||
|
|||||||
@@ -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.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
||||||
const ColorsReducer = (
|
const ColorsReducer = (
|
||||||
state = {
|
state = {
|
||||||
|
/* TODO(colinmegill) #632 remove hardcode for dev */
|
||||||
colorMode: null /* by continuous, by expression */,
|
colorMode: null /* by continuous, by expression */,
|
||||||
colorAccessor: null /* tissue, Apod */,
|
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 categorical metadata":
|
||||||
case "color by continuous metadata": {
|
case "color by continuous metadata": {
|
||||||
/* toggle between this mode and reset */
|
/* 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;
|
||||||
@@ -17,6 +17,7 @@ import controls from "./controls";
|
|||||||
import annotations from "./annotations";
|
import annotations from "./annotations";
|
||||||
import genesets from "./genesets";
|
import genesets from "./genesets";
|
||||||
import genesetsUI from "./genesetsUI";
|
import genesetsUI from "./genesetsUI";
|
||||||
|
import dotplot from "./dotplot";
|
||||||
import autosave from "./autosave";
|
import autosave from "./autosave";
|
||||||
import centroidLabels from "./centroidLabels";
|
import centroidLabels from "./centroidLabels";
|
||||||
import pointDialation from "./pointDilation";
|
import pointDialation from "./pointDilation";
|
||||||
@@ -32,6 +33,7 @@ const Reducer = undoable(
|
|||||||
["annotations", annotations],
|
["annotations", annotations],
|
||||||
["genesets", genesets],
|
["genesets", genesets],
|
||||||
["genesetsUI", genesetsUI],
|
["genesetsUI", genesetsUI],
|
||||||
|
["dotplot", dotplot],
|
||||||
["layoutChoice", layoutChoice],
|
["layoutChoice", layoutChoice],
|
||||||
["categoricalSelection", categoricalSelection],
|
["categoricalSelection", categoricalSelection],
|
||||||
["continuousSelection", continuousSelection],
|
["continuousSelection", continuousSelection],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function setToDefaultLayout(schema: any) {
|
|||||||
const LayoutChoice = (
|
const LayoutChoice = (
|
||||||
state = {
|
state = {
|
||||||
available: [], // all available choices
|
available: [], // all available choices
|
||||||
|
dotplot: false, // is the dotplot toggled on, or not
|
||||||
current: undefined, // name of the current layout, eg, 'umap'
|
current: undefined, // name of the current layout, eg, 'umap'
|
||||||
currentDimNames: [], // dimension name
|
currentDimNames: [], // dimension name
|
||||||
},
|
},
|
||||||
@@ -46,6 +47,13 @@ const LayoutChoice = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "toggle dotplot": {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dotplot: !state.dotplot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "set layout choice": {
|
case "set layout choice": {
|
||||||
const { schema } = nextSharedState.annoMatrix;
|
const { schema } = nextSharedState.annoMatrix;
|
||||||
const current = action.layoutChoice;
|
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.
|
filter state are entirely at the discretion of the action filter.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
import { Reducer, AnyAction } from "redux";
|
||||||
import fromEntries from "../util/fromEntries";
|
import fromEntries from "../util/fromEntries";
|
||||||
|
|
||||||
const historyKeyPrefix = "@@undoable/";
|
export const pastKey = "@@undoable/past";
|
||||||
const pastKey = `${historyKeyPrefix}past`;
|
export const futureKey = "@@undoable/future";
|
||||||
const futureKey = `${historyKeyPrefix}future`;
|
export const filterStateKey = "@@undoable/filterState";
|
||||||
const filterStateKey = `${historyKeyPrefix}filterState`;
|
export const filterActionKey = "@@undoable/filterAction";
|
||||||
const filterActionKey = `${historyKeyPrefix}filterAction`;
|
export const pendingKey = "@@undoable/pending";
|
||||||
const pendingKey = `${historyKeyPrefix}pending`;
|
|
||||||
const defaultHistoryLimit = -100;
|
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.
|
export interface UndoableFilterState {
|
||||||
const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
[name: string]: unknown;
|
||||||
// @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 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;
|
let { historyLimit } = options;
|
||||||
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
||||||
if (historyLimit > 0) historyLimit = -historyLimit;
|
if (historyLimit > 0) historyLimit = -historyLimit;
|
||||||
const actionFilter =
|
const actionFilter: ActionFilterFn<FilterStateType> =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
options?.actionFilter ?? (() => ({ [filterActionKey]: "save" }));
|
||||||
(options as any).actionFilter || (() => ({ [filterActionKey]: "save" }));
|
|
||||||
|
|
||||||
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
||||||
throw new Error("undoable keys array must be specified");
|
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
|
Undo the current to previous history
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function undo(
|
||||||
function undo(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey];
|
const past = currentState[pastKey];
|
||||||
const future = currentState[futureKey];
|
const future = currentState[futureKey];
|
||||||
if (past.length === 0) return currentState;
|
if (past.length === 0) return currentState;
|
||||||
@@ -89,7 +118,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
const newPast = [...past];
|
const newPast = [...past];
|
||||||
const newState = newPast.pop();
|
const newState = newPast.pop() || [];
|
||||||
const newFuture = push(future, currentUndoableState);
|
const newFuture = push(future, currentUndoableState);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -104,8 +133,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Replay future, previously undone.
|
Replay future, previously undone.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function redo(
|
||||||
function redo(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const future = currentState[futureKey] || [];
|
const future = currentState[futureKey] || [];
|
||||||
if (future.length === 0) return currentState;
|
if (future.length === 0) return currentState;
|
||||||
@@ -113,7 +143,7 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
const newFuture = [...future];
|
const newFuture = [...future];
|
||||||
const newState = newFuture.pop();
|
const newState = newFuture.pop() || [];
|
||||||
const newPast = push(past, currentUndoableState);
|
const newPast = push(past, currentUndoableState);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -128,13 +158,14 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Clear the history state. No side-effects on current state.
|
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(
|
||||||
function clear(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
[pastKey]: [],
|
[pastKey]: [],
|
||||||
[futureKey]: [],
|
[futureKey]: [],
|
||||||
[filterStateKey]: {},
|
[filterStateKey]: undefined,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -142,8 +173,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Reduce current action, with no history side-effects
|
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(
|
||||||
function skip(currentState: any, action: any, filterState: any) {
|
currentState: UndoableState<FilterStateType>,
|
||||||
|
action: AnyAction,
|
||||||
|
filterState: UndoableFilterState
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const future = currentState[futureKey] || [];
|
const future = currentState[futureKey] || [];
|
||||||
const pending = currentState[pendingKey];
|
const pending = currentState[pendingKey];
|
||||||
@@ -160,8 +194,11 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Save current state in the history, then reduce action.
|
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(
|
||||||
function save(currentState: any, action: any, filterState: any) {
|
currentState: UndoableState<FilterStateType>,
|
||||||
|
action: AnyAction,
|
||||||
|
filterState: UndoableFilterState
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const past = currentState[pastKey] || [];
|
const past = currentState[pastKey] || [];
|
||||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||||
undoableKeysSet.has(kv[0])
|
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.
|
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(
|
||||||
function stashPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
const currentUndoableState = Object.entries(currentState).filter((kv) =>
|
||||||
undoableKeysSet.has(kv[0])
|
undoableKeysSet.has(kv[0])
|
||||||
);
|
);
|
||||||
@@ -195,8 +233,9 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Cancel pending history state change. No other side effects.
|
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(
|
||||||
function cancelPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
|
): UndoableState<FilterStateType> {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
@@ -206,10 +245,12 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
/*
|
/*
|
||||||
Push pending state onto the history stack
|
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(
|
||||||
function applyPending(currentState: any) {
|
currentState: UndoableState<FilterStateType>
|
||||||
const past = currentState[pastKey] || [];
|
): UndoableState<FilterStateType> {
|
||||||
|
const past = currentState[pastKey];
|
||||||
const pendingState = currentState[pendingKey];
|
const pendingState = currentState[pendingKey];
|
||||||
|
if (pendingState === null) return currentState;
|
||||||
const newPast = push(past, pendingState, historyLimit);
|
const newPast = push(past, pendingState, historyLimit);
|
||||||
const nextState = {
|
const nextState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -221,14 +262,13 @@ const Undoable = (reducer: any, undoableKeys: any, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
currentState = {
|
currentState: UndoableState<FilterStateType> = {
|
||||||
[pastKey]: [],
|
[pastKey]: [],
|
||||||
[futureKey]: [],
|
[futureKey]: [],
|
||||||
[filterStateKey]: {},
|
[filterStateKey]: undefined,
|
||||||
[pendingKey]: null,
|
[pendingKey]: null,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
action: AnyAction
|
||||||
action: any
|
|
||||||
) => {
|
) => {
|
||||||
if (debug > 1) console.log("---- ACTION", action.type);
|
if (debug > 1) console.log("---- ACTION", action.type);
|
||||||
const aType = 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<T = unknown>(arr: T[], val: T, limit?: number) {
|
||||||
function push(arr: any, val: any, limit = undefined) {
|
|
||||||
/*
|
/*
|
||||||
functional array push, with a max length limit to the new array.
|
functional array push, with a max length limit to the new array.
|
||||||
Like Array.push, except it returns new array and discards as needed
|
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";
|
import createFsmTransitions from "./undoableFsm";
|
||||||
|
|
||||||
const actionKey = "@@undoable/filterAction";
|
|
||||||
const stateKey = "@@undoable/filterState";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
these actions will not affect history
|
these actions will not affect history
|
||||||
*/
|
*/
|
||||||
const skipOnActions = new Set([
|
const skipOnActions = new Set<string>([
|
||||||
"annoMatrix: init complete",
|
"annoMatrix: init complete",
|
||||||
"url changed",
|
"url changed",
|
||||||
"initial data load start",
|
"initial data load start",
|
||||||
@@ -58,12 +64,12 @@ const skipOnActions = new Set([
|
|||||||
identical, repeated occurances of these action types will be debounced.
|
identical, repeated occurances of these action types will be debounced.
|
||||||
Entire action must be identical (all keys).
|
Entire action must be identical (all keys).
|
||||||
*/
|
*/
|
||||||
const debounceOnActions = new Set([]);
|
const debounceOnActions = new Set<string>([]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
history will be cleared when these actions occur
|
history will be cleared when these actions occur
|
||||||
*/
|
*/
|
||||||
const clearOnActions = new Set([
|
const clearOnActions = new Set<string>([
|
||||||
"initial data load complete",
|
"initial data load complete",
|
||||||
"initial data load error",
|
"initial data load error",
|
||||||
]);
|
]);
|
||||||
@@ -71,7 +77,7 @@ const clearOnActions = new Set([
|
|||||||
/*
|
/*
|
||||||
An immediate history save will be done for these
|
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 select",
|
||||||
"categorical metadata filter deselect",
|
"categorical metadata filter deselect",
|
||||||
"categorical metadata filter all of these",
|
"categorical metadata filter all of these",
|
||||||
@@ -82,6 +88,10 @@ const saveOnActions = new Set([
|
|||||||
"color by expression",
|
"color by expression",
|
||||||
"color by geneset mean expression",
|
"color by geneset mean expression",
|
||||||
|
|
||||||
|
"set dotplot row",
|
||||||
|
"set dotplot column",
|
||||||
|
"toggle dotplot",
|
||||||
|
|
||||||
"show centroid labels for category",
|
"show centroid labels for category",
|
||||||
|
|
||||||
"set scatterplot x",
|
"set scatterplot x",
|
||||||
@@ -119,35 +129,43 @@ StateMachine - processing complex action handling - see FSM graph for
|
|||||||
actual structure, in undoableFsm.js
|
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.
|
Default FSM actions. Used to side-effect transitions in the graph.
|
||||||
See graph definition for the transitions that use each.
|
See graph definition for the transitions that use each.
|
||||||
|
|
||||||
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const stashPending: FsmActionFn<MyUndoableAction> = (
|
||||||
const stashPending = (fsm: any) => ({
|
fsm: StateMachine<MyUndoableAction>
|
||||||
[actionKey]: "stashPending",
|
) => ({
|
||||||
[stateKey]: { fsm },
|
[filterActionKey]: "stashPending",
|
||||||
|
[filterStateKey]: { fsm },
|
||||||
});
|
});
|
||||||
const cancelPending = () => ({
|
const cancelPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
[actionKey]: "cancelPending",
|
[filterActionKey]: "cancelPending",
|
||||||
[stateKey]: { fsm: null },
|
[filterStateKey]: { fsm: null },
|
||||||
});
|
});
|
||||||
const applyPending = () => ({
|
const applyPending: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
[actionKey]: "applyPending",
|
[filterActionKey]: "applyPending",
|
||||||
[stateKey]: { fsm: null },
|
[filterStateKey]: { fsm: null },
|
||||||
});
|
});
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
const skip: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||||
const skip = (fsm, transition) => ({
|
[filterActionKey]: "skip",
|
||||||
[actionKey]: "skip",
|
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
|
||||||
});
|
});
|
||||||
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } });
|
const clear: FsmActionFn<MyUndoableAction> = () => ({
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'fsm' implicitly has an 'any' type.
|
[filterActionKey]: "clear",
|
||||||
const save = (fsm, transition) => ({
|
[filterStateKey]: { fsm: null },
|
||||||
[actionKey]: "save",
|
});
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
const save: FsmActionFn<MyUndoableAction> = (fsm, transition) => ({
|
||||||
|
[filterActionKey]: "save",
|
||||||
|
[filterStateKey]: { fsm: transition.to !== "done" ? fsm : null },
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -156,11 +174,13 @@ StateMachine when it doesn't know what to do.
|
|||||||
|
|
||||||
Signature: (fsm, event, from) => undoableAction
|
Signature: (fsm, event, from) => undoableAction
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const onFsmError: FsmErrorFn<MyUndoableAction> = (fsm, event, from) => {
|
||||||
const onFsmError = (fsm: any, event: any, from: any) => {
|
|
||||||
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
||||||
// In production, try to recover gracefully if we have unexpected state
|
// In production, try to recover gracefully if we have unexpected state
|
||||||
return clear();
|
return {
|
||||||
|
[filterActionKey]: "clear",
|
||||||
|
[filterStateKey]: { fsm: null },
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -175,7 +195,11 @@ const fsmTransitions = createFsmTransitions(
|
|||||||
save
|
save
|
||||||
);
|
);
|
||||||
/* State machine we clone whenever we need to run it */
|
/* 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.
|
See undoable.js for description action filter interface description.
|
||||||
@@ -185,53 +209,52 @@ Basic approach:
|
|||||||
* only implement complex state machines where absolutely required (eg,
|
* only implement complex state machines where absolutely required (eg,
|
||||||
multi-event selection and the like)
|
multi-event selection and the like)
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const actionFilter =
|
||||||
const actionFilter = (debug: any) => (
|
(debug: boolean) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(
|
||||||
state: any,
|
state: UndoableState<MyFilterState>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
action: AnyAction,
|
||||||
action: any,
|
prevFilterState: MyFilterState | undefined
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
): UndoableAction<MyFilterState> => {
|
||||||
prevFilterState: any
|
const actionType = action.type;
|
||||||
) => {
|
prevFilterState = prevFilterState || { fsm: null };
|
||||||
const actionType = action.type;
|
const filterState: MyFilterState = {
|
||||||
const filterState = {
|
...prevFilterState,
|
||||||
...prevFilterState,
|
prevAction: action,
|
||||||
prevAction: action,
|
};
|
||||||
};
|
if (skipOnActions.has(actionType)) {
|
||||||
if (skipOnActions.has(actionType)) {
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
}
|
||||||
}
|
if (
|
||||||
if (
|
debounceOnActions.has(actionType) &&
|
||||||
// @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
|
prevFilterState.prevAction &&
|
||||||
debounceOnActions.has(actionType) &&
|
shallowObjectEq(action, prevFilterState.prevAction)
|
||||||
shallowObjectEq(action, prevFilterState.prevAction)
|
) {
|
||||||
) {
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
}
|
||||||
}
|
if (clearOnActions.has(actionType)) {
|
||||||
if (clearOnActions.has(actionType)) {
|
return { [filterActionKey]: "clear", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
}
|
||||||
}
|
if (saveOnActions.has(actionType)) {
|
||||||
if (saveOnActions.has(actionType)) {
|
return { [filterActionKey]: "save", [filterStateKey]: filterState };
|
||||||
return { [actionKey]: "save", [stateKey]: filterState };
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Else, something more complex OR unknown to us....
|
Else, something more complex OR unknown to us....
|
||||||
*/
|
*/
|
||||||
if (seedFsm.events.has(actionType)) {
|
if (seedFsm.events.has(actionType)) {
|
||||||
let { fsm } = filterState;
|
let { fsm } = filterState;
|
||||||
if (!fsm) {
|
if (!fsm) {
|
||||||
/* no active FSM, so create one in init state */
|
/* no active FSM, so create one in init state */
|
||||||
fsm = seedFsm.clone("init");
|
fsm = seedFsm.clone("init");
|
||||||
|
}
|
||||||
|
return fsm.next(action.type, { state, action });
|
||||||
}
|
}
|
||||||
return fsm.next(action.type, { state, action });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* else, we have no idea what this is - skip it */
|
/* else, we have no idea what this is - skip it */
|
||||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
return { [filterActionKey]: "skip", [filterStateKey]: filterState };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
return true if objA and objB are ===, OR if:
|
return true if objA and objB are ===, OR if:
|
||||||
@@ -239,8 +262,10 @@ return true if objA and objB are ===, OR if:
|
|||||||
- have same own properties
|
- have same own properties
|
||||||
- all values are strict equal (===)
|
- all values are strict equal (===)
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function shallowObjectEq(
|
||||||
function shallowObjectEq(objA: any, objB: any) {
|
objA: Record<string | number | symbol, unknown>,
|
||||||
|
objB: Record<string | number | symbol, unknown>
|
||||||
|
) {
|
||||||
if (objA === objB) return true;
|
if (objA === objB) return true;
|
||||||
if (!objA || !objB) return false;
|
if (!objA || !objB) return false;
|
||||||
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
||||||
@@ -252,8 +277,7 @@ function shallowObjectEq(objA: any, objB: any) {
|
|||||||
return true if arrA and arrB contain the same strict-equal values,
|
return true if arrA and arrB contain the same strict-equal values,
|
||||||
in the same order.
|
in the same order.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function shallowArrayEq(arrA: unknown[], arrB: unknown[]) {
|
||||||
function shallowArrayEq(arrA: any, arrB: any) {
|
|
||||||
if (arrA.length !== arrB.length) return false;
|
if (arrA.length !== arrB.length) return false;
|
||||||
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
||||||
if (arrA[i] !== arrB[i]) return false;
|
if (arrA[i] !== arrB[i]) return false;
|
||||||
@@ -268,7 +292,7 @@ Set to true or 1 for base logging, high number for more verbosity (currently onl
|
|||||||
or 2).
|
or 2).
|
||||||
*/
|
*/
|
||||||
const debug = false;
|
const debug = false;
|
||||||
const undoableConfig = {
|
const undoableConfig: UndoableConfig<MyFilterState> = {
|
||||||
debug,
|
debug,
|
||||||
historyLimit: 50, // maximum history size
|
historyLimit: 50, // maximum history size
|
||||||
actionFilter: actionFilter(debug),
|
actionFilter: actionFilter(debug),
|
||||||
@@ -307,7 +331,7 @@ if (debug) {
|
|||||||
);
|
);
|
||||||
if (trivialOverlapWithFsm.size > 0) {
|
if (trivialOverlapWithFsm.size > 0) {
|
||||||
console.error(
|
console.error(
|
||||||
"Undoable misconfiguration - trivival action filter blocking FSM filter",
|
"Undoable misconfiguration - trivial action filter blocking FSM filter",
|
||||||
[...trivialOverlapWithFsm]
|
[...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.
|
import { StateMachine, FsmTransition, FsmActionFn } from "../util/statemachine";
|
||||||
const createFsmTransitions = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
const createFsmTransitions = <ActionReturnType>(
|
||||||
stashPending: any,
|
stashPending: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
cancelPending: FsmActionFn<ActionReturnType>,
|
||||||
cancelPending: any,
|
applyPending: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
skip: FsmActionFn<ActionReturnType>,
|
||||||
applyPending: any,
|
_clear: FsmActionFn<ActionReturnType>,
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
save: FsmActionFn<ActionReturnType>
|
||||||
skip: any,
|
): FsmTransition<ActionReturnType>[] => [
|
||||||
// @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
|
|
||||||
) => [
|
|
||||||
/* graph selection brushing */
|
/* graph selection brushing */
|
||||||
{
|
{
|
||||||
event: "graph brush start",
|
event: "graph brush start",
|
||||||
@@ -52,12 +46,14 @@ const createFsmTransitions = (
|
|||||||
from: "graph brush in progress",
|
from: "graph brush in progress",
|
||||||
to: "done",
|
to: "done",
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
/* if current selection is all, cancelPending. Else, applyPending */
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
action: (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fsm: StateMachine<ActionReturnType>,
|
||||||
action: (fsm: any, transition: any, data: any) =>
|
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"
|
data.state.graphSelection.selection.mode === "all"
|
||||||
? cancelPending()
|
? cancelPending(fsm, transition, data)
|
||||||
: applyPending(),
|
: applyPending(fsm, transition, data),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "graph brush end",
|
event: "graph brush end",
|
||||||
@@ -84,12 +80,14 @@ const createFsmTransitions = (
|
|||||||
from: "graph lasso in progress",
|
from: "graph lasso in progress",
|
||||||
to: "done",
|
to: "done",
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
/* if current selection is all, cancelPending. Else, applyPending */
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'fsm' is declared but its value is never read.
|
action: (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fsm: StateMachine<ActionReturnType>,
|
||||||
action: (fsm: any, transition: any, data: any) =>
|
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"
|
data.state.graphSelection.selection.mode === "all"
|
||||||
? cancelPending()
|
? cancelPending(fsm, transition, data)
|
||||||
: applyPending(),
|
: applyPending(fsm, transition, data),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: "graph lasso end",
|
event: "graph lasso end",
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
|
import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
|
||||||
// weird cross-dependency that we should clean up someday...
|
// weird cross-dependency that we should clean up someday...
|
||||||
import {
|
import { callOnceLazy, memoize, __getMemoId } from "./util";
|
||||||
isTypedArray,
|
import { isTypedArray, isAnyArray } from "../../common/types/arraytypes";
|
||||||
isArrayOrTypedArray,
|
|
||||||
callOnceLazy,
|
|
||||||
memoize,
|
|
||||||
__getMemoId,
|
|
||||||
} from "./util";
|
|
||||||
import {
|
import {
|
||||||
summarizeContinuous,
|
summarizeContinuous,
|
||||||
summarizeCategorical as _summarizeCategorical,
|
summarizeCategorical as _summarizeCategorical,
|
||||||
@@ -163,7 +158,7 @@ class Dataframe {
|
|||||||
if (!Array.isArray(columnarData)) {
|
if (!Array.isArray(columnarData)) {
|
||||||
throw new TypeError("Dataframe constructor requires array of columns");
|
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");
|
throw new TypeError("Dataframe columns must all be Array or TypedArray");
|
||||||
}
|
}
|
||||||
if (!isLabelIndex(rowIndex)) {
|
if (!isLabelIndex(rowIndex)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Dataframe histogram
|
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.
|
// 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) {
|
function _histogramContinuous(column: any, bins: any, min: any, max: any) {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Private utility code for dataframe
|
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.
|
// 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) {
|
export function callOnceLazy(f: any) {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Helper functions for user-editable annotations state management.
|
|||||||
See also reducers/annotations.js
|
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
|
There are a number of state constraints assumed throughout the
|
||||||
|
|||||||
@@ -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: {
|
default: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { flatbuffers } from "flatbuffers";
|
import { flatbuffers } from "flatbuffers";
|
||||||
import { NetEncoding } from "./matrix_generated";
|
import { NetEncoding } from "./matrix_generated";
|
||||||
import { isTypedArray, isFpTypedArray } from "../typeHelpers";
|
import { isTypedArray, isFloatTypedArray } from "../../common/types/arraytypes";
|
||||||
import {
|
import {
|
||||||
Dataframe,
|
Dataframe,
|
||||||
IdentityInt32Index,
|
IdentityInt32Index,
|
||||||
@@ -188,7 +188,7 @@ function promoteTypedArray(o: any) {
|
|||||||
TODO - future optimization: not all int32/uint32 data series require
|
TODO - future optimization: not all int32/uint32 data series require
|
||||||
promotion to float64. We COULD simply look at the data to decide.
|
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;
|
let TypedArrayCtor;
|
||||||
switch (o.constructor) {
|
switch (o.constructor) {
|
||||||
@@ -246,7 +246,7 @@ export function matrixFBSToDataframe(arrayBuffers: any) {
|
|||||||
.map((fb: any) =>
|
.map((fb: any) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
fb.columns.map((c: any) => {
|
fb.columns.map((c: any) => {
|
||||||
if (isFpTypedArray(c) || Array.isArray(c)) return c;
|
if (isFloatTypedArray(c) || Array.isArray(c)) return c;
|
||||||
return promoteTypedArray(c);
|
return promoteTypedArray(c);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import catLabelSort from "../catLabelSort";
|
|||||||
import {
|
import {
|
||||||
RawSchema,
|
RawSchema,
|
||||||
Schema,
|
Schema,
|
||||||
LayoutColumn,
|
EmbeddingSchema,
|
||||||
AnnotationColumn,
|
AnnotationColumnSchema,
|
||||||
} from "../../common/types/entities";
|
} from "../../common/types/schema";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
System wide schema assumptions:
|
System wide schema assumptions:
|
||||||
@@ -86,7 +86,7 @@ export function removeObsAnnoColumn(schema: Schema, name: string): Schema {
|
|||||||
export function addObsAnnoColumn(
|
export function addObsAnnoColumn(
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
_: string,
|
_: string,
|
||||||
defn: AnnotationColumn
|
defn: AnnotationColumnSchema
|
||||||
): Schema {
|
): Schema {
|
||||||
const newSchema = _copyObsAnno(schema);
|
const newSchema = _copyObsAnno(schema);
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export function addObsAnnoCategory(schema: any, name: any, category: any) {
|
|||||||
return newSchema;
|
return newSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addObsLayout(schema: Schema, layout: LayoutColumn): Schema {
|
export function addObsLayout(schema: Schema, layout: EmbeddingSchema): Schema {
|
||||||
/* add or replace a layout */
|
/* add or replace a layout */
|
||||||
const newSchema = _copyObsLayout(schema);
|
const newSchema = _copyObsLayout(schema);
|
||||||
newSchema.layout.obs.push(layout);
|
newSchema.layout.obs.push(layout);
|
||||||
|
|||||||
@@ -13,18 +13,17 @@ Where:
|
|||||||
to: state_name_transitioning_to,
|
to: state_name_transitioning_to,
|
||||||
from: state_name_transitioning_from,
|
from: state_name_transitioning_from,
|
||||||
event: value_that_will_cause_transition,
|
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
|
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.
|
may be stored in the transition object for use by the action callback.
|
||||||
* onErrorCallback - a callback function called if the FSM receives an event
|
* onErrorCallback - a callback function called if the FSM receives an event
|
||||||
for which it has no defined transition.
|
for which it has no defined transition.
|
||||||
|
|
||||||
|
|
||||||
Interface:
|
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.
|
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]
|
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
|
||||||
* clone() - clone the entire statemachine.
|
* clone() - clone the entire statemachine.
|
||||||
* next(eventValue) - drive the FSM to the next state. If the event
|
* 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") });
|
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
|
||||||
fsm.next("yo"); // returns 42
|
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.
|
export type FsmState = number | string;
|
||||||
graph: any;
|
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.
|
export type FsmActionFn<ActionReturnType> = (
|
||||||
onError: any;
|
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.
|
export interface FsmTransition<ActionReturnType> {
|
||||||
state: any;
|
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.
|
export type FsmErrorFn<ActionReturnType> = (
|
||||||
states: any;
|
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.
|
export class StateMachine<ActionReturnType> {
|
||||||
constructor(initState: any, transitions: any, onError: any) {
|
events: Set<FsmEvent>;
|
||||||
this.onError = onError || (() => undefined);
|
|
||||||
|
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;
|
this.state = initState;
|
||||||
|
|
||||||
// all states
|
// all states
|
||||||
this.states = new Set(
|
this.states = new Set(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
transitions.reduce(
|
||||||
transitions.reduce((names: any, tsn: any) => {
|
(names: Array<FsmState>, tsn: FsmTransition<ActionReturnType>) => {
|
||||||
names.push(tsn.from);
|
names.push(tsn.from);
|
||||||
names.push(tsn.to);
|
names.push(tsn.to);
|
||||||
return names;
|
return names;
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// all transition names (aka events)
|
// 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(
|
||||||
this.events = new Set(transitions.map((tsn: any) => tsn.event));
|
transitions.map((tsn: FsmTransition<ActionReturnType>) => tsn.event)
|
||||||
|
);
|
||||||
|
|
||||||
// the transition graph.
|
// the transition graph.
|
||||||
// graph[event][from] -> transition
|
// 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, tsn) => {
|
||||||
this.graph = transitions.reduce((graph: any, tsn: any) => {
|
|
||||||
const { event, from } = tsn;
|
const { event, from } = tsn;
|
||||||
if (!graph.has(event)) graph.set(event, new Map());
|
if (!graph.has(event)) graph.set(event, new Map());
|
||||||
const tsnMap = graph.get(event);
|
const tsnMap = graph.get(event);
|
||||||
@@ -89,29 +110,23 @@ export default class StateMachine {
|
|||||||
}, new Map());
|
}, 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: FsmState): StateMachine<ActionReturnType> {
|
||||||
clone(initState: any) {
|
const fsm = new StateMachine<ActionReturnType>(initState, [], this.onError);
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
|
||||||
const fsm = new StateMachine(initState, []);
|
|
||||||
fsm.onError = this.onError;
|
|
||||||
fsm.states = this.states;
|
fsm.states = this.states;
|
||||||
fsm.events = this.events;
|
fsm.events = this.events;
|
||||||
fsm.graph = this.graph;
|
fsm.graph = this.graph;
|
||||||
return fsm;
|
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: FsmEvent, data: unknown): ActionReturnType {
|
||||||
next(event: any, data: any) {
|
|
||||||
const { graph, state } = this;
|
const { graph, state } = this;
|
||||||
const tsnMap = graph.get(event);
|
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);
|
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;
|
this.state = transition.to;
|
||||||
return transition.action
|
return transition.action(this, transition, data);
|
||||||
? transition.action(this, transition, data)
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
upperBoundIndirect,
|
||||||
} from "./sort";
|
} from "./sort";
|
||||||
import { makeSortIndex } from "./util";
|
import { makeSortIndex } from "./util";
|
||||||
|
import { isAnyArray } from "../../common/types/arraytypes";
|
||||||
|
|
||||||
class NotImplementedError extends Error {
|
class NotImplementedError extends Error {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
@@ -450,7 +451,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
value,
|
value,
|
||||||
new ValueArrayType(data.length)
|
new ValueArrayType(data.length)
|
||||||
);
|
);
|
||||||
} else if (isArrayOrTypedArray(value)) {
|
} else if (isAnyArray(value)) {
|
||||||
// Create value array from user-provided array. Typically used
|
// Create value array from user-provided array. Typically used
|
||||||
// only by enumerated dimensions
|
// only by enumerated dimensions
|
||||||
array = this._createValueArray(
|
array = this._createValueArray(
|
||||||
@@ -729,15 +730,6 @@ export const DimTypes = {
|
|||||||
spatial: ImmutableSpatialDimension,
|
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 */
|
/* return bounding box of the polygon */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
||||||
function polygonBoundingBox(polygon: any) {
|
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 */
|
/* 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);
|
return quicksort(arr, 0, arr.length - 1);
|
||||||
}
|
}
|
||||||
if (isTypedArray(arr)) {
|
if (isTypedArray(arr)) {
|
||||||
if (isFpTypedArray(arr)) {
|
if (isFloatTypedArray(arr)) {
|
||||||
return quicksortFloats(arr, 0, arr.length - 1);
|
return quicksortFloats(arr, 0, arr.length - 1);
|
||||||
}
|
}
|
||||||
return quicksort(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.
|
// 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) {
|
export function sortIndex(index: any, source: any) {
|
||||||
if (isFpTypedArray(source))
|
if (isFloatTypedArray(source))
|
||||||
return quicksortFloatsIndirect(index, source, 0, index.length - 1);
|
return quicksortFloatsIndirect(index, source, 0, index.length - 1);
|
||||||
return quicksortIndirect(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.
|
// 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) {
|
export function lowerBound(valueArray: any, value: any, first: any, last: any) {
|
||||||
if (isFpTypedArray(valueArray)) {
|
if (isFloatTypedArray(valueArray)) {
|
||||||
return lowerBoundFloat(valueArray, value, first, last);
|
return lowerBoundFloat(valueArray, value, first, last);
|
||||||
}
|
}
|
||||||
return lowerBoundNonFloat(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.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||||
last: any
|
last: any
|
||||||
) {
|
) {
|
||||||
if (isFpTypedArray(valueArray)) {
|
if (isFloatTypedArray(valueArray)) {
|
||||||
return lowerBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
return lowerBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
||||||
}
|
}
|
||||||
return lowerBoundNonFloatIndirect(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.
|
// 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) {
|
export function upperBound(valueArray: any, value: any, first: any, last: any) {
|
||||||
if (isFpTypedArray(valueArray)) {
|
if (isFloatTypedArray(valueArray)) {
|
||||||
return upperBoundFloat(valueArray, value, first, last);
|
return upperBoundFloat(valueArray, value, first, last);
|
||||||
}
|
}
|
||||||
return upperBoundNonFloat(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.
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
||||||
last: any
|
last: any
|
||||||
) {
|
) {
|
||||||
if (isFpTypedArray(valueArray)) {
|
if (isFloatTypedArray(valueArray)) {
|
||||||
return upperBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
return upperBoundFloatIndirect(valueArray, indexArray, value, first, last);
|
||||||
}
|
}
|
||||||
return upperBoundNonFloatIndirect(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)
|
- 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`
|
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`).
|
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).
|
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.
|
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.
|
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