mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-04 00:08:11 +08:00
Merge branch 'master' into colinmegill/geneset-prototype
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
export const jest_env = process.env.JEST_ENV;
|
export const jestEnv = process.env.JEST_ENV;
|
||||||
export const appPort = process.env.CXG_SERVER_PORT;
|
export const appPort = process.env.CXG_SERVER_PORT;
|
||||||
export const appUrlBase =
|
export const appUrlBase =
|
||||||
process.env.CXG_URL_BASE || `http://localhost:${appPort}`;
|
process.env.CXG_URL_BASE || `http://localhost:${appPort}`;
|
||||||
export const DEV = jest_env === "dev";
|
export const DEV = jestEnv === "dev";
|
||||||
export const DEBUG = jest_env === "debug";
|
export const DEBUG = jestEnv === "debug";
|
||||||
export const DATASET = "pbmc3k";
|
export const DATASET = "pbmc3k";
|
||||||
|
|
||||||
if (DEBUG) jest.setTimeout(100000);
|
if (DEBUG) jest.setTimeout(100000);
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { appUrlBase, DATASET } from "./config";
|
|||||||
import { setupTestBrowser } from "./testBrowser";
|
import { setupTestBrowser } from "./testBrowser";
|
||||||
import { datasets } from "./data";
|
import { datasets } from "./data";
|
||||||
|
|
||||||
let browser, page, utils, cxgActions;
|
let browser;
|
||||||
|
let page;
|
||||||
|
let utils;
|
||||||
|
let cxgActions;
|
||||||
const data = datasets[DATASET];
|
const data = datasets[DATASET];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -29,6 +32,17 @@ describe("did launch", () => {
|
|||||||
);
|
);
|
||||||
expect(element).toBe(data.title);
|
expect(element).toBe(data.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("terms of service, if they are there", async () => {
|
||||||
|
try {
|
||||||
|
await utils.clickOn("tos-cookies-accept", { timeout: 500 });
|
||||||
|
} catch {
|
||||||
|
console.warn("No terms of service footer detected.");
|
||||||
|
}
|
||||||
|
page.waitFor(50); // give the footer a chance to disappear
|
||||||
|
const result = await page.$("[data-testid='tos-cookies-accept']");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("metadata loads", () => {
|
describe("metadata loads", () => {
|
||||||
@@ -119,7 +133,7 @@ describe("gene entry", () => {
|
|||||||
testGenes
|
testGenes
|
||||||
);
|
);
|
||||||
expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
|
expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
|
||||||
expect(allHistograms.length).toEqual(testGenes.length);
|
expect(allHistograms).toHaveLength(testGenes.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +147,7 @@ describe("differential expression", () => {
|
|||||||
expect(allHistograms).toEqual(
|
expect(allHistograms).toEqual(
|
||||||
expect.arrayContaining(data.diffexp["gene-results"])
|
expect.arrayContaining(data.diffexp["gene-results"])
|
||||||
);
|
);
|
||||||
expect(allHistograms.length).toEqual(data.diffexp["gene-results"].length);
|
expect(allHistograms).toHaveLength(data.diffexp["gene-results"].length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { appUrlBase, DATASET } from "./config";
|
|||||||
import { setupTestBrowser } from "./testBrowser";
|
import { setupTestBrowser } from "./testBrowser";
|
||||||
import { datasets } from "./data";
|
import { datasets } from "./data";
|
||||||
|
|
||||||
let browser, page, utils, actions;
|
let browser;
|
||||||
|
let page;
|
||||||
|
let utils;
|
||||||
|
let actions;
|
||||||
const data = datasets[DATASET];
|
const data = datasets[DATASET];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ import { appUrlBase, DEBUG, DEV, DATASET } from "./config";
|
|||||||
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
|
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
|
||||||
import { datasets } from "./data";
|
import { datasets } from "./data";
|
||||||
|
|
||||||
let browser, page, utils, cxgActions, spy;
|
let browser;
|
||||||
|
let page;
|
||||||
|
let utils;
|
||||||
|
let cxgActions;
|
||||||
|
let spy;
|
||||||
const browserViewport = { width: 1280, height: 960 };
|
const browserViewport = { width: 1280, height: 960 };
|
||||||
let data = datasets[DATASET].features;
|
const data = datasets[DATASET].features;
|
||||||
|
|
||||||
if (DEBUG) jest.setTimeout(100000);
|
if (DEBUG) jest.setTimeout(100000);
|
||||||
if (DEV) jest.setTimeout(10000);
|
if (DEV) jest.setTimeout(10000);
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const puppeteerUtils = (page) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async clickOn(testid, options = {}) {
|
async clickOn(testid, options = {}) {
|
||||||
await this.waitByID(testid);
|
await this.waitByID(testid, options);
|
||||||
const click = await page.click(`[data-testid='${testid}']`, options);
|
const click = await page.click(`[data-testid='${testid}']`);
|
||||||
await page.waitFor(50);
|
await page.waitFor(50);
|
||||||
return click;
|
return click;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export async function setupTestBrowser() {
|
|||||||
page.on("console", async (msg) => {
|
page.on("console", async (msg) => {
|
||||||
// If there is a console.error but an error is not thrown, this will ensure the test fails
|
// If there is a console.error but an error is not thrown, this will ensure the test fails
|
||||||
if (msg.type() === "error") {
|
if (msg.type() === "error") {
|
||||||
|
// TODO: chromium does not currently support the CSP directive on the
|
||||||
|
// line below, so we swallow this error. Remove this when the test
|
||||||
|
// suite uses a browser version that supports this directive.
|
||||||
|
if (msg.text() === "Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.\n") return;
|
||||||
const errorMsgText = await Promise.all(
|
const errorMsgText = await Promise.all(
|
||||||
// TODO can we do this without internal properties?
|
// TODO can we do this without internal properties?
|
||||||
msg.args().map((arg) => arg._remoteObject.description)
|
msg.args().map((arg) => arg._remoteObject.description)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PromiseLimit } from "../../src/util/promiseLimit";
|
import PromiseLimit from "../../src/util/promiseLimit";
|
||||||
import { range } from "../../src/util/range";
|
import { range } from "../../src/util/range";
|
||||||
|
|
||||||
const delay = (t) => new Promise((resolve, reject) => setTimeout(resolve, t));
|
const delay = (t) => new Promise((resolve, reject) => setTimeout(resolve, t));
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CspHashPlugin {
|
|||||||
.createHash("sha256")
|
.createHash("sha256")
|
||||||
.update(str, "utf8")
|
.update(str, "utf8")
|
||||||
.digest("base64");
|
.digest("base64");
|
||||||
return "sha256-" + hash;
|
return `sha256-${hash}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
doBinaryRequest,
|
doBinaryRequest,
|
||||||
dispatchNetworkErrorMessageToUser,
|
dispatchNetworkErrorMessageToUser,
|
||||||
} from "../util/actionHelpers";
|
} from "../util/actionHelpers";
|
||||||
import { PromiseLimit } from "../util/promiseLimit";
|
import PromiseLimit from "../util/promiseLimit";
|
||||||
import { requestReembed, reembedResetWorldToUniverse } from "./reembed";
|
import { requestReembed, reembedResetWorldToUniverse } from "./reembed";
|
||||||
import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
|
import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
saveInProgress: state.autosave?.saveInProgress ?? false,
|
saveInProgress: state.autosave?.saveInProgress ?? false,
|
||||||
lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations,
|
lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations,
|
||||||
error: state.autosave?.error,
|
error: state.autosave?.error,
|
||||||
writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
}))
|
}))
|
||||||
class FilenameDialog extends React.Component {
|
class FilenameDialog extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import FilenameDialog from "./filenameDialog";
|
import FilenameDialog from "./filenameDialog";
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ import FilenameDialog from "./filenameDialog";
|
|||||||
saveInProgress: state.autosave?.saveInProgress ?? false,
|
saveInProgress: state.autosave?.saveInProgress ?? false,
|
||||||
lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations,
|
lastSavedObsAnnotations: state.autosave?.lastSavedObsAnnotations,
|
||||||
error: state.autosave?.error,
|
error: state.autosave?.error,
|
||||||
writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
initialDataLoadComplete: state.autosave?.initialDataLoadComplete,
|
initialDataLoadComplete: state.autosave?.initialDataLoadComplete,
|
||||||
}))
|
}))
|
||||||
class Autosave extends React.Component {
|
class Autosave extends React.Component {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Category extends React.Component {
|
|||||||
} else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) {
|
} else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) {
|
||||||
/* to be explicit... */
|
/* to be explicit... */
|
||||||
this.checkbox.indeterminate = true;
|
this.checkbox.indeterminate = true;
|
||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false }); // eslint-disable-line react/no-did-update-set-state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,14 +77,15 @@ class Category extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleAll() {
|
handleCategoryClick = () => {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { annotations, metadataField, onExpansionChange } = this.props;
|
||||||
dispatch({
|
const editingCategory =
|
||||||
type: "categorical metadata filter all of these",
|
annotations.isEditingCategoryName &&
|
||||||
metadataField,
|
annotations.categoryBeingEdited === metadataField;
|
||||||
});
|
if (!editingCategory) {
|
||||||
this.setState({ isChecked: true });
|
onExpansionChange(metadataField);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
toggleNone() {
|
toggleNone() {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
@@ -95,6 +96,15 @@ class Category extends React.Component {
|
|||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAll() {
|
||||||
|
const { dispatch, metadataField } = this.props;
|
||||||
|
dispatch({
|
||||||
|
type: "categorical metadata filter all of these",
|
||||||
|
metadataField,
|
||||||
|
});
|
||||||
|
this.setState({ isChecked: true });
|
||||||
|
}
|
||||||
|
|
||||||
handleToggleAllClick() {
|
handleToggleAllClick() {
|
||||||
const { isChecked } = this.state;
|
const { isChecked } = this.state;
|
||||||
// || this.checkbox.indeterminate === false
|
// || this.checkbox.indeterminate === false
|
||||||
@@ -115,6 +125,8 @@ class Category extends React.Component {
|
|||||||
globals.categoryDisplayStringMaxLength
|
globals.categoryDisplayStringMaxLength
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const checkboxID = `category-select-${metadataField}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -135,8 +147,8 @@ class Category extends React.Component {
|
|||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="bp3-control bp3-checkbox">
|
<label htmlFor={checkboxID} className="bp3-control bp3-checkbox">
|
||||||
<input disabled checked type="checkbox" />
|
<input disabled id={checkboxID} checked type="checkbox" />
|
||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
</label>
|
</label>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -174,9 +186,7 @@ class Category extends React.Component {
|
|||||||
metadataField,
|
metadataField,
|
||||||
categoricalSelection,
|
categoricalSelection,
|
||||||
isColorAccessor,
|
isColorAccessor,
|
||||||
annotations,
|
|
||||||
isExpanded,
|
isExpanded,
|
||||||
onExpansionChange,
|
|
||||||
schema,
|
schema,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -185,6 +195,8 @@ class Category extends React.Component {
|
|||||||
return this.renderIsStillLoading();
|
return this.renderIsStillLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkboxID = `category-select-${metadataField}`;
|
||||||
|
|
||||||
const isUserAnno =
|
const isUserAnno =
|
||||||
schema?.annotations?.obsByName[metadataField]?.writable ?? false;
|
schema?.annotations?.obsByName[metadataField]?.writable ?? false;
|
||||||
const isTruncated = _.get(
|
const isTruncated = _.get(
|
||||||
@@ -225,8 +237,9 @@ class Category extends React.Component {
|
|||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="bp3-control bp3-checkbox">
|
<label className="bp3-control bp3-checkbox" htmlFor={checkboxID}>
|
||||||
<input
|
<input
|
||||||
|
id={checkboxID}
|
||||||
data-testclass="category-select"
|
data-testclass="category-select"
|
||||||
data-testid={`${metadataField}:category-select`}
|
data-testid={`${metadataField}:category-select`}
|
||||||
onChange={this.handleToggleAllClick.bind(this)}
|
onChange={this.handleToggleAllClick.bind(this)}
|
||||||
@@ -251,19 +264,19 @@ class Category extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
role="menuitem"
|
||||||
|
tabIndex="0"
|
||||||
data-testid={`${metadataField}:category-expand`}
|
data-testid={`${metadataField}:category-expand`}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
this.handleCategoryClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={this.handleCategoryClick}
|
||||||
const editingCategory =
|
|
||||||
annotations.isEditingCategoryName &&
|
|
||||||
annotations.categoryBeingEdited === metadataField;
|
|
||||||
if (!editingCategory) {
|
|
||||||
onExpansionChange(metadataField);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isUserAnno ? (
|
{isUserAnno ? (
|
||||||
<Icon style={{ marginRight: 5 }} icon="tag" iconSize={16} />
|
<Icon style={{ marginRight: 5 }} icon="tag" iconSize={16} />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import LabelInput from "./labelInput";
|
|||||||
import { labelPrompt } from "./labelUtil";
|
import { labelPrompt } from "./labelUtil";
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
schema: state.world?.schema,
|
schema: state.world?.schema,
|
||||||
config: state.config,
|
config: state.config,
|
||||||
ontology: state.ontology,
|
ontology: state.ontology,
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export default class LabelInput extends React.PureComponent {
|
|||||||
* popoverProps -- will be passed to <Suggest>
|
* popoverProps -- will be passed to <Suggest>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* maxinum number of suggestions */
|
||||||
|
static QueryResultLimit = 100;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -103,9 +106,6 @@ export default class LabelInput extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* maxinum number of suggestions */
|
|
||||||
static QueryResultLimit = 100;
|
|
||||||
|
|
||||||
filterLabels(query) {
|
filterLabels(query) {
|
||||||
const { labelSuggestions } = this.props;
|
const { labelSuggestions } = this.props;
|
||||||
if (!labelSuggestions) return [];
|
if (!labelSuggestions) return [];
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ class CategoryValue extends React.Component {
|
|||||||
prevProps.metadataField !== metadataField ||
|
prevProps.metadataField !== metadataField ||
|
||||||
prevProps.categoryIndex !== categoryIndex
|
prevProps.categoryIndex !== categoryIndex
|
||||||
) {
|
) {
|
||||||
|
// adequately checked to prevent looping
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({
|
this.setState({
|
||||||
editedLabelText: this.currentLabel(),
|
editedLabelText: this.currentLabel(),
|
||||||
});
|
});
|
||||||
@@ -327,6 +329,8 @@ class CategoryValue extends React.Component {
|
|||||||
annotations.isEditingLabelName &&
|
annotations.isEditingLabelName &&
|
||||||
annotations.labelEditable.label === categoryIndex;
|
annotations.labelEditable.label === categoryIndex;
|
||||||
|
|
||||||
|
const valueToggleLabel = `value-toggle-checkbox-${displayString}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -360,8 +364,13 @@ class CategoryValue extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "baseline" }}>
|
<div style={{ display: "flex", alignItems: "baseline" }}>
|
||||||
<label className="bp3-control bp3-checkbox" style={{ margin: 0 }}>
|
<label
|
||||||
|
htmlFor={valueToggleLabel}
|
||||||
|
className="bp3-control bp3-checkbox"
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
|
id={valueToggleLabel}
|
||||||
onChange={selected ? this.toggleOff : this.toggleOn}
|
onChange={selected ? this.toggleOff : this.toggleOn}
|
||||||
data-testclass="categorical-value-select"
|
data-testclass="categorical-value-select"
|
||||||
data-testid={`categorical-value-select-${metadataField}-${displayString}`}
|
data-testid={`categorical-value-select-${metadataField}-${displayString}`}
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ const ToastTopCenter = Toaster.create({
|
|||||||
position: Position.TOP,
|
position: Position.TOP,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ToastBottomCenter = Toaster.create({
|
|
||||||
className: "recipe-toaster",
|
|
||||||
position: Position.BOTTOM,
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A "user" error - eg, bad input
|
A "user" error - eg, bad input
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ class AddGenes extends React.Component {
|
|||||||
}}
|
}}
|
||||||
itemListPredicate={filterGenes}
|
itemListPredicate={filterGenes}
|
||||||
onActiveItemChange={(item) => this.setState({ activeItem: item })}
|
onActiveItemChange={(item) => this.setState({ activeItem: item })}
|
||||||
itemRenderer={renderGene.bind(this)}
|
itemRenderer={renderGene}
|
||||||
items={varIndex || ["No genes"]}
|
items={varIndex || ["No genes"]}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import setupSVGandBrushElements from "./setupSVGandBrush";
|
|||||||
import _camera from "../../util/camera";
|
import _camera from "../../util/camera";
|
||||||
import _drawPoints from "./drawPointsRegl";
|
import _drawPoints from "./drawPointsRegl";
|
||||||
import { isTypedArray } from "../../util/typeHelpers";
|
import { isTypedArray } from "../../util/typeHelpers";
|
||||||
import styles from "./graph.css";
|
|
||||||
|
|
||||||
import GraphOverlayLayer from "./overlays/graphOverlayLayer";
|
import GraphOverlayLayer from "./overlays/graphOverlayLayer";
|
||||||
import CentroidLabels from "./overlays/centroidLabels";
|
import CentroidLabels from "./overlays/centroidLabels";
|
||||||
@@ -190,7 +189,7 @@ class Graph extends React.Component {
|
|||||||
tool: null,
|
tool: null,
|
||||||
container: null,
|
container: null,
|
||||||
cameraRender: 0,
|
cameraRender: 0,
|
||||||
viewport
|
viewport,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +208,10 @@ class Graph extends React.Component {
|
|||||||
|
|
||||||
// create all default rendering transformations
|
// create all default rendering transformations
|
||||||
const modelTF = createModelTF();
|
const modelTF = createModelTF();
|
||||||
const projectionTF = createProjectionTF(this.reglCanvas.width, this.reglCanvas.height);
|
const projectionTF = createProjectionTF(
|
||||||
|
this.reglCanvas.width,
|
||||||
|
this.reglCanvas.height
|
||||||
|
);
|
||||||
|
|
||||||
// initial draw to canvas
|
// initial draw to canvas
|
||||||
this.renderPoints(
|
this.renderPoints(
|
||||||
@@ -250,7 +252,8 @@ class Graph extends React.Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
const { regl, toolSVG, camera, modelTF, viewport } = this.state;
|
const { regl, toolSVG, camera, modelTF, viewport } = this.state;
|
||||||
let { projectionTF } = this.state;
|
let { projectionTF } = this.state;
|
||||||
const hasResized = prevState.viewport.height !== this.reglCanvas.height ||
|
const hasResized =
|
||||||
|
prevState.viewport.height !== this.reglCanvas.height ||
|
||||||
prevState.viewport.width !== this.reglCanvas.width;
|
prevState.viewport.width !== this.reglCanvas.width;
|
||||||
let stateChanges = {};
|
let stateChanges = {};
|
||||||
let needsRepaint = hasResized;
|
let needsRepaint = hasResized;
|
||||||
@@ -261,7 +264,10 @@ class Graph extends React.Component {
|
|||||||
const { drawPoints, pointBuffer, colorBuffer, flagBuffer } = this.state;
|
const { drawPoints, pointBuffer, colorBuffer, flagBuffer } = this.state;
|
||||||
|
|
||||||
if (hasResized) {
|
if (hasResized) {
|
||||||
projectionTF = createProjectionTF(this.reglCanvas.width, this.reglCanvas.height);
|
projectionTF = createProjectionTF(
|
||||||
|
this.reglCanvas.width,
|
||||||
|
this.reglCanvas.height
|
||||||
|
);
|
||||||
stateChanges = {
|
stateChanges = {
|
||||||
...stateChanges,
|
...stateChanges,
|
||||||
projectionTF,
|
projectionTF,
|
||||||
@@ -322,7 +328,10 @@ class Graph extends React.Component {
|
|||||||
...stateChanges,
|
...stateChanges,
|
||||||
...this.createToolSVG(),
|
...this.createToolSVG(),
|
||||||
};
|
};
|
||||||
} else if ((viewport.height && viewport.width && !toolSVG) || selectionTool !== prevProps.selectionTool ) {
|
} else if (
|
||||||
|
(viewport.height && viewport.width && !toolSVG) ||
|
||||||
|
selectionTool !== prevProps.selectionTool
|
||||||
|
) {
|
||||||
// first time or change of selection tool
|
// first time or change of selection tool
|
||||||
stateChanges = { ...stateChanges, ...this.createToolSVG() };
|
stateChanges = { ...stateChanges, ...this.createToolSVG() };
|
||||||
} else if (prevProps.graphInteractionMode !== graphInteractionMode) {
|
} else if (prevProps.graphInteractionMode !== graphInteractionMode) {
|
||||||
@@ -349,6 +358,7 @@ class Graph extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (Object.keys(stateChanges).length > 0) {
|
if (Object.keys(stateChanges).length > 0) {
|
||||||
|
// Preventing update loop via stateChanges and diff checks
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState(stateChanges);
|
this.setState(stateChanges);
|
||||||
}
|
}
|
||||||
@@ -371,12 +381,11 @@ class Graph extends React.Component {
|
|||||||
const { viewportRef } = this.props;
|
const { viewportRef } = this.props;
|
||||||
return {
|
return {
|
||||||
height: viewportRef.clientHeight,
|
height: viewportRef.clientHeight,
|
||||||
width: viewportRef.clientWidth
|
width: viewportRef.clientWidth,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCanvasEvent = (e) => {
|
||||||
handleCanvasEvent = e => {
|
|
||||||
const { camera, projectionTF } = this.state;
|
const { camera, projectionTF } = this.state;
|
||||||
if (e.type !== "wheel") e.preventDefault();
|
if (e.type !== "wheel") e.preventDefault();
|
||||||
if (camera.handleEvent(e, projectionTF)) {
|
if (camera.handleEvent(e, projectionTF)) {
|
||||||
@@ -547,12 +556,8 @@ class Graph extends React.Component {
|
|||||||
vec2.transformMat3(xy, xy, projectionTF);
|
vec2.transformMat3(xy, xy, projectionTF);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Math.round(
|
Math.round(((xy[0] + 1) * viewport.width) / 2),
|
||||||
(xy[0] + 1) * viewport.width / 2
|
Math.round(-((xy[1] + 1) / 2 - 1) * viewport.height),
|
||||||
),
|
|
||||||
Math.round(
|
|
||||||
-((xy[1] + 1) / 2 - 1) * viewport.height
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +703,7 @@ class Graph extends React.Component {
|
|||||||
count: this.count,
|
count: this.count,
|
||||||
projView,
|
projView,
|
||||||
nPoints: universe.nObs,
|
nPoints: universe.nObs,
|
||||||
minViewportDimension: Math.min(width, height)
|
minViewportDimension: Math.min(width, height),
|
||||||
});
|
});
|
||||||
regl._gl.flush();
|
regl._gl.flush();
|
||||||
}
|
}
|
||||||
@@ -744,9 +749,11 @@ class Graph extends React.Component {
|
|||||||
cameraTF={cameraTF}
|
cameraTF={cameraTF}
|
||||||
modelTF={modelTF}
|
modelTF={modelTF}
|
||||||
projectionTF={projectionTF}
|
projectionTF={projectionTF}
|
||||||
handleCanvasEvent={graphInteractionMode === "zoom" ? this.handleCanvasEvent : undefined}
|
handleCanvasEvent={
|
||||||
|
graphInteractionMode === "zoom" ? this.handleCanvasEvent : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CentroidLabels/>
|
<CentroidLabels />
|
||||||
</GraphOverlayLayer>
|
</GraphOverlayLayer>
|
||||||
<svg
|
<svg
|
||||||
id="lasso-layer"
|
id="lasso-layer"
|
||||||
@@ -756,13 +763,11 @@ class Graph extends React.Component {
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
zIndex: 1
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
width={viewport.width}
|
width={viewport.width}
|
||||||
height={viewport.height}
|
height={viewport.height}
|
||||||
pointerEvents={
|
pointerEvents={graphInteractionMode === "select" ? "auto" : "none"}
|
||||||
graphInteractionMode === "select" ? "auto" : "none"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
width={viewport.width}
|
width={viewport.width}
|
||||||
@@ -777,7 +782,9 @@ class Graph extends React.Component {
|
|||||||
}}
|
}}
|
||||||
className="graph-canvas"
|
className="graph-canvas"
|
||||||
data-testid="layout-graph"
|
data-testid="layout-graph"
|
||||||
ref={canvas => { this.reglCanvas = canvas; }}
|
ref={(canvas) => {
|
||||||
|
this.reglCanvas = canvas;
|
||||||
|
}}
|
||||||
onMouseDown={this.handleCanvasEvent}
|
onMouseDown={this.handleCanvasEvent}
|
||||||
onMouseUp={this.handleCanvasEvent}
|
onMouseUp={this.handleCanvasEvent}
|
||||||
onMouseMove={this.handleCanvasEvent}
|
onMouseMove={this.handleCanvasEvent}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
|
|
||||||
import React, { PureComponent } from "react";
|
import React, { PureComponent } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ class CentroidLabels extends PureComponent {
|
|||||||
// Notify overlay layer of display change
|
// Notify overlay layer of display change
|
||||||
overlayToggled("centroidLabels", displayChangeOn);
|
overlayToggled("centroidLabels", displayChangeOn);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -59,6 +57,7 @@ class CentroidLabels extends PureComponent {
|
|||||||
|
|
||||||
labelSVGS.push(
|
labelSVGS.push(
|
||||||
<g
|
<g
|
||||||
|
// label is unique so disabling eslint rule
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
key={label}
|
key={label}
|
||||||
className="centroid-label"
|
className="centroid-label"
|
||||||
@@ -66,6 +65,8 @@ class CentroidLabels extends PureComponent {
|
|||||||
data-testclass="centroid-label"
|
data-testclass="centroid-label"
|
||||||
data-testid={`${label}-centroid-label`}
|
data-testid={`${label}-centroid-label`}
|
||||||
>
|
>
|
||||||
|
{/* The mouse actions for centroid labels do not have a screen reader alternative */}
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
|
||||||
<text
|
<text
|
||||||
transform={inverseTransform}
|
transform={inverseTransform}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import React, { PureComponent, cloneElement } from "react";
|
|||||||
|
|
||||||
import styles from "../graph.css";
|
import styles from "../graph.css";
|
||||||
|
|
||||||
export default
|
export default class GraphOverlayLayer extends PureComponent {
|
||||||
class GraphOverlayLayer extends PureComponent {
|
|
||||||
/*
|
/*
|
||||||
This component takes its children (assumed in the data coordinate space ([0, 1] range, origin in bottom left corner))
|
This component takes its children (assumed in the data coordinate space ([0, 1] range, origin in bottom left corner))
|
||||||
and transforms itself multiple times resulting in screen space ([0, screenWidth/Height] range, origin in top left corner)
|
and transforms itself multiple times resulting in screen space ([0, screenWidth/Height] range, origin in top left corner)
|
||||||
@@ -49,7 +48,6 @@ class GraphOverlayLayer extends PureComponent {
|
|||||||
handleCanvasEvent,
|
handleCanvasEvent,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
style
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { display } = this.state;
|
const { display } = this.state;
|
||||||
|
|
||||||
@@ -63,7 +61,7 @@ class GraphOverlayLayer extends PureComponent {
|
|||||||
cameraTF
|
cameraTF
|
||||||
)} ${this.reverseMatrixScaleTransformString(
|
)} ${this.reverseMatrixScaleTransformString(
|
||||||
projectionTF
|
projectionTF
|
||||||
)} scale(1 2) scale(1 ${1 / (-height)}) scale(2 1) scale(${1 / width} 1)`;
|
)} scale(1 2) scale(1 ${1 / -height}) scale(2 1) scale(${1 / width} 1)`;
|
||||||
|
|
||||||
// Copy the children passed with the overlay and add the inverse transform and onDisplayChange props
|
// Copy the children passed with the overlay and add the inverse transform and onDisplayChange props
|
||||||
const newChildren = React.Children.map(children, (child) =>
|
const newChildren = React.Children.map(children, (child) =>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { connect } from "react-redux";
|
|||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Logo from "../framework/logo";
|
import Logo from "../framework/logo";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect((state) => ({
|
||||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||||
aboutURL: state.config?.links?.["about-dataset"],
|
aboutURL: state.config?.links?.["about-dataset"],
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||||
@@ -75,7 +75,7 @@ class LeftSideBar extends React.Component {
|
|||||||
title={datasetTitle}
|
title={datasetTitle}
|
||||||
>
|
>
|
||||||
{aboutURL ? (
|
{aboutURL ? (
|
||||||
<a href={aboutURL} target="_blank">
|
<a href={aboutURL} target="_blank" rel="noopener noreferrer">
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class CellSetButton extends React.PureComponent {
|
|||||||
<AnchorButton
|
<AnchorButton
|
||||||
type="button"
|
type="button"
|
||||||
disabled={differential.diffExp}
|
disabled={differential.diffExp}
|
||||||
onClick={this.set.bind(this)}
|
onClick={() => {
|
||||||
|
this.set();
|
||||||
|
}}
|
||||||
data-testid={`cellset-button-${eitherCellSetOneOrTwo}`}
|
data-testid={`cellset-button-${eitherCellSetOneOrTwo}`}
|
||||||
>
|
>
|
||||||
{eitherCellSetOneOrTwo}
|
{eitherCellSetOneOrTwo}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||||
Popover,
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
AnchorButton,
|
|
||||||
Tooltip,
|
|
||||||
Position,
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import styles from "./menubar.css";
|
import styles from "./menubar.css";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
@@ -21,16 +14,9 @@ import CellSetButton from "./cellSetButtons";
|
|||||||
celllist1: state.differential?.celllist1,
|
celllist1: state.differential?.celllist1,
|
||||||
celllist2: state.differential?.celllist2,
|
celllist2: state.differential?.celllist2,
|
||||||
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
|
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
|
||||||
diffexpCellcountMax: state.config?.limits?.diffexp_cellcount_max,
|
diffexpCellcountMax: state.config?.limits?.["diffexp_cellcount_max"],
|
||||||
}))
|
}))
|
||||||
class DiffexpButtons extends React.Component {
|
class DiffexpButtons extends React.Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
userDismissedPopover: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeDiffExp = () => {
|
computeDiffExp = () => {
|
||||||
const { dispatch, differential } = this.props;
|
const { dispatch, differential } = this.props;
|
||||||
if (differential.celllist1 && differential.celllist2) {
|
if (differential.celllist1 && differential.celllist2) {
|
||||||
@@ -54,16 +40,9 @@ class DiffexpButtons extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePopoverDismiss = () => {
|
|
||||||
this.setState({
|
|
||||||
userDismissedPopover: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
/* diffexp-related buttons may be disabled */
|
/* diffexp-related buttons may be disabled */
|
||||||
const { differential, diffexpMayBeSlow, diffexpCellcountMax } = this.props;
|
const { differential, diffexpMayBeSlow, diffexpCellcountMax } = this.props;
|
||||||
const { userDismissedPopover } = this.state;
|
|
||||||
|
|
||||||
const haveBothCellSets =
|
const haveBothCellSets =
|
||||||
!!differential.celllist1 && !!differential.celllist2;
|
!!differential.celllist1 && !!differential.celllist2;
|
||||||
@@ -86,7 +65,7 @@ class DiffexpButtons extends React.Component {
|
|||||||
diffexpCellcountMax;
|
diffexpCellcountMax;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup className={styles.menubarButton} >
|
<ButtonGroup className={styles.menubarButton}>
|
||||||
<CellSetButton
|
<CellSetButton
|
||||||
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
|
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
eitherCellSetOneOrTwo={1}
|
eitherCellSetOneOrTwo={1}
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ class Embedding extends React.PureComponent {
|
|||||||
const { layoutChoice } = this.props;
|
const { layoutChoice } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup
|
<ButtonGroup className={styles.menubarButton}>
|
||||||
className={styles.menubarButton}
|
|
||||||
>
|
|
||||||
<Popover
|
<Popover
|
||||||
target={
|
target={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import styles from "./menubar.css";
|
import styles from "./menubar.css";
|
||||||
@@ -28,7 +28,7 @@ import DiffexpButtons from "./diffexpButtons";
|
|||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||||
celllist1: state.differential.celllist1,
|
celllist1: state.differential.celllist1,
|
||||||
celllist2: state.differential.celllist2,
|
celllist2: state.differential.celllist2,
|
||||||
libraryVersions: state.config?.library_versions, // eslint-disable-line camelcase
|
libraryVersions: state.config?.["library_versions"],
|
||||||
undoDisabled: state["@@undoable/past"].length === 0,
|
undoDisabled: state["@@undoable/past"].length === 0,
|
||||||
redoDisabled: state["@@undoable/future"].length === 0,
|
redoDisabled: state["@@undoable/future"].length === 0,
|
||||||
aboutLink: state.config?.links?.["about-dataset"],
|
aboutLink: state.config?.links?.["about-dataset"],
|
||||||
@@ -207,9 +207,10 @@ class MenuBar extends React.Component {
|
|||||||
const { pendingClipPercentiles } = this.state;
|
const { pendingClipPercentiles } = this.state;
|
||||||
|
|
||||||
// constants used to create selection tool button
|
// constants used to create selection tool button
|
||||||
const [selectionTooltip, selectionButtonIcon] = selectionTool === "brush"
|
const [selectionTooltip, selectionButtonIcon] =
|
||||||
? ["Brush selection", "Lasso selection"]
|
selectionTool === "brush"
|
||||||
: ["select", "polygon-filter"];
|
? ["Brush selection", "Lasso selection"]
|
||||||
|
: ["select", "polygon-filter"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -245,80 +246,82 @@ class MenuBar extends React.Component {
|
|||||||
handleClipCommit={this.handleClipCommit}
|
handleClipCommit={this.handleClipCommit}
|
||||||
isClipDisabled={this.isClipDisabled}
|
isClipDisabled={this.isClipDisabled}
|
||||||
handleClipOnKeyPress={this.handleClipOnKeyPress}
|
handleClipOnKeyPress={this.handleClipOnKeyPress}
|
||||||
handleClipPercentileMaxValueChange={this.handleClipPercentileMaxValueChange}
|
handleClipPercentileMaxValueChange={
|
||||||
handleClipPercentileMinValueChange={this.handleClipPercentileMinValueChange}
|
this.handleClipPercentileMaxValueChange
|
||||||
|
}
|
||||||
|
handleClipPercentileMinValueChange={
|
||||||
|
this.handleClipPercentileMinValueChange
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Embedding />
|
<Embedding />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="When a category is colored by, show labels on the graph"
|
content="When a category is colored by, show labels on the graph"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
disabled={graphInteractionMode === "zoom"}
|
disabled={graphInteractionMode === "zoom"}
|
||||||
>
|
>
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
className={styles.menubarButton}
|
className={styles.menubarButton}
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="centroid-label-toggle"
|
data-testid="centroid-label-toggle"
|
||||||
icon="property"
|
icon="property"
|
||||||
onClick={this.handleCentroidChange}
|
onClick={this.handleCentroidChange}
|
||||||
active={showCentroidLabels}
|
active={showCentroidLabels}
|
||||||
intent={showCentroidLabels ? "primary" : "none"}
|
intent={showCentroidLabels ? "primary" : "none"}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ButtonGroup
|
<ButtonGroup className={styles.menubarButton}>
|
||||||
className={styles.menubarButton}
|
<Tooltip
|
||||||
>
|
content={selectionTooltip}
|
||||||
<Tooltip
|
position="bottom"
|
||||||
content={selectionTooltip}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
position="bottom"
|
>
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
<AnchorButton
|
||||||
>
|
type="button"
|
||||||
<AnchorButton
|
data-testid="mode-lasso"
|
||||||
type="button"
|
icon={selectionButtonIcon}
|
||||||
data-testid="mode-lasso"
|
active={graphInteractionMode === "select"}
|
||||||
icon={selectionButtonIcon}
|
onClick={() => {
|
||||||
active={graphInteractionMode === "select"}
|
dispatch({
|
||||||
onClick={() => {
|
type: "change graph interaction mode",
|
||||||
dispatch({
|
data: "select",
|
||||||
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={this.subsetPossible()}
|
||||||
|
subsetResetPossible={this.subsetResetPossible()}
|
||||||
|
handleSubset={() => {
|
||||||
|
dispatch(actions.setWorldToSelection());
|
||||||
|
dispatch({ type: "increment graph render counter" });
|
||||||
|
}}
|
||||||
|
handleSubsetReset={() => {
|
||||||
|
dispatch(actions.resetWorldToUniverse());
|
||||||
|
dispatch({ type: "increment graph render counter" });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
{disableDiffexp ? null : <DiffexpButtons />}
|
||||||
<Tooltip
|
</div>
|
||||||
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={this.subsetPossible()}
|
|
||||||
subsetResetPossible={this.subsetResetPossible()}
|
|
||||||
handleSubset={() => {
|
|
||||||
dispatch(actions.setWorldToSelection());
|
|
||||||
dispatch({ type: "increment graph render counter" });
|
|
||||||
}}
|
|
||||||
handleSubsetReset={() => {
|
|
||||||
dispatch(actions.resetWorldToUniverse());
|
|
||||||
dispatch({ type: "increment graph render counter" });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{disableDiffexp ? null : <DiffexpButtons/>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
viewport: {
|
viewport: {
|
||||||
height: null,
|
height: null,
|
||||||
width: null,
|
width: null,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
svg,
|
svg,
|
||||||
drawPoints,
|
drawPoints,
|
||||||
projectionTF,
|
projectionTF,
|
||||||
viewport
|
viewport,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,20 +286,11 @@ class Scatterplot extends React.PureComponent {
|
|||||||
return {
|
return {
|
||||||
viewport: {
|
viewport: {
|
||||||
height: window.height,
|
height: window.height,
|
||||||
width: window.width
|
width: window.width,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
handleResize = () => {
|
|
||||||
const { state } = this.state;
|
|
||||||
const viewport = this.getViewportDimensions();
|
|
||||||
this.setState({
|
|
||||||
...state,
|
|
||||||
viewport,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
static setupScales(expressionX, expressionY) {
|
static setupScales(expressionX, expressionY) {
|
||||||
const xScale = d3
|
const xScale = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
@@ -316,6 +307,15 @@ class Scatterplot extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
const { state } = this.state;
|
||||||
|
const viewport = this.getViewportDimensions();
|
||||||
|
this.setState({
|
||||||
|
...state,
|
||||||
|
viewport,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
updateViewportDimensions = () => {
|
updateViewportDimensions = () => {
|
||||||
this.setState(this.getViewportDimensions());
|
this.setState(this.getViewportDimensions());
|
||||||
};
|
};
|
||||||
@@ -390,7 +390,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
minViewportDimension: Math.min(
|
minViewportDimension: Math.min(
|
||||||
viewport.width - globals.leftSidebarWidth || width,
|
viewport.width - globals.leftSidebarWidth || width,
|
||||||
viewport.height || height
|
viewport.height || height
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
regl._gl.flush();
|
regl._gl.flush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import {
|
|||||||
Colors,
|
Colors,
|
||||||
Icon,
|
Icon,
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import * as globals from "../../globals";
|
|
||||||
import { termsOfServiceToast } from "../framework/toasters";
|
|
||||||
|
|
||||||
const CookieDecision = "cxg.cookieDecision";
|
const CookieDecision = "cxg.cookieDecision";
|
||||||
|
|
||||||
@@ -26,12 +24,14 @@ function storageGet(key, defaultValue = null) {
|
|||||||
function storageSet(key, value) {
|
function storageSet(key, value) {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(key, value);
|
window.localStorage.setItem(key, value);
|
||||||
} catch {}
|
} catch {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
tosURL: state.config?.parameters?.about_legal_tos,
|
tosURL: state.config?.parameters?.["about_legal_tos"],
|
||||||
privacyURL: state.config?.parameters?.about_legal_privacy,
|
privacyURL: state.config?.parameters?.["about_legal_privacy"],
|
||||||
}))
|
}))
|
||||||
class TermsPrompt extends React.PureComponent {
|
class TermsPrompt extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -59,7 +59,9 @@ class TermsPrompt extends React.PureComponent {
|
|||||||
if (window.cookieDecisionCallback instanceof Function) {
|
if (window.cookieDecisionCallback instanceof Function) {
|
||||||
try {
|
try {
|
||||||
window.cookieDecisionCallback();
|
window.cookieDecisionCallback();
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,6 +84,7 @@ class TermsPrompt extends React.PureComponent {
|
|||||||
}}
|
}}
|
||||||
href={tosURL}
|
href={tosURL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
terms of service
|
terms of service
|
||||||
</a>
|
</a>
|
||||||
@@ -103,6 +106,7 @@ class TermsPrompt extends React.PureComponent {
|
|||||||
}}
|
}}
|
||||||
href={privacyURL}
|
href={privacyURL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
privacy policy
|
privacy policy
|
||||||
</a>
|
</a>
|
||||||
@@ -118,12 +122,10 @@ class TermsPrompt extends React.PureComponent {
|
|||||||
<Drawer
|
<Drawer
|
||||||
onclose={this.drawerClose}
|
onclose={this.drawerClose}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={"120px"}
|
size="120px"
|
||||||
position={Position.BOTTOM}
|
position={Position.BOTTOM}
|
||||||
canOutsideClickClose={false}
|
canOutsideClickClose={false}
|
||||||
hasBackdrop={
|
hasBackdrop /* if the user can't use the app or click outside to dismiss, that should be visually represented with a backdrop */
|
||||||
true /* if the user can't use the app or click outside to dismiss, that should be visually represented with a backdrop */
|
|
||||||
}
|
|
||||||
enforceFocus={false}
|
enforceFocus={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
portal={false}
|
portal={false}
|
||||||
@@ -149,12 +151,9 @@ class TermsPrompt extends React.PureComponent {
|
|||||||
onClick={this.handleOK}
|
onClick={this.handleOK}
|
||||||
data-testid="tos-cookies-accept"
|
data-testid="tos-cookies-accept"
|
||||||
>
|
>
|
||||||
I'm OK with cookies!
|
I'm OK with cookies!
|
||||||
</Button>{" "}
|
</Button>{" "}
|
||||||
<Button
|
<Button onClick={this.handleNo} data-testid="tos-cookies-reject">
|
||||||
onClick={this.handleNo}
|
|
||||||
data-testid="tos-cookies-reject"
|
|
||||||
>
|
|
||||||
No thanks
|
No thanks
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
/* eslint-disable no-console */
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const ColorsReducer = (
|
|||||||
case "annotation: delete label": {
|
case "annotation: delete label": {
|
||||||
const { world } = nextSharedState;
|
const { world } = nextSharedState;
|
||||||
const { colorMode, colorAccessor } = state;
|
const { colorMode, colorAccessor } = state;
|
||||||
const { metadataField, colors } = action;
|
const { metadataField } = action;
|
||||||
if (
|
if (
|
||||||
colorMode !== "color by categorical metadata" ||
|
colorMode !== "color by categorical metadata" ||
|
||||||
colorAccessor !== metadataField
|
colorAccessor !== metadataField
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const LayoutChoice = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "reembed: add reembedding": {
|
case "reembed: add reembedding": {
|
||||||
const name = action.schema.name;
|
const { name } = action.schema;
|
||||||
const available = Array.from(new Set(state.available).add(name));
|
const available = Array.from(new Set(state.available).add(name));
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ const Ontology = (
|
|||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "configuration load complete": {
|
case "configuration load complete": {
|
||||||
/* eslint-disable camelcase */
|
|
||||||
const enabled =
|
const enabled =
|
||||||
action.config?.parameters?.annotations_cell_ontology_enabled ?? false;
|
action.config?.parameters?.["annotations_cell_ontology_enabled"] ??
|
||||||
const terms = action.config?.parameters?.annotations_cell_ontology_terms;
|
false;
|
||||||
/* eslint-enable camelcase */
|
const terms =
|
||||||
|
action.config?.parameters?.["annotations_cell_ontology_terms"];
|
||||||
|
|
||||||
const termSet = new Set(terms);
|
const termSet = new Set(terms);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const WorldReducer = (
|
|||||||
const { dim } = action;
|
const { dim } = action;
|
||||||
|
|
||||||
// we don't clip anything except for varData and obsAnnotations
|
// we don't clip anything except for varData and obsAnnotations
|
||||||
let unclipped = state.unclipped;
|
let { unclipped } = state;
|
||||||
if (dim == "varData" || dim == "obsAnnotations") {
|
if (dim === "varData" || dim === "obsAnnotations") {
|
||||||
unclipped = {
|
unclipped = {
|
||||||
...unclipped,
|
...unclipped,
|
||||||
[dim]: universe[dim].clone(),
|
[dim]: universe[dim].clone(),
|
||||||
@@ -298,7 +298,7 @@ const WorldReducer = (
|
|||||||
const { obsLayout: origObsLayout, schema: origSchema } = state;
|
const { obsLayout: origObsLayout, schema: origSchema } = state;
|
||||||
const { embedding, schema: embeddingSchema } = action;
|
const { embedding, schema: embeddingSchema } = action;
|
||||||
|
|
||||||
const { dims, name } = embeddingSchema;
|
const { dims } = embeddingSchema;
|
||||||
let obsLayout = origObsLayout;
|
let obsLayout = origObsLayout;
|
||||||
let schema = origSchema;
|
let schema = origSchema;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
/**
|
/**
|
||||||
Label indexing - map a label to & from an integer offset. See Dataframe
|
Label indexing - map a label to & from an integer offset. See Dataframe
|
||||||
for how this is used.
|
for how this is used.
|
||||||
@@ -23,7 +24,6 @@ function extent(tarr) {
|
|||||||
return [min, max];
|
return [min, max];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
class IdentityInt32Index {
|
class IdentityInt32Index {
|
||||||
/*
|
/*
|
||||||
identity/noop index, with small assumptions that labels are int32
|
identity/noop index, with small assumptions that labels are int32
|
||||||
@@ -41,11 +41,13 @@ class IdentityInt32Index {
|
|||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getOffset(i) {
|
getOffset(i) {
|
||||||
// label to offset
|
// label to offset
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getLabel(i) {
|
getLabel(i) {
|
||||||
// offset to label
|
// offset to label
|
||||||
return i;
|
return i;
|
||||||
@@ -93,9 +95,6 @@ class IdentityInt32Index {
|
|||||||
return this.__promote(labelArray);
|
return this.__promote(labelArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-enable class-methods-use-this */
|
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
class DenseInt32Index {
|
class DenseInt32Index {
|
||||||
/*
|
/*
|
||||||
DenseInt32Index indexes integer labels, and uses Int32Array typed arrays
|
DenseInt32Index indexes integer labels, and uses Int32Array typed arrays
|
||||||
@@ -177,9 +176,7 @@ class DenseInt32Index {
|
|||||||
return this.__promote(labelArray);
|
return this.__promote(labelArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-enable class-methods-use-this */
|
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
class KeyIndex {
|
class KeyIndex {
|
||||||
/*
|
/*
|
||||||
KeyIndex indexes arbitrary JS primitive types, and uses a Map()
|
KeyIndex indexes arbitrary JS primitive types, and uses a Map()
|
||||||
@@ -223,6 +220,7 @@ class KeyIndex {
|
|||||||
return this.rindex.length;
|
return this.rindex.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
subsetLabels(labelArray) {
|
subsetLabels(labelArray) {
|
||||||
return new KeyIndex(labelArray);
|
return new KeyIndex(labelArray);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ return Promise.all([
|
|||||||
plimit.add(() => fetch('/baz'))
|
plimit.add(() => fetch('/baz'))
|
||||||
])
|
])
|
||||||
*/
|
*/
|
||||||
export class PromiseLimit {
|
export default class PromiseLimit {
|
||||||
constructor(maxConcurrency) {
|
constructor(maxConcurrency) {
|
||||||
this.queue = new Set();
|
this.queue = new Set();
|
||||||
this.maxConcurrency = maxConcurrency;
|
this.maxConcurrency = maxConcurrency;
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
export default (n) => {
|
export default (n) => {
|
||||||
return n
|
return n
|
||||||
.toExponential()
|
.toExponential()
|
||||||
.replace(/e[\+\-0-9]*$/, "")
|
.replace(/e[+\-0-9]*$/, "")
|
||||||
.replace(/^0\.?0*|\./, "").length;
|
.replace(/^0\.?0*|\./, "").length;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import PositiveIntervals from "./positiveIntervals";
|
import PositiveIntervals from "./positiveIntervals";
|
||||||
import BitArray from "./bitArray";
|
import BitArray from "./bitArray";
|
||||||
import {
|
import {
|
||||||
@@ -409,7 +410,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
this.index = makeSortIndex(array);
|
this.index = makeSortIndex(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_createValueArray(data, mapf, array) {
|
_createValueArray(data, mapf, array) {
|
||||||
// create dimension value array
|
// create dimension value array
|
||||||
const len = data.length;
|
const len = data.length;
|
||||||
@@ -419,7 +420,6 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
}
|
}
|
||||||
return larray;
|
return larray;
|
||||||
}
|
}
|
||||||
/* eslint-enable class-methods-use-this */
|
|
||||||
|
|
||||||
select(spec) {
|
select(spec) {
|
||||||
const { mode } = spec;
|
const { mode } = spec;
|
||||||
@@ -466,7 +466,7 @@ class ImmutableScalarDimension extends _ImmutableBaseDimension {
|
|||||||
const ranges = [];
|
const ranges = [];
|
||||||
const r = [
|
const r = [
|
||||||
lowerBoundIndirect(value, index, lo, 0, value.length),
|
lowerBoundIndirect(value, index, lo, 0, value.length),
|
||||||
!!inclusive
|
inclusive
|
||||||
? upperBoundIndirect(value, index, hi, 0, value.length)
|
? upperBoundIndirect(value, index, hi, 0, value.length)
|
||||||
: lowerBoundIndirect(value, index, hi, 0, value.length),
|
: lowerBoundIndirect(value, index, hi, 0, value.length),
|
||||||
];
|
];
|
||||||
@@ -514,11 +514,10 @@ class ImmutableEnumDimension extends ImmutableScalarDimension {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
// eslint-disable-next-line class-methods-use-this
|
||||||
selectRange() {
|
selectRange() {
|
||||||
throw new Error("range selection unsupported on Enumerated dimension");
|
throw new Error("range selection unsupported on Enumerated dimension");
|
||||||
}
|
}
|
||||||
/* eslint-enable class-methods-use-this */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmutableSpatialDimension extends _ImmutableBaseDimension {
|
class ImmutableSpatialDimension extends _ImmutableBaseDimension {
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ def get_data_adaptor(dataset=None):
|
|||||||
raise DatasetAccessError("Invalid dataset {dataset}")
|
raise DatasetAccessError("Invalid dataset {dataset}")
|
||||||
|
|
||||||
if datapath is None:
|
if datapath is None:
|
||||||
return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, f"Invalid dataset NONE", loglevel=logging.INFO)
|
return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, "Invalid dataset NONE", loglevel=logging.INFO)
|
||||||
|
|
||||||
cache_manager = current_app.matrix_data_cache_manager
|
cache_manager = current_app.matrix_data_cache_manager
|
||||||
return cache_manager.data_adaptor(datapath, config)
|
return cache_manager.data_adaptor(datapath, config)
|
||||||
|
|||||||
@@ -279,13 +279,13 @@ class AppConfig(object):
|
|||||||
if self.server__csp_directives is not None:
|
if self.server__csp_directives is not None:
|
||||||
for k, v in self.server__csp_directives.items():
|
for k, v in self.server__csp_directives.items():
|
||||||
if not isinstance(k, str):
|
if not isinstance(k, str):
|
||||||
raise ConfigurationError(f"CSP directive names must be a string.")
|
raise ConfigurationError("CSP directive names must be a string.")
|
||||||
if isinstance(v, list):
|
if isinstance(v, list):
|
||||||
for policy in v:
|
for policy in v:
|
||||||
if not isinstance(policy, str):
|
if not isinstance(policy, str):
|
||||||
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
raise ConfigurationError("CSP directive value must be a string or list of strings.")
|
||||||
elif not isinstance(v, str):
|
elif not isinstance(v, str):
|
||||||
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
raise ConfigurationError("CSP directive value must be a string or list of strings.")
|
||||||
|
|
||||||
# scripts can be string (filename) or dict (attributes). Convert string to dict.
|
# scripts can be string (filename) or dict (attributes). Convert string to dict.
|
||||||
scripts = []
|
scripts = []
|
||||||
@@ -476,8 +476,8 @@ class AppConfig(object):
|
|||||||
with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
|
with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
|
||||||
if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False):
|
if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False):
|
||||||
context["messagefn"](
|
context["messagefn"](
|
||||||
f"CAUTION: due to the size of your dataset, "
|
"CAUTION: due to the size of your dataset, "
|
||||||
f"running differential expression may take longer or fail."
|
"running differential expression may take longer or fail."
|
||||||
)
|
)
|
||||||
|
|
||||||
max_workers = self.diffexp__alg_cxg__max_workers
|
max_workers = self.diffexp__alg_cxg__max_workers
|
||||||
|
|||||||
@@ -183,13 +183,13 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
def _validate_and_initialize(self):
|
def _validate_and_initialize(self):
|
||||||
if anndata_version_is_pre_070() and self.config.adaptor__anndata_adaptor__backed:
|
if anndata_version_is_pre_070() and self.config.adaptor__anndata_adaptor__backed:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Use of --backed mode with anndata versions older than 0.7 will have serious "
|
"Use of --backed mode with anndata versions older than 0.7 will have serious "
|
||||||
"performance issues. Please update to at least anndata 0.7 or later."
|
"performance issues. Please update to at least anndata 0.7 or later."
|
||||||
)
|
)
|
||||||
|
|
||||||
# var and obs column names must be unique
|
# var and obs column names must be unique
|
||||||
if not self.data.obs.columns.is_unique or not self.data.var.columns.is_unique:
|
if not self.data.obs.columns.is_unique or not self.data.var.columns.is_unique:
|
||||||
raise KeyError(f"All annotation column names must be unique.")
|
raise KeyError("All annotation column names must be unique.")
|
||||||
|
|
||||||
self._alias_annotation_names()
|
self._alias_annotation_names()
|
||||||
self._validate_data_types()
|
self._validate_data_types()
|
||||||
@@ -222,8 +222,8 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
X0 = self.data.X[0, 0:1]
|
X0 = self.data.X[0, 0:1]
|
||||||
if sparse.isspmatrix(X0) and not sparse.isspmatrix_csc(X0):
|
if sparse.isspmatrix(X0) and not sparse.isspmatrix_csc(X0):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Anndata data matrix is sparse, but not a CSC (columnar) matrix. "
|
"Anndata data matrix is sparse, but not a CSC (columnar) matrix. "
|
||||||
f"Performance may be improved by using CSC."
|
"Performance may be improved by using CSC."
|
||||||
)
|
)
|
||||||
if self.data.X.dtype != "float32":
|
if self.data.X.dtype != "float32":
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
@@ -295,7 +295,7 @@ class AnndataAdaptor(DataAdaptor):
|
|||||||
valid_layouts.append(layout)
|
valid_layouts.append(layout)
|
||||||
|
|
||||||
if len(valid_layouts) == 0:
|
if len(valid_layouts) == 0:
|
||||||
raise PrepareError(f"No valid layout data.")
|
raise PrepareError("No valid layout data.")
|
||||||
|
|
||||||
# cap layouts to MAX_LAYOUTS
|
# cap layouts to MAX_LAYOUTS
|
||||||
return layouts[0:MAX_LAYOUTS]
|
return layouts[0:MAX_LAYOUTS]
|
||||||
|
|||||||
@@ -230,18 +230,19 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
|
|
||||||
# all labels must have a name, which must be unique and not used in obs column names
|
# all labels must have a name, which must be unique and not used in obs column names
|
||||||
if not labels_df.columns.is_unique:
|
if not labels_df.columns.is_unique:
|
||||||
raise KeyError(f"All column names specified in user annotations must be unique.")
|
raise KeyError("All column names specified in user annotations must be unique.")
|
||||||
|
|
||||||
# the label index must be unique, and must have same values the anndata obs index
|
# the label index must be unique, and must have same values the anndata obs index
|
||||||
if not labels_df.index.is_unique:
|
if not labels_df.index.is_unique:
|
||||||
raise KeyError(f"All row index values specified in user annotations must be unique.")
|
raise KeyError("All row index values specified in user annotations must be unique.")
|
||||||
|
|
||||||
obs_columns = self.get_obs_columns()
|
obs_columns = self.get_obs_columns()
|
||||||
|
|
||||||
duplicate_columns = list(set(labels_df.columns) & set(obs_columns))
|
duplicate_columns = list(set(labels_df.columns) & set(obs_columns))
|
||||||
if len(duplicate_columns) > 0:
|
if len(duplicate_columns) > 0:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
f"Labels file may not contain column names which overlap " f"with h5ad obs columns {duplicate_columns}"
|
"Labels file may not contain column names which overlap "
|
||||||
|
f"with h5ad obs columns {duplicate_columns}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# labels must have same count as obs annotations
|
# labels must have same count as obs annotations
|
||||||
@@ -351,13 +352,13 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
embeddings = self.get_embedding_names() if fields is None or len(fields) == 0 else fields
|
embeddings = self.get_embedding_names() if fields is None or len(fields) == 0 else fields
|
||||||
layout_data = []
|
layout_data = []
|
||||||
with ServerTiming.time(f"layout.query"):
|
with ServerTiming.time("layout.query"):
|
||||||
for ename in embeddings:
|
for ename in embeddings:
|
||||||
embedding = self.get_embedding_array(ename, 2)
|
embedding = self.get_embedding_array(ename, 2)
|
||||||
normalized_layout = DataAdaptor.normalize_embedding(embedding)
|
normalized_layout = DataAdaptor.normalize_embedding(embedding)
|
||||||
layout_data.append(pd.DataFrame(normalized_layout, columns=[f"{ename}_0", f"{ename}_1"]))
|
layout_data.append(pd.DataFrame(normalized_layout, columns=[f"{ename}_0", f"{ename}_1"]))
|
||||||
|
|
||||||
with ServerTiming.time(f"layout.encode"):
|
with ServerTiming.time("layout.encode"):
|
||||||
if layout_data:
|
if layout_data:
|
||||||
df = pd.concat(layout_data, axis=1, copy=False)
|
df = pd.concat(layout_data, axis=1, copy=False)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class MatrixDataLoader(object):
|
|||||||
self.matrix_data_type = self.__matrix_data_type()
|
self.matrix_data_type = self.__matrix_data_type()
|
||||||
|
|
||||||
if not self.__matrix_data_type_allowed(app_config):
|
if not self.__matrix_data_type_allowed(app_config):
|
||||||
raise DatasetAccessError(f"Dataset does not have an allowed type.")
|
raise DatasetAccessError("Dataset does not have an allowed type.")
|
||||||
|
|
||||||
if self.matrix_data_type == MatrixDataType.H5AD:
|
if self.matrix_data_type == MatrixDataType.H5AD:
|
||||||
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
from server.data_anndata.anndata_adaptor import AnndataAdaptor
|
||||||
@@ -272,7 +272,7 @@ class MatrixDataLoader(object):
|
|||||||
|
|
||||||
def pre_load_validation(self):
|
def pre_load_validation(self):
|
||||||
if self.matrix_data_type == MatrixDataType.UNKNOWN:
|
if self.matrix_data_type == MatrixDataType.UNKNOWN:
|
||||||
raise DatasetAccessError(f"Dataset does not have a recognized type: .h5ad or .cxg")
|
raise DatasetAccessError("Dataset does not have a recognized type: .h5ad or .cxg")
|
||||||
self.matrix_type.pre_load_validation(self.location)
|
self.matrix_type.pre_load_validation(self.location)
|
||||||
|
|
||||||
def file_size(self):
|
def file_size(self):
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
# function to get the embedding
|
# function to get the embedding
|
||||||
# this function to iterate through embeddings.
|
# this function to iterate through embeddings.
|
||||||
def get_embedding_names(self):
|
def get_embedding_names(self):
|
||||||
with ServerTiming.time(f"layout.lsuri"):
|
with ServerTiming.time("layout.lsuri"):
|
||||||
pemb = self.get_path("emb")
|
pemb = self.get_path("emb")
|
||||||
embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"]
|
embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"]
|
||||||
if len(embeddings) == 0:
|
if len(embeddings) == 0:
|
||||||
@@ -311,7 +311,7 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
A = self.open_array(ax)
|
A = self.open_array(ax)
|
||||||
schema_hints = json.loads(A.meta["cxg_schema"]) if "cxg_schema" in A.meta else {}
|
schema_hints = json.loads(A.meta["cxg_schema"]) if "cxg_schema" in A.meta else {}
|
||||||
if type(schema_hints) is not dict:
|
if type(schema_hints) is not dict:
|
||||||
raise TypeError(f"Array schema was malformed.")
|
raise TypeError("Array schema was malformed.")
|
||||||
|
|
||||||
cols = []
|
cols = []
|
||||||
for attr in A.schema:
|
for attr in A.schema:
|
||||||
|
|||||||
+3
-3
@@ -156,7 +156,7 @@ try:
|
|||||||
|
|
||||||
dataroot = os.getenv("CXG_DATAROOT")
|
dataroot = os.getenv("CXG_DATAROOT")
|
||||||
if dataroot:
|
if dataroot:
|
||||||
logging.info(f"Configuration from CXG_DATAROOT")
|
logging.info("Configuration from CXG_DATAROOT")
|
||||||
app_config.update(multi_dataset__dataroot=dataroot)
|
app_config.update(multi_dataset__dataroot=dataroot)
|
||||||
|
|
||||||
secret_name = os.getenv("CXG_AWS_SECRET_NAME")
|
secret_name = os.getenv("CXG_AWS_SECRET_NAME")
|
||||||
@@ -171,7 +171,7 @@ try:
|
|||||||
if not secret_region_name:
|
if not secret_region_name:
|
||||||
secret_region_name = discover_s3_region_name(config_file)
|
secret_region_name = discover_s3_region_name(config_file)
|
||||||
if not secret_region_name:
|
if not secret_region_name:
|
||||||
logging.error(f"Could not determine the AWS Secret Manager region")
|
logging.error("Could not determine the AWS Secret Manager region")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
flask_secret_key = get_flask_secret_key(secret_region_name, secret_name)
|
flask_secret_key = get_flask_secret_key(secret_region_name, secret_name)
|
||||||
@@ -188,7 +188,7 @@ try:
|
|||||||
|
|
||||||
if not app_config.server__flask_secret_key:
|
if not app_config.server__flask_secret_key:
|
||||||
logging.critical(
|
logging.critical(
|
||||||
f"flask_secret_key is not provided. Either set in config file, CXG_SECRET_KEY environment variable, "
|
"flask_secret_key is not provided. Either set in config file, CXG_SECRET_KEY environment variable, "
|
||||||
"or in AWS Secret Manager"
|
"or in AWS Secret Manager"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -29,16 +29,12 @@ def check(expected, custom):
|
|||||||
# cdict must only have exact requirements (==)
|
# cdict must only have exact requirements (==)
|
||||||
for cname, cspecs in cdict.items():
|
for cname, cspecs in cdict.items():
|
||||||
if len(cspecs) != 1 or cspecs[0][0] != "==":
|
if len(cspecs) != 1 or cspecs[0][0] != "==":
|
||||||
print(
|
print(f"Error, spec must be an exact requirement {custom}: {cname} {str(cspecs)}")
|
||||||
f"Error, spec must be an exact requirement {custom}: {cname} {str(cspecs)}"
|
|
||||||
)
|
|
||||||
okay = False
|
okay = False
|
||||||
|
|
||||||
for ename, especs in edict.items():
|
for ename, especs in edict.items():
|
||||||
if ename not in cdict:
|
if ename not in cdict:
|
||||||
print(
|
print(f"Error, missing requirement from {custom}: {ename} {str(especs)}")
|
||||||
f"Error, missing requirement from {custom}: {ename} {str(especs)}"
|
|
||||||
)
|
|
||||||
okay = False
|
okay = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -46,9 +42,7 @@ def check(expected, custom):
|
|||||||
for espec in especs:
|
for espec in especs:
|
||||||
rokay = check_version(cver, espec[0], Version(espec[1]))
|
rokay = check_version(cver, espec[0], Version(espec[1]))
|
||||||
if not rokay:
|
if not rokay:
|
||||||
print(
|
print(f"Error, failed requirement from {custom}: {ename} {espec}, {cver}")
|
||||||
f"Error, failed requirement from {custom}: {ename} {espec}, {cver}"
|
|
||||||
)
|
|
||||||
okay = False
|
okay = False
|
||||||
|
|
||||||
if okay:
|
if okay:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
anndata>=0.6.20
|
anndata>=0.6.20
|
||||||
boto3>=1.12.18
|
boto3>=1.12.18
|
||||||
click>=6.7
|
click>=7.1.2
|
||||||
fastobo>=0.6.1
|
fastobo>=0.6.1
|
||||||
Flask>=1.0.2
|
Flask>=1.0.2
|
||||||
Flask-Compress>=1.4.0
|
Flask-Compress>=1.4.0
|
||||||
|
|||||||
@@ -185,14 +185,14 @@ class EndPoints(object):
|
|||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_data_mimetype_error(self):
|
def test_data_mimetype_error(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
header = {"Accept": "xxx"}
|
header = {"Accept": "xxx"}
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
result = self.session.put(url, headers=header)
|
result = self.session.put(url, headers=header)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
|
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
|
|
||||||
def test_fbs_default(self):
|
def test_fbs_default(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
result = self.session.put(url)
|
result = self.session.put(url)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
@@ -202,21 +202,21 @@ class EndPoints(object):
|
|||||||
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
|
||||||
def test_data_put_fbs(self):
|
def test_data_put_fbs(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
result = self.session.put(url, headers=header)
|
result = self.session.put(url, headers=header)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_data_get_fbs(self):
|
def test_data_get_fbs(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
result = self.session.get(url, headers=header)
|
result = self.session.get(url, headers=header)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_data_put_filter_fbs(self):
|
def test_data_put_filter_fbs(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
filter = {"filter": {"var": {"index": [0, 1, 4]}}}
|
filter = {"filter": {"var": {"index": [0, 1, 4]}}}
|
||||||
@@ -233,8 +233,8 @@ class EndPoints(object):
|
|||||||
|
|
||||||
def test_data_get_filter_fbs(self):
|
def test_data_get_filter_fbs(self):
|
||||||
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
||||||
|
endpoint = "data/var"
|
||||||
query = f"var:{index_col_name}=SIK1"
|
query = f"var:{index_col_name}=SIK1"
|
||||||
endpoint = f"data/var"
|
|
||||||
url = f"{self.URL_BASE}{endpoint}?{query}"
|
url = f"{self.URL_BASE}{endpoint}?{query}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
result = self.session.get(url, headers=header)
|
result = self.session.get(url, headers=header)
|
||||||
@@ -245,7 +245,7 @@ class EndPoints(object):
|
|||||||
self.assertEqual(df["n_cols"], 1)
|
self.assertEqual(df["n_cols"], 1)
|
||||||
|
|
||||||
def test_data_put_single_var(self):
|
def test_data_put_single_var(self):
|
||||||
endpoint = f"data/var"
|
endpoint = "data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
||||||
@@ -306,7 +306,7 @@ class EndPointsAnnotations(EndPoints):
|
|||||||
query = "annotation-collection-name=test_annotations"
|
query = "annotation-collection-name=test_annotations"
|
||||||
url = f"{self.URL_BASE}{endpoint}?{query}"
|
url = f"{self.URL_BASE}{endpoint}?{query}"
|
||||||
n_rows = self.data.get_shape()[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = make_fbs({"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category")})
|
fbs = make_fbs({"cat_A": pd.Series(["label_A"] * n_rows, dtype="category")})
|
||||||
result = self.session.put(url, data=fbs)
|
result = self.session.put(url, data=fbs)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
def test_error_checks(self):
|
def test_error_checks(self):
|
||||||
# verify that the expected errors are generated
|
# verify that the expected errors are generated
|
||||||
n_rows = self.data.get_shape()[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs_bad = make_fbs({"louvain": pd.Series(["undefined" for l in range(0, n_rows)], dtype="category")})
|
fbs_bad = make_fbs({"louvain": pd.Series(["undefined"] * n_rows, dtype="category")})
|
||||||
|
|
||||||
# ensure we catch attempt to overwrite non-writable data
|
# ensure we catch attempt to overwrite non-writable data
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
@@ -38,8 +38,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
n_rows = self.data.get_shape()[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A"] * n_rows, dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B"] * n_rows, dtype="category"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
res = self.annotation_put_fbs(fbs)
|
res = self.annotation_put_fbs(fbs)
|
||||||
@@ -49,14 +49,14 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
self.assertEqual(df.shape, (n_rows, 2))
|
self.assertEqual(df.shape, (n_rows, 2))
|
||||||
self.assertEqual(set(df.columns), {"cat_A", "cat_B"})
|
self.assertEqual(set(df.columns), {"cat_A", "cat_B"})
|
||||||
self.assertTrue(self.data.original_obs_index.equals(df.index))
|
self.assertTrue(self.data.original_obs_index.equals(df.index))
|
||||||
self.assertTrue(np.all(df["cat_A"] == ["label_A" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_A"] == ["label_A"] * n_rows))
|
||||||
self.assertTrue(np.all(df["cat_B"] == ["label_B" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_B"] == ["label_B"] * n_rows))
|
||||||
|
|
||||||
# verify complete overwrite on second attempt, AND rotation occurs
|
# verify complete overwrite on second attempt, AND rotation occurs
|
||||||
fbs = make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A1" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A1"] * n_rows, dtype="category"),
|
||||||
"cat_C": pd.Series(["label_C" for l in range(0, n_rows)], dtype="category"),
|
"cat_C": pd.Series(["label_C"] * n_rows, dtype="category"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
res = self.annotation_put_fbs(fbs)
|
res = self.annotation_put_fbs(fbs)
|
||||||
@@ -64,8 +64,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
self.assertTrue(path.exists(self.annotations.output_file))
|
self.assertTrue(path.exists(self.annotations.output_file))
|
||||||
df = pd.read_csv(self.annotations.output_file, index_col=0, header=0, comment="#")
|
df = pd.read_csv(self.annotations.output_file, index_col=0, header=0, comment="#")
|
||||||
self.assertEqual(set(df.columns), {"cat_A", "cat_C"})
|
self.assertEqual(set(df.columns), {"cat_A", "cat_C"})
|
||||||
self.assertTrue(np.all(df["cat_A"] == ["label_A1" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_A"] == ["label_A1"] * n_rows))
|
||||||
self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)]))
|
self.assertTrue(np.all(df["cat_C"] == ["label_C"] * n_rows))
|
||||||
|
|
||||||
# rotation
|
# rotation
|
||||||
name, ext = path.splitext(self.annotations.output_file)
|
name, ext = path.splitext(self.annotations.output_file)
|
||||||
@@ -79,8 +79,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
n_rows = self.data.get_shape()[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A"] * n_rows, dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B"] * n_rows, dtype="category"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for i in range(0, 11):
|
for i in range(0, 11):
|
||||||
@@ -100,8 +100,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
n_rows = self.data.get_shape()[0]
|
n_rows = self.data.get_shape()[0]
|
||||||
fbs = make_fbs(
|
fbs = make_fbs(
|
||||||
{
|
{
|
||||||
"cat_A": pd.Series(["label_A" for l in range(0, n_rows)], dtype="category"),
|
"cat_A": pd.Series(["label_A"] * n_rows, dtype="category"),
|
||||||
"cat_B": pd.Series(["label_B" for l in range(0, n_rows)], dtype="category"),
|
"cat_B": pd.Series(["label_B"] * n_rows, dtype="category"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ class WritableAnnotationTest(unittest.TestCase):
|
|||||||
[obs_index_col_name, "n_genes", "percent_mito", "n_counts", "louvain", "cat_A", "cat_B"],
|
[obs_index_col_name, "n_genes", "percent_mito", "n_counts", "louvain", "cat_A", "cat_B"],
|
||||||
)
|
)
|
||||||
col_idx = annotations["col_idx"]
|
col_idx = annotations["col_idx"]
|
||||||
self.assertEqual(annotations["columns"][col_idx.index("cat_A")], ["label_A" for l in range(0, n_rows)])
|
self.assertEqual(annotations["columns"][col_idx.index("cat_A")], ["label_A"] * n_rows)
|
||||||
self.assertEqual(annotations["columns"][col_idx.index("cat_B")], ["label_B" for l in range(0, n_rows)])
|
self.assertEqual(annotations["columns"][col_idx.index("cat_B")], ["label_B"] * n_rows)
|
||||||
|
|
||||||
# verify the schema was updated
|
# verify the schema was updated
|
||||||
all_col_schema = {c["name"]: c for c in schema["annotations"]["obs"]["columns"]}
|
all_col_schema = {c["name"]: c for c in schema["annotations"]["obs"]["columns"]}
|
||||||
|
|||||||
Reference in New Issue
Block a user