Compare commits

...
Author SHA1 Message Date
Colin Megill 87ba3ff870 Merge branch 'main' into colinmegill/#632 2021-08-16 12:14:34 -07:00
Severiano Badajoz 3fdf5cac9d fix: remove spaces in param setup (#2380) 2021-08-13 17:01:10 +00:00
Severiano Badajozandmaniarathi 3c3a794986 update: release process (#2277)
Co-authored-by: maniarathi <mani.arathi@gmail.com>
2021-08-13 00:25:58 +00:00
Bruce Martin 4b417cb5a5 undoable TS typing (#2374)
* type undoable-related TS

* style change to type declaration
2021-08-12 17:14:09 -07:00
Severiano Badajoz 925b785b1f fix: disable FE auth testing on compatibility tests (#2377) 2021-08-12 18:34:04 +00:00
Bruce Martin 660dff256c add array type foundations (#2376) 2021-08-12 07:17:05 -07:00
Timmy Huang 59c475b821 chore: extract schema types (#2375)
* chore: extract schema types

* address comments
2021-08-12 03:12:21 +00:00
Colin Megill b553da0264 async 1 2021-08-05 16:39:22 -07:00
Colin Megill b67142e98f embedding to tsx 2021-08-04 16:02:52 -07:00
Colin Megill 22a0921147 Merge branch 'main' into colinmegill/#632 2021-08-04 14:43:09 -07:00
Colin Megill 020e562f5c merge typescript changes 2021-08-04 14:42:31 -07:00
Colin Megill 60d89b9478 flip scale 2021-07-30 10:59:42 -07:00
Colin Megill c2b12abe2b toggle and scale dotplot 2021-07-26 13:23:21 -07:00
Colin Megill 2462d4afb1 color scale, button 2021-07-23 13:29:27 -07:00
Colin Megill 02e79d502f add classnames to canvases 2021-07-23 13:29:27 -07:00
Colin Megill 9c2323b7bc queries 2021-07-23 13:29:27 -07:00
Colin Megill e7200ce6c3 todo comment 2021-07-23 13:29:27 -07:00
Colin Megill b7ffb2748d colorby type 2021-07-23 13:29:27 -07:00
Colin Megill 7ab8a8894d no overlap 2021-07-23 13:29:27 -07:00
Colin Megill 7b01e9e67b remove hardcoded geneset 2021-07-23 13:29:27 -07:00
Colin Megill 72ee670620 remove logs 2021-07-23 13:29:27 -07:00
Colin Megill e21cac65bf set row and column 2021-07-23 13:29:27 -07:00
Colin Megill bb5bbaac8a reducer 2021-07-23 13:29:27 -07:00
Colin Megill e29a6f72c2 d3 scale for dot size 2021-07-23 13:29:10 -07:00
Colin Megill ed97013277 dotplot button 2021-07-23 13:29:10 -07:00
Colin Megill 2b29a152b9 metadata as var, maxsize todo 2021-07-23 13:28:17 -07:00
Colin Megill f0e9b1ab91 dotplot proto full 2021-07-23 13:28:17 -07:00
Colin Megill c489221296 geneset iterate 2021-07-23 13:28:17 -07:00
Colin Megill 0a69af98c5 break out load and err 2021-07-23 13:28:17 -07:00
Colin Megill 11570273e0 dotplot 1 2021-07-23 13:28:17 -07:00
Colin Megill 5f9d0a6b34 logging out values 2021-07-23 13:28:17 -07:00
41 changed files with 1380 additions and 335 deletions
@@ -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
View File
@@ -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
+8 -9
View File
@@ -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.
+2 -2
View File
@@ -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)
+85
View File
@@ -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 -58
View File
@@ -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;
}
+58
View File
@@ -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;
}
+10 -2
View File
@@ -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 />
+237
View File
@@ -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
// );
// }
+76
View File
@@ -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;
+20
View File
@@ -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;
+129
View File
@@ -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;
+30
View File
@@ -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;
+3 -2
View File
@@ -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,
+2
View File
@@ -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
+348
View File
@@ -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,
+12
View File
@@ -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 */
+30
View File
@@ -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;
+2
View File
@@ -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],
+8
View File
@@ -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;
+79 -40
View File
@@ -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
+103 -79
View File
@@ -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]
); );
} }
+24 -26
View File
@@ -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",
+3 -8
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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;
} }
+3 -3
View File
@@ -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);
+56 -41
View File
@@ -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;
} }
} }
-32
View File
@@ -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) {
+7 -7
View File
@@ -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);
+1 -1
View File
@@ -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.