mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 22:18:13 +08:00
TS Revert (1) (#2402)
* revert all commits to before Typescript migration * update compat workflow to match latest deps (#2335) * update compat workflow to match latest deps * attempt to debug * attempt to debug * remove debugging code * typo * update deps to match desktop (#2340) * fix: don't run lint with `--fix` on push tests (#2273) * fix: don't run lint with `--fix` on push tests * npx Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com> * rename X_approx_distribution to X_approximate_distribution (#2337) * Correctly handle non-finite numbers in heuristic determination of X distribution (#2342) * handle non-finites explicitly * improve and test edge case handling for distribution estimation * revert debugging changes * code readability * clean up type inferencing (#2332) * unit tests for 64 bit conversion * clean up type handling * type inference tests * more type inference fixes * use schema to determine user intent for data typing * stop using deprecated API * fbs type encoding test * add missing test * add more tests * correctly infer X type for CXG adaptor * lint * fix typo * ts migration * cleanup from PR review * lint * PR review changes * remove unused packages from client (#2359) * remove unused packages from client * add missing peer dep * fix: disable FE auth testing on compatibility tests (#2377) * update: release process (#2277) Co-authored-by: maniarathi <mani.arathi@gmail.com> * fix: remove spaces in param setup (#2380) * delete deploy workflow (#2396) * undo reformatting which now does not pass lint * fix snapshots which changed due to npm dep changes * add missing quoting to snapshot * another snapshot typo fix * TS Revert (2) - replay PR #2347 and #2354 (#2403) * replay edits from PR 2347 * TS Revert (3) - replay edits in PR #2327 (#2404) * replay edits in PR 2327 * TS Revert (4) - replay PR #2355 (#2405) * replay edits in PR 2355 * add additional babel config * reformat with new prettier config Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com> Co-authored-by: maniarathi <mani.arathi@gmail.com> Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
This commit is contained in:
co-authored by
maniarathi
Madison Dunitz
Severiano Badajoz
parent
295590a7c6
commit
eaae6df5e3
@@ -0,0 +1,506 @@
|
|||||||
|
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
||||||
|
import { strict as assert } from "assert";
|
||||||
|
import {
|
||||||
|
clearInputAndTypeInto,
|
||||||
|
clickOn,
|
||||||
|
getAllByClass,
|
||||||
|
getOneElementInnerText,
|
||||||
|
typeInto,
|
||||||
|
waitByID,
|
||||||
|
waitByClass,
|
||||||
|
waitForAllByIds,
|
||||||
|
clickOnUntil,
|
||||||
|
getTestClass,
|
||||||
|
getTestId,
|
||||||
|
isElementPresent,
|
||||||
|
goToPage,
|
||||||
|
} from "./puppeteerUtils";
|
||||||
|
|
||||||
|
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
|
||||||
|
|
||||||
|
export async function drag(testId, start, end, lasso = false) {
|
||||||
|
const layout = await waitByID(testId);
|
||||||
|
const elBox = await layout.boxModel();
|
||||||
|
const x1 = elBox.content[0].x + start.x;
|
||||||
|
const x2 = elBox.content[0].x + end.x;
|
||||||
|
const y1 = elBox.content[0].y + start.y;
|
||||||
|
const y2 = elBox.content[0].y + end.y;
|
||||||
|
await page.mouse.move(x1, y1);
|
||||||
|
await page.mouse.down();
|
||||||
|
if (lasso) {
|
||||||
|
await page.mouse.move(x2, y1);
|
||||||
|
await page.mouse.move(x2, y2);
|
||||||
|
await page.mouse.move(x1, y2);
|
||||||
|
await page.mouse.move(x1, y1);
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(x2, y2);
|
||||||
|
}
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickOnCoordinate(testId, coord) {
|
||||||
|
const layout = await expect(page).toMatchElement(getTestId(testId));
|
||||||
|
const elBox = await layout.boxModel();
|
||||||
|
|
||||||
|
if (!elBox) {
|
||||||
|
throw Error("Layout's boxModel is not available!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = elBox.content[0].x + coord.x;
|
||||||
|
const y = elBox.content[0].y + coord.y;
|
||||||
|
await page.mouse.click(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllHistograms(testclass, testIds) {
|
||||||
|
const histTestIds = testIds.map((tid) => `histogram-${tid}`);
|
||||||
|
|
||||||
|
// these load asynchronously, so we need to wait for each histogram individually,
|
||||||
|
// and they may be quite slow in some cases.
|
||||||
|
await waitForAllByIds(histTestIds, { timeout: 4 * 60 * 1000 });
|
||||||
|
|
||||||
|
const allHistograms = await getAllByClass(testclass);
|
||||||
|
|
||||||
|
const testIDs = await Promise.all(
|
||||||
|
allHistograms.map((hist) => page.evaluate((elem) => elem.dataset.testid, hist))
|
||||||
|
);
|
||||||
|
|
||||||
|
return testIDs.map((id) => id.replace(/^histogram-/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllCategoriesAndCounts(category) {
|
||||||
|
// these load asynchronously, so we have to wait for the specific category.
|
||||||
|
await waitByID(`category-${category}`);
|
||||||
|
|
||||||
|
return page.$$eval(
|
||||||
|
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
||||||
|
(rows) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
rows.map((row) => {
|
||||||
|
const cat = row
|
||||||
|
.querySelector("[data-testclass='categorical-value']")
|
||||||
|
.getAttribute("aria-label");
|
||||||
|
|
||||||
|
const count = row.querySelector(
|
||||||
|
"[data-testclass='categorical-value-count']"
|
||||||
|
).innerText;
|
||||||
|
|
||||||
|
return [cat, count];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCellSetCount(num) {
|
||||||
|
await clickOn(`cellset-button-${num}`);
|
||||||
|
return getOneElementInnerText(`[data-testid='cellset-count-${num}']`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetCategory(category) {
|
||||||
|
const checkboxId = `${category}:category-select`;
|
||||||
|
await waitByID(checkboxId);
|
||||||
|
const checkedPseudoclass = await page.$eval(
|
||||||
|
`[data-testid='${checkboxId}']`,
|
||||||
|
(el) => el.matches(":checked")
|
||||||
|
);
|
||||||
|
if (!checkedPseudoclass) await clickOn(checkboxId);
|
||||||
|
|
||||||
|
const categoryRow = await waitByID(`${category}:category-expand`);
|
||||||
|
|
||||||
|
const isExpanded = await categoryRow.$(
|
||||||
|
"[data-testclass='category-expand-is-expanded']"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isExpanded) await clickOn(`${category}:category-expand`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calcCoordinate(testId, xAsPercent, yAsPercent) {
|
||||||
|
const el = await waitByID(testId);
|
||||||
|
const size = await el.boxModel();
|
||||||
|
return {
|
||||||
|
x: Math.floor(size.width * xAsPercent),
|
||||||
|
y: Math.floor(size.height * yAsPercent),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calcDragCoordinates(testId, coordinateAsPercent) {
|
||||||
|
return {
|
||||||
|
start: await calcCoordinate(
|
||||||
|
testId,
|
||||||
|
coordinateAsPercent.x1,
|
||||||
|
coordinateAsPercent.y1
|
||||||
|
),
|
||||||
|
end: await calcCoordinate(
|
||||||
|
testId,
|
||||||
|
coordinateAsPercent.x2,
|
||||||
|
coordinateAsPercent.y2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectCategory(category, values, reset = true) {
|
||||||
|
if (reset) await resetCategory(category);
|
||||||
|
|
||||||
|
await clickOn(`${category}:category-expand`);
|
||||||
|
await clickOn(`${category}:category-select`);
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
await clickOn(`categorical-value-select-${category}-${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expandCategory(category) {
|
||||||
|
const expand = await waitByID(`${category}:category-expand`);
|
||||||
|
const notExpanded = await expand.$(
|
||||||
|
"[data-testclass='category-expand-is-not-expanded']"
|
||||||
|
);
|
||||||
|
if (notExpanded) await clickOn(`${category}:category-expand`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clip(min = 0, max = 100) {
|
||||||
|
await clickOn("visualization-settings");
|
||||||
|
await clearInputAndTypeInto("clip-min-input", min);
|
||||||
|
await clearInputAndTypeInto("clip-max-input", max);
|
||||||
|
await clickOn("clip-commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCategory(categoryName) {
|
||||||
|
await clickOnUntil("open-annotation-dialog", async () => {
|
||||||
|
await expect(page).toMatchElement(getTestId("new-category-name"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await typeInto("new-category-name", categoryName);
|
||||||
|
await clickOn("submit-category");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
GENESET
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function colorByGeneset(genesetName) {
|
||||||
|
await clickOn(`${genesetName}:colorby-entire-geneset`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function colorByGene(gene) {
|
||||||
|
await clickOn(`colorby-${gene}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertColorLegendLabel(label) {
|
||||||
|
const handle = await waitByID("continuous_legend_color_by_label");
|
||||||
|
|
||||||
|
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
||||||
|
|
||||||
|
return expect(result).toBe(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expandGeneset(genesetName) {
|
||||||
|
const expand = await waitByID(`${genesetName}:geneset-expand`);
|
||||||
|
const notExpanded = await expand.$(
|
||||||
|
"[data-testclass='geneset-expand-is-not-expanded']"
|
||||||
|
);
|
||||||
|
if (notExpanded) await clickOn(`${genesetName}:geneset-expand`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createGeneset(genesetName) {
|
||||||
|
await clickOnUntil("open-create-geneset-dialog", async () => {
|
||||||
|
await expect(page).toMatchElement(getTestId("create-geneset-input"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await typeInto("create-geneset-input", genesetName);
|
||||||
|
await clickOn("submit-geneset");
|
||||||
|
await waitByClass("autosave-complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editGenesetName(genesetName, editText) {
|
||||||
|
const editButton = `${genesetName}:edit-genesetName-mode`;
|
||||||
|
const submitButton = `${genesetName}:submit-geneset`;
|
||||||
|
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
||||||
|
await expect(page).toMatchElement(getTestId(editButton));
|
||||||
|
});
|
||||||
|
await clickOn(editButton);
|
||||||
|
await typeInto("rename-geneset-modal", editText);
|
||||||
|
await clickOn(submitButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGeneset(genesetName) {
|
||||||
|
const targetId = `${genesetName}:delete-geneset`;
|
||||||
|
|
||||||
|
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
||||||
|
await expect(page).toMatchElement(getTestId(targetId));
|
||||||
|
});
|
||||||
|
|
||||||
|
await clickOn(targetId);
|
||||||
|
|
||||||
|
await assertGenesetDoesNotExist(genesetName);
|
||||||
|
await waitByClass("autosave-complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertGenesetDoesNotExist(genesetName) {
|
||||||
|
const result = await isElementPresent(
|
||||||
|
getTestId(`${genesetName}:geneset-name`)
|
||||||
|
);
|
||||||
|
await expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertGenesetExists(genesetName) {
|
||||||
|
const handle = await waitByID(`${genesetName}:geneset-name`);
|
||||||
|
|
||||||
|
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
||||||
|
|
||||||
|
return expect(result).toBe(genesetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
GENE
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function addGeneToSet(genesetName, geneToAddToSet) {
|
||||||
|
const submitButton = `${genesetName}:submit-gene`;
|
||||||
|
|
||||||
|
await clickOn(`${genesetName}:add-new-gene-to-geneset`);
|
||||||
|
await typeInto("add-genes", geneToAddToSet);
|
||||||
|
await clickOn(submitButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeGene(geneSymbol) {
|
||||||
|
const targetId = `delete-from-geneset:${geneSymbol}`;
|
||||||
|
|
||||||
|
await clickOn(targetId);
|
||||||
|
|
||||||
|
await waitByClass("autosave-complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertGeneExistsInGeneset(geneSymbol) {
|
||||||
|
const handle = await waitByID(`${geneSymbol}:gene-label`);
|
||||||
|
|
||||||
|
const result = await handle.evaluate((node) => node.getAttribute("aria-label"));
|
||||||
|
|
||||||
|
return expect(result).toBe(geneSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertGeneDoesNotExist(geneSymbol) {
|
||||||
|
const result = await isElementPresent(getTestId(`${geneSymbol}:gene-label`));
|
||||||
|
|
||||||
|
await expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expandGene(geneSymbol) {
|
||||||
|
await clickOn(`maximize-${geneSymbol}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
CATEGORY
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function duplicateCategory(categoryName) {
|
||||||
|
await clickOn("open-annotation-dialog");
|
||||||
|
|
||||||
|
await typeInto("new-category-name", categoryName);
|
||||||
|
|
||||||
|
const dropdownOptionClass = "duplicate-category-dropdown-option";
|
||||||
|
|
||||||
|
await clickOnUntil("duplicate-category-dropdown", async () => {
|
||||||
|
await expect(page).toMatchElement(getTestClass(dropdownOptionClass));
|
||||||
|
});
|
||||||
|
|
||||||
|
const option = await expect(page).toMatchElement(
|
||||||
|
getTestClass(dropdownOptionClass)
|
||||||
|
);
|
||||||
|
|
||||||
|
await option.click();
|
||||||
|
|
||||||
|
await clickOnUntil("submit-category", async () => {
|
||||||
|
await expect(page).toMatchElement(
|
||||||
|
getTestId(`${categoryName}:category-expand`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitByClass("autosave-complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renameCategory(oldCategoryName, newCategoryName) {
|
||||||
|
await clickOn(`${oldCategoryName}:see-actions`);
|
||||||
|
await clickOn(`${oldCategoryName}:edit-category-mode`);
|
||||||
|
await clearInputAndTypeInto(
|
||||||
|
`${oldCategoryName}:edit-category-name-text`,
|
||||||
|
newCategoryName
|
||||||
|
);
|
||||||
|
await clickOn(`${oldCategoryName}:submit-category-edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCategory(categoryName) {
|
||||||
|
const targetId = `${categoryName}:delete-category`;
|
||||||
|
|
||||||
|
await clickOnUntil(`${categoryName}:see-actions`, async () => {
|
||||||
|
await expect(page).toMatchElement(getTestId(targetId));
|
||||||
|
});
|
||||||
|
|
||||||
|
await clickOn(targetId);
|
||||||
|
|
||||||
|
await assertCategoryDoesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLabel(categoryName, labelName) {
|
||||||
|
/**
|
||||||
|
* (thuang): This explicit wait is needed, since currently showing
|
||||||
|
* the modal again quickly after the previous action dismissing the
|
||||||
|
* modal will persist the input value from the previous action.
|
||||||
|
*
|
||||||
|
* To reproduce:
|
||||||
|
* 1. Click on the plus sign to show the modal to add a new label to the category
|
||||||
|
* 2. Type `123` in the input box
|
||||||
|
* 3. Hover over your mouse over the plus sign and double click to quickly dismiss and
|
||||||
|
* invoke the modal again
|
||||||
|
* 4. You will see `123` is persisted in the input box
|
||||||
|
* 5. Expected behavior is to get an empty input box
|
||||||
|
*/
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await clickOn(`${categoryName}:see-actions`);
|
||||||
|
|
||||||
|
await clickOn(`${categoryName}:add-new-label-to-category`);
|
||||||
|
|
||||||
|
await typeInto(`${categoryName}:new-label-name`, labelName);
|
||||||
|
|
||||||
|
await clickOn(`${categoryName}:submit-label`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLabel(categoryName, labelName) {
|
||||||
|
await expandCategory(categoryName);
|
||||||
|
await clickOn(`${categoryName}:${labelName}:see-actions`);
|
||||||
|
await clickOn(`${categoryName}:${labelName}:delete-label`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renameLabel(categoryName, oldLabelName, newLabelName) {
|
||||||
|
await expandCategory(categoryName);
|
||||||
|
await clickOn(`${categoryName}:${oldLabelName}:see-actions`);
|
||||||
|
await clickOn(`${categoryName}:${oldLabelName}:edit-label`);
|
||||||
|
await clearInputAndTypeInto(
|
||||||
|
`${categoryName}:${oldLabelName}:edit-label-name`,
|
||||||
|
newLabelName
|
||||||
|
);
|
||||||
|
await clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addGeneToSearch(geneName) {
|
||||||
|
await typeInto("gene-search", geneName);
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await page.waitForSelector(`[data-testid='histogram-${geneName}']`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subset(coordinatesAsPercent) {
|
||||||
|
// In order to deselect the selection after the subset, make sure we have some clear part
|
||||||
|
// of the scatterplot we can click on
|
||||||
|
assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99);
|
||||||
|
const lassoSelection = await calcDragCoordinates(
|
||||||
|
"layout-graph",
|
||||||
|
coordinatesAsPercent
|
||||||
|
);
|
||||||
|
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
|
||||||
|
await clickOn("subset-button");
|
||||||
|
const clearCoordinate = await calcCoordinate("layout-graph", 0.5, 0.99);
|
||||||
|
await clickOnCoordinate("layout-graph", clearCoordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSellSet(cellSet, cellSetNum) {
|
||||||
|
const selections = cellSet.filter((sel) => sel.kind === "categorical");
|
||||||
|
|
||||||
|
for (const selection of selections) {
|
||||||
|
await selectCategory(selection.metadata, selection.values, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await getCellSetCount(cellSetNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runDiffExp(cellSet1, cellSet2) {
|
||||||
|
await setSellSet(cellSet1, 1);
|
||||||
|
await setSellSet(cellSet2, 2);
|
||||||
|
await clickOn("diffexp-button");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkAddGenes(geneNames) {
|
||||||
|
await clickOn("section-bulk-add");
|
||||||
|
await typeInto("input-bulk-add", geneNames.join(","));
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertCategoryDoesNotExist(categoryName) {
|
||||||
|
const result = await isElementPresent(
|
||||||
|
getTestId(`${categoryName}:category-label`)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(result).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login() {
|
||||||
|
await goToPage(appUrlBase);
|
||||||
|
|
||||||
|
await clickOn("log-in");
|
||||||
|
|
||||||
|
// (thuang): Auth0 form is unstable and unsafe for input until verified
|
||||||
|
await waitUntilFormFieldStable('[name="email"]');
|
||||||
|
|
||||||
|
await expect(page).toFillForm("form", {
|
||||||
|
email: TEST_EMAIL,
|
||||||
|
password: TEST_PASSWORD,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
||||||
|
expect(page).toClick('[name="submit"]'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(page.url()).toContain(appUrlBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
await clickOnUntil("user-info", async () => {
|
||||||
|
await waitByID("log-out");
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
||||||
|
clickOn("log-out"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitByID("log-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilFormFieldStable(selector) {
|
||||||
|
const MAX_RETRY = 10;
|
||||||
|
const WAIT_FOR_MS = 200;
|
||||||
|
|
||||||
|
const EXPECTED_VALUE = "aaa";
|
||||||
|
|
||||||
|
let retry = 0;
|
||||||
|
|
||||||
|
while (retry < MAX_RETRY) {
|
||||||
|
try {
|
||||||
|
await expect(page).toFill(selector, EXPECTED_VALUE);
|
||||||
|
|
||||||
|
const fieldHandle = await expect(page).toMatchElement(selector);
|
||||||
|
|
||||||
|
const fieldValue = await page.evaluate(
|
||||||
|
(input) => input.value,
|
||||||
|
fieldHandle
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fieldValue).toBe(EXPECTED_VALUE);
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
retry += 1;
|
||||||
|
|
||||||
|
await page.waitForTimeout(WAIT_FOR_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry === MAX_RETRY) {
|
||||||
|
throw Error("clickOnUntil() assertion failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
|
||||||
import { strict as assert } from "assert";
|
|
||||||
import {
|
|
||||||
clearInputAndTypeInto,
|
|
||||||
clickOn,
|
|
||||||
getAllByClass,
|
|
||||||
getOneElementInnerText,
|
|
||||||
typeInto,
|
|
||||||
waitByID,
|
|
||||||
waitByClass,
|
|
||||||
waitForAllByIds,
|
|
||||||
clickOnUntil,
|
|
||||||
getTestClass,
|
|
||||||
getTestId,
|
|
||||||
isElementPresent,
|
|
||||||
goToPage,
|
|
||||||
} from "./puppeteerUtils";
|
|
||||||
|
|
||||||
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function drag(testId: any, start: any, end: any, lasso = false) {
|
|
||||||
const layout = await waitByID(testId);
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const elBox = await layout.boxModel();
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const x1 = elBox.content[0].x + start.x;
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const x2 = elBox.content[0].x + end.x;
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const y1 = elBox.content[0].y + start.y;
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const y2 = elBox.content[0].y + end.y;
|
|
||||||
await page.mouse.move(x1, y1);
|
|
||||||
await page.mouse.down();
|
|
||||||
if (lasso) {
|
|
||||||
await page.mouse.move(x2, y1);
|
|
||||||
await page.mouse.move(x2, y2);
|
|
||||||
await page.mouse.move(x1, y2);
|
|
||||||
await page.mouse.move(x1, y1);
|
|
||||||
} else {
|
|
||||||
await page.mouse.move(x2, y2);
|
|
||||||
}
|
|
||||||
await page.mouse.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function clickOnCoordinate(testId: any, coord: any) {
|
|
||||||
const layout = await expect(page).toMatchElement(getTestId(testId));
|
|
||||||
const elBox = await layout.boxModel();
|
|
||||||
|
|
||||||
if (!elBox) {
|
|
||||||
throw Error("Layout's boxModel is not available!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = elBox.content[0].x + coord.x;
|
|
||||||
const y = elBox.content[0].y + coord.y;
|
|
||||||
await page.mouse.click(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getAllHistograms(testclass: any, testIds: any) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const histTestIds = testIds.map((tid: any) => `histogram-${tid}`);
|
|
||||||
|
|
||||||
// these load asynchronously, so we need to wait for each histogram individually,
|
|
||||||
// and they may be quite slow in some cases.
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 2.
|
|
||||||
await waitForAllByIds(histTestIds, { timeout: 4 * 60 * 1000 });
|
|
||||||
|
|
||||||
const allHistograms = await getAllByClass(testclass);
|
|
||||||
|
|
||||||
const testIDs = await Promise.all(
|
|
||||||
allHistograms.map((hist) =>
|
|
||||||
page.evaluate((elem) => elem.dataset.testid, hist)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return testIDs.map((id) => id.replace(/^histogram-/, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getAllCategoriesAndCounts(category: any) {
|
|
||||||
// these load asynchronously, so we have to wait for the specific category.
|
|
||||||
await waitByID(`category-${category}`);
|
|
||||||
|
|
||||||
return page.$$eval(
|
|
||||||
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
|
||||||
(rows) =>
|
|
||||||
Object.fromEntries(
|
|
||||||
rows.map((row) => {
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const cat = row
|
|
||||||
.querySelector("[data-testclass='categorical-value']")
|
|
||||||
.getAttribute("aria-label");
|
|
||||||
|
|
||||||
const count = (
|
|
||||||
row.querySelector(
|
|
||||||
"[data-testclass='categorical-value-count']"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) as any
|
|
||||||
).innerText;
|
|
||||||
|
|
||||||
return [cat, count];
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getCellSetCount(num: any) {
|
|
||||||
await clickOn(`cellset-button-${num}`);
|
|
||||||
return getOneElementInnerText(`[data-testid='cellset-count-${num}']`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function resetCategory(category: any) {
|
|
||||||
const checkboxId = `${category}:category-select`;
|
|
||||||
await waitByID(checkboxId);
|
|
||||||
const checkedPseudoclass = await page.$eval(
|
|
||||||
`[data-testid='${checkboxId}']`,
|
|
||||||
(el) => el.matches(":checked")
|
|
||||||
);
|
|
||||||
if (!checkedPseudoclass) await clickOn(checkboxId);
|
|
||||||
|
|
||||||
const categoryRow = await waitByID(`${category}:category-expand`);
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const isExpanded = await categoryRow.$(
|
|
||||||
"[data-testclass='category-expand-is-expanded']"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isExpanded) await clickOn(`${category}:category-expand`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function calcCoordinate(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
testId: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
xAsPercent: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
yAsPercent: any
|
|
||||||
) {
|
|
||||||
const el = await waitByID(testId);
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const size = await el.boxModel();
|
|
||||||
return {
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
x: Math.floor(size.width * xAsPercent),
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
y: Math.floor(size.height * yAsPercent),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function calcDragCoordinates(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
testId: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
coordinateAsPercent: any
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
start: await calcCoordinate(
|
|
||||||
testId,
|
|
||||||
coordinateAsPercent.x1,
|
|
||||||
coordinateAsPercent.y1
|
|
||||||
),
|
|
||||||
end: await calcCoordinate(
|
|
||||||
testId,
|
|
||||||
coordinateAsPercent.x2,
|
|
||||||
coordinateAsPercent.y2
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function selectCategory(category: any, values: any, reset = true) {
|
|
||||||
if (reset) await resetCategory(category);
|
|
||||||
|
|
||||||
await clickOn(`${category}:category-expand`);
|
|
||||||
await clickOn(`${category}:category-select`);
|
|
||||||
|
|
||||||
for (const value of values) {
|
|
||||||
await clickOn(`categorical-value-select-${category}-${value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function expandCategory(category: any) {
|
|
||||||
const expand = await waitByID(`${category}:category-expand`);
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const notExpanded = await expand.$(
|
|
||||||
"[data-testclass='category-expand-is-not-expanded']"
|
|
||||||
);
|
|
||||||
if (notExpanded) await clickOn(`${category}:category-expand`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clip(min = "0", max = "100"): Promise<void> {
|
|
||||||
await clickOn("visualization-settings");
|
|
||||||
await clearInputAndTypeInto("clip-min-input", min);
|
|
||||||
await clearInputAndTypeInto("clip-max-input", max);
|
|
||||||
await clickOn("clip-commit");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function createCategory(categoryName: any) {
|
|
||||||
await clickOnUntil("open-annotation-dialog", async () => {
|
|
||||||
await expect(page).toMatchElement(getTestId("new-category-name"));
|
|
||||||
});
|
|
||||||
|
|
||||||
await typeInto("new-category-name", categoryName);
|
|
||||||
await clickOn("submit-category");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GENESET
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function colorByGeneset(genesetName: any) {
|
|
||||||
await clickOn(`${genesetName}:colorby-entire-geneset`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function colorByGene(gene: any) {
|
|
||||||
await clickOn(`colorby-${gene}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertColorLegendLabel(label: any) {
|
|
||||||
const handle = await waitByID("continuous_legend_color_by_label");
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const result = await handle.evaluate((node) =>
|
|
||||||
node.getAttribute("aria-label")
|
|
||||||
);
|
|
||||||
|
|
||||||
return expect(result).toBe(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function expandGeneset(genesetName: any) {
|
|
||||||
const expand = await waitByID(`${genesetName}:geneset-expand`);
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const notExpanded = await expand.$(
|
|
||||||
"[data-testclass='geneset-expand-is-not-expanded']"
|
|
||||||
);
|
|
||||||
if (notExpanded) await clickOn(`${genesetName}:geneset-expand`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function createGeneset(genesetName: any) {
|
|
||||||
await clickOnUntil("open-create-geneset-dialog", async () => {
|
|
||||||
await expect(page).toMatchElement(getTestId("create-geneset-input"));
|
|
||||||
});
|
|
||||||
|
|
||||||
await typeInto("create-geneset-input", genesetName);
|
|
||||||
await clickOn("submit-geneset");
|
|
||||||
await waitByClass("autosave-complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function editGenesetName(genesetName: any, editText: any) {
|
|
||||||
const editButton = `${genesetName}:edit-genesetName-mode`;
|
|
||||||
const submitButton = `${genesetName}:submit-geneset`;
|
|
||||||
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
|
||||||
await expect(page).toMatchElement(getTestId(editButton));
|
|
||||||
});
|
|
||||||
await clickOn(editButton);
|
|
||||||
await typeInto("rename-geneset-modal", editText);
|
|
||||||
await clickOn(submitButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function deleteGeneset(genesetName: any) {
|
|
||||||
const targetId = `${genesetName}:delete-geneset`;
|
|
||||||
|
|
||||||
await clickOnUntil(`${genesetName}:see-actions`, async () => {
|
|
||||||
await expect(page).toMatchElement(getTestId(targetId));
|
|
||||||
});
|
|
||||||
|
|
||||||
await clickOn(targetId);
|
|
||||||
|
|
||||||
await assertGenesetDoesNotExist(genesetName);
|
|
||||||
await waitByClass("autosave-complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertGenesetDoesNotExist(genesetName: any) {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
const result = await isElementPresent(
|
|
||||||
getTestId(`${genesetName}:geneset-name`)
|
|
||||||
);
|
|
||||||
await expect(result).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertGenesetExists(genesetName: any) {
|
|
||||||
const handle = await waitByID(`${genesetName}:geneset-name`);
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const result = await handle.evaluate((node) =>
|
|
||||||
node.getAttribute("aria-label")
|
|
||||||
);
|
|
||||||
|
|
||||||
return expect(result).toBe(genesetName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GENE
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function addGeneToSet(genesetName: any, geneToAddToSet: any) {
|
|
||||||
const submitButton = `${genesetName}:submit-gene`;
|
|
||||||
|
|
||||||
await clickOn(`${genesetName}:add-new-gene-to-geneset`);
|
|
||||||
await typeInto("add-genes", geneToAddToSet);
|
|
||||||
await clickOn(submitButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function removeGene(geneSymbol: any) {
|
|
||||||
const targetId = `delete-from-geneset:${geneSymbol}`;
|
|
||||||
|
|
||||||
await clickOn(targetId);
|
|
||||||
|
|
||||||
await waitByClass("autosave-complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertGeneExistsInGeneset(geneSymbol: any) {
|
|
||||||
const handle = await waitByID(`${geneSymbol}:gene-label`);
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const result = await handle.evaluate((node) =>
|
|
||||||
node.getAttribute("aria-label")
|
|
||||||
);
|
|
||||||
|
|
||||||
return expect(result).toBe(geneSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertGeneDoesNotExist(geneSymbol: any) {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
const result = await isElementPresent(getTestId(`${geneSymbol}:gene-label`));
|
|
||||||
|
|
||||||
await expect(result).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function expandGene(geneSymbol: any) {
|
|
||||||
await clickOn(`maximize-${geneSymbol}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CATEGORY
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function duplicateCategory(categoryName: any) {
|
|
||||||
await clickOn("open-annotation-dialog");
|
|
||||||
|
|
||||||
await typeInto("new-category-name", categoryName);
|
|
||||||
|
|
||||||
const dropdownOptionClass = "duplicate-category-dropdown-option";
|
|
||||||
|
|
||||||
await clickOnUntil("duplicate-category-dropdown", async () => {
|
|
||||||
await expect(page).toMatchElement(getTestClass(dropdownOptionClass));
|
|
||||||
});
|
|
||||||
|
|
||||||
const option = await expect(page).toMatchElement(
|
|
||||||
getTestClass(dropdownOptionClass)
|
|
||||||
);
|
|
||||||
|
|
||||||
await option.click();
|
|
||||||
|
|
||||||
await clickOnUntil("submit-category", async () => {
|
|
||||||
await expect(page).toMatchElement(
|
|
||||||
getTestId(`${categoryName}:category-expand`)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitByClass("autosave-complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function renameCategory(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
oldCategoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newCategoryName: any
|
|
||||||
) {
|
|
||||||
await clickOn(`${oldCategoryName}:see-actions`);
|
|
||||||
await clickOn(`${oldCategoryName}:edit-category-mode`);
|
|
||||||
await clearInputAndTypeInto(
|
|
||||||
`${oldCategoryName}:edit-category-name-text`,
|
|
||||||
newCategoryName
|
|
||||||
);
|
|
||||||
await clickOn(`${oldCategoryName}:submit-category-edit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function deleteCategory(categoryName: any) {
|
|
||||||
const targetId = `${categoryName}:delete-category`;
|
|
||||||
|
|
||||||
await clickOnUntil(`${categoryName}:see-actions`, async () => {
|
|
||||||
await expect(page).toMatchElement(getTestId(targetId));
|
|
||||||
});
|
|
||||||
|
|
||||||
await clickOn(targetId);
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
|
||||||
await assertCategoryDoesNotExist();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function createLabel(categoryName: any, labelName: any) {
|
|
||||||
/**
|
|
||||||
* (thuang): This explicit wait is needed, since currently showing
|
|
||||||
* the modal again quickly after the previous action dismissing the
|
|
||||||
* modal will persist the input value from the previous action.
|
|
||||||
*
|
|
||||||
* To reproduce:
|
|
||||||
* 1. Click on the plus sign to show the modal to add a new label to the category
|
|
||||||
* 2. Type `123` in the input box
|
|
||||||
* 3. Hover over your mouse over the plus sign and double click to quickly dismiss and
|
|
||||||
* invoke the modal again
|
|
||||||
* 4. You will see `123` is persisted in the input box
|
|
||||||
* 5. Expected behavior is to get an empty input box
|
|
||||||
*/
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await clickOn(`${categoryName}:see-actions`);
|
|
||||||
|
|
||||||
await clickOn(`${categoryName}:add-new-label-to-category`);
|
|
||||||
|
|
||||||
await typeInto(`${categoryName}:new-label-name`, labelName);
|
|
||||||
|
|
||||||
await clickOn(`${categoryName}:submit-label`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function deleteLabel(categoryName: any, labelName: any) {
|
|
||||||
await expandCategory(categoryName);
|
|
||||||
await clickOn(`${categoryName}:${labelName}:see-actions`);
|
|
||||||
await clickOn(`${categoryName}:${labelName}:delete-label`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function renameLabel(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
oldLabelName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newLabelName: any
|
|
||||||
) {
|
|
||||||
await expandCategory(categoryName);
|
|
||||||
await clickOn(`${categoryName}:${oldLabelName}:see-actions`);
|
|
||||||
await clickOn(`${categoryName}:${oldLabelName}:edit-label`);
|
|
||||||
await clearInputAndTypeInto(
|
|
||||||
`${categoryName}:${oldLabelName}:edit-label-name`,
|
|
||||||
newLabelName
|
|
||||||
);
|
|
||||||
await clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function addGeneToSearch(geneName: any) {
|
|
||||||
await typeInto("gene-search", geneName);
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await page.waitForSelector(`[data-testid='histogram-${geneName}']`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function subset(coordinatesAsPercent: any) {
|
|
||||||
// In order to deselect the selection after the subset, make sure we have some clear part
|
|
||||||
// of the scatterplot we can click on
|
|
||||||
assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99);
|
|
||||||
const lassoSelection = await calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
coordinatesAsPercent
|
|
||||||
);
|
|
||||||
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
|
|
||||||
await clickOn("subset-button");
|
|
||||||
const clearCoordinate = await calcCoordinate("layout-graph", 0.5, 0.99);
|
|
||||||
await clickOnCoordinate("layout-graph", clearCoordinate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function setSellSet(cellSet: any, cellSetNum: any) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const selections = cellSet.filter((sel: any) => sel.kind === "categorical");
|
|
||||||
|
|
||||||
for (const selection of selections) {
|
|
||||||
await selectCategory(selection.metadata, selection.values, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
await getCellSetCount(cellSetNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function runDiffExp(cellSet1: any, cellSet2: any) {
|
|
||||||
await setSellSet(cellSet1, 1);
|
|
||||||
await setSellSet(cellSet2, 2);
|
|
||||||
await clickOn("diffexp-button");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function bulkAddGenes(geneNames: any) {
|
|
||||||
await clickOn("section-bulk-add");
|
|
||||||
await typeInto("input-bulk-add", geneNames.join(","));
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function assertCategoryDoesNotExist(categoryName: any) {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
const result = await isElementPresent(
|
|
||||||
getTestId(`${categoryName}:category-label`)
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(result).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function login() {
|
|
||||||
await goToPage(appUrlBase);
|
|
||||||
|
|
||||||
await clickOn("log-in");
|
|
||||||
|
|
||||||
// (thuang): Auth0 form is unstable and unsafe for input until verified
|
|
||||||
await waitUntilFormFieldStable('[name="email"]');
|
|
||||||
|
|
||||||
await expect(page).toFillForm("form", {
|
|
||||||
email: TEST_EMAIL,
|
|
||||||
password: TEST_PASSWORD,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
|
||||||
expect(page).toClick('[name="submit"]'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(page.url()).toContain(appUrlBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function logout() {
|
|
||||||
await clickOnUntil("user-info", async () => {
|
|
||||||
await waitByID("log-out");
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: "networkidle0" }),
|
|
||||||
clickOn("log-out"),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitByID("log-in");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
async function waitUntilFormFieldStable(selector: any) {
|
|
||||||
const MAX_RETRY = 10;
|
|
||||||
const WAIT_FOR_MS = 200;
|
|
||||||
|
|
||||||
const EXPECTED_VALUE = "aaa";
|
|
||||||
|
|
||||||
let retry = 0;
|
|
||||||
|
|
||||||
while (retry < MAX_RETRY) {
|
|
||||||
try {
|
|
||||||
await expect(page).toFill(selector, EXPECTED_VALUE);
|
|
||||||
|
|
||||||
const fieldHandle = await expect(page).toMatchElement(selector);
|
|
||||||
|
|
||||||
const fieldValue = await page.evaluate(
|
|
||||||
(input) => input.value,
|
|
||||||
fieldHandle
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(fieldValue).toBe(EXPECTED_VALUE);
|
|
||||||
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
retry += 1;
|
|
||||||
|
|
||||||
await page.waitForTimeout(WAIT_FOR_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry === MAX_RETRY) {
|
|
||||||
throw Error("clickOnUntil() assertion failed!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
|
||||||
@@ -58,12 +58,10 @@ describe("metadata loads", () => {
|
|||||||
const categories = await getAllCategoriesAndCounts(label);
|
const categories = await getAllCategoriesAndCounts(label);
|
||||||
|
|
||||||
expect(Object.keys(categories)).toMatchObject(
|
expect(Object.keys(categories)).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
Object.keys(data.categorical[label])
|
Object.keys(data.categorical[label])
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(Object.values(categories)).toMatchObject(
|
expect(Object.values(categories)).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
Object.values(data.categorical[label])
|
Object.values(data.categorical[label])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,12 +159,10 @@ describe("subset", () => {
|
|||||||
const categories = await getAllCategoriesAndCounts(label);
|
const categories = await getAllCategoriesAndCounts(label);
|
||||||
|
|
||||||
expect(Object.keys(categories)).toMatchObject(
|
expect(Object.keys(categories)).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
Object.keys(data.subset.categorical[label])
|
Object.keys(data.subset.categorical[label])
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(Object.values(categories)).toMatchObject(
|
expect(Object.values(categories)).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
Object.values(data.subset.categorical[label])
|
Object.values(data.subset.categorical[label])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,7 +254,6 @@ describe("centroid labels", () => {
|
|||||||
const generatedLabels = await getAllByClass("centroid-label");
|
const generatedLabels = await getAllByClass("centroid-label");
|
||||||
// Number of labels generated should be equal to size of the object
|
// Number of labels generated should be equal to size of the object
|
||||||
expect(generatedLabels).toHaveLength(
|
expect(generatedLabels).toHaveLength(
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
Object.keys(data.categorical[label]).length
|
Object.keys(data.categorical[label]).length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -280,7 +275,6 @@ describe("graph overlay", () => {
|
|||||||
data.pan["coordinates-as-percent"]
|
data.pan["coordinates-as-percent"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
const categoryValue = Object.keys(data.categorical[category])[0];
|
const categoryValue = Object.keys(data.categorical[category])[0];
|
||||||
const initialCoordinates = await getElementCoordinates(
|
const initialCoordinates = await getElementCoordinates(
|
||||||
`${categoryValue}-centroid-label`
|
`${categoryValue}-centroid-label`
|
||||||
+9
-29
@@ -82,8 +82,7 @@ const genesetDescriptionID =
|
|||||||
const genesetDescriptionString = "fourth_gene_set: fourth description";
|
const genesetDescriptionString = "fourth_gene_set: fourth description";
|
||||||
const genesetToCheckForDescription = "fourth_gene_set";
|
const genesetToCheckForDescription = "fourth_gene_set";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function setup(config) {
|
||||||
async function setup(config: any) {
|
|
||||||
await goToPage(appUrlBase);
|
await goToPage(appUrlBase);
|
||||||
|
|
||||||
if (config.categoricalAnno) {
|
if (config.categoricalAnno) {
|
||||||
@@ -157,8 +156,7 @@ describe.each([
|
|||||||
await expect(page).toClick(getTestClass("pop-1-geneset-expand"));
|
await expect(page).toClick(getTestClass("pop-1-geneset-expand"));
|
||||||
|
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(selector) => !document.querySelector(selector),
|
||||||
(selector: any) => !document.querySelector(selector),
|
|
||||||
{},
|
{},
|
||||||
getTestClass("gene-loading-spinner")
|
getTestClass("gene-loading-spinner")
|
||||||
);
|
);
|
||||||
@@ -173,8 +171,7 @@ describe.each([
|
|||||||
await expect(page).toClick(getTestClass("pop-2-geneset-expand"));
|
await expect(page).toClick(getTestClass("pop-2-geneset-expand"));
|
||||||
|
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(selector) => !document.querySelector(selector),
|
||||||
(selector: any) => !document.querySelector(selector),
|
|
||||||
{},
|
{},
|
||||||
getTestClass("gene-loading-spinner")
|
getTestClass("gene-loading-spinner")
|
||||||
);
|
);
|
||||||
@@ -405,13 +402,8 @@ describe.each([
|
|||||||
expect(actualLabelName).toBe(expectedLabelName);
|
expect(actualLabelName).toBe(expectedLabelName);
|
||||||
expect(actualLabelCount).toBe(expectedLabelCount);
|
expect(actualLabelCount).toBe(expectedLabelCount);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function getInnerText(element, className) {
|
||||||
async function getInnerText(element: any, className: any) {
|
return element.$eval(getTestClass(className), (node) => node?.innerText);
|
||||||
return element.$eval(
|
|
||||||
getTestClass(className),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(node: any) => node?.innerText
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -436,9 +428,7 @@ describe.each([
|
|||||||
`categorical-value-count-${perTestCategoryName}-${perTestLabelName}`
|
`categorical-value-count-${perTestCategoryName}-${perTestLabelName}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
expect(await result.evaluate((node) => node.innerText)).toBe(
|
expect(await result.evaluate((node) => node.innerText)).toBe(
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'boolean' cannot be used as an index type.
|
|
||||||
data.categoryLabel.newCount.bySubsetConfig[config.withSubset]
|
data.categoryLabel.newCount.bySubsetConfig[config.withSubset]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -498,7 +488,6 @@ describe.each([
|
|||||||
await createLabel(perTestCategoryName, labelName);
|
await createLabel(perTestCategoryName, labelName);
|
||||||
await assertLabelExists(perTestCategoryName, labelName);
|
await assertLabelExists(perTestCategoryName, labelName);
|
||||||
await clickOn("undo");
|
await clickOn("undo");
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
await assertLabelDoesNotExist(perTestCategoryName);
|
await assertLabelDoesNotExist(perTestCategoryName);
|
||||||
await clickOn("redo");
|
await clickOn("redo");
|
||||||
await assertLabelExists(perTestCategoryName, labelName);
|
await assertLabelExists(perTestCategoryName, labelName);
|
||||||
@@ -508,12 +497,10 @@ describe.each([
|
|||||||
await setup(config);
|
await setup(config);
|
||||||
|
|
||||||
await deleteLabel(perTestCategoryName, perTestLabelName);
|
await deleteLabel(perTestCategoryName, perTestLabelName);
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
await assertLabelDoesNotExist(perTestCategoryName);
|
await assertLabelDoesNotExist(perTestCategoryName);
|
||||||
await clickOn("undo");
|
await clickOn("undo");
|
||||||
await assertLabelExists(perTestCategoryName, perTestLabelName);
|
await assertLabelExists(perTestCategoryName, perTestLabelName);
|
||||||
await clickOn("redo");
|
await clickOn("redo");
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
await assertLabelDoesNotExist(perTestCategoryName);
|
await assertLabelDoesNotExist(perTestCategoryName);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,9 +531,7 @@ describe.each([
|
|||||||
const labels = await getAllByClass("categorical-row");
|
const labels = await getAllByClass("categorical-row");
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
labels.map((label) =>
|
labels.map((label) => page.evaluate((element) => element.outerHTML, label))
|
||||||
page.evaluate((element) => element.outerHTML, label)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
@@ -574,11 +559,9 @@ describe.each([
|
|||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function assertCategoryExists(categoryName) {
|
||||||
async function assertCategoryExists(categoryName: any) {
|
|
||||||
const handle = await waitByID(`${categoryName}:category-label`);
|
const handle = await waitByID(`${categoryName}:category-label`);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const result = await handle.evaluate((node) =>
|
const result = await handle.evaluate((node) =>
|
||||||
node.getAttribute("aria-label")
|
node.getAttribute("aria-label")
|
||||||
);
|
);
|
||||||
@@ -586,8 +569,7 @@ describe.each([
|
|||||||
return expect(result).toBe(categoryName);
|
return expect(result).toBe(categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function assertLabelExists(categoryName, labelName) {
|
||||||
async function assertLabelExists(categoryName: any, labelName: any) {
|
|
||||||
await expect(page).toMatchElement(
|
await expect(page).toMatchElement(
|
||||||
getTestId(`${categoryName}:category-expand`)
|
getTestId(`${categoryName}:category-expand`)
|
||||||
);
|
);
|
||||||
@@ -599,13 +581,11 @@ describe.each([
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
await previous.evaluate((node) => node.getAttribute("aria-label"))
|
await previous.evaluate((node) => node.getAttribute("aria-label"))
|
||||||
).toBe(labelName);
|
).toBe(labelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function assertLabelDoesNotExist(categoryName, labelName) {
|
||||||
async function assertLabelDoesNotExist(categoryName: any, labelName: any) {
|
|
||||||
await expandCategory(categoryName);
|
await expandCategory(categoryName);
|
||||||
const result = await page.$(
|
const result = await page.$(
|
||||||
`[data-testid='categorical-value-${categoryName}-${labelName}']`
|
`[data-testid='categorical-value-${categoryName}-${labelName}']`
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"testRunner": "jest-circus/runner",
|
"testRunner": "jest-circus/runner",
|
||||||
"preset": "jest-puppeteer",
|
"preset": "jest-puppeteer",
|
||||||
"testMatch": ["**/__tests__/**/?(*.)(spec|test).ts?(x)"],
|
"testMatch": ["**/__tests__/**/?(*.)(spec|test).js?(x)"],
|
||||||
"setupFiles": ["../setupMissingGlobals.ts"],
|
"setupFiles": ["../setupMissingGlobals.js"],
|
||||||
"setupFilesAfterEnv": ["expect-puppeteer", "./puppeteer.setup.ts"],
|
"setupFilesAfterEnv": ["expect-puppeteer", "./puppeteer.setup.js"],
|
||||||
"globalSetup": "../globalSetup.ts",
|
"globalSetup": "../globalSetup.js",
|
||||||
"globalTeardown": "jest-environment-puppeteer/teardown",
|
"globalTeardown": "jest-environment-puppeteer/teardown",
|
||||||
"testEnvironment": "./screenshot_env.js"
|
"testEnvironment": "./screenshot_env.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ beforeEach(async () => {
|
|||||||
const userAgent = await browser.userAgent();
|
const userAgent = await browser.userAgent();
|
||||||
await page.setUserAgent(`${userAgent}bot`);
|
await page.setUserAgent(`${userAgent}bot`);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2341) FIXME: Property '_client' is private and only accessible ... Remove this comment to see the full error message
|
|
||||||
await page._client.send("Animation.setPlaybackRate", { playbackRate: 12 });
|
await page._client.send("Animation.setPlaybackRate", { playbackRate: 12 });
|
||||||
|
|
||||||
page.on("pageerror", (err) => {
|
page.on("pageerror", (err) => {
|
||||||
@@ -50,8 +49,7 @@ beforeEach(async () => {
|
|||||||
}
|
}
|
||||||
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?
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
msg.args().map((arg) => arg._remoteObject.description)
|
||||||
msg.args().map((arg: any) => arg._remoteObject.description)
|
|
||||||
);
|
);
|
||||||
throw new Error(`Console error: ${errorMsgText}`);
|
throw new Error(`Console error: ${errorMsgText}`);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
||||||
|
export function getTestId(id) {
|
||||||
|
return `[data-testid='${id}']`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTestClass(className) {
|
||||||
|
return `[data-testclass='${className}']`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitByID(testId, props = {}) {
|
||||||
|
return page.waitForSelector(getTestId(testId), props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitByClass(testClass, props = {}) {
|
||||||
|
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForAllByIds(testIds) {
|
||||||
|
await Promise.all(
|
||||||
|
testIds.map((testId) => page.waitForSelector(getTestId(testId)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllByClass(testClass) {
|
||||||
|
return page.$$(`[data-testclass=${testClass}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function typeInto(testId, text) {
|
||||||
|
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
|
||||||
|
// only works for text without special characters
|
||||||
|
await waitByID(testId);
|
||||||
|
const selector = getTestId(testId);
|
||||||
|
// type ahead can be annoying if you don't pause before you type
|
||||||
|
await page.click(selector);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.type(selector, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearInputAndTypeInto(testId, text) {
|
||||||
|
await waitByID(testId);
|
||||||
|
const selector = getTestId(testId);
|
||||||
|
// only works for text without special characters
|
||||||
|
// type ahead can be annoying if you don't pause before you type
|
||||||
|
await page.click(selector);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
// select all
|
||||||
|
await page.click(selector, { clickCount: 3 });
|
||||||
|
await page.keyboard.press("Backspace");
|
||||||
|
await page.type(selector, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickOn(testId, options = {}) {
|
||||||
|
await expect(page).toClick(getTestId(testId), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (thuang): There are times when Puppeteer clicks on a button and the page doesn't respond.
|
||||||
|
* So I added clickOnUntil() to retry clicking until a given condition is met.
|
||||||
|
*/
|
||||||
|
export async function clickOnUntil(testId, assert) {
|
||||||
|
const MAX_RETRY = 10;
|
||||||
|
const WAIT_FOR_MS = 200;
|
||||||
|
|
||||||
|
let retry = 0;
|
||||||
|
|
||||||
|
while (retry < MAX_RETRY) {
|
||||||
|
try {
|
||||||
|
await clickOn(testId);
|
||||||
|
await assert();
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
retry += 1;
|
||||||
|
|
||||||
|
await page.waitForTimeout(WAIT_FOR_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry === MAX_RETRY) {
|
||||||
|
throw Error("clickOnUntil() assertion failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOneElementInnerHTML(selector, options = {}) {
|
||||||
|
await page.waitForSelector(selector, options);
|
||||||
|
|
||||||
|
return page.$eval(selector, (el) => el.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOneElementInnerText(selector) {
|
||||||
|
expect(page).toMatchElement(selector);
|
||||||
|
|
||||||
|
return page.$eval(selector, (el) => el.innerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getElementCoordinates(testId) {
|
||||||
|
return page.$eval(getTestId(testId), (elem) => {
|
||||||
|
const { left, top } = elem.getBoundingClientRect();
|
||||||
|
return [left, top];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickTermsOfService() {
|
||||||
|
if (!(await isElementPresent(getTestId("tos-cookies-accept")))) return;
|
||||||
|
|
||||||
|
await clickOn("tos-cookies-accept");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nameNewAnnotation() {
|
||||||
|
if (await isElementPresent(getTestId("annotation-dialog"))) {
|
||||||
|
await typeInto("new-annotation-name", "ignoreE2E");
|
||||||
|
await clickOn("submit-annotation");
|
||||||
|
|
||||||
|
// wait for the page to load
|
||||||
|
await waitByClass("autosave-complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function goToPage(url) {
|
||||||
|
await page.goto(url, {
|
||||||
|
waitUntil: "networkidle0",
|
||||||
|
});
|
||||||
|
|
||||||
|
await nameNewAnnotation();
|
||||||
|
await clickTermsOfService();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isElementPresent(selector, options) {
|
||||||
|
return Boolean(await page.$(selector, options));
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function getTestId(id: any) {
|
|
||||||
return `[data-testid='${id}']`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function getTestClass(className: any) {
|
|
||||||
return `[data-testclass='${className}']`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function waitByID(testId: any, props = {}) {
|
|
||||||
return page.waitForSelector(getTestId(testId), props);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function waitByClass(testClass: any, props = {}) {
|
|
||||||
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function waitForAllByIds(testIds: any) {
|
|
||||||
await Promise.all(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
testIds.map((testId: any) => page.waitForSelector(getTestId(testId)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getAllByClass(testClass: any) {
|
|
||||||
return page.$$(`[data-testclass=${testClass}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function typeInto(testId: any, text: any) {
|
|
||||||
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
|
|
||||||
// only works for text without special characters
|
|
||||||
await waitByID(testId);
|
|
||||||
const selector = getTestId(testId);
|
|
||||||
// type ahead can be annoying if you don't pause before you type
|
|
||||||
await page.click(selector);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await page.type(selector, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function clearInputAndTypeInto(testId: any, text: any) {
|
|
||||||
await waitByID(testId);
|
|
||||||
const selector = getTestId(testId);
|
|
||||||
// only works for text without special characters
|
|
||||||
// type ahead can be annoying if you don't pause before you type
|
|
||||||
await page.click(selector);
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
// select all
|
|
||||||
await page.click(selector, { clickCount: 3 });
|
|
||||||
await page.keyboard.press("Backspace");
|
|
||||||
await page.type(selector, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function clickOn(testId: any, options = {}) {
|
|
||||||
await expect(page).toClick(getTestId(testId), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (thuang): There are times when Puppeteer clicks on a button and the page doesn't respond.
|
|
||||||
* So I added clickOnUntil() to retry clicking until a given condition is met.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function clickOnUntil(testId: any, assert: any) {
|
|
||||||
const MAX_RETRY = 10;
|
|
||||||
const WAIT_FOR_MS = 200;
|
|
||||||
|
|
||||||
let retry = 0;
|
|
||||||
|
|
||||||
while (retry < MAX_RETRY) {
|
|
||||||
try {
|
|
||||||
await clickOn(testId);
|
|
||||||
await assert();
|
|
||||||
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
retry += 1;
|
|
||||||
|
|
||||||
await page.waitForTimeout(WAIT_FOR_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry === MAX_RETRY) {
|
|
||||||
throw Error("clickOnUntil() assertion failed!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getOneElementInnerHTML(selector: any, options = {}) {
|
|
||||||
await page.waitForSelector(selector, options);
|
|
||||||
|
|
||||||
return page.$eval(selector, (el) => el.innerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getOneElementInnerText(selector: any) {
|
|
||||||
expect(page).toMatchElement(selector);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
return page.$eval(selector, (el) => (el as any).innerText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function getElementCoordinates(testId: any) {
|
|
||||||
return page.$eval(getTestId(testId), (elem) => {
|
|
||||||
const { left, top } = elem.getBoundingClientRect();
|
|
||||||
return [left, top];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickTermsOfService() {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
if (!(await isElementPresent(getTestId("tos-cookies-accept")))) return;
|
|
||||||
|
|
||||||
await clickOn("tos-cookies-accept");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function nameNewAnnotation() {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
|
||||||
if (await isElementPresent(getTestId("annotation-dialog"))) {
|
|
||||||
await typeInto("new-annotation-name", "ignoreE2E");
|
|
||||||
await clickOn("submit-annotation");
|
|
||||||
|
|
||||||
// wait for the page to load
|
|
||||||
await waitByClass("autosave-complete");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function goToPage(url: any) {
|
|
||||||
await page.goto(url, {
|
|
||||||
waitUntil: "networkidle0",
|
|
||||||
});
|
|
||||||
|
|
||||||
await nameNewAnnotation();
|
|
||||||
await clickTermsOfService();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function isElementPresent(selector: any, options: any) {
|
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 2.
|
|
||||||
return Boolean(await page.$(selector, options));
|
|
||||||
}
|
|
||||||
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
||||||
require("jest-circus");
|
require("jest-circus");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const ENV_DEFAULT = require("../../../environment.default.json");
|
const ENV_DEFAULT = require("../../../environment.default.json");
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2451) FIXME: Cannot redeclare block-scoped variable 'takeScreen... Remove this comment to see the full error message
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const takeScreenshot = require("./takeScreenshot");
|
const takeScreenshot = require("./takeScreenshot");
|
||||||
|
|
||||||
class ScreenshotEnvironment extends PuppeteerEnvironment {
|
class ScreenshotEnvironment extends PuppeteerEnvironment {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
// @ts-ignore FIXME: 'globalSetup.ts' cannot be compiled under '--isola... Remove this comment to see the full error message
|
|
||||||
const {
|
const {
|
||||||
SecretsManagerClient,
|
SecretsManagerClient,
|
||||||
GetSecretValueCommand,
|
GetSecretValueCommand,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
} = require("@aws-sdk/client-secrets-manager");
|
} = require("@aws-sdk/client-secrets-manager");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const { setup } = require("jest-environment-puppeteer");
|
const { setup } = require("jest-environment-puppeteer");
|
||||||
|
|
||||||
const client = new SecretsManagerClient({ region: "us-west-2" });
|
const client = new SecretsManagerClient({ region: "us-west-2" });
|
||||||
+2
-20
@@ -20,16 +20,7 @@ describe("cascade", () => {
|
|||||||
const reducer = cascadeReducers([
|
const reducer = cascadeReducers([
|
||||||
[
|
[
|
||||||
"foo",
|
"foo",
|
||||||
(
|
(currentState, action, nextSharedState, prevSharedState) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
currentState: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
action: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
nextSharedState: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
prevSharedState: any
|
|
||||||
) => {
|
|
||||||
expect(currentState).toBeUndefined();
|
expect(currentState).toBeUndefined();
|
||||||
expect(action).toEqual(topLevelAction);
|
expect(action).toEqual(topLevelAction);
|
||||||
expect(nextSharedState).toStrictEqual({});
|
expect(nextSharedState).toStrictEqual({});
|
||||||
@@ -39,16 +30,7 @@ describe("cascade", () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"bar",
|
"bar",
|
||||||
(
|
(currentState, action, nextSharedState, prevSharedState) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
currentState: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
action: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
nextSharedState: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
prevSharedState: any
|
|
||||||
) => {
|
|
||||||
expect(currentState).toBeUndefined();
|
expect(currentState).toBeUndefined();
|
||||||
expect(action).toEqual(topLevelAction);
|
expect(action).toEqual(topLevelAction);
|
||||||
expect(nextSharedState).toStrictEqual({ foo: 0 });
|
expect(nextSharedState).toStrictEqual({ foo: 0 });
|
||||||
-2
@@ -501,7 +501,6 @@ describe("geneset: set tid", () => {
|
|||||||
test("not a number error", () => {
|
test("not a number error", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
genesetsReducer(
|
genesetsReducer(
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'undefined... Remove this comment to see the full error message
|
|
||||||
{ lastTid: 1 },
|
{ lastTid: 1 },
|
||||||
{
|
{
|
||||||
type: "geneset: set tid",
|
type: "geneset: set tid",
|
||||||
@@ -514,7 +513,6 @@ describe("geneset: set tid", () => {
|
|||||||
test("decrement error", () => {
|
test("decrement error", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
genesetsReducer(
|
genesetsReducer(
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'undefined... Remove this comment to see the full error message
|
|
||||||
{ lastTid: 1 },
|
{ lastTid: 1 },
|
||||||
{
|
{
|
||||||
type: "geneset: set tid",
|
type: "geneset: set tid",
|
||||||
+5
-8
@@ -1,12 +1,9 @@
|
|||||||
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", () => {
|
||||||
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();
|
||||||
});
|
});
|
||||||
@@ -26,7 +23,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 };
|
||||||
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
const reducer = (state) => ({ 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" });
|
||||||
@@ -44,8 +41,8 @@ describe("undo", () => {
|
|||||||
|
|
||||||
describe("redo", () => {
|
describe("redo", () => {
|
||||||
const initialState = { a: 0, b: 1000 };
|
const initialState = { a: 0, b: 1000 };
|
||||||
const reducer: Reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
const reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
|
||||||
let UR: Reducer;
|
let UR;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UR = undoable(reducer, ["a"]);
|
UR = undoable(reducer, ["a"]);
|
||||||
@@ -5,6 +5,5 @@ the jest test environment).
|
|||||||
|
|
||||||
import { TextDecoder, TextEncoder } from "util";
|
import { TextDecoder, TextEncoder } from "util";
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'typeof TextDecoder' is not assignable to typ... Remove this comment to see the full error message
|
|
||||||
global.TextDecoder = TextDecoder;
|
global.TextDecoder = TextDecoder;
|
||||||
global.TextEncoder = TextEncoder;
|
global.TextEncoder = TextEncoder;
|
||||||
+5
-9
@@ -11,18 +11,14 @@ describe("rangeEncodeIndices", () => {
|
|||||||
|
|
||||||
test("sorted flag", () => {
|
test("sorted flag", () => {
|
||||||
expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([
|
expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([
|
||||||
1,
|
1, 9, 432,
|
||||||
9,
|
|
||||||
432,
|
|
||||||
]);
|
]);
|
||||||
expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([
|
expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([
|
||||||
1,
|
1, 9, 432,
|
||||||
9,
|
|
||||||
432,
|
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)).toMatchObject(
|
||||||
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)
|
[[0, 3], [9, 10], 432]
|
||||||
).toMatchObject([[0, 3], [9, 10], 432]);
|
);
|
||||||
expect(
|
expect(
|
||||||
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, false)
|
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, false)
|
||||||
).toMatchObject([[0, 3], [9, 10], 432]);
|
).toMatchObject([[0, 3], [9, 10], 432]);
|
||||||
+54
-79
@@ -10,18 +10,14 @@ import {
|
|||||||
isubsetMask,
|
isubsetMask,
|
||||||
} from "../../../src/annoMatrix";
|
} from "../../../src/annoMatrix";
|
||||||
import { Dataframe } from "../../../src/util/dataframe";
|
import { Dataframe } from "../../../src/util/dataframe";
|
||||||
import { Field } from "../../../src/common/types/schema";
|
|
||||||
|
|
||||||
enableFetchMocks();
|
enableFetchMocks();
|
||||||
|
|
||||||
describe("AnnoMatrix", () => {
|
describe("AnnoMatrix", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let annoMatrix;
|
||||||
let annoMatrix: any;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.resetMocks(); // reset all fetch mocking state
|
||||||
(fetch as any).resetMocks(); // reset all fetch mocking state
|
|
||||||
// reset all fetch mocking state
|
|
||||||
annoMatrix = new AnnoMatrixLoader(
|
annoMatrix = new AnnoMatrixLoader(
|
||||||
serverMocks.baseDataURL,
|
serverMocks.baseDataURL,
|
||||||
serverMocks.schema.schema
|
serverMocks.schema.schema
|
||||||
@@ -35,67 +31,58 @@ describe("AnnoMatrix", () => {
|
|||||||
expect(annoMatrix.nObs).toEqual(serverMocks.schema.schema.dataframe.nObs);
|
expect(annoMatrix.nObs).toEqual(serverMocks.schema.schema.dataframe.nObs);
|
||||||
expect(annoMatrix.nVar).toEqual(serverMocks.schema.schema.dataframe.nVar);
|
expect(annoMatrix.nVar).toEqual(serverMocks.schema.schema.dataframe.nVar);
|
||||||
expect(annoMatrix.isView).toBeFalsy();
|
expect(annoMatrix.isView).toBeFalsy();
|
||||||
expect(annoMatrix.viewOf).toBe(annoMatrix);
|
expect(annoMatrix.viewOf).toBeUndefined();
|
||||||
expect(annoMatrix.rowIndex).toBeDefined();
|
expect(annoMatrix.rowIndex).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple single column fetch", async () => {
|
test("simple single column fetch", async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.annotationsObs(["name_0"]));
|
||||||
(fetch as any).once(serverMocks.annotationsObs(["name_0"]));
|
|
||||||
|
|
||||||
const df = await annoMatrix.fetch(Field.obs, "name_0");
|
const df = await annoMatrix.fetch("obs", "name_0");
|
||||||
expect(df).toBeInstanceOf(Dataframe);
|
expect(df).toBeInstanceOf(Dataframe);
|
||||||
expect(df.colIndex.labels()).toEqual(["name_0"]);
|
expect(df.colIndex.labels()).toEqual(["name_0"]);
|
||||||
expect(df.dims).toEqual([annoMatrix.nObs, 1]);
|
expect(df.dims).toEqual([annoMatrix.nObs, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple multi column fetch", async () => {
|
test("simple multi column fetch", async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch
|
||||||
(fetch as any)
|
|
||||||
.once(serverMocks.annotationsObs(["name_0"]))
|
.once(serverMocks.annotationsObs(["name_0"]))
|
||||||
.once(serverMocks.annotationsObs(["n_genes"]));
|
.once(serverMocks.annotationsObs(["n_genes"]));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
annoMatrix.fetch(Field.obs, ["name_0", "n_genes"])
|
annoMatrix.fetch("obs", ["name_0", "n_genes"])
|
||||||
).resolves.toBeInstanceOf(Dataframe);
|
).resolves.toBeInstanceOf(Dataframe);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fetch from field", () => {
|
describe("fetch from field", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const getLastTwo = async (field) => {
|
||||||
const getLastTwo = async (field: any) => {
|
|
||||||
const columnNames = annoMatrix.getMatrixColumns(field).slice(-2);
|
const columnNames = annoMatrix.getMatrixColumns(field).slice(-2);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockResponses(...columnNames.map(() => serverMocks.responder));
|
||||||
(fetch as any).mockResponses(
|
|
||||||
...columnNames.map(() => serverMocks.responder)
|
|
||||||
);
|
|
||||||
await expect(
|
await expect(
|
||||||
annoMatrix.fetch(field, columnNames)
|
annoMatrix.fetch(field, columnNames)
|
||||||
).resolves.toBeInstanceOf(Dataframe);
|
).resolves.toBeInstanceOf(Dataframe);
|
||||||
};
|
};
|
||||||
|
|
||||||
test(Field.obs, async () => getLastTwo(Field.obs));
|
test("obs", async () => getLastTwo("obs"));
|
||||||
test("var", async () => getLastTwo("var"));
|
test("var", async () => getLastTwo("var"));
|
||||||
test("emb", async () => getLastTwo("emb"));
|
test("emb", async () => getLastTwo("emb"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fetch - test all query forms", async () => {
|
test("fetch - test all query forms", async () => {
|
||||||
// single string is a column name
|
// single string is a column name
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.annotationsObs(["n_genes"]));
|
||||||
(fetch as any).once(serverMocks.annotationsObs(["n_genes"]));
|
await expect(annoMatrix.fetch("obs", "n_genes")).resolves.toBeInstanceOf(
|
||||||
await expect(
|
Dataframe
|
||||||
annoMatrix.fetch(Field.obs, "n_genes")
|
);
|
||||||
).resolves.toBeInstanceOf(Dataframe);
|
|
||||||
|
|
||||||
// array of column names, expecting n_genes to be cached.
|
// array of column names, expecting n_genes to be cached.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.annotationsObs(["percent_mito"]));
|
||||||
(fetch as any).once(serverMocks.annotationsObs(["percent_mito"]));
|
|
||||||
await expect(
|
await expect(
|
||||||
annoMatrix.fetch(Field.obs, ["n_genes", "percent_mito"])
|
annoMatrix.fetch("obs", ["n_genes", "percent_mito"])
|
||||||
).resolves.toBeInstanceOf(Dataframe);
|
).resolves.toBeInstanceOf(Dataframe);
|
||||||
|
|
||||||
// more complex value filter query, enumerated
|
// more complex value filter query, enumerated
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.responder);
|
||||||
(fetch as any).once(serverMocks.responder);
|
|
||||||
await expect(
|
await expect(
|
||||||
annoMatrix.fetch("X", {
|
annoMatrix.fetch("X", {
|
||||||
where: {
|
where: {
|
||||||
@@ -108,8 +95,7 @@ describe("AnnoMatrix", () => {
|
|||||||
|
|
||||||
// more complex value filter query, range
|
// more complex value filter query, range
|
||||||
const varIndex = annoMatrix.schema.annotations.var.index;
|
const varIndex = annoMatrix.schema.annotations.var.index;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch
|
||||||
(fetch as any)
|
|
||||||
.once(
|
.once(
|
||||||
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "SUMO3"]])
|
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "SUMO3"]])
|
||||||
)
|
)
|
||||||
@@ -152,9 +138,9 @@ describe("AnnoMatrix", () => {
|
|||||||
|
|
||||||
test("schema accessors", () => {
|
test("schema accessors", () => {
|
||||||
expect(annoMatrix.getMatrixFields()).toEqual(
|
expect(annoMatrix.getMatrixFields()).toEqual(
|
||||||
expect.arrayContaining(["X", Field.obs, "emb", "var"])
|
expect.arrayContaining(["X", "obs", "emb", "var"])
|
||||||
);
|
);
|
||||||
expect(annoMatrix.getMatrixColumns(Field.obs)).toEqual(
|
expect(annoMatrix.getMatrixColumns("obs")).toEqual(
|
||||||
expect.arrayContaining(["name_0", "n_genes", "louvain"])
|
expect.arrayContaining(["name_0", "n_genes", "louvain"])
|
||||||
);
|
);
|
||||||
expect(annoMatrix.getColumnSchema("emb", "umap")).toEqual({
|
expect(annoMatrix.getColumnSchema("emb", "umap")).toEqual({
|
||||||
@@ -172,7 +158,7 @@ describe("AnnoMatrix", () => {
|
|||||||
test the mask & label access to subset via isubset and isubsetMask
|
test the mask & label access to subset via isubset and isubsetMask
|
||||||
*/
|
*/
|
||||||
test("isubset", async () => {
|
test("isubset", async () => {
|
||||||
const rowList = new Int32Array([0, 10]);
|
const rowList = [0, 10];
|
||||||
const rowMask = new Uint8Array(annoMatrix.nObs);
|
const rowMask = new Uint8Array(annoMatrix.nObs);
|
||||||
for (let i = 0; i < rowList.length; i += 1) {
|
for (let i = 0; i < rowList.length; i += 1) {
|
||||||
rowMask[rowList[i]] = 1;
|
rowMask[rowList[i]] = 1;
|
||||||
@@ -185,14 +171,11 @@ describe("AnnoMatrix", () => {
|
|||||||
expect(am1.nObs).toEqual(am2.nObs);
|
expect(am1.nObs).toEqual(am2.nObs);
|
||||||
expect(am1.nVar).toEqual(am2.nVar);
|
expect(am1.nVar).toEqual(am2.nVar);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch
|
||||||
(fetch as any)
|
|
||||||
.once(serverMocks.annotationsObs(["n_genes"]))
|
.once(serverMocks.annotationsObs(["n_genes"]))
|
||||||
.once(serverMocks.annotationsObs(["n_genes"]));
|
.once(serverMocks.annotationsObs(["n_genes"]));
|
||||||
const ng1 = (await am1.fetch(Field.obs, "n_genes")) as Dataframe;
|
const ng1 = await am1.fetch("obs", "n_genes");
|
||||||
const ng2 = (await am2.fetch(Field.obs, "n_genes")) as Dataframe;
|
const ng2 = await am2.fetch("obs", "n_genes");
|
||||||
expect(ng1).toBeDefined();
|
|
||||||
expect(ng2).toBeDefined();
|
|
||||||
expect(ng1).toHaveLength(ng2.length);
|
expect(ng1).toHaveLength(ng2.length);
|
||||||
expect(ng1.colIndex.labels()).toEqual(ng2.colIndex.labels());
|
expect(ng1.colIndex.labels()).toEqual(ng2.colIndex.labels());
|
||||||
expect(ng1.col("n_genes").asArray()).toEqual(
|
expect(ng1.col("n_genes").asArray()).toEqual(
|
||||||
@@ -202,12 +185,10 @@ describe("AnnoMatrix", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("add/drop column", () => {
|
describe("add/drop column", () => {
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'base' implicitly has an 'any' type.
|
|
||||||
async function addDrop(base) {
|
async function addDrop(base) {
|
||||||
expect(base.getMatrixColumns(Field.obs)).not.toContain("foo");
|
expect(base.getMatrixColumns("obs")).not.toContain("foo");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(base.fetch("obs", "foo")).rejects.toThrow(
|
||||||
await expect(base.fetch(Field.obs, "foo")).rejects.toThrow(
|
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -217,9 +198,9 @@ describe("AnnoMatrix", () => {
|
|||||||
Float32Array,
|
Float32Array,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
expect(base.getMatrixColumns(Field.obs)).not.toContain("foo");
|
expect(base.getMatrixColumns("obs")).not.toContain("foo");
|
||||||
expect(am1.getMatrixColumns(Field.obs)).toContain("foo");
|
expect(am1.getMatrixColumns("obs")).toContain("foo");
|
||||||
const foo: Dataframe = await am1.fetch(Field.obs, "foo");
|
const foo = await am1.fetch("obs", "foo");
|
||||||
expect(foo).toBeDefined();
|
expect(foo).toBeDefined();
|
||||||
expect(foo).toBeInstanceOf(Dataframe);
|
expect(foo).toBeInstanceOf(Dataframe);
|
||||||
expect(foo).toHaveLength(am1.nObs);
|
expect(foo).toHaveLength(am1.nObs);
|
||||||
@@ -229,11 +210,10 @@ describe("AnnoMatrix", () => {
|
|||||||
|
|
||||||
/* drop */
|
/* drop */
|
||||||
const am2 = am1.dropObsColumn("foo");
|
const am2 = am1.dropObsColumn("foo");
|
||||||
expect(base.getMatrixColumns(Field.obs)).not.toContain("foo");
|
expect(base.getMatrixColumns("obs")).not.toContain("foo");
|
||||||
expect(am2.getMatrixColumns(Field.obs)).not.toContain("foo");
|
expect(am2.getMatrixColumns("obs")).not.toContain("foo");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(am2.fetch("obs", "foo")).rejects.toThrow(
|
||||||
await expect(am2.fetch(Field.obs, "foo")).rejects.toThrow(
|
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,25 +226,23 @@ describe("AnnoMatrix", () => {
|
|||||||
const am1 = clip(annoMatrix, 0.1, 0.9);
|
const am1 = clip(annoMatrix, 0.1, 0.9);
|
||||||
await addDrop(am1);
|
await addDrop(am1);
|
||||||
|
|
||||||
const am2 = isubset(am1, new Int32Array([0, 1, 2, 20, 30, 400]));
|
const am2 = isubset(am1, [0, 1, 2, 20, 30, 400]);
|
||||||
await addDrop(am2);
|
await addDrop(am2);
|
||||||
|
|
||||||
const am3 = isubset(annoMatrix, new Int32Array([10, 0, 7, 3]));
|
const am3 = isubset(annoMatrix, [10, 0, 7, 3]);
|
||||||
await addDrop(am3);
|
await addDrop(am3);
|
||||||
|
|
||||||
const am4 = clip(am3, 0, 1);
|
const am4 = clip(am3, 0, 1);
|
||||||
await addDrop(am4);
|
await addDrop(am4);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockResponse(serverMocks.responder);
|
||||||
(fetch as any).mockResponse(serverMocks.responder);
|
|
||||||
|
|
||||||
await am1.fetch(Field.obs, am1.getMatrixColumns(Field.obs));
|
await am1.fetch("obs", am1.getMatrixColumns("obs"));
|
||||||
await am2.fetch(Field.obs, am2.getMatrixColumns(Field.obs));
|
await am2.fetch("obs", am2.getMatrixColumns("obs"));
|
||||||
await am3.fetch(Field.obs, am3.getMatrixColumns(Field.obs));
|
await am3.fetch("obs", am3.getMatrixColumns("obs"));
|
||||||
await am4.fetch(Field.obs, am4.getMatrixColumns(Field.obs));
|
await am4.fetch("obs", am4.getMatrixColumns("obs"));
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.resetMocks();
|
||||||
(fetch as any).resetMocks();
|
|
||||||
|
|
||||||
await addDrop(am1);
|
await addDrop(am1);
|
||||||
await addDrop(am2);
|
await addDrop(am2);
|
||||||
@@ -274,7 +252,6 @@ describe("AnnoMatrix", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setObsColumnValues", () => {
|
describe("setObsColumnValues", () => {
|
||||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'base' implicitly has an 'any' type.
|
|
||||||
async function addSetDrop(base) {
|
async function addSetDrop(base) {
|
||||||
/* add column */
|
/* add column */
|
||||||
let am = base.addObsColumn(
|
let am = base.addObsColumn(
|
||||||
@@ -288,7 +265,7 @@ describe("AnnoMatrix", () => {
|
|||||||
"unassigned"
|
"unassigned"
|
||||||
);
|
);
|
||||||
|
|
||||||
const testVal = await am.fetch(Field.obs, "test");
|
const testVal = await am.fetch("obs", "test");
|
||||||
expect(testVal.col("test").asArray()).toEqual(
|
expect(testVal.col("test").asArray()).toEqual(
|
||||||
new Array(am.nObs).fill("unassigned")
|
new Array(am.nObs).fill("unassigned")
|
||||||
);
|
);
|
||||||
@@ -296,7 +273,7 @@ describe("AnnoMatrix", () => {
|
|||||||
/* set values in column */
|
/* set values in column */
|
||||||
const whichRows = [1, 2, 10];
|
const whichRows = [1, 2, 10];
|
||||||
const am1 = await am.setObsColumnValues("test", whichRows, "yo");
|
const am1 = await am.setObsColumnValues("test", whichRows, "yo");
|
||||||
const testVal1 = await am1.fetch(Field.obs, "test");
|
const testVal1 = await am1.fetch("obs", "test");
|
||||||
const expt = new Array(am1.nObs).fill("unassigned");
|
const expt = new Array(am1.nObs).fill("unassigned");
|
||||||
for (let i = 0; i < whichRows.length; i += 1) {
|
for (let i = 0; i < whichRows.length; i += 1) {
|
||||||
const offset = am1.rowIndex.getOffset(whichRows[i]);
|
const offset = am1.rowIndex.getOffset(whichRows[i]);
|
||||||
@@ -304,16 +281,15 @@ describe("AnnoMatrix", () => {
|
|||||||
}
|
}
|
||||||
expect(testVal1).not.toBe(testVal);
|
expect(testVal1).not.toBe(testVal);
|
||||||
expect(testVal1.col("test").asArray()).toEqual(expt);
|
expect(testVal1.col("test").asArray()).toEqual(expt);
|
||||||
expect(am1.getColumnSchema(Field.obs, "test").type).toBe("categorical");
|
expect(am1.getColumnSchema("obs", "test").type).toBe("categorical");
|
||||||
expect(am1.getColumnSchema(Field.obs, "test").categories).toEqual(
|
expect(am1.getColumnSchema("obs", "test").categories).toEqual(
|
||||||
expect.arrayContaining(["unassigned", "red", "green", "yo"])
|
expect.arrayContaining(["unassigned", "red", "green", "yo"])
|
||||||
);
|
);
|
||||||
|
|
||||||
/* drop column */
|
/* drop column */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
|
||||||
am = am1.dropObsColumn("test");
|
am = am1.dropObsColumn("test");
|
||||||
await expect(am.fetch(Field.obs, "test")).rejects.toThrow(
|
await expect(am.fetch("obs", "test")).rejects.toThrow(
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -326,18 +302,17 @@ describe("AnnoMatrix", () => {
|
|||||||
const am1 = clip(annoMatrix, 0.1, 0.9);
|
const am1 = clip(annoMatrix, 0.1, 0.9);
|
||||||
await addSetDrop(am1);
|
await addSetDrop(am1);
|
||||||
|
|
||||||
const am2 = isubset(am1, new Int32Array([0, 1, 2, 10, 20, 30, 400]));
|
const am2 = isubset(am1, [0, 1, 2, 10, 20, 30, 400]);
|
||||||
await addSetDrop(am2);
|
await addSetDrop(am2);
|
||||||
|
|
||||||
const am3 = isubset(annoMatrix, new Int32Array([10, 1, 0, 30, 2]));
|
const am3 = isubset(annoMatrix, [10, 1, 0, 30, 2]);
|
||||||
await addSetDrop(am3);
|
await addSetDrop(am3);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockResponse(serverMocks.responder);
|
||||||
(fetch as any).mockResponse(serverMocks.responder);
|
|
||||||
|
|
||||||
await am1.fetch(Field.obs, am1.getMatrixColumns(Field.obs));
|
await am1.fetch("obs", am1.getMatrixColumns("obs"));
|
||||||
await am2.fetch(Field.obs, am2.getMatrixColumns(Field.obs));
|
await am2.fetch("obs", am2.getMatrixColumns("obs"));
|
||||||
await am3.fetch(Field.obs, am3.getMatrixColumns(Field.obs));
|
await am3.fetch("obs", am3.getMatrixColumns("obs"));
|
||||||
|
|
||||||
await addSetDrop(am1);
|
await addSetDrop(am1);
|
||||||
await addSetDrop(am2);
|
await addSetDrop(am2);
|
||||||
+129
-196
@@ -12,25 +12,19 @@ import {
|
|||||||
AnnoMatrixObsCrossfilter,
|
AnnoMatrixObsCrossfilter,
|
||||||
isubsetMask,
|
isubsetMask,
|
||||||
} from "../../../src/annoMatrix";
|
} from "../../../src/annoMatrix";
|
||||||
import { Dataframe } from "../../../src/util/dataframe";
|
|
||||||
import { rangeFill } from "../../../src/util/range";
|
import { rangeFill } from "../../../src/util/range";
|
||||||
import { Field, Schema } from "../../../src/common/types/schema";
|
|
||||||
|
|
||||||
enableFetchMocks();
|
enableFetchMocks();
|
||||||
|
|
||||||
describe("AnnoMatrixCrossfilter", () => {
|
describe("AnnoMatrixCrossfilter", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let annoMatrix;
|
||||||
let annoMatrix: any;
|
let crossfilter;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
let crossfilter: any;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.resetMocks(); // reset all fetch mocking state
|
||||||
(fetch as any).resetMocks(); // reset all fetch mocking state
|
|
||||||
// reset all fetch mocking state
|
|
||||||
annoMatrix = new AnnoMatrixLoader(
|
annoMatrix = new AnnoMatrixLoader(
|
||||||
serverMocks.baseDataURL,
|
serverMocks.baseDataURL,
|
||||||
serverMocks.schema.schema as Schema
|
serverMocks.schema.schema
|
||||||
);
|
);
|
||||||
crossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
crossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||||
});
|
});
|
||||||
@@ -73,11 +67,8 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
crossfilter.obsCrossfilter.hasDimension("obs/louvain")
|
crossfilter.obsCrossfilter.hasDimension("obs/louvain")
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
|
||||||
(fetch as any).once(
|
let newCrossfilter = await crossfilter.select("obs", "louvain", {
|
||||||
serverMocks.dataframeResponse(["louvain"], [obsLouvain])
|
|
||||||
);
|
|
||||||
let newCrossfilter = await crossfilter.select(Field.obs, "louvain", {
|
|
||||||
mode: "none",
|
mode: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,10 +76,9 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
expect(
|
expect(
|
||||||
newCrossfilter.obsCrossfilter.hasDimension("obs/louvain")
|
newCrossfilter.obsCrossfilter.hasDimension("obs/louvain")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
expect(fetch.mock.calls).toHaveLength(1);
|
||||||
expect((fetch as any).mock.calls).toHaveLength(1);
|
|
||||||
|
|
||||||
newCrossfilter = await crossfilter.select(Field.obs, "louvain", {
|
newCrossfilter = await crossfilter.select("obs", "louvain", {
|
||||||
mode: "all",
|
mode: "all",
|
||||||
});
|
});
|
||||||
expect(newCrossfilter.countSelected()).toEqual(annoMatrix.nObs);
|
expect(newCrossfilter.countSelected()).toEqual(annoMatrix.nObs);
|
||||||
@@ -97,11 +87,8 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
test("simple column select", async () => {
|
test("simple column select", async () => {
|
||||||
let xfltr;
|
let xfltr;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
|
||||||
(fetch as any).once(
|
xfltr = await crossfilter.select("obs", "louvain", {
|
||||||
serverMocks.dataframeResponse(["louvain"], [obsLouvain])
|
|
||||||
);
|
|
||||||
xfltr = await crossfilter.select(Field.obs, "louvain", {
|
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: ["NK cells", "B cells"],
|
values: ["NK cells", "B cells"],
|
||||||
});
|
});
|
||||||
@@ -118,7 +105,6 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
expect(xfltr.allSelectedLabels()).toEqual(
|
expect(xfltr.allSelectedLabels()).toEqual(
|
||||||
Int32Array.from(
|
Int32Array.from(
|
||||||
obsLouvain.reduce((acc, val, idx) => {
|
obsLouvain.reduce((acc, val, idx) => {
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number' is not assignable to par... Remove this comment to see the full error message
|
|
||||||
if (val === "NK cells" || val === "B cells") acc.push(idx);
|
if (val === "NK cells" || val === "B cells") acc.push(idx);
|
||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
@@ -134,20 +120,17 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const df: Dataframe = await annoMatrix.fetch(Field.obs, "louvain");
|
const df = await annoMatrix.fetch("obs", "louvain");
|
||||||
const values = df.col("louvain").asArray();
|
const values = df.col("louvain").asArray();
|
||||||
const selected = xfltr.allSelectedMask();
|
const selected = xfltr.allSelectedMask();
|
||||||
values.every(
|
values.every(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
|
||||||
(val: any, idx: any) =>
|
|
||||||
!["NK cells", "B cells"].includes(val) !== !selected[idx]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(
|
||||||
(fetch as any).once(
|
|
||||||
serverMocks.dataframeResponse(["n_genes"], [new Int32Array(obsNGenes)])
|
serverMocks.dataframeResponse(["n_genes"], [new Int32Array(obsNGenes)])
|
||||||
);
|
);
|
||||||
xfltr = await xfltr.select(Field.obs, "n_genes", {
|
xfltr = await xfltr.select("obs", "n_genes", {
|
||||||
mode: "range",
|
mode: "range",
|
||||||
lo: 0,
|
lo: 0,
|
||||||
hi: 500,
|
hi: 500,
|
||||||
@@ -163,7 +146,6 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
val < 500 &&
|
val < 500 &&
|
||||||
(louvain === "NK cells" || louvain === "B cells")
|
(louvain === "NK cells" || louvain === "B cells")
|
||||||
)
|
)
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number' is not assignable to par... Remove this comment to see the full error message
|
|
||||||
acc.push(idx);
|
acc.push(idx);
|
||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
@@ -178,8 +160,7 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
const varIndex = annoMatrix.schema.annotations.var.index;
|
const varIndex = annoMatrix.schema.annotations.var.index;
|
||||||
|
|
||||||
const { nObs } = annoMatrix.schema.dataframe;
|
const { nObs } = annoMatrix.schema.dataframe;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(
|
||||||
(fetch as any).once(
|
|
||||||
serverMocks.dataframeResponse(
|
serverMocks.dataframeResponse(
|
||||||
["TEST"],
|
["TEST"],
|
||||||
[rangeFill(new Float32Array(nObs), 0, 0.1)]
|
[rangeFill(new Float32Array(nObs), 0, 0.1)]
|
||||||
@@ -215,19 +196,14 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
const values = df.icol(0).asArray();
|
const values = df.icol(0).asArray();
|
||||||
const selected = xfltr.allSelectedMask();
|
const selected = xfltr.allSelectedMask();
|
||||||
values.every(
|
values.every((val, idx) => !(val >= 0 && val <= 50) !== !selected[idx]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
expect(selected.reduce((acc, val) => (val ? acc + 1 : acc), 0)).toEqual(
|
||||||
(val: any, idx: any) => !(val >= 0 && val <= 50) !== !selected[idx]
|
xfltr.countSelected()
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
selected.reduce((acc: any, val: any) => (val ? acc + 1 : acc), 0)
|
|
||||||
).toEqual(xfltr.countSelected());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spatial column select", async () => {
|
test("spatial column select", async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(
|
||||||
(fetch as any).once(
|
|
||||||
serverMocks.dataframeResponse(
|
serverMocks.dataframeResponse(
|
||||||
["umap_0", "umap_1"],
|
["umap_0", "umap_1"],
|
||||||
[Float32Array.from(embUmap[0]), Float32Array.from(embUmap[1])]
|
[Float32Array.from(embUmap[0]), Float32Array.from(embUmap[1])]
|
||||||
@@ -246,7 +222,6 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
test("select on subset", async () => {
|
test("select on subset", async () => {
|
||||||
const mask = new Uint8Array(annoMatrix.nObs).fill(0);
|
const mask = new Uint8Array(annoMatrix.nObs).fill(0);
|
||||||
for (let i = 0; i < mask.length; i += 2) {
|
for (let i = 0; i < mask.length; i += 2) {
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean' is not assignable to type 'number'.
|
|
||||||
mask[i] = true;
|
mask[i] = true;
|
||||||
}
|
}
|
||||||
const annoMatrixSubset = isubsetMask(annoMatrix, mask);
|
const annoMatrixSubset = isubsetMask(annoMatrix, mask);
|
||||||
@@ -255,11 +230,8 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
let xfltr = new AnnoMatrixObsCrossfilter(annoMatrixSubset);
|
let xfltr = new AnnoMatrixObsCrossfilter(annoMatrixSubset);
|
||||||
expect(xfltr.countSelected()).toEqual(annoMatrixSubset.nObs);
|
expect(xfltr.countSelected()).toEqual(annoMatrixSubset.nObs);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
|
||||||
(fetch as any).once(
|
xfltr = await xfltr.select("obs", "louvain", {
|
||||||
serverMocks.dataframeResponse(["louvain"], [obsLouvain])
|
|
||||||
);
|
|
||||||
xfltr = await xfltr.select(Field.obs, "louvain", {
|
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: ["NK cells", "B cells"],
|
values: ["NK cells", "B cells"],
|
||||||
});
|
});
|
||||||
@@ -267,17 +239,11 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
expect(xfltr).toBeDefined();
|
expect(xfltr).toBeDefined();
|
||||||
expect(xfltr.countSelected()).toEqual(240);
|
expect(xfltr.countSelected()).toEqual(240);
|
||||||
|
|
||||||
const df: Dataframe = (await annoMatrixSubset.fetch(
|
const df = await annoMatrixSubset.fetch("obs", "louvain");
|
||||||
Field.obs,
|
|
||||||
"louvain"
|
|
||||||
)) as Dataframe;
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
const values = df.col("louvain").asArray();
|
const values = df.col("louvain").asArray();
|
||||||
const selected = xfltr.allSelectedMask();
|
const selected = xfltr.allSelectedMask();
|
||||||
values.every(
|
values.every(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
|
||||||
(val: any, idx: any) =>
|
|
||||||
!["NK cells", "B cells"].includes(val) !== !selected[idx]
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,9 +256,8 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
"unable to obsSelect upon the var dimension"
|
"unable to obsSelect upon the var dimension"
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(crossfilter.select("obs", "foo")).rejects.toThrow(
|
||||||
await expect(crossfilter.select(Field.obs, "foo")).rejects.toThrow(
|
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -302,32 +267,27 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
/*
|
/*
|
||||||
test the matrix mutators via crossfilter proxy
|
test the matrix mutators via crossfilter proxy
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function helperAddTestCol(cf, colName, colSchema = null) {
|
||||||
async function helperAddTestCol(cf: any, colName: any, colSchema = null) {
|
|
||||||
expect(
|
expect(
|
||||||
cf.annoMatrix.getMatrixColumns(Field.obs).includes(colName)
|
cf.annoMatrix.getMatrixColumns("obs").includes(colName)
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
|
|
||||||
if (colSchema === null) {
|
if (colSchema === null) {
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ name: any; type: string; categories: strin... Remove this comment to see the full error message
|
|
||||||
colSchema = {
|
colSchema = {
|
||||||
name: colName,
|
name: colName,
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["toasty"],
|
categories: ["toasty"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
colSchema.name = colName;
|
colSchema.name = colName;
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
const initValue = colSchema.categories[0];
|
const initValue = colSchema.categories[0];
|
||||||
const xfltr = cf.addObsColumn(colSchema, Array, initValue);
|
const xfltr = cf.addObsColumn(colSchema, Array, initValue);
|
||||||
expect(
|
expect(
|
||||||
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(v) => v.name === colName
|
||||||
(v: any) => v.name === colName
|
|
||||||
)
|
)
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
const df = await xfltr.annoMatrix.fetch(Field.obs, colName);
|
const df = await xfltr.annoMatrix.fetch("obs", colName);
|
||||||
expect(df.hasCol(colName)).toBeTruthy();
|
expect(df.hasCol(colName)).toBeTruthy();
|
||||||
return xfltr;
|
return xfltr;
|
||||||
}
|
}
|
||||||
@@ -335,7 +295,7 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
test("addObsColumn", async () => {
|
test("addObsColumn", async () => {
|
||||||
expect(crossfilter.countSelected()).toBe(annoMatrix.nObs);
|
expect(crossfilter.countSelected()).toBe(annoMatrix.nObs);
|
||||||
expect(
|
expect(
|
||||||
crossfilter.annoMatrix.getMatrixColumns(Field.obs).includes("foo")
|
crossfilter.annoMatrix.getMatrixColumns("obs").includes("foo")
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
const xfltr = crossfilter.addObsColumn(
|
const xfltr = crossfilter.addObsColumn(
|
||||||
{ name: "foo", type: "categorical", categories: ["A"] },
|
{ name: "foo", type: "categorical", categories: ["A"] },
|
||||||
@@ -346,7 +306,7 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
// check schema updates correctly.
|
// check schema updates correctly.
|
||||||
expect(xfltr.countSelected()).toBe(annoMatrix.nObs);
|
expect(xfltr.countSelected()).toBe(annoMatrix.nObs);
|
||||||
expect(
|
expect(
|
||||||
xfltr.annoMatrix.getMatrixColumns(Field.obs).includes("foo")
|
xfltr.annoMatrix.getMatrixColumns("obs").includes("foo")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toMatchObject({
|
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toMatchObject({
|
||||||
name: "foo",
|
name: "foo",
|
||||||
@@ -354,18 +314,17 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(v) => v.name === "foo"
|
||||||
(v: any) => v.name === "foo"
|
|
||||||
)
|
)
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
|
|
||||||
// check data update.
|
// check data update.
|
||||||
const df: Dataframe = await xfltr.annoMatrix.fetch(Field.obs, "foo");
|
const df = await xfltr.annoMatrix.fetch("obs", "foo");
|
||||||
expect(
|
expect(
|
||||||
df
|
df
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.every((v: any) => v === "A")
|
.every((v) => v === "A")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
// check that we catch dups
|
// check that we catch dups
|
||||||
@@ -402,30 +361,27 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
xfltr = xfltr.dropObsColumn("foo");
|
xfltr = xfltr.dropObsColumn("foo");
|
||||||
expect(
|
expect(
|
||||||
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(v) => v.name === "foo"
|
||||||
(v: any) => v.name === "foo"
|
|
||||||
)
|
)
|
||||||
).toHaveLength(0);
|
).toHaveLength(0);
|
||||||
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toBeUndefined();
|
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toBeUndefined();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
|
||||||
await expect(xfltr.annoMatrix.fetch(Field.obs, "foo")).rejects.toThrow(
|
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
|
|
||||||
// now same, but ensure we have built an index before doing the drop
|
// now same, but ensure we have built an index before doing the drop
|
||||||
xfltr = await helperAddTestCol(crossfilter, "bar");
|
xfltr = await helperAddTestCol(crossfilter, "bar");
|
||||||
xfltr = await xfltr.select(Field.obs, "bar", {
|
xfltr = await xfltr.select("obs", "bar", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "whatever",
|
values: "whatever",
|
||||||
});
|
});
|
||||||
xfltr = xfltr.dropObsColumn("bar");
|
xfltr = xfltr.dropObsColumn("bar");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
|
||||||
await expect(
|
"unknown column name"
|
||||||
xfltr.select(Field.obs, "bar", { mode: "all" })
|
);
|
||||||
).rejects.toThrow("unknown column name");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renameObsColumn", async () => {
|
test("renameObsColumn", async () => {
|
||||||
@@ -442,43 +398,38 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
// add a column, then rename it.
|
// add a column, then rename it.
|
||||||
xfltr = await helperAddTestCol(crossfilter, "foo");
|
xfltr = await helperAddTestCol(crossfilter, "foo");
|
||||||
xfltr = xfltr.renameObsColumn("foo", "bar");
|
xfltr = xfltr.renameObsColumn("foo", "bar");
|
||||||
expect(
|
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toBeUndefined();
|
||||||
xfltr.annoMatrix.getColumnSchema(Field.obs, "foo")
|
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
|
||||||
).toBeUndefined();
|
|
||||||
expect(xfltr.annoMatrix.getColumnSchema(Field.obs, "bar")).toMatchObject({
|
|
||||||
name: "bar",
|
name: "bar",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
|
||||||
await expect(xfltr.annoMatrix.fetch(Field.obs, "foo")).rejects.toThrow(
|
|
||||||
"unknown column name"
|
"unknown column name"
|
||||||
);
|
);
|
||||||
const df = await xfltr.annoMatrix.fetch(Field.obs, "bar");
|
const df = await xfltr.annoMatrix.fetch("obs", "bar");
|
||||||
expect(df.hasCol("bar")).toBeTruthy();
|
expect(df.hasCol("bar")).toBeTruthy();
|
||||||
|
|
||||||
// now same, but ensure we have built an index before doing the rename
|
// now same, but ensure we have built an index before doing the rename
|
||||||
xfltr = await helperAddTestCol(crossfilter, "bar");
|
xfltr = await helperAddTestCol(crossfilter, "bar");
|
||||||
xfltr = await xfltr.select(Field.obs, "bar", {
|
xfltr = await xfltr.select("obs", "bar", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "whatever",
|
values: "whatever",
|
||||||
});
|
});
|
||||||
xfltr = xfltr.renameObsColumn("bar", "xyz");
|
xfltr = xfltr.renameObsColumn("bar", "xyz");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.mockRejectOnce(new Error("unknown column name"));
|
||||||
(fetch as any).mockRejectOnce(new Error("unknown column name"));
|
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
|
||||||
|
"unknown column name"
|
||||||
|
);
|
||||||
await expect(
|
await expect(
|
||||||
xfltr.select(Field.obs, "bar", { mode: "all" })
|
xfltr.select("obs", "xyz", { mode: "none" })
|
||||||
).rejects.toThrow("unknown column name");
|
|
||||||
await expect(
|
|
||||||
xfltr.select(Field.obs, "xyz", { mode: "none" })
|
|
||||||
).resolves.toBeInstanceOf(AnnoMatrixObsCrossfilter);
|
).resolves.toBeInstanceOf(AnnoMatrixObsCrossfilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("addObsAnnoCategory", async () => {
|
test("addObsAnnoCategory", async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let xfltr;
|
||||||
let xfltr: any;
|
|
||||||
|
|
||||||
// catch unknown or readonly columns
|
// catch unknown or readonly columns
|
||||||
expect(() => crossfilter.addObsAnnoCategory("louvain", "mumble")).toThrow(
|
expect(() => crossfilter.addObsAnnoCategory("louvain", "mumble")).toThrow(
|
||||||
@@ -489,14 +440,13 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
).toThrow("Unknown or readonly obs column");
|
).toThrow("Unknown or readonly obs column");
|
||||||
|
|
||||||
// add a column and then add category to it
|
// add a column and then add category to it
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: string; type: string; ca... Remove this comment to see the full error message
|
|
||||||
xfltr = await helperAddTestCol(crossfilter, "foo", {
|
xfltr = await helperAddTestCol(crossfilter, "foo", {
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["unassigned"],
|
categories: ["unassigned"],
|
||||||
});
|
});
|
||||||
xfltr = xfltr.addObsAnnoCategory("foo", "a-new-label");
|
xfltr = xfltr.addObsAnnoCategory("foo", "a-new-label");
|
||||||
expect(xfltr.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject({
|
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
|
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
|
||||||
@@ -508,18 +458,17 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// now same, but ensure we have built an index before doing the operation
|
// now same, but ensure we have built an index before doing the operation
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: string; type: string; ca... Remove this comment to see the full error message
|
|
||||||
xfltr = await helperAddTestCol(crossfilter, "bar", {
|
xfltr = await helperAddTestCol(crossfilter, "bar", {
|
||||||
name: "bar",
|
name: "bar",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["unassigned"],
|
categories: ["unassigned"],
|
||||||
});
|
});
|
||||||
xfltr = await xfltr.select(Field.obs, "bar", {
|
xfltr = await xfltr.select("obs", "bar", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "something",
|
values: "something",
|
||||||
});
|
});
|
||||||
xfltr = xfltr.addObsAnnoCategory("bar", "a-new-label");
|
xfltr = xfltr.addObsAnnoCategory("bar", "a-new-label");
|
||||||
expect(xfltr.annoMatrix.getColumnSchema(Field.obs, "bar")).toMatchObject({
|
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
|
||||||
name: "bar",
|
name: "bar",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
|
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
|
||||||
@@ -537,20 +486,19 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
crossfilter.removeObsAnnoCategory("undefined-name", "mumble")
|
crossfilter.removeObsAnnoCategory("undefined-name", "mumble")
|
||||||
).rejects.toThrow("Unknown or readonly obs column");
|
).rejects.toThrow("Unknown or readonly obs column");
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: string; type: string; ca... Remove this comment to see the full error message
|
|
||||||
xfltr = await helperAddTestCol(crossfilter, "foo", {
|
xfltr = await helperAddTestCol(crossfilter, "foo", {
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["unassigned", "red", "green", "blue"],
|
categories: ["unassigned", "red", "green", "blue"],
|
||||||
});
|
});
|
||||||
xfltr = await xfltr.select(Field.obs, "foo", { mode: "all" });
|
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
|
||||||
expect(
|
expect(
|
||||||
(await xfltr.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.every((v: any) => v === "unassigned")
|
.every((v) => v === "unassigned")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(xfltr.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject({
|
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: expect.arrayContaining([
|
categories: expect.arrayContaining([
|
||||||
@@ -564,23 +512,21 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
// remove an unused category
|
// remove an unused category
|
||||||
const xfltr1 = await xfltr.removeObsAnnoCategory("foo", "red", "mumble");
|
const xfltr1 = await xfltr.removeObsAnnoCategory("foo", "red", "mumble");
|
||||||
expect(
|
expect(
|
||||||
(await xfltr1.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr1.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.every((v: any) => v === "unassigned")
|
.every((v) => v === "unassigned")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(xfltr1.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject(
|
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
{
|
name: "foo",
|
||||||
name: "foo",
|
type: "categorical",
|
||||||
type: "categorical",
|
categories: expect.arrayContaining([
|
||||||
categories: expect.arrayContaining([
|
"unassigned",
|
||||||
"unassigned",
|
"green",
|
||||||
"green",
|
"blue",
|
||||||
"blue",
|
"mumble",
|
||||||
"mumble",
|
]),
|
||||||
]),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// remove a used category
|
// remove a used category
|
||||||
const xfltr2 = await xfltr.removeObsAnnoCategory(
|
const xfltr2 = await xfltr.removeObsAnnoCategory(
|
||||||
@@ -589,18 +535,16 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
"red"
|
"red"
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
(await xfltr2.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr2.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.every((v: any) => v === "red")
|
.every((v) => v === "red")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(xfltr2.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject(
|
expect(xfltr2.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
{
|
name: "foo",
|
||||||
name: "foo",
|
type: "categorical",
|
||||||
type: "categorical",
|
categories: expect.arrayContaining(["green", "blue", "red"]),
|
||||||
categories: expect.arrayContaining(["green", "blue", "red"]),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("setObsColumnValues", async () => {
|
test("setObsColumnValues", async () => {
|
||||||
@@ -612,13 +556,12 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
crossfilter.setObsColumnValues("undefined-name", [0], "mumble")
|
crossfilter.setObsColumnValues("undefined-name", [0], "mumble")
|
||||||
).rejects.toThrow("Unknown or readonly obs column");
|
).rejects.toThrow("Unknown or readonly obs column");
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: string; type: string; ca... Remove this comment to see the full error message
|
|
||||||
let xfltr = await helperAddTestCol(crossfilter, "foo", {
|
let xfltr = await helperAddTestCol(crossfilter, "foo", {
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["unassigned", "red", "green", "blue"],
|
categories: ["unassigned", "red", "green", "blue"],
|
||||||
});
|
});
|
||||||
xfltr = await xfltr.select(Field.obs, "foo", { mode: "all" });
|
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
|
||||||
|
|
||||||
// catch unknown row label
|
// catch unknown row label
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
@@ -627,43 +570,40 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
|
|
||||||
// set a few rows
|
// set a few rows
|
||||||
expect(
|
expect(
|
||||||
(await xfltr.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.every((v: any) => v === "unassigned")
|
.every((v) => v === "unassigned")
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
const xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
|
const xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
|
||||||
expect(
|
expect(
|
||||||
(await xfltr1.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr1.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray()
|
.asArray()
|
||||||
.every(
|
.every(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(v, i) =>
|
||||||
(v: any, i: any) =>
|
|
||||||
v === "unassigned" || (v === "purple" && (i === 0 || i === 10))
|
v === "unassigned" || (v === "purple" && (i === 0 || i === 10))
|
||||||
)
|
)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(xfltr1.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject(
|
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
{
|
name: "foo",
|
||||||
name: "foo",
|
type: "categorical",
|
||||||
type: "categorical",
|
categories: expect.arrayContaining([
|
||||||
categories: expect.arrayContaining([
|
"unassigned",
|
||||||
"unassigned",
|
"red",
|
||||||
"red",
|
"green",
|
||||||
"green",
|
"blue",
|
||||||
"blue",
|
"purple",
|
||||||
"purple",
|
]),
|
||||||
]),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(xfltr1.countSelected()).toEqual(xfltr1.annoMatrix.nObs);
|
expect(xfltr1.countSelected()).toEqual(xfltr1.annoMatrix.nObs);
|
||||||
const xfltr2 = await xfltr1.select(Field.obs, "foo", {
|
const xfltr2 = await xfltr1.select("obs", "foo", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: ["purple"],
|
values: ["purple"],
|
||||||
});
|
});
|
||||||
expect(xfltr2.countSelected()).toEqual(2);
|
expect(xfltr2.countSelected()).toEqual(2);
|
||||||
expect(xfltr2.allSelectedLabels()).toEqual(Array.from([0, 10]));
|
expect(xfltr2.allSelectedLabels()).toEqual(Int32Array.from([0, 10]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resetObsColumnValues", async () => {
|
test("resetObsColumnValues", async () => {
|
||||||
@@ -675,13 +615,12 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
crossfilter.resetObsColumnValues("undefined-name", "red", "blue")
|
crossfilter.resetObsColumnValues("undefined-name", "red", "blue")
|
||||||
).rejects.toThrow("Unknown or readonly obs column");
|
).rejects.toThrow("Unknown or readonly obs column");
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: string; type: string; ca... Remove this comment to see the full error message
|
|
||||||
let xfltr = await helperAddTestCol(crossfilter, "foo", {
|
let xfltr = await helperAddTestCol(crossfilter, "foo", {
|
||||||
name: "foo",
|
name: "foo",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: ["unassigned", "red", "green", "blue"],
|
categories: ["unassigned", "red", "green", "blue"],
|
||||||
});
|
});
|
||||||
xfltr = await xfltr.select(Field.obs, "foo", {
|
xfltr = await xfltr.select("obs", "foo", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "red",
|
values: "red",
|
||||||
});
|
});
|
||||||
@@ -692,60 +631,54 @@ describe("AnnoMatrixCrossfilter", () => {
|
|||||||
).rejects.toThrow("unknown category");
|
).rejects.toThrow("unknown category");
|
||||||
|
|
||||||
let xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
|
let xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
|
||||||
xfltr1 = await xfltr1.select(Field.obs, "foo", {
|
xfltr1 = await xfltr1.select("obs", "foo", {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "purple",
|
values: "purple",
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
(await xfltr1.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr1.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.filter((v: any) => v === "purple")
|
.filter((v) => v === "purple")
|
||||||
).toHaveLength(2);
|
).toHaveLength(2);
|
||||||
|
|
||||||
xfltr1 = await xfltr1.resetObsColumnValues("foo", "purple", "magenta");
|
xfltr1 = await xfltr1.resetObsColumnValues("foo", "purple", "magenta");
|
||||||
expect(
|
expect(
|
||||||
(await xfltr1.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr1.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.filter((v: any) => v === "magenta")
|
.filter((v) => v === "magenta")
|
||||||
).toHaveLength(2);
|
).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
(await xfltr1.annoMatrix.fetch(Field.obs, "foo"))
|
(await xfltr1.annoMatrix.fetch("obs", "foo"))
|
||||||
.col("foo")
|
.col("foo")
|
||||||
.asArray() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.asArray()
|
||||||
.filter((v: any) => v === "purple")
|
.filter((v) => v === "purple")
|
||||||
).toHaveLength(0);
|
).toHaveLength(0);
|
||||||
expect(xfltr1.annoMatrix.getColumnSchema(Field.obs, "foo")).toMatchObject(
|
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
|
||||||
{
|
name: "foo",
|
||||||
name: "foo",
|
type: "categorical",
|
||||||
type: "categorical",
|
categories: expect.arrayContaining([
|
||||||
categories: expect.arrayContaining([
|
"unassigned",
|
||||||
"unassigned",
|
"red",
|
||||||
"red",
|
"green",
|
||||||
"green",
|
"blue",
|
||||||
"blue",
|
"purple",
|
||||||
"purple",
|
"magenta",
|
||||||
"magenta",
|
]),
|
||||||
]),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
test("transition from empty annoMatrix", async () => {
|
test("transition from empty annoMatrix", async () => {
|
||||||
// select before fetch needs to work
|
// select before fetch needs to work
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
|
||||||
(fetch as any).once(
|
const xfltr = await crossfilter.select("obs", "louvain", {
|
||||||
serverMocks.dataframeResponse(["louvain"], [obsLouvain])
|
|
||||||
);
|
|
||||||
const xfltr = await crossfilter.select(Field.obs, "louvain", {
|
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values: "B cells",
|
values: "B cells",
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
expect(fetch.mock.calls).toHaveLength(1);
|
||||||
expect((fetch as any).mock.calls).toHaveLength(1);
|
|
||||||
expect(xfltr.obsCrossfilter.hasDimension("obs/louvain")).toBeTruthy();
|
expect(xfltr.obsCrossfilter.hasDimension("obs/louvain")).toBeTruthy();
|
||||||
expect(xfltr.obsCrossfilter.all()).toBe(xfltr.annoMatrix._cache.obs);
|
expect(xfltr.obsCrossfilter.all()).toBe(xfltr.annoMatrix._cache.obs);
|
||||||
expect(xfltr.countSelected()).toEqual(
|
expect(xfltr.countSelected()).toEqual(
|
||||||
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
export const baseDataURL = "https://a.fake.url/api/v0.2";
|
export const baseDataURL = "https://a.fake.url/api/v0.2";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
window.CELLXGENE = {
|
||||||
(window as any).CELLXGENE = {
|
|
||||||
API: {
|
API: {
|
||||||
prefix: baseDataURL,
|
prefix: baseDataURL,
|
||||||
version: "v0.2/",
|
version: "v0.2/",
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { schema } from "./schema";
|
||||||
|
import { Dataframe, KeyIndex } from "../../../../src/util/dataframe";
|
||||||
|
import { encodeMatrixFBS } from "../../../../src/util/stateManager/matrix";
|
||||||
|
|
||||||
|
const indexedSchema = {
|
||||||
|
obsByName: Object.fromEntries(
|
||||||
|
schema.schema.annotations.obs.columns.map((v) => [v.name, v]) ?? []
|
||||||
|
),
|
||||||
|
varByName: Object.fromEntries(
|
||||||
|
schema.schema.annotations.var.columns.map((v) => [v.name, v]) ?? []
|
||||||
|
),
|
||||||
|
embByName: Object.fromEntries(
|
||||||
|
schema.schema.layout.obs.map((v) => [v.name, v]) ?? []
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeMockColumn(s, length) {
|
||||||
|
const { type } = s;
|
||||||
|
switch (type) {
|
||||||
|
case "int32":
|
||||||
|
return new Int32Array(length).fill(Math.floor(99 * Math.random()));
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
return new Array(length).fill("test");
|
||||||
|
|
||||||
|
case "float32":
|
||||||
|
return new Float32Array(length).fill(99 * Math.random());
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
return new Array(length).fill(false);
|
||||||
|
|
||||||
|
case "categorical":
|
||||||
|
return new Array(length).fill(s.categories[0]);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("unkonwn type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEncodedDataframe(colNames, length, colSchemas) {
|
||||||
|
const colIndex = new KeyIndex(colNames);
|
||||||
|
const columns = colSchemas.map((s) => makeMockColumn(s, length));
|
||||||
|
const df = new Dataframe([length, colNames.length], columns, null, colIndex);
|
||||||
|
const body = encodeMatrixFBS(df);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataframeResponse(colNames, columns) {
|
||||||
|
const colIndex = new KeyIndex(colNames);
|
||||||
|
const df = new Dataframe(
|
||||||
|
[columns[0].length, colNames.length],
|
||||||
|
columns,
|
||||||
|
null,
|
||||||
|
colIndex
|
||||||
|
);
|
||||||
|
const body = encodeMatrixFBS(df);
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
});
|
||||||
|
return () => Promise.resolve({ body, init: { status: 200, headers } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotationObsResponse(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const params = Array.from(url.searchParams.entries());
|
||||||
|
const names = params
|
||||||
|
.filter(([k]) => k === "annotation-name")
|
||||||
|
.map(([, v]) => v);
|
||||||
|
if (!names.every((n) => indexedSchema.obsByName[n])) {
|
||||||
|
return Promise.reject(new Error("bad obs annotation name in URL"));
|
||||||
|
}
|
||||||
|
const colSchemas = names.map((n) => indexedSchema.obsByName[n]);
|
||||||
|
const body = getEncodedDataframe(
|
||||||
|
names,
|
||||||
|
schema.schema.dataframe.nObs,
|
||||||
|
colSchemas
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
body,
|
||||||
|
init: { status: 200, headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotationVarResponse(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const params = Array.from(url.searchParams.entries());
|
||||||
|
const names = params
|
||||||
|
.filter(([k]) => k === "annotation-name")
|
||||||
|
.map(([, v]) => v);
|
||||||
|
if (!names.every((n) => indexedSchema.varByName[n])) {
|
||||||
|
return Promise.reject(new Error("bad var annotation name in URL"));
|
||||||
|
}
|
||||||
|
const colSchemas = names.map((n) => indexedSchema.varByName[n]);
|
||||||
|
const body = getEncodedDataframe(
|
||||||
|
names,
|
||||||
|
schema.schema.dataframe.nVar,
|
||||||
|
colSchemas
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
body,
|
||||||
|
init: { status: 200, headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutObsResponse(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const params = Array.from(url.searchParams.entries());
|
||||||
|
const names = params.filter(([k]) => k === "layout-name").map(([, v]) => v);
|
||||||
|
if (!names.every((n) => indexedSchema.embByName[n])) {
|
||||||
|
return Promise.reject(new Error("bad layout name in URL"));
|
||||||
|
}
|
||||||
|
const dims = names.map((n) => indexedSchema.embByName[n].dims).flat();
|
||||||
|
const colSchemas = names
|
||||||
|
.map((n) => [indexedSchema.embByName[n], indexedSchema.embByName[n]])
|
||||||
|
.flat();
|
||||||
|
const body = getEncodedDataframe(
|
||||||
|
dims,
|
||||||
|
schema.schema.dataframe.nObs,
|
||||||
|
colSchemas
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
body,
|
||||||
|
init: { status: 200, headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataVarResponse(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const params = Array.from(url.searchParams.entries());
|
||||||
|
|
||||||
|
const colNames = params.map((v) => `${v[0]}/${v[1]}`);
|
||||||
|
const colSchemas = colNames.map(() => schema.schema.dataframe);
|
||||||
|
const body = getEncodedDataframe(
|
||||||
|
colNames,
|
||||||
|
schema.schema.dataframe.nObs,
|
||||||
|
colSchemas
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
body,
|
||||||
|
init: { status: 200, headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function responder(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const { pathname } = url;
|
||||||
|
if (pathname.endsWith("/annotations/obs")) {
|
||||||
|
return annotationObsResponse(request);
|
||||||
|
}
|
||||||
|
if (pathname.endsWith("/annotations/var")) {
|
||||||
|
return annotationVarResponse(request);
|
||||||
|
}
|
||||||
|
if (pathname.endsWith("/layout/obs")) {
|
||||||
|
return layoutObsResponse(request);
|
||||||
|
}
|
||||||
|
if (pathname.endsWith("/data/var")) {
|
||||||
|
return dataVarResponse(request);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("bad URL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withExpected(expectedURL, expectedParams) {
|
||||||
|
/*
|
||||||
|
Do some additional error checking
|
||||||
|
*/
|
||||||
|
return (request) => {
|
||||||
|
// if URL is bogus, reject the promise
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (!url.pathname.endsWith(expectedURL)) {
|
||||||
|
return Promise.reject(new Error("Unexpected URL!"));
|
||||||
|
}
|
||||||
|
const params = Array.from(url.searchParams.entries()).sort(
|
||||||
|
(a, b) => a[0] < b[0]
|
||||||
|
);
|
||||||
|
expectedParams = expectedParams.slice().sort((a, b) => a[0] < b[0]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
params.length !== expectedParams.length ||
|
||||||
|
!params.every(
|
||||||
|
(p, i) => p[0] === expectedParams[i][0] && p[1] === expectedParams[i][1]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error("unexpected name requested in URL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responder(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function annotationsObs(names) {
|
||||||
|
return withExpected(
|
||||||
|
"/annotations/obs",
|
||||||
|
names.map((name) => ["annotation-name", name])
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
import { schema } from "./schema";
|
|
||||||
import { Dataframe, KeyIndex } from "../../../../src/util/dataframe";
|
|
||||||
import { encodeMatrixFBS } from "../../../../src/util/stateManager/matrix";
|
|
||||||
|
|
||||||
const indexedSchema = {
|
|
||||||
obsByName: Object.fromEntries(
|
|
||||||
schema.schema.annotations.obs.columns.map((v) => [v.name, v]) ?? []
|
|
||||||
),
|
|
||||||
varByName: Object.fromEntries(
|
|
||||||
schema.schema.annotations.var.columns.map((v) => [v.name, v]) ?? []
|
|
||||||
),
|
|
||||||
embByName: Object.fromEntries(
|
|
||||||
schema.schema.layout.obs.map((v) => [v.name, v]) ?? []
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function makeMockColumn(s: any, length: any) {
|
|
||||||
const { type } = s;
|
|
||||||
switch (type) {
|
|
||||||
case "int32":
|
|
||||||
return new Int32Array(length).fill(Math.floor(99 * Math.random()));
|
|
||||||
|
|
||||||
case "string":
|
|
||||||
return new Array(length).fill("test");
|
|
||||||
|
|
||||||
case "float32":
|
|
||||||
return new Float32Array(length).fill(99 * Math.random());
|
|
||||||
|
|
||||||
case "boolean":
|
|
||||||
return new Array(length).fill(false);
|
|
||||||
|
|
||||||
case "categorical":
|
|
||||||
return new Array(length).fill(s.categories[0]);
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error("unknown type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function getEncodedDataframe(colNames: any, length: any, colSchemas: any) {
|
|
||||||
const colIndex = new KeyIndex(colNames);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const columns = colSchemas.map((s: any) => makeMockColumn(s, length));
|
|
||||||
const df = new Dataframe([length, colNames.length], columns, null, colIndex);
|
|
||||||
const body = encodeMatrixFBS(df);
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function dataframeResponse(colNames: any, columns: any) {
|
|
||||||
const colIndex = new KeyIndex(colNames);
|
|
||||||
const df = new Dataframe(
|
|
||||||
[columns[0].length, colNames.length],
|
|
||||||
columns,
|
|
||||||
null,
|
|
||||||
colIndex
|
|
||||||
);
|
|
||||||
const body = encodeMatrixFBS(df);
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
});
|
|
||||||
return () => Promise.resolve({ body, init: { status: 200, headers } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function annotationObsResponse(request: any) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const params = Array.from((url.searchParams as any).entries());
|
|
||||||
const names = params
|
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
.filter(([k]) => k === "annotation-name")
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '([, v]: [any, any]) => any' is n... Remove this comment to see the full error message
|
|
||||||
.map(([, v]) => v);
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
if (!names.every((n) => indexedSchema.obsByName[n])) {
|
|
||||||
return Promise.reject(new Error("bad obs annotation name in URL"));
|
|
||||||
}
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
const colSchemas = names.map((n) => indexedSchema.obsByName[n]);
|
|
||||||
const body = getEncodedDataframe(
|
|
||||||
names,
|
|
||||||
schema.schema.dataframe.nObs,
|
|
||||||
colSchemas
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
body,
|
|
||||||
init: { status: 200, headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function annotationVarResponse(request: any) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const params = Array.from((url.searchParams as any).entries());
|
|
||||||
const names = params
|
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
.filter(([k]) => k === "annotation-name")
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '([, v]: [any, any]) => any' is n... Remove this comment to see the full error message
|
|
||||||
.map(([, v]) => v);
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
if (!names.every((n) => indexedSchema.varByName[n])) {
|
|
||||||
return Promise.reject(new Error("bad var annotation name in URL"));
|
|
||||||
}
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
const colSchemas = names.map((n) => indexedSchema.varByName[n]);
|
|
||||||
const body = getEncodedDataframe(
|
|
||||||
names,
|
|
||||||
schema.schema.dataframe.nVar,
|
|
||||||
colSchemas
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
body,
|
|
||||||
init: { status: 200, headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function layoutObsResponse(request: any) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const params = Array.from((url.searchParams as any).entries());
|
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
const names = params.filter(([k]) => k === "layout-name").map(([, v]) => v);
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
if (!names.every((n) => indexedSchema.embByName[n])) {
|
|
||||||
return Promise.reject(new Error("bad layout name in URL"));
|
|
||||||
}
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
const dims = names.map((n) => indexedSchema.embByName[n].dims).flat();
|
|
||||||
const colSchemas = names
|
|
||||||
// @ts-expect-error ts-migrate(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
.map((n) => [indexedSchema.embByName[n], indexedSchema.embByName[n]])
|
|
||||||
.flat();
|
|
||||||
const body = getEncodedDataframe(
|
|
||||||
dims,
|
|
||||||
schema.schema.dataframe.nObs,
|
|
||||||
colSchemas
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
body,
|
|
||||||
init: { status: 200, headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function dataVarResponse(request: any) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const params = Array.from((url.searchParams as any).entries());
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const colNames = params.map((v) => `${(v as any)[0]}/${(v as any)[1]}`);
|
|
||||||
const colSchemas = colNames.map(() => schema.schema.dataframe);
|
|
||||||
const body = getEncodedDataframe(
|
|
||||||
colNames,
|
|
||||||
schema.schema.dataframe.nObs,
|
|
||||||
colSchemas
|
|
||||||
);
|
|
||||||
|
|
||||||
const headers = new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
});
|
|
||||||
return Promise.resolve({
|
|
||||||
body,
|
|
||||||
init: { status: 200, headers },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function responder(request: any) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const { pathname } = url;
|
|
||||||
if (pathname.endsWith("/annotations/obs")) {
|
|
||||||
return annotationObsResponse(request);
|
|
||||||
}
|
|
||||||
if (pathname.endsWith("/annotations/var")) {
|
|
||||||
return annotationVarResponse(request);
|
|
||||||
}
|
|
||||||
if (pathname.endsWith("/layout/obs")) {
|
|
||||||
return layoutObsResponse(request);
|
|
||||||
}
|
|
||||||
if (pathname.endsWith("/data/var")) {
|
|
||||||
return dataVarResponse(request);
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error("bad URL"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function withExpected(expectedURL: any, expectedParams: any) {
|
|
||||||
/*
|
|
||||||
Do some additional error checking
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
return (request: any) => {
|
|
||||||
// if URL is bogus, reject the promise
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (!url.pathname.endsWith(expectedURL)) {
|
|
||||||
return Promise.reject(new Error("Unexpected URL!"));
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const params = Array.from((url.searchParams as any).entries()).sort(
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(a: unknown, b: unknown) => bool... Remove this comment to see the full error message
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(a, b) => (a as any)[0] < (b as any)[0]
|
|
||||||
);
|
|
||||||
expectedParams = expectedParams
|
|
||||||
.slice() // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
.sort((a: any, b: any) => a[0] < b[0]);
|
|
||||||
|
|
||||||
if (
|
|
||||||
params.length !== expectedParams.length ||
|
|
||||||
!params.every(
|
|
||||||
(p, i) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(p as any)[0] === expectedParams[i][0] && // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(p as any)[1] === expectedParams[i][1]
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return Promise.reject(new Error("unexpected name requested in URL"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return responder(request);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function annotationsObs(names: any) {
|
|
||||||
return withExpected(
|
|
||||||
"/annotations/obs",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
names.map((name: any) => ["annotation-name", name])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+1
-3
@@ -1,6 +1,4 @@
|
|||||||
import { RawSchema } from "../../../../src/common/types/schema";
|
export const schema = {
|
||||||
|
|
||||||
export const schema: { schema: RawSchema } = {
|
|
||||||
schema: {
|
schema: {
|
||||||
annotations: {
|
annotations: {
|
||||||
obs: {
|
obs: {
|
||||||
+1691
-5275
File diff suppressed because it is too large
Load Diff
+60
-83
@@ -4,34 +4,13 @@ import {
|
|||||||
_whereCacheCreate,
|
_whereCacheCreate,
|
||||||
_whereCacheMerge,
|
_whereCacheMerge,
|
||||||
} from "../../../src/annoMatrix/whereCache";
|
} from "../../../src/annoMatrix/whereCache";
|
||||||
import { Field, Schema } from "../../../src/common/types/schema";
|
|
||||||
import { Query } from "../../../src/annoMatrix/query";
|
|
||||||
|
|
||||||
const schema = {} as Schema;
|
const schema = {};
|
||||||
|
|
||||||
describe("whereCache", () => {
|
describe("whereCache", () => {
|
||||||
test("whereCacheGet - where query, missing cache values", () => {
|
test("whereCacheGet - where query, missing cache values", () => {
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet({}, schema, Field.X, {
|
_whereCacheGet({}, schema, "X", {
|
||||||
where: {
|
|
||||||
field: Field.var,
|
|
||||||
column: "foo",
|
|
||||||
value: "bar",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).toEqual([undefined]);
|
|
||||||
expect(
|
|
||||||
_whereCacheGet({}, schema, Field.X, {
|
|
||||||
summarize: {
|
|
||||||
method: "mean",
|
|
||||||
field: Field.var,
|
|
||||||
column: "foo",
|
|
||||||
values: ["bar"],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).toEqual([undefined]);
|
|
||||||
expect(
|
|
||||||
_whereCacheGet({ where: { X: {} } }, schema, Field.X, {
|
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "foo",
|
column: "foo",
|
||||||
@@ -40,7 +19,25 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([undefined]);
|
).toEqual([undefined]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet({ where: { X: { var: new Map() } } }, schema, Field.X, {
|
_whereCacheGet({}, schema, "X", {
|
||||||
|
summarize: {
|
||||||
|
field: "var",
|
||||||
|
column: "foo",
|
||||||
|
values: ["bar"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual([undefined]);
|
||||||
|
expect(
|
||||||
|
_whereCacheGet({ where: { X: {} } }, schema, "X", {
|
||||||
|
where: {
|
||||||
|
field: "var",
|
||||||
|
column: "foo",
|
||||||
|
value: "bar",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual([undefined]);
|
||||||
|
expect(
|
||||||
|
_whereCacheGet({ where: { X: { var: new Map() } } }, schema, "X", {
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "foo",
|
column: "foo",
|
||||||
@@ -52,7 +49,7 @@ describe("whereCache", () => {
|
|||||||
_whereCacheGet(
|
_whereCacheGet(
|
||||||
{ where: { X: { var: new Map([["foo", new Map()]]) } } },
|
{ where: { X: { var: new Map([["foo", new Map()]]) } } },
|
||||||
schema,
|
schema,
|
||||||
Field.X,
|
"X",
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
@@ -66,7 +63,7 @@ describe("whereCache", () => {
|
|||||||
|
|
||||||
test("whereCacheGet - summarize query, missing cache values", () => {
|
test("whereCacheGet - summarize query, missing cache values", () => {
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet({}, schema, Field.X, {
|
_whereCacheGet({}, schema, "X", {
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
field: "var",
|
field: "var",
|
||||||
@@ -79,7 +76,7 @@ describe("whereCache", () => {
|
|||||||
_whereCacheGet(
|
_whereCacheGet(
|
||||||
{ summarize: { X: { mean: { var: new Map() } } } },
|
{ summarize: { X: { mean: { var: new Map() } } } },
|
||||||
schema,
|
schema,
|
||||||
Field.X,
|
"X",
|
||||||
{
|
{
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
@@ -125,7 +122,7 @@ describe("whereCache", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "foo",
|
column: "foo",
|
||||||
@@ -134,7 +131,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([0]);
|
).toEqual([0]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
field: "var",
|
field: "var",
|
||||||
@@ -144,7 +141,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([0]);
|
).toEqual([0]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "foo",
|
column: "foo",
|
||||||
@@ -153,7 +150,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([1, 2]);
|
).toEqual([1, 2]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
field: "var",
|
field: "var",
|
||||||
@@ -162,12 +159,9 @@ describe("whereCache", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
).toEqual([1, 2]);
|
).toEqual([1, 2]);
|
||||||
|
expect(_whereCacheGet(whereCache, schema, "Y", {})).toEqual([undefined]);
|
||||||
expect(
|
expect(
|
||||||
// Force invalid field value Y
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
_whereCacheGet(whereCache, schema, "Y" as Field, {} as Query)
|
|
||||||
).toEqual([undefined]);
|
|
||||||
expect(
|
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
|
||||||
where: {
|
where: {
|
||||||
field: "whoknows",
|
field: "whoknows",
|
||||||
column: "whatever",
|
column: "whatever",
|
||||||
@@ -176,7 +170,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([undefined]);
|
).toEqual([undefined]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "whatever",
|
column: "whatever",
|
||||||
@@ -185,7 +179,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual([undefined]);
|
).toEqual([undefined]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(whereCache, schema, Field.X, {
|
_whereCacheGet(whereCache, schema, "X", {
|
||||||
where: {
|
where: {
|
||||||
field: "var",
|
field: "var",
|
||||||
column: "foo",
|
column: "foo",
|
||||||
@@ -204,7 +198,7 @@ describe("whereCache", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const wc = _whereCacheCreate(
|
const wc = _whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
@@ -218,27 +212,18 @@ describe("whereCache", () => {
|
|||||||
expect(wc).toEqual(
|
expect(wc).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
where: {
|
where: {
|
||||||
[Field.obs]: {
|
field: {
|
||||||
queryField: expect.any(Map),
|
queryField: expect.any(Map),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
expect(wc.where.field.queryField.has("queryColumn")).toEqual(true);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
expect(wc.where.field.queryField.get("queryColumn")).toBeInstanceOf(Map);
|
||||||
expect((wc.where as any)[Field.obs].queryField.has("queryColumn")).toEqual(true);
|
|
||||||
expect(
|
expect(
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
wc.where.field.queryField.get("queryColumn").has("queryValue")
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(wc.where as any)[Field.obs].queryField.get("queryColumn")
|
|
||||||
).toBeInstanceOf(Map);
|
|
||||||
expect(
|
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(wc.where as any)[Field.obs].queryField.get("queryColumn").has("queryValue")
|
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert wc to be non-null
|
expect(_whereCacheGet(wc, schema, "field", query)).toEqual([0, 1, 2]);
|
||||||
expect(_whereCacheGet(wc!, schema, Field.obs, query)).toEqual([0, 1, 2]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("whereCacheCreate, summarize query", () => {
|
test("whereCacheCreate, summarize query", () => {
|
||||||
@@ -250,14 +235,12 @@ describe("whereCache", () => {
|
|||||||
values: ["queryValue"],
|
values: ["queryValue"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const wc = _whereCacheCreate(Field.obs, query, [0, 1, 2]);
|
const wc = _whereCacheCreate("field", query, [0, 1, 2]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert wc to be non-null
|
expect(_whereCacheGet(wc, schema, "field", query)).toEqual([0, 1, 2]);
|
||||||
expect(_whereCacheGet(wc!, schema, Field.obs, query)).toEqual([0, 1, 2]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("whereCacheCreate, unknown query type", () => {
|
test("whereCacheCreate, unknown query type", () => {
|
||||||
// @ts-expect-error --- force invalid query {foobar: true}
|
expect(_whereCacheCreate("field", { foobar: true }, [1])).toEqual({});
|
||||||
expect(_whereCacheCreate(Field.obs, { foobar: true }, [1])).toEqual({});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("whereCacheMerge, where queries", () => {
|
test("whereCacheMerge, where queries", () => {
|
||||||
@@ -265,20 +248,19 @@ describe("whereCache", () => {
|
|||||||
|
|
||||||
// remember, will mutate dst
|
// remember, will mutate dst
|
||||||
const src = _whereCacheCreate(
|
const src = _whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{ where: { field: "queryField", column: "queryColumn", value: "foo" } },
|
{ where: { field: "queryField", column: "queryColumn", value: "foo" } },
|
||||||
["foo"]
|
["foo"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dst1 = _whereCacheCreate(
|
const dst1 = _whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
|
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
|
||||||
["dst1"]
|
["dst1"]
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert dst1 and src to be non-null
|
wc = _whereCacheMerge(dst1, src);
|
||||||
wc = _whereCacheMerge(dst1!, src!);
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -287,7 +269,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual(["foo"]);
|
).toEqual(["foo"]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -297,14 +279,13 @@ describe("whereCache", () => {
|
|||||||
).toEqual(["dst1"]);
|
).toEqual(["dst1"]);
|
||||||
|
|
||||||
const dst2 = _whereCacheCreate(
|
const dst2 = _whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
|
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
|
||||||
["dst2"]
|
["dst2"]
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert dst2m, dst1 and src to be non-null
|
wc = _whereCacheMerge(dst2, dst1, src);
|
||||||
wc = _whereCacheMerge(dst2!, dst1!, src!);
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -313,7 +294,7 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual(["foo"]);
|
).toEqual(["foo"]);
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -322,20 +303,17 @@ describe("whereCache", () => {
|
|||||||
})
|
})
|
||||||
).toEqual(["dst1"]);
|
).toEqual(["dst1"]);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert src to be non-null
|
wc = _whereCacheMerge({}, src);
|
||||||
wc = _whereCacheMerge({}, src!);
|
|
||||||
expect(wc).toEqual(src);
|
expect(wc).toEqual(src);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert src to be non-null
|
wc = _whereCacheMerge({ where: { field: { queryField: new Map() } } }, src);
|
||||||
wc = _whereCacheMerge({ where: { obs: { queryField: new Map() } } }, src!);
|
|
||||||
expect(wc).toEqual(src);
|
expect(wc).toEqual(src);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("whereCacheMerge, mixed queries", () => {
|
test("whereCacheMerge, mixed queries", () => {
|
||||||
const wc = _whereCacheMerge(
|
const wc = _whereCacheMerge(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert wc to be non-null
|
|
||||||
_whereCacheCreate(
|
_whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
@@ -344,10 +322,9 @@ describe("whereCache", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
["a"]
|
["a"]
|
||||||
)!,
|
),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion --- assert wc to be non-null
|
|
||||||
_whereCacheCreate(
|
_whereCacheCreate(
|
||||||
Field.obs,
|
"field",
|
||||||
{
|
{
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
@@ -357,11 +334,11 @@ describe("whereCache", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
["b"]
|
["b"]
|
||||||
)!
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -371,7 +348,7 @@ describe("whereCache", () => {
|
|||||||
).toEqual(["a"]);
|
).toEqual(["a"]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "mean",
|
method: "mean",
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
@@ -382,7 +359,7 @@ describe("whereCache", () => {
|
|||||||
).toEqual(["b"]);
|
).toEqual(["b"]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
where: {
|
where: {
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
column: "queryColumn",
|
column: "queryColumn",
|
||||||
@@ -392,7 +369,7 @@ describe("whereCache", () => {
|
|||||||
).toEqual([undefined]);
|
).toEqual([undefined]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
_whereCacheGet(wc, schema, Field.obs, {
|
_whereCacheGet(wc, schema, "field", {
|
||||||
summarize: {
|
summarize: {
|
||||||
method: "no-such-method",
|
method: "no-such-method",
|
||||||
field: "queryField",
|
field: "queryField",
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
|
||||||
import { NumberArray } from "../../src/common/types/arraytypes";
|
|
||||||
import calcCentroid from "../../src/util/centroid";
|
import calcCentroid from "../../src/util/centroid";
|
||||||
import quantile from "../../src/util/quantile";
|
import quantile from "../../src/util/quantile";
|
||||||
import { matrixFBSToDataframe } from "../../src/util/stateManager/matrix";
|
import { matrixFBSToDataframe } from "../../src/util/stateManager/matrix";
|
||||||
import * as REST from "./stateManager/sampleResponses";
|
import * as REST from "./stateManager/sampleResponses";
|
||||||
import { indexEntireSchema } from "../../src/util/stateManager/schemaHelpers";
|
import { indexEntireSchema } from "../../src/util/stateManager/schemaHelpers";
|
||||||
import { normalizeWritableCategoricalSchema } from "../../src/annoMatrix/normalize";
|
import { normalizeWritableCategoricalSchema } from "../../src/annoMatrix/normalize";
|
||||||
import { Dataframe } from "../../src/util/dataframe";
|
|
||||||
|
|
||||||
describe("centroid", () => {
|
describe("centroid", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let schema;
|
||||||
let schema: any;
|
let obsAnnotations;
|
||||||
let obsAnnotations: Dataframe;
|
let obsLayout;
|
||||||
let obsLayout: Dataframe;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
schema = indexEntireSchema(cloneDeep(REST.schema.schema));
|
schema = indexEntireSchema(cloneDeep(REST.schema.schema));
|
||||||
@@ -43,12 +40,11 @@ describe("centroid", () => {
|
|||||||
|
|
||||||
// This expected result assumes that all cells belong in all categorical values inside of sample response
|
// This expected result assumes that all cells belong in all categorical values inside of sample response
|
||||||
const expectedResult = [
|
const expectedResult = [
|
||||||
quantile([0.5], obsLayout.col("umap_0").asArray() as NumberArray)[0],
|
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
|
||||||
quantile([0.5], obsLayout.col("umap_1").asArray() as NumberArray)[0],
|
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
centroidResult.forEach((coordinate) => {
|
||||||
centroidResult.forEach((coordinate: any) => {
|
|
||||||
expect(coordinate).toEqual(expectedResult);
|
expect(coordinate).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -68,12 +64,11 @@ describe("centroid", () => {
|
|||||||
|
|
||||||
// This expected result assumes that all cells belong in all categorical values inside of sample response
|
// This expected result assumes that all cells belong in all categorical values inside of sample response
|
||||||
const expectedResult = [
|
const expectedResult = [
|
||||||
quantile([0.5], obsLayout.col("umap_0").asArray() as NumberArray)[0],
|
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
|
||||||
quantile([0.5], obsLayout.col("umap_1").asArray() as NumberArray)[0],
|
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
centroidResult.forEach((coordinate) => {
|
||||||
centroidResult.forEach((coordinate: any) => {
|
|
||||||
expect(coordinate).toEqual(expectedResult);
|
expect(coordinate).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+127
-168
@@ -6,8 +6,7 @@ describe("dataframe constructor", () => {
|
|||||||
expect(df).toBeDefined();
|
expect(df).toBeDefined();
|
||||||
expect(df.dims).toEqual([0, 0]);
|
expect(df.dims).toEqual([0, 0]);
|
||||||
expect(df).toHaveLength(0);
|
expect(df).toHaveLength(0);
|
||||||
expect(df.ihasCol(0)).toBeFalsy();
|
expect(df.icol(0)).not.toBeDefined();
|
||||||
expect(() => df.icol(0)).toThrow(RangeError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create with default indices", () => {
|
test("create with default indices", () => {
|
||||||
@@ -24,13 +23,6 @@ describe("dataframe constructor", () => {
|
|||||||
expect(df.at(2, 1)).toEqual(1);
|
expect(df.at(2, 1)).toEqual(1);
|
||||||
expect(df.iat(0, 0)).toEqual(0);
|
expect(df.iat(0, 0)).toEqual(0);
|
||||||
expect(df.iat(2, 1)).toEqual(1);
|
expect(df.iat(2, 1)).toEqual(1);
|
||||||
|
|
||||||
expect(Array.from(df.rowIndex.labels())).toEqual(
|
|
||||||
df.rowIndex.getLabels(df.rowIndex.getOffsets(df.rowIndex.labels()))
|
|
||||||
);
|
|
||||||
expect(Array.from(df.colIndex.labels())).toEqual(
|
|
||||||
df.colIndex.getLabels(df.colIndex.getOffsets(df.colIndex.labels()))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create with labelled indices", () => {
|
test("create with labelled indices", () => {
|
||||||
@@ -129,9 +121,7 @@ describe("simple data access", () => {
|
|||||||
expect(df.has(3, "foo")).toBeFalsy();
|
expect(df.has(3, "foo")).toBeFalsy();
|
||||||
expect(df.has(-1, "numbers")).toBeFalsy();
|
expect(df.has(-1, "numbers")).toBeFalsy();
|
||||||
expect(df.has(-1, -1)).toBeFalsy();
|
expect(df.has(-1, -1)).toBeFalsy();
|
||||||
expect(
|
expect(df.has(null, null)).toBeFalsy();
|
||||||
df.has(null as unknown as number, null as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(df.has(0, "foo")).toBeFalsy();
|
expect(df.has(0, "foo")).toBeFalsy();
|
||||||
expect(df.has(99, "numbers")).toBeFalsy();
|
expect(df.has(99, "numbers")).toBeFalsy();
|
||||||
expect(df.has(99, "foo")).toBeFalsy();
|
expect(df.has(99, "foo")).toBeFalsy();
|
||||||
@@ -259,12 +249,11 @@ describe("dataframe subsetting", () => {
|
|||||||
const df = sourceDf.subset(
|
const df = sourceDf.subset(
|
||||||
null,
|
null,
|
||||||
["int32", "float32"],
|
["int32", "float32"],
|
||||||
new Dataframe.DenseInt32Index([2, 1])
|
new Dataframe.DenseInt32Index([3, 2, 1])
|
||||||
);
|
);
|
||||||
expect(df.dims).toEqual([2, 2]);
|
|
||||||
expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex);
|
expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex);
|
||||||
expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index);
|
expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index);
|
||||||
expect(df.at(2, "int32")).toEqual(df.iat(0, 0));
|
expect(df.at(3, "int32")).toEqual(df.iat(0, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("withRowIndex error checks", () => {
|
test("withRowIndex error checks", () => {
|
||||||
@@ -843,7 +832,7 @@ describe("dataframe factories", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("dataframe col", () => {
|
describe("dataframe col", () => {
|
||||||
let df: Dataframe.Dataframe;
|
let df = null;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
df = new Dataframe.Dataframe(
|
df = new Dataframe.Dataframe(
|
||||||
[2, 2],
|
[2, 2],
|
||||||
@@ -860,8 +849,8 @@ describe("dataframe col", () => {
|
|||||||
expect(df).toBeDefined();
|
expect(df).toBeDefined();
|
||||||
expect(df.col("A")).toBe(df.icol(0));
|
expect(df.col("A")).toBe(df.icol(0));
|
||||||
expect(df.col("B")).toBe(df.icol(1));
|
expect(df.col("B")).toBe(df.icol(1));
|
||||||
expect(() => df.col("undefined")).toThrow(RangeError);
|
expect(df.col("undefined")).toBeUndefined();
|
||||||
expect(() => df.icol("undefined" as unknown as number)).toThrow(RangeError);
|
expect(df.icol("undefined")).toBeUndefined();
|
||||||
|
|
||||||
const colA = df.col("A");
|
const colA = df.col("A");
|
||||||
expect(colA).toBeInstanceOf(Function);
|
expect(colA).toBeInstanceOf(Function);
|
||||||
@@ -915,13 +904,13 @@ describe("dataframe col", () => {
|
|||||||
expect(df.col("A").indexOf(true)).toEqual(0);
|
expect(df.col("A").indexOf(true)).toEqual(0);
|
||||||
expect(df.col("A").indexOf(false)).toEqual(1);
|
expect(df.col("A").indexOf(false)).toEqual(1);
|
||||||
expect(df.col("A").indexOf(99)).toBeUndefined();
|
expect(df.col("A").indexOf(99)).toBeUndefined();
|
||||||
expect(df.col("A").indexOf(undefined as unknown as number)).toBeUndefined();
|
expect(df.col("A").indexOf(undefined)).toBeUndefined();
|
||||||
expect(df.col("A").indexOf(1)).toBeUndefined();
|
expect(df.col("A").indexOf(1)).toBeUndefined();
|
||||||
|
|
||||||
expect(df.col("B").indexOf(1)).toEqual(0);
|
expect(df.col("B").indexOf(1)).toEqual(0);
|
||||||
expect(df.col("B").indexOf(0)).toEqual(1);
|
expect(df.col("B").indexOf(0)).toEqual(1);
|
||||||
expect(df.col("B").indexOf(99)).toBeUndefined();
|
expect(df.col("B").indexOf(99)).toBeUndefined();
|
||||||
expect(df.col("B").indexOf(undefined as unknown as number)).toBeUndefined();
|
expect(df.col("B").indexOf(undefined)).toBeUndefined();
|
||||||
expect(df.col("B").indexOf(true)).toBeUndefined();
|
expect(df.col("B").indexOf(true)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -964,7 +953,7 @@ describe("label indexing", () => {
|
|||||||
|
|
||||||
test("offsets", () => {
|
test("offsets", () => {
|
||||||
expect(idx.getOffset(1)).toEqual(1);
|
expect(idx.getOffset(1)).toEqual(1);
|
||||||
expect(idx.getOffsets([1, 3])).toEqual(new Int32Array([1, 3]));
|
expect(idx.getOffsets([1, 3])).toEqual([1, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("subset", () => {
|
test("subset", () => {
|
||||||
@@ -1042,7 +1031,7 @@ describe("label indexing", () => {
|
|||||||
false,
|
false,
|
||||||
])
|
])
|
||||||
.labels()
|
.labels()
|
||||||
).toEqual([]);
|
).toEqual(new Int32Array([]));
|
||||||
expect(
|
expect(
|
||||||
idx
|
idx
|
||||||
.isubsetMask([
|
.isubsetMask([
|
||||||
@@ -1136,14 +1125,16 @@ describe("label indexing", () => {
|
|||||||
expect(idx.labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22]));
|
expect(idx.labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22]));
|
||||||
expect(idx.size()).toEqual(5);
|
expect(idx.size()).toEqual(5);
|
||||||
expect(idx.getLabel(0)).toEqual(99);
|
expect(idx.getLabel(0)).toEqual(99);
|
||||||
expect(idx.getLabels(new Int32Array([2, 4]))).toEqual([48, 22]);
|
expect(idx.getLabels(new Int32Array([2, 4]))).toEqual(
|
||||||
|
new Int32Array([48, 22])
|
||||||
|
);
|
||||||
expect(idx.getLabels([2, 4])).toEqual([48, 22]);
|
expect(idx.getLabels([2, 4])).toEqual([48, 22]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("offsets", () => {
|
test("offsets", () => {
|
||||||
expect(idx.getOffset(1002)).toEqual(1);
|
expect(idx.getOffset(1002)).toEqual(1);
|
||||||
expect(idx.getOffset(0)).toEqual(3);
|
expect(idx.getOffset(0)).toEqual(3);
|
||||||
expect(idx.getOffsets([0, 48])).toEqual(new Int32Array([3, 2]));
|
expect(idx.getOffsets([0, 48])).toEqual([3, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("subset", () => {
|
test("subset", () => {
|
||||||
@@ -1170,7 +1161,7 @@ describe("label indexing", () => {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
idx.isubsetMask([false, false, false, false, false]).labels()
|
idx.isubsetMask([false, false, false, false, false]).labels()
|
||||||
).toEqual([]);
|
).toEqual(new Int32Array([]));
|
||||||
expect(idx.isubsetMask([true, true, false, true, true]).labels()).toEqual(
|
expect(idx.isubsetMask([true, true, false, true, true]).labels()).toEqual(
|
||||||
new Int32Array([99, 1002, 0, 22])
|
new Int32Array([99, 1002, 0, 22])
|
||||||
);
|
);
|
||||||
@@ -1202,7 +1193,6 @@ describe("label indexing", () => {
|
|||||||
test("create", () => {
|
test("create", () => {
|
||||||
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
|
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
|
||||||
expect(() => new Dataframe.KeyIndex(["dup", "dup"])).toThrow(Error);
|
expect(() => new Dataframe.KeyIndex(["dup", "dup"])).toThrow(Error);
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
|
||||||
expect(new Dataframe.KeyIndex().size()).toEqual(0);
|
expect(new Dataframe.KeyIndex().size()).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1272,67 +1262,59 @@ describe("corner cases", () => {
|
|||||||
const idx = new Dataframe.IdentityInt32Index(10);
|
const idx = new Dataframe.IdentityInt32Index(10);
|
||||||
expect(idx.getOffset(0)).toBe(0);
|
expect(idx.getOffset(0)).toBe(0);
|
||||||
expect(idx.getOffset(9)).toBe(9);
|
expect(idx.getOffset(9)).toBe(9);
|
||||||
expect(idx.getOffset(10)).toBe(-1);
|
expect(idx.getOffset(10)).toBeUndefined();
|
||||||
expect(idx.getOffset(-1)).toBe(-1);
|
expect(idx.getOffset(-1)).toBeUndefined();
|
||||||
expect(idx.getOffset("sort")).toBe(-1);
|
expect(idx.getOffset("sort")).toBeUndefined();
|
||||||
expect(idx.getOffset("length")).toBe(-1);
|
expect(idx.getOffset("length")).toBeUndefined();
|
||||||
expect(idx.getOffset(true as unknown as string)).toBe(-1);
|
expect(idx.getOffset(true)).toBeUndefined();
|
||||||
expect(idx.getOffset(0.001)).toBe(-1);
|
expect(idx.getOffset(0.001)).toBeUndefined();
|
||||||
expect(idx.getOffset({} as unknown as string)).toBe(-1);
|
expect(idx.getOffset({})).toBeUndefined();
|
||||||
expect(idx.getOffset([] as unknown as string)).toBe(-1);
|
expect(idx.getOffset([])).toBeUndefined();
|
||||||
expect(idx.getOffset(new Float32Array() as unknown as string)).toBe(-1);
|
expect(idx.getOffset(new Float32Array())).toBeUndefined();
|
||||||
expect(idx.getOffset("__proto__")).toBe(-1);
|
expect(idx.getOffset("__proto__")).toBeUndefined();
|
||||||
|
|
||||||
expect(idx.getLabel(0)).toBe(0);
|
expect(idx.getLabel(0)).toBe(0);
|
||||||
expect(idx.getLabel(9)).toBe(9);
|
expect(idx.getLabel(9)).toBe(9);
|
||||||
expect(idx.getLabel(10)).toBeUndefined();
|
expect(idx.getLabel(10)).toBeUndefined();
|
||||||
expect(idx.getLabel(-1)).toBeUndefined();
|
expect(idx.getLabel(-1)).toBeUndefined();
|
||||||
expect(idx.getLabel("sort" as unknown as number)).toBeUndefined();
|
expect(idx.getLabel("sort")).toBeUndefined();
|
||||||
expect(idx.getLabel("length" as unknown as number)).toBeUndefined();
|
expect(idx.getLabel("length")).toBeUndefined();
|
||||||
expect(idx.getLabel(true as unknown as number)).toBeUndefined();
|
expect(idx.getLabel(true)).toBeUndefined();
|
||||||
expect(idx.getLabel(0.001)).toBeUndefined();
|
expect(idx.getLabel(0.001)).toBeUndefined();
|
||||||
expect(idx.getLabel({} as unknown as number)).toBeUndefined();
|
expect(idx.getLabel({})).toBeUndefined();
|
||||||
expect(idx.getLabel([] as unknown as number)).toBeUndefined();
|
expect(idx.getLabel([])).toBeUndefined();
|
||||||
expect(
|
expect(idx.getLabel(new Float32Array())).toBeUndefined();
|
||||||
idx.getLabel(new Float32Array() as unknown as number)
|
expect(idx.getLabel("__proto__")).toBeUndefined();
|
||||||
).toBeUndefined();
|
|
||||||
expect(idx.getLabel("__proto__" as unknown as number)).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("dense integer index rejects non-integer labels", () => {
|
test("dense integer index rejects non-integer labels", () => {
|
||||||
const idx = new Dataframe.DenseInt32Index([-10, 0, 3, 9, 10]);
|
const idx = new Dataframe.DenseInt32Index([-10, 0, 3, 9, 10]);
|
||||||
expect(idx.getOffset(-10)).toBe(0);
|
|
||||||
expect(idx.getOffset(0)).toBe(1);
|
expect(idx.getOffset(0)).toBe(1);
|
||||||
expect(idx.getOffset(3)).toBe(2);
|
|
||||||
expect(idx.getOffset(9)).toBe(3);
|
expect(idx.getOffset(9)).toBe(3);
|
||||||
expect(idx.getOffset(10)).toBe(4);
|
expect(idx.getOffset(1)).toBeUndefined();
|
||||||
|
expect(idx.getOffset(11)).toBeUndefined();
|
||||||
expect(idx.getOffset(1)).toBe(-1);
|
expect(idx.getOffset(-1)).toBeUndefined();
|
||||||
expect(idx.getOffset(11)).toBe(-1);
|
expect(idx.getOffset("sort")).toBeUndefined();
|
||||||
expect(idx.getOffset(-1)).toBe(-1);
|
expect(idx.getOffset("length")).toBeUndefined();
|
||||||
expect(idx.getOffset("sort")).toBe(-1);
|
expect(idx.getOffset(true)).toBeUndefined();
|
||||||
expect(idx.getOffset("length")).toBe(-1);
|
expect(idx.getOffset(0.001)).toBeUndefined();
|
||||||
expect(idx.getOffset(true as unknown as string)).toBe(-1);
|
expect(idx.getOffset({})).toBeUndefined();
|
||||||
expect(idx.getOffset(0.001)).toBe(-1);
|
expect(idx.getOffset([])).toBeUndefined();
|
||||||
expect(idx.getOffset({} as unknown as string)).toBe(-1);
|
expect(idx.getOffset(new Float32Array())).toBeUndefined();
|
||||||
expect(idx.getOffset([] as unknown as string)).toBe(-1);
|
expect(idx.getOffset("__proto__")).toBeUndefined();
|
||||||
expect(idx.getOffset(new Float32Array() as unknown as string)).toBe(-1);
|
|
||||||
expect(idx.getOffset("__proto__")).toBe(-1);
|
|
||||||
|
|
||||||
expect(idx.getLabel(0)).toBe(-10);
|
expect(idx.getLabel(0)).toBe(-10);
|
||||||
expect(idx.getLabel(4)).toBe(10);
|
expect(idx.getLabel(4)).toBe(10);
|
||||||
expect(idx.getLabel(10)).toBeUndefined();
|
expect(idx.getLabel(10)).toBeUndefined();
|
||||||
expect(idx.getLabel(-1)).toBeUndefined();
|
expect(idx.getLabel(-1)).toBeUndefined();
|
||||||
expect(idx.getLabel("sort" as unknown as number)).toBeUndefined();
|
expect(idx.getLabel("sort")).toBeUndefined();
|
||||||
expect(idx.getLabel("length" as unknown as number)).toBeUndefined();
|
expect(idx.getLabel("length")).toBeUndefined();
|
||||||
expect(idx.getLabel(true as unknown as number)).toBeUndefined();
|
expect(idx.getLabel(true)).toBeUndefined();
|
||||||
expect(idx.getLabel(0.001)).toBeUndefined();
|
expect(idx.getLabel(0.001)).toBeUndefined();
|
||||||
expect(idx.getLabel({} as unknown as number)).toBeUndefined();
|
expect(idx.getLabel({})).toBeUndefined();
|
||||||
expect(idx.getLabel([] as unknown as number)).toBeUndefined();
|
expect(idx.getLabel([])).toBeUndefined();
|
||||||
expect(
|
expect(idx.getLabel(new Float32Array())).toBeUndefined();
|
||||||
idx.getLabel(new Float32Array() as unknown as number)
|
expect(idx.getLabel("__proto__")).toBeUndefined();
|
||||||
).toBeUndefined();
|
|
||||||
expect(idx.getLabel("__proto__" as unknown as number)).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Empty dataframe rejects bogus labels", () => {
|
test("Empty dataframe rejects bogus labels", () => {
|
||||||
@@ -1340,55 +1322,39 @@ describe("corner cases", () => {
|
|||||||
|
|
||||||
expect(df.hasCol("sort")).toBeFalsy();
|
expect(df.hasCol("sort")).toBeFalsy();
|
||||||
expect(df.hasCol(0)).toBeFalsy();
|
expect(df.hasCol(0)).toBeFalsy();
|
||||||
expect(df.hasCol(true as unknown as number)).toBeFalsy();
|
expect(df.hasCol(true)).toBeFalsy();
|
||||||
expect(df.hasCol(false as unknown as number)).toBeFalsy();
|
expect(df.hasCol(false)).toBeFalsy();
|
||||||
expect(df.hasCol([] as unknown as number)).toBeFalsy();
|
expect(df.hasCol([])).toBeFalsy();
|
||||||
expect(df.hasCol({} as unknown as number)).toBeFalsy();
|
expect(df.hasCol({})).toBeFalsy();
|
||||||
expect(df.hasCol(null as unknown as number)).toBeFalsy();
|
expect(df.hasCol(null)).toBeFalsy();
|
||||||
expect(df.hasCol(undefined as unknown as number)).toBeFalsy();
|
expect(df.hasCol(undefined)).toBeFalsy();
|
||||||
|
|
||||||
expect(() => df.col("sort")).toThrow(RangeError);
|
expect(df.col("sort")).toBeUndefined();
|
||||||
expect(() => df.col(0)).toThrow(RangeError);
|
expect(df.col(0)).toBeUndefined();
|
||||||
expect(() => df.col(true as unknown as string)).toThrow(RangeError);
|
expect(df.col(true)).toBeUndefined();
|
||||||
expect(() => df.col(false as unknown as string)).toThrow(RangeError);
|
expect(df.col(false)).toBeUndefined();
|
||||||
expect(() => df.col([] as unknown as string)).toThrow(RangeError);
|
expect(df.col([])).toBeUndefined();
|
||||||
expect(() => df.col({} as unknown as string)).toThrow(RangeError);
|
expect(df.col({})).toBeUndefined();
|
||||||
expect(() => df.col(null as unknown as string)).toThrow(RangeError);
|
expect(df.col(null)).toBeUndefined();
|
||||||
expect(() => df.col(undefined as unknown as string)).toThrow(RangeError);
|
expect(df.col(undefined)).toBeUndefined();
|
||||||
|
|
||||||
expect(() => df.icol("sort" as unknown as number)).toThrow(RangeError);
|
expect(df.icol("sort")).toBeUndefined();
|
||||||
expect(() => df.icol(0)).toThrow(RangeError);
|
expect(df.icol(0)).toBeUndefined();
|
||||||
expect(() => df.icol(true as unknown as number)).toThrow(RangeError);
|
expect(df.icol(true)).toBeUndefined();
|
||||||
expect(() => df.icol(false as unknown as number)).toThrow(RangeError);
|
expect(df.icol(false)).toBeUndefined();
|
||||||
expect(() => df.icol([] as unknown as number)).toThrow(RangeError);
|
expect(df.icol([])).toBeUndefined();
|
||||||
expect(() => df.icol({} as unknown as number)).toThrow(RangeError);
|
expect(df.icol({})).toBeUndefined();
|
||||||
expect(() => df.icol(null as unknown as number)).toThrow(RangeError);
|
expect(df.icol(null)).toBeUndefined();
|
||||||
expect(() => df.icol(undefined as unknown as number)).toThrow(RangeError);
|
expect(df.icol(undefined)).toBeUndefined();
|
||||||
|
|
||||||
expect(
|
expect(df.ihas("sort", "length")).toBeFalsy();
|
||||||
df.ihas("sort" as unknown as number, "length" as unknown as number)
|
expect(df.ihas("0", "0")).toBeFalsy();
|
||||||
).toBeFalsy();
|
expect(df.ihas("", "")).toBeFalsy();
|
||||||
expect(
|
expect(df.ihas(null, null)).toBeFalsy();
|
||||||
df.ihas("0" as unknown as number, "0" as unknown as number)
|
expect(df.ihas(undefined, undefined)).toBeFalsy();
|
||||||
).toBeFalsy();
|
expect(df.ihas(true, true)).toBeFalsy();
|
||||||
expect(
|
expect(df.ihas([], [])).toBeFalsy();
|
||||||
df.ihas("" as unknown as number, "" as unknown as number)
|
expect(df.ihas({}, {})).toBeFalsy();
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas(null as unknown as number, null as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas(undefined as unknown as number, undefined as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas(true as unknown as number, true as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas([] as unknown as number, [] as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas({} as unknown as number, {} as unknown as number)
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Dataframe rejects bogus labels", () => {
|
test("Dataframe rejects bogus labels", () => {
|
||||||
@@ -1405,58 +1371,51 @@ describe("corner cases", () => {
|
|||||||
expect(df.hasCol("sort")).toBeFalsy();
|
expect(df.hasCol("sort")).toBeFalsy();
|
||||||
expect(df.hasCol("__proto__")).toBeFalsy();
|
expect(df.hasCol("__proto__")).toBeFalsy();
|
||||||
expect(df.hasCol(0)).toBeFalsy();
|
expect(df.hasCol(0)).toBeFalsy();
|
||||||
expect(df.hasCol(true as unknown as string)).toBeFalsy();
|
expect(df.hasCol(true)).toBeFalsy();
|
||||||
expect(df.hasCol(false as unknown as string)).toBeFalsy();
|
expect(df.hasCol(false)).toBeFalsy();
|
||||||
expect(df.hasCol([] as unknown as string)).toBeFalsy();
|
expect(df.hasCol([])).toBeFalsy();
|
||||||
expect(df.hasCol({} as unknown as string)).toBeFalsy();
|
expect(df.hasCol({})).toBeFalsy();
|
||||||
expect(df.hasCol(null as unknown as string)).toBeFalsy();
|
expect(df.hasCol(null)).toBeFalsy();
|
||||||
expect(df.hasCol(undefined as unknown as string)).toBeFalsy();
|
expect(df.hasCol(undefined)).toBeFalsy();
|
||||||
|
|
||||||
expect(() => df.col("sort")).toThrow(RangeError);
|
expect(df.col("sort")).toBeUndefined();
|
||||||
expect(() => df.col("__proto__")).toThrow(RangeError);
|
expect(df.col("__proto__")).toBeUndefined();
|
||||||
expect(() => df.col(0)).toThrow(RangeError);
|
expect(df.col(0)).toBeUndefined();
|
||||||
expect(() => df.col(true as unknown as string)).toThrow(RangeError);
|
expect(df.col(true)).toBeUndefined();
|
||||||
expect(() => df.col(false as unknown as string)).toThrow(RangeError);
|
expect(df.col(false)).toBeUndefined();
|
||||||
expect(() => df.col([] as unknown as string)).toThrow(RangeError);
|
expect(df.col([])).toBeUndefined();
|
||||||
expect(() => df.col({} as unknown as string)).toThrow(RangeError);
|
expect(df.col({})).toBeUndefined();
|
||||||
expect(() => df.col(null as unknown as string)).toThrow(RangeError);
|
expect(df.col(null)).toBeUndefined();
|
||||||
expect(() => df.col(undefined as unknown as string)).toThrow(RangeError);
|
expect(df.col(undefined)).toBeUndefined();
|
||||||
|
|
||||||
expect(() => df.icol("sort" as unknown as number)).toThrow(RangeError);
|
expect(df.icol("sort")).toBeUndefined();
|
||||||
expect(() => df.icol("__proto__" as unknown as number)).toThrow(RangeError);
|
expect(df.icol("__proto__")).toBeUndefined();
|
||||||
expect(() => df.icol(-1)).toThrow(RangeError);
|
expect(df.icol(-1)).toBeUndefined();
|
||||||
expect(() => df.icol(true as unknown as number)).toThrow(RangeError);
|
expect(df.icol(true)).toBeUndefined();
|
||||||
expect(() => df.icol(false as unknown as number)).toThrow(RangeError);
|
expect(df.icol(false)).toBeUndefined();
|
||||||
expect(() => df.icol([] as unknown as number)).toThrow(RangeError);
|
expect(df.icol([])).toBeUndefined();
|
||||||
expect(() => df.icol({} as unknown as number)).toThrow(RangeError);
|
expect(df.icol({})).toBeUndefined();
|
||||||
expect(() => df.icol(null as unknown as number)).toThrow(RangeError);
|
expect(df.icol(null)).toBeUndefined();
|
||||||
expect(() => df.icol(undefined as unknown as number)).toThrow(RangeError);
|
expect(df.icol(undefined)).toBeUndefined();
|
||||||
|
|
||||||
expect(
|
expect(df.ihas("sort", "length")).toBeFalsy();
|
||||||
df.ihas("sort" as unknown as number, "length" as unknown as number)
|
expect(df.ihas("__proto__", "__proto__")).toBeFalsy();
|
||||||
).toBeFalsy();
|
|
||||||
expect(
|
|
||||||
df.ihas(
|
|
||||||
"__proto__" as unknown as number,
|
|
||||||
"__proto__" as unknown as number
|
|
||||||
)
|
|
||||||
).toBeFalsy();
|
|
||||||
|
|
||||||
expect(df.ihas(-1, 0)).toBeFalsy();
|
expect(df.ihas(-1, 0)).toBeFalsy();
|
||||||
expect(df.ihas("0" as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas("0", 0)).toBeFalsy();
|
||||||
expect(df.ihas("" as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas("", 0)).toBeFalsy();
|
||||||
expect(df.ihas(null as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas(null, 0)).toBeFalsy();
|
||||||
expect(df.ihas(undefined as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas(undefined, 0)).toBeFalsy();
|
||||||
expect(df.ihas([] as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas([], 0)).toBeFalsy();
|
||||||
expect(df.ihas({} as unknown as number, 0)).toBeFalsy();
|
expect(df.ihas({}, 0)).toBeFalsy();
|
||||||
|
|
||||||
expect(df.ihas(0, -1)).toBeFalsy();
|
expect(df.ihas(0, -1)).toBeFalsy();
|
||||||
expect(df.ihas(0, "0" as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, "0")).toBeFalsy();
|
||||||
expect(df.ihas(0, "" as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, "")).toBeFalsy();
|
||||||
expect(df.ihas(0, null as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, null)).toBeFalsy();
|
||||||
expect(df.ihas(0, undefined as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, undefined)).toBeFalsy();
|
||||||
expect(df.ihas(0, [] as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, [])).toBeFalsy();
|
||||||
expect(df.ihas(0, {} as unknown as number)).toBeFalsy();
|
expect(df.ihas(0, {})).toBeFalsy();
|
||||||
|
|
||||||
expect(df.has("sort", "length")).toBeFalsy();
|
expect(df.has("sort", "length")).toBeFalsy();
|
||||||
expect(df.has("length", "sort")).toBeFalsy();
|
expect(df.has("length", "sort")).toBeFalsy();
|
||||||
@@ -1465,17 +1424,17 @@ describe("corner cases", () => {
|
|||||||
expect(df.has(-1, "A")).toBeFalsy();
|
expect(df.has(-1, "A")).toBeFalsy();
|
||||||
expect(df.has("0", "A")).toBeFalsy();
|
expect(df.has("0", "A")).toBeFalsy();
|
||||||
expect(df.has("", "A")).toBeFalsy();
|
expect(df.has("", "A")).toBeFalsy();
|
||||||
expect(df.has(null as unknown as string, "A")).toBeFalsy();
|
expect(df.has(null, "A")).toBeFalsy();
|
||||||
expect(df.has(undefined as unknown as string, "A")).toBeFalsy();
|
expect(df.has(undefined, "A")).toBeFalsy();
|
||||||
expect(df.has([] as unknown as string, "A")).toBeFalsy();
|
expect(df.has([], "A")).toBeFalsy();
|
||||||
expect(df.has({} as unknown as string, "A")).toBeFalsy();
|
expect(df.has({}, "A")).toBeFalsy();
|
||||||
|
|
||||||
expect(df.has(0, -1)).toBeFalsy();
|
expect(df.has(0, -1)).toBeFalsy();
|
||||||
expect(df.has(0, "0")).toBeFalsy();
|
expect(df.has(0, "0")).toBeFalsy();
|
||||||
expect(df.has(0, "")).toBeFalsy();
|
expect(df.has(0, "")).toBeFalsy();
|
||||||
expect(df.has(0, null as unknown as string)).toBeFalsy();
|
expect(df.has(0, null)).toBeFalsy();
|
||||||
expect(df.has(0, undefined as unknown as string)).toBeFalsy();
|
expect(df.has(0, undefined)).toBeFalsy();
|
||||||
expect(df.has(0, [] as unknown as string)).toBeFalsy();
|
expect(df.has(0, [])).toBeFalsy();
|
||||||
expect(df.has(0, {} as unknown as string)).toBeFalsy();
|
expect(df.has(0, {})).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+14
-16
@@ -9,7 +9,7 @@ describe("Dataframe column histogram", () => {
|
|||||||
new Dataframe.KeyIndex(["name", "cat", "value"])
|
new Dataframe.KeyIndex(["name", "cat", "value"])
|
||||||
);
|
);
|
||||||
|
|
||||||
const h1 = df.col("cat").histogramCategoricalBy(df.col("name"));
|
const h1 = df.col("cat").histogram(df.col("name"));
|
||||||
expect(h1).toMatchObject(
|
expect(h1).toMatchObject(
|
||||||
new Map([
|
new Map([
|
||||||
["n1", new Map([["c1", 1]])],
|
["n1", new Map([["c1", 1]])],
|
||||||
@@ -18,9 +18,7 @@ describe("Dataframe column histogram", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
// memoized?
|
// memoized?
|
||||||
expect(df.col("cat").histogramCategoricalBy(df.col("name"))).toMatchObject(
|
expect(df.col("cat").histogram(df.col("name"))).toMatchObject(h1);
|
||||||
h1
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("continuous by categorical", () => {
|
test("continuous by categorical", () => {
|
||||||
@@ -31,7 +29,7 @@ describe("Dataframe column histogram", () => {
|
|||||||
new Dataframe.KeyIndex(["name", "cat", "value"])
|
new Dataframe.KeyIndex(["name", "cat", "value"])
|
||||||
);
|
);
|
||||||
|
|
||||||
const h1 = df.col("value").histogramContinuousBy(3, [0, 2], df.col("name"));
|
const h1 = df.col("value").histogram(3, [0, 2], df.col("name"));
|
||||||
expect(h1).toMatchObject(
|
expect(h1).toMatchObject(
|
||||||
new Map([
|
new Map([
|
||||||
["n1", [1, 0, 0]],
|
["n1", [1, 0, 0]],
|
||||||
@@ -40,9 +38,9 @@ describe("Dataframe column histogram", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
// memoized?
|
// memoized?
|
||||||
expect(
|
expect(df.col("value").histogram(3, [0, 2], df.col("name"))).toMatchObject(
|
||||||
df.col("value").histogramContinuousBy(3, [0, 2], df.col("name"))
|
h1
|
||||||
).toMatchObject(h1);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("categorical", () => {
|
test("categorical", () => {
|
||||||
@@ -53,7 +51,7 @@ describe("Dataframe column histogram", () => {
|
|||||||
new Dataframe.KeyIndex(["name", "cat", "value"])
|
new Dataframe.KeyIndex(["name", "cat", "value"])
|
||||||
);
|
);
|
||||||
|
|
||||||
const h1 = df.col("cat").histogramCategorical();
|
const h1 = df.col("cat").histogram();
|
||||||
expect(h1).toMatchObject(
|
expect(h1).toMatchObject(
|
||||||
new Map([
|
new Map([
|
||||||
["c1", 1],
|
["c1", 1],
|
||||||
@@ -62,7 +60,7 @@ describe("Dataframe column histogram", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
// memoized?
|
// memoized?
|
||||||
expect(df.col("cat").histogramCategorical()).toMatchObject(h1);
|
expect(df.col("value").histogram(3, [0, 2])).toMatchObject(h1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("continuous", () => {
|
test("continuous", () => {
|
||||||
@@ -73,10 +71,10 @@ describe("Dataframe column histogram", () => {
|
|||||||
new Dataframe.KeyIndex(["name", "cat", "value"])
|
new Dataframe.KeyIndex(["name", "cat", "value"])
|
||||||
);
|
);
|
||||||
|
|
||||||
const h1 = df.col("value").histogramContinuous(3, [0, 2]);
|
const h1 = df.col("value").histogram(3, [0, 2]);
|
||||||
expect(h1).toMatchObject([1, 1, 1]);
|
expect(h1).toMatchObject([1, 1, 1]);
|
||||||
// memoized?
|
// memoized?
|
||||||
expect(df.col("value").histogramContinuous(3, [0, 2])).toMatchObject(h1);
|
expect(df.col("value").histogram(3, [0, 2])).toMatchObject(h1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("continuous thesholds correct", () => {
|
test("continuous thesholds correct", () => {
|
||||||
@@ -86,10 +84,10 @@ describe("Dataframe column histogram", () => {
|
|||||||
[new Int32Array(vals), new Float32Array(vals)]
|
[new Int32Array(vals), new Float32Array(vals)]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(df.col(0).histogramContinuous(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
|
expect(df.col(0).histogram(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
|
||||||
expect(df.col(1).histogramContinuous(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
|
expect(df.col(1).histogram(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
|
||||||
expect(df.col(0).histogramContinuous(2, [0, 10])).toEqual([2, 2]);
|
expect(df.col(0).histogram(2, [0, 10])).toEqual([2, 2]);
|
||||||
expect(df.col(0).histogramContinuous(10, [0, 100])).toEqual([
|
expect(df.col(0).histogram(10, [0, 100])).toEqual([
|
||||||
3, 2, 1, 0, 0, 0, 0, 0, 0, 2,
|
3, 2, 1, 0, 0, 0, 0, 0, 0, 2,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
+20
-23
@@ -1,14 +1,13 @@
|
|||||||
import * as Dataframe from "../../../src/util/dataframe";
|
import * as Dataframe from "../../../src/util/dataframe";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function float32Conversion(f) {
|
||||||
function float32Conversion(f: any) {
|
|
||||||
return new Float32Array([f])[0];
|
return new Float32Array([f])[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Dataframe column summary", () => {
|
describe("Dataframe column summary", () => {
|
||||||
test("empty column test", () => {
|
test("empty column test", () => {
|
||||||
const df = Dataframe.Dataframe.create([0, 1], [[]]);
|
const df = Dataframe.Dataframe.create([0, 1], [[]]);
|
||||||
const summary = df.icol(0).summarizeCategorical();
|
const summary = df.icol(0).summarize();
|
||||||
expect(summary).toEqual(
|
expect(summary).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
@@ -41,7 +40,7 @@ describe("Dataframe column summary", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(df.icol(0).summarizeCategorical()).toEqual(
|
expect(df.icol(0).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: ["n1"],
|
categories: ["n1"],
|
||||||
@@ -49,7 +48,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 1,
|
numCategories: 1,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(1).summarizeCategorical()).toEqual(
|
expect(df.icol(1).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: ["hi"],
|
categories: ["hi"],
|
||||||
@@ -57,7 +56,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 1,
|
numCategories: 1,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(2).summarizeCategorical()).toEqual(
|
expect(df.icol(2).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: [true],
|
categories: [true],
|
||||||
@@ -65,7 +64,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 1,
|
numCategories: 1,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(3).summarizeContinuous()).toEqual(
|
expect(df.icol(3).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: float32Conversion(39.3),
|
min: float32Conversion(39.3),
|
||||||
@@ -75,7 +74,7 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 0,
|
pinf: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(4).summarizeContinuous()).toEqual(
|
expect(df.icol(4).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: 99,
|
min: 99,
|
||||||
@@ -85,7 +84,7 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 0,
|
pinf: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(5).summarizeCategorical()).toEqual(
|
expect(df.icol(5).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: [1],
|
categories: [1],
|
||||||
@@ -117,7 +116,7 @@ describe("Dataframe column summary", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(df.icol(0).summarizeCategorical()).toEqual(
|
expect(df.icol(0).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
||||||
@@ -129,7 +128,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 3,
|
numCategories: 3,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(1).summarizeCategorical()).toEqual(
|
expect(df.icol(1).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
categories: expect.arrayContaining(["hi", "bye"]),
|
||||||
@@ -140,7 +139,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 2,
|
numCategories: 2,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(2).summarizeCategorical()).toEqual(
|
expect(df.icol(2).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining([true, false]),
|
categories: expect.arrayContaining([true, false]),
|
||||||
@@ -151,7 +150,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 2,
|
numCategories: 2,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(3).summarizeContinuous()).toEqual(
|
expect(df.icol(3).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: 0,
|
min: 0,
|
||||||
@@ -161,7 +160,7 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 0,
|
pinf: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(4).summarizeContinuous()).toEqual(
|
expect(df.icol(4).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: 99,
|
min: 99,
|
||||||
@@ -171,11 +170,10 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 0,
|
pinf: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(5).summarizeCategorical()).toEqual(
|
expect(df.icol(5).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
categories: expect.arrayContaining([1, false, "0"]),
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
categoryCounts: new Map([
|
categoryCounts: new Map([
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[false, 1],
|
[false, 1],
|
||||||
@@ -213,7 +211,7 @@ describe("Dataframe column summary", () => {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(df.icol(0).summarizeCategorical()).toEqual(
|
expect(df.icol(0).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
||||||
@@ -225,7 +223,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 3,
|
numCategories: 3,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(1).summarizeCategorical()).toEqual(
|
expect(df.icol(1).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
categories: expect.arrayContaining(["hi", "bye"]),
|
||||||
@@ -236,7 +234,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 2,
|
numCategories: 2,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(2).summarizeCategorical()).toEqual(
|
expect(df.icol(2).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining([true, false]),
|
categories: expect.arrayContaining([true, false]),
|
||||||
@@ -247,7 +245,7 @@ describe("Dataframe column summary", () => {
|
|||||||
numCategories: 2,
|
numCategories: 2,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(3).summarizeContinuous()).toEqual(
|
expect(df.icol(3).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: float32Conversion(39.3),
|
min: float32Conversion(39.3),
|
||||||
@@ -257,7 +255,7 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 1,
|
pinf: 1,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(4).summarizeContinuous()).toEqual(
|
expect(df.icol(4).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: false,
|
categorical: false,
|
||||||
min: 99,
|
min: 99,
|
||||||
@@ -267,11 +265,10 @@ describe("Dataframe column summary", () => {
|
|||||||
pinf: 0,
|
pinf: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(df.icol(5).summarizeCategorical()).toEqual(
|
expect(df.icol(5).summarize()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
categorical: true,
|
categorical: true,
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
categories: expect.arrayContaining([1, false, "0"]),
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
categoryCounts: new Map([
|
categoryCounts: new Map([
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[false, 1],
|
[false, 1],
|
||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
import PromiseLimit from "../../src/util/promiseLimit";
|
import PromiseLimit from "../../src/util/promiseLimit";
|
||||||
import { range } from "../../src/util/range";
|
import { range } from "../../src/util/range";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
|
||||||
const delay = (t: any) => new Promise((resolve) => setTimeout(resolve, t));
|
|
||||||
|
|
||||||
describe("PromiseLimit", () => {
|
describe("PromiseLimit", () => {
|
||||||
test("simple evaluation, concurrency 1", async () => {
|
test("simple evaluation, concurrency 1", async () => {
|
||||||
@@ -52,7 +51,7 @@ describe("PromiseLimit", () => {
|
|||||||
running -= 1;
|
running -= 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all(range(10).map(() => plimit.add(() => callback())));
|
await Promise.all(range(10).map((i) => plimit.add(() => callback(i))));
|
||||||
|
|
||||||
expect(maxRunning).toEqual(2);
|
expect(maxRunning).toEqual(2);
|
||||||
});
|
});
|
||||||
@@ -19,11 +19,7 @@ describe("quantile", () => {
|
|||||||
test("multi q", () => {
|
test("multi q", () => {
|
||||||
const arr = new Float32Array([9, 3, 5, 6, 0]);
|
const arr = new Float32Array([9, 3, 5, 6, 0]);
|
||||||
expect(quantile([0, 0.25, 0.5, 0.75, 1.0], arr)).toMatchObject([
|
expect(quantile([0, 0.25, 0.5, 0.75, 1.0], arr)).toMatchObject([
|
||||||
0,
|
0, 3, 5, 6, 9,
|
||||||
3,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
9,
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+8
-20
@@ -95,7 +95,6 @@ describe("categorical color helpers", () => {
|
|||||||
const data = obsDataframe.col("categoricalColumn").asArray();
|
const data = obsDataframe.col("categoricalColumn").asArray();
|
||||||
const cats = schema.annotations.obsByName.categoricalColumn.categories;
|
const cats = schema.annotations.obsByName.categoricalColumn.categories;
|
||||||
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
|
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
|
||||||
// @ts-expect-error ts-migrate(2722) FIXME: Cannot invoke an object which is possibly 'undefin... Remove this comment to see the full error message
|
|
||||||
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
|
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,7 +112,6 @@ describe("categorical color helpers", () => {
|
|||||||
const data = obsDataframe.col("categoricalColumn").asArray();
|
const data = obsDataframe.col("categoricalColumn").asArray();
|
||||||
const cats = schemaClone.annotations.obsByName.categoricalColumn.categories;
|
const cats = schemaClone.annotations.obsByName.categoricalColumn.categories;
|
||||||
for (let i = 0; i < schemaClone.dataframe.nObs; i += 1) {
|
for (let i = 0; i < schemaClone.dataframe.nObs; i += 1) {
|
||||||
// @ts-expect-error ts-migrate(2722) FIXME: Cannot invoke an object which is possibly 'undefin... Remove this comment to see the full error message
|
|
||||||
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
|
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -124,8 +122,7 @@ describe("categorical color helpers", () => {
|
|||||||
Array.from(schema.annotations.obsByName.categoricalColumn.categories)
|
Array.from(schema.annotations.obsByName.categoricalColumn.categories)
|
||||||
);
|
);
|
||||||
const userDefinedColorTable = {
|
const userDefinedColorTable = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
categoricalColumn: shuffleCats.reduce((acc, label) => {
|
||||||
categoricalColumn: shuffleCats.reduce((acc: any, label: any) => {
|
|
||||||
acc[label] = randRGBColor();
|
acc[label] = randRGBColor();
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
@@ -139,14 +136,12 @@ describe("categorical color helpers", () => {
|
|||||||
"categoricalColumn",
|
"categoricalColumn",
|
||||||
obsDataframe,
|
obsDataframe,
|
||||||
schema,
|
schema,
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{}' is not assignable to paramet... Remove this comment to see the full error message
|
|
||||||
userColors
|
userColors
|
||||||
);
|
);
|
||||||
expect(ct).toBeDefined();
|
expect(ct).toBeDefined();
|
||||||
const data = obsDataframe.col("categoricalColumn").asArray();
|
const data = obsDataframe.col("categoricalColumn").asArray();
|
||||||
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
|
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
|
||||||
expect(makeScale(ct.rgb[i])).toEqual(
|
expect(makeScale(ct.rgb[i])).toEqual(
|
||||||
// @ts-expect-error ts-migrate(2722) FIXME: Cannot invoke an object which is possibly 'undefin... Remove this comment to see the full error message
|
|
||||||
ct.scale(cats.indexOf(data[i])).toString()
|
ct.scale(cats.indexOf(data[i])).toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -159,38 +154,31 @@ TODO:
|
|||||||
2. user defined colors
|
2. user defined colors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function indexSchema(schema) {
|
||||||
function indexSchema(schema: any) {
|
|
||||||
schema.annotations.obsByName = Object.fromEntries(
|
schema.annotations.obsByName = Object.fromEntries(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
schema.annotations?.obs?.columns?.map((v) => [v.name, v]) ?? []
|
||||||
schema.annotations?.obs?.columns?.map((v: any) => [v.name, v]) ?? []
|
|
||||||
);
|
);
|
||||||
schema.annotations.varByName = Object.fromEntries(
|
schema.annotations.varByName = Object.fromEntries(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
schema.annotations?.var?.columns?.map((v) => [v.name, v]) ?? []
|
||||||
schema.annotations?.var?.columns?.map((v: any) => [v.name, v]) ?? []
|
|
||||||
);
|
);
|
||||||
schema.layout.obsByName = Object.fromEntries(
|
schema.layout.obsByName = Object.fromEntries(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
schema.layout?.obs?.map((v) => [v.name, v]) ?? []
|
||||||
schema.layout?.obs?.map((v: any) => [v.name, v]) ?? []
|
|
||||||
);
|
);
|
||||||
schema.layout.varByName = Object.fromEntries(
|
schema.layout.varByName = Object.fromEntries(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
schema.layout?.var?.map((v) => [v.name, v]) ?? []
|
||||||
schema.layout?.var?.map((v: any) => [v.name, v]) ?? []
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function makeScale(rgb) {
|
||||||
function makeScale(rgb: any) {
|
|
||||||
// make a scale string from a rgb float triple
|
// make a scale string from a rgb float triple
|
||||||
return `rgb(${(rgb[0] * 255) >>> 0}, ${(rgb[1] * 255) >>> 0}, ${
|
return `rgb(${(rgb[0] * 255) >>> 0}, ${(rgb[1] * 255) >>> 0}, ${
|
||||||
(rgb[2] * 256) >>> 0
|
(rgb[2] * 256) >>> 0
|
||||||
})`;
|
})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function shuffle(array) {
|
||||||
function shuffle(array: any) {
|
|
||||||
for (let i = array.length - 1; i > 0; i -= 1) {
|
for (let i = array.length - 1; i > 0; i -= 1) {
|
||||||
const j = (Math.random() * (i + 1)) >>> 0;
|
const j = (Math.random() * (i + 1)) >>> 0;
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
+11
-25
@@ -5,7 +5,6 @@ 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/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.
|
||||||
@@ -30,7 +29,7 @@ const aConfigResponse = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const aSchemaResponse: { schema: RawSchema } = {
|
const aSchemaResponse = {
|
||||||
schema: {
|
schema: {
|
||||||
dataframe: {
|
dataframe: {
|
||||||
nObs,
|
nObs,
|
||||||
@@ -41,30 +40,28 @@ const aSchemaResponse: { schema: RawSchema } = {
|
|||||||
obs: {
|
obs: {
|
||||||
index: "name",
|
index: "name",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: "name", type: "string", writable: false },
|
{ name: "name", type: "string" },
|
||||||
{ name: "field1", type: "int32", writable: false },
|
{ name: "field1", type: "int32" },
|
||||||
{ name: "field2", type: "float32", writable: false },
|
{ name: "field2", type: "float32" },
|
||||||
{ name: "field3", type: "boolean", writable: false },
|
{ name: "field3", type: "boolean" },
|
||||||
{
|
{
|
||||||
name: "field4",
|
name: "field4",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: field4Categories,
|
categories: field4Categories,
|
||||||
writable: false,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
var: {
|
var: {
|
||||||
index: "name",
|
index: "name",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: "name", type: "string", writable: false },
|
{ name: "name", type: "string" },
|
||||||
{ name: "fieldA", type: "int32", writable: false },
|
{ name: "fieldA", type: "int32" },
|
||||||
{ name: "fieldB", type: "float32", writable: false },
|
{ name: "fieldB", type: "float32" },
|
||||||
{ name: "fieldC", type: "boolean", writable: false },
|
{ name: "fieldC", type: "boolean" },
|
||||||
{
|
{
|
||||||
name: "fieldD",
|
name: "fieldD",
|
||||||
type: "categorical",
|
type: "categorical",
|
||||||
categories: fieldDCategories,
|
categories: fieldDCategories,
|
||||||
writable: false,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -78,7 +75,6 @@ const aSchemaResponse: { schema: RawSchema } = {
|
|||||||
|
|
||||||
const anAnnotationsObsJSONResponse = {
|
const anAnnotationsObsJSONResponse = {
|
||||||
names: ["name", "field1", "field2", "field3", "field4"],
|
names: ["name", "field1", "field2", "field3", "field4"],
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
|
||||||
data: _()
|
data: _()
|
||||||
.range(nObs)
|
.range(nObs)
|
||||||
.map((idx) => [
|
.map((idx) => [
|
||||||
@@ -95,7 +91,6 @@ const anAnnotationsObsJSONResponse = {
|
|||||||
|
|
||||||
const anAnnotationsVarJSONResponse = {
|
const anAnnotationsVarJSONResponse = {
|
||||||
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
|
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
|
||||||
data: _()
|
data: _()
|
||||||
.range(nVar)
|
.range(nVar)
|
||||||
.map((idx) => [
|
.map((idx) => [
|
||||||
@@ -110,11 +105,8 @@ const anAnnotationsVarJSONResponse = {
|
|||||||
.value(),
|
.value(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function encodeTypedArray(builder, uType, uData) {
|
||||||
function encodeTypedArray(builder: any, uType: any, uData: any) {
|
|
||||||
// @ts-expect-error --- FIXME: Element implicitly has an 'any' type.
|
|
||||||
const uTypeName = NetEncoding.TypedArray[uType];
|
const uTypeName = NetEncoding.TypedArray[uType];
|
||||||
// @ts-expect-error --- FIXME: Element implicitly has an 'any' type.
|
|
||||||
const ArrayType = NetEncoding[uTypeName];
|
const ArrayType = NetEncoding[uTypeName];
|
||||||
const dv = ArrayType.createDataVector(builder, uData);
|
const dv = ArrayType.createDataVector(builder, uData);
|
||||||
builder.startObject(1);
|
builder.startObject(1);
|
||||||
@@ -122,8 +114,7 @@ function encodeTypedArray(builder: any, uType: any, uData: any) {
|
|||||||
return builder.endObject();
|
return builder.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function encodeMatrix(columns, colIndex = undefined) {
|
||||||
function encodeMatrix(columns: any, colIndex = undefined) {
|
|
||||||
/*
|
/*
|
||||||
IMPORTANT: this is not a general purpose encoder. in particular,
|
IMPORTANT: this is not a general purpose encoder. in particular,
|
||||||
it doesn't correctly handle all column index types, nor does it
|
it doesn't correctly handle all column index types, nor does it
|
||||||
@@ -132,7 +123,6 @@ function encodeMatrix(columns: any, colIndex = undefined) {
|
|||||||
encodeMatrixFBS in matrix.py is more general. This is used only
|
encodeMatrixFBS in matrix.py is more general. This is used only
|
||||||
as a testing santity check (alt implementation).
|
as a testing santity check (alt implementation).
|
||||||
*/
|
*/
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
|
||||||
const utf8Encoder = new TextEncoder("utf-8");
|
const utf8Encoder = new TextEncoder("utf-8");
|
||||||
const builder = new flatbuffers.Builder(1024);
|
const builder = new flatbuffers.Builder(1024);
|
||||||
const cols = map(columns, (carr) => {
|
const cols = map(columns, (carr) => {
|
||||||
@@ -182,13 +172,11 @@ function encodeMatrix(columns: any, colIndex = undefined) {
|
|||||||
|
|
||||||
const anAnnotationsObsFBSResponse = (() => {
|
const anAnnotationsObsFBSResponse = (() => {
|
||||||
const columns = zip(...anAnnotationsObsJSONResponse.data).slice(1);
|
const columns = zip(...anAnnotationsObsJSONResponse.data).slice(1);
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string[]' is not assignable to p... Remove this comment to see the full error message
|
|
||||||
return encodeMatrix(columns, anAnnotationsObsJSONResponse.names);
|
return encodeMatrix(columns, anAnnotationsObsJSONResponse.names);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const anAnnotationsVarFBSResponse = (() => {
|
const anAnnotationsVarFBSResponse = (() => {
|
||||||
const columns = zip(...anAnnotationsVarJSONResponse.data).slice(1);
|
const columns = zip(...anAnnotationsVarJSONResponse.data).slice(1);
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string[]' is not assignable to p... Remove this comment to see the full error message
|
|
||||||
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
|
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -197,13 +185,11 @@ const aLayoutFBSResponse = (() => {
|
|||||||
new Float32Array(nObs).fill(Math.random()),
|
new Float32Array(nObs).fill(Math.random()),
|
||||||
new Float32Array(nObs).fill(Math.random()),
|
new Float32Array(nObs).fill(Math.random()),
|
||||||
];
|
];
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string[]' is not assignable to p... Remove this comment to see the full error message
|
|
||||||
return encodeMatrix(coords, ["umap_0", "umap_1"]);
|
return encodeMatrix(coords, ["umap_0", "umap_1"]);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const aDataObsResponse = {
|
const aDataObsResponse = {
|
||||||
var: [2, 4, 29],
|
var: [2, 4, 29],
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
|
||||||
obs: _()
|
obs: _()
|
||||||
.range(nObs)
|
.range(nObs)
|
||||||
.map((idx) => [idx, Math.random(), Math.random(), Math.random()])
|
.map((idx) => [idx, Math.random(), Math.random(), Math.random()])
|
||||||
+16
-60
@@ -126,8 +126,7 @@ const someData = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let payments = null;
|
||||||
let payments: any = null;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
payments = new Crossfilter(someData);
|
payments = new Crossfilter(someData);
|
||||||
});
|
});
|
||||||
@@ -139,13 +138,7 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
expect(payments.all()).toEqual(someData);
|
expect(payments.all()).toEqual(someData);
|
||||||
|
|
||||||
const p = payments
|
const p = payments
|
||||||
.addDimension(
|
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
||||||
"quantity",
|
|
||||||
"scalar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(i: any, d: any) => d[i].quantity,
|
|
||||||
Int32Array
|
|
||||||
)
|
|
||||||
.select("quantity", { mode: "all" });
|
.select("quantity", { mode: "all" });
|
||||||
expect(p).toBeDefined();
|
expect(p).toBeDefined();
|
||||||
expect(p.all()).toEqual(someData);
|
expect(p.all()).toEqual(someData);
|
||||||
@@ -165,8 +158,7 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
const p2 = payments.addDimension(
|
const p2 = payments.addDimension(
|
||||||
"quantity",
|
"quantity",
|
||||||
"scalar",
|
"scalar",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(i, data) => data[i].quantity,
|
||||||
(i: any, data: any) => data[i].quantity,
|
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -183,22 +175,10 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
|
|
||||||
test("select all and none", () => {
|
test("select all and none", () => {
|
||||||
let p = payments
|
let p = payments
|
||||||
.addDimension(
|
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
||||||
"quantity",
|
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
|
||||||
"scalar",
|
.addDimension("total", "scalar", (i, d) => d[i].total, Float32Array)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
.addDimension("type", "enum", (i, d) => d[i].type);
|
||||||
(i: any, d: any) => d[i].quantity,
|
|
||||||
Int32Array
|
|
||||||
) // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
.addDimension("tip", "scalar", (i: any, d: any) => d[i].tip, Float32Array)
|
|
||||||
.addDimension(
|
|
||||||
"total",
|
|
||||||
"scalar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(i: any, d: any) => d[i].total,
|
|
||||||
Float32Array
|
|
||||||
) // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
.addDimension("type", "enum", (i: any, d: any) => d[i].type);
|
|
||||||
expect(p).toBeDefined();
|
expect(p).toBeDefined();
|
||||||
|
|
||||||
/* expect all records to be selected - default init state */
|
/* expect all records to be selected - default init state */
|
||||||
@@ -250,24 +230,11 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("scalar dimension", () => {
|
describe("scalar dimension", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let p;
|
||||||
let p: any;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
p = payments
|
p = payments
|
||||||
.addDimension(
|
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
||||||
"quantity",
|
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
|
||||||
"scalar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(i: any, d: any) => d[i].quantity,
|
|
||||||
Int32Array
|
|
||||||
)
|
|
||||||
.addDimension(
|
|
||||||
"tip",
|
|
||||||
"scalar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(i: any, d: any) => d[i].tip,
|
|
||||||
Float32Array
|
|
||||||
)
|
|
||||||
.select("tip", { mode: "all" });
|
.select("tip", { mode: "all" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,11 +277,9 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("enum dimension", () => {
|
describe("enum dimension", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let p;
|
||||||
let p: any;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
p = payments.addDimension("type", "enum", (i, d) => d[i].type);
|
||||||
p = payments.addDimension("type", "enum", (i: any, d: any) => d[i].type);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("all", () => {
|
test("all", () => {
|
||||||
@@ -352,8 +317,7 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("spatial dimension", () => {
|
describe("spatial dimension", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let p;
|
||||||
let p: any;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const X = someData.map((r) => r.coords[0]);
|
const X = someData.map((r) => r.coords[0]);
|
||||||
const Y = someData.map((r) => r.coords[1]);
|
const Y = someData.map((r) => r.coords[1]);
|
||||||
@@ -442,22 +406,14 @@ describe("ImmutableTypedCrossfilter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("non-finite scalars", () => {
|
describe("non-finite scalars", () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
let p;
|
||||||
let p: any;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
p = payments
|
p = payments
|
||||||
.addDimension(
|
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
||||||
"quantity",
|
|
||||||
"scalar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(i: any, d: any) => d[i].quantity,
|
|
||||||
Int32Array
|
|
||||||
)
|
|
||||||
.addDimension(
|
.addDimension(
|
||||||
"nonFinite",
|
"nonFinite",
|
||||||
"scalar",
|
"scalar",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(i, d) => d[i].nonFinite,
|
||||||
(i: any, d: any) => d[i].nonFinite,
|
|
||||||
Float32Array
|
Float32Array
|
||||||
)
|
)
|
||||||
.select("quantity", { mode: "all" });
|
.select("quantity", { mode: "all" });
|
||||||
+3
-3
@@ -151,9 +151,9 @@ describe("intersection", () => {
|
|||||||
[1, 2],
|
[1, 2],
|
||||||
[6, 9],
|
[6, 9],
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual(
|
||||||
PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])
|
[[1363, 2638]]
|
||||||
).toEqual([[1363, 2638]]);
|
);
|
||||||
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
|
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
|
||||||
[1, 2],
|
[1, 2],
|
||||||
]);
|
]);
|
||||||
+5
-18
@@ -15,8 +15,7 @@ paths for:
|
|||||||
const pInf = Number.POSITIVE_INFINITY;
|
const pInf = Number.POSITIVE_INFINITY;
|
||||||
const nInf = Number.NEGATIVE_INFINITY;
|
const nInf = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function fillRange(arr, start = 0) {
|
||||||
function fillRange(arr: any, start = 0) {
|
|
||||||
const larr = arr;
|
const larr = arr;
|
||||||
for (let i = 0, len = larr.length; i < len; i += 1) {
|
for (let i = 0, len = larr.length; i < len; i += 1) {
|
||||||
larr[i] = i + start;
|
larr[i] = i + start;
|
||||||
@@ -24,8 +23,7 @@ function fillRange(arr: any, start = 0) {
|
|||||||
return larr;
|
return larr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function fillRand(arr) {
|
||||||
function fillRand(arr: any) {
|
|
||||||
for (let i = 0, len = arr.length; i < len; i += 1) {
|
for (let i = 0, len = arr.length; i < len; i += 1) {
|
||||||
arr[i] = Math.random();
|
arr[i] = Math.random();
|
||||||
}
|
}
|
||||||
@@ -50,22 +48,16 @@ describe("sortArray", () => {
|
|||||||
describe("finite numbers", () => {
|
describe("finite numbers", () => {
|
||||||
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
|
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
|
||||||
test(Type.name, () => {
|
test(Type.name, () => {
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1, 0]))).toMatchObject(
|
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1, 0]))).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
Type.from([0, 1, 2, 3, 4, 5, 6])
|
Type.from([0, 1, 2, 3, 4, 5, 6])
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1]))).toMatchObject(
|
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1]))).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
Type.from([1, 2, 3, 4, 5, 6])
|
Type.from([1, 2, 3, 4, 5, 6])
|
||||||
);
|
);
|
||||||
|
|
||||||
const source = fillRand(new Type(1000));
|
const source = fillRand(new Type(1000));
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
expect(sortArray(Type.from(source))).toMatchObject(
|
expect(sortArray(Type.from(source))).toMatchObject(
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
Type.from(source).sort()
|
Type.from(source).sort()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -138,27 +130,22 @@ describe("sortIndex", () => {
|
|||||||
describe("finite numbers", () => {
|
describe("finite numbers", () => {
|
||||||
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
|
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
|
||||||
test(Type.name, () => {
|
test(Type.name, () => {
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
const source1 = Type.from([6, 5, 4, 3, 2, 1, 0]);
|
const source1 = Type.from([6, 5, 4, 3, 2, 1, 0]);
|
||||||
const index1 = fillRange(new Uint32Array(source1.length));
|
const index1 = fillRange(new Uint32Array(source1.length));
|
||||||
expect(sortIndex(index1, source1)).toMatchObject(
|
expect(sortIndex(index1, source1)).toMatchObject(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
index1.sort((a, b) => source1[a] - source1[b])
|
||||||
index1.sort((a: any, b: any) => source1[a] - source1[b])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
|
|
||||||
const source2 = Type.from([6, 5, 4, 3, 2, 1]);
|
const source2 = Type.from([6, 5, 4, 3, 2, 1]);
|
||||||
const index2 = fillRange(new Uint32Array(source2.length));
|
const index2 = fillRange(new Uint32Array(source2.length));
|
||||||
expect(sortIndex(index2, source2)).toMatchObject(
|
expect(sortIndex(index2, source2)).toMatchObject(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
index2.sort((a, b) => source1[a] - source1[b])
|
||||||
index2.sort((a: any, b: any) => source1[a] - source1[b])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const source3 = fillRand(new Type(1000));
|
const source3 = fillRand(new Type(1000));
|
||||||
const index3 = fillRange(new Uint32Array(source3.length));
|
const index3 = fillRange(new Uint32Array(source3.length));
|
||||||
expect(sortIndex(index3, source3)).toMatchObject(
|
expect(sortIndex(index3, source3)).toMatchObject(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
index3.sort((a, b) => source1[a] - source1[b])
|
||||||
index3.sort((a: any, b: any) => source1[a] - source1[b])
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -11,13 +11,13 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@babel/preset-react",
|
"@babel/preset-react",
|
||||||
"@babel/preset-typescript",
|
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
"@babel/plugin-proposal-function-bind",
|
"@babel/plugin-proposal-function-bind",
|
||||||
["@babel/plugin-proposal-decorators", { legacy: true }],
|
["@babel/plugin-proposal-decorators", { legacy: true }],
|
||||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||||
["@babel/plugin-proposal-private-methods", { loose: true }],
|
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@babel/preset-react",
|
"@babel/preset-react",
|
||||||
"@babel/preset-typescript",
|
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
"@babel/plugin-proposal-function-bind",
|
"@babel/plugin-proposal-function-bind",
|
||||||
["@babel/plugin-proposal-decorators", { legacy: true }],
|
["@babel/plugin-proposal-decorators", { legacy: true }],
|
||||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||||
["@babel/plugin-proposal-private-methods", { loose: true }],
|
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||||
|
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
"@babel/plugin-transform-react-constant-elements",
|
"@babel/plugin-transform-react-constant-elements",
|
||||||
"@babel/plugin-transform-runtime",
|
"@babel/plugin-transform-runtime",
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
/* eslint-disable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "babel-eslint",
|
||||||
extends: [
|
extends: [
|
||||||
"airbnb-typescript",
|
"airbnb",
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:eslint-comments/recommended",
|
"plugin:eslint-comments/recommended",
|
||||||
"plugin:@blueprintjs/recommended",
|
"plugin:@blueprintjs/recommended",
|
||||||
"plugin:compat/recommended",
|
"plugin:compat/recommended",
|
||||||
@@ -37,50 +35,18 @@ module.exports = {
|
|||||||
jsx: true,
|
jsx: true,
|
||||||
generators: true,
|
generators: true,
|
||||||
},
|
},
|
||||||
// (thuang): Pairing with `tsconfigRootDir`, which points to the directory
|
|
||||||
// of eslint.js
|
|
||||||
project: "../../tsconfig.json",
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react/jsx-no-target-blank": "off",
|
"react/jsx-no-target-blank": "off",
|
||||||
"eslint-comments/require-description": ["error"],
|
"eslint-comments/require-description": ["error"],
|
||||||
"no-magic-numbers": "off",
|
"no-magic-numbers": "off",
|
||||||
"@typescript-eslint/no-magic-numbers": "off",
|
|
||||||
"no-nested-ternary": "off",
|
"no-nested-ternary": "off",
|
||||||
"func-style": "off",
|
"func-style": "off",
|
||||||
"arrow-parens": "off",
|
"arrow-parens": "off",
|
||||||
"no-use-before-define": "off",
|
"no-use-before-define": "off",
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
|
||||||
"react/jsx-filename-extension": "off",
|
"react/jsx-filename-extension": "off",
|
||||||
"comma-dangle": "off",
|
"comma-dangle": "off",
|
||||||
"@typescript-eslint/comma-dangle": "off",
|
|
||||||
"no-underscore-dangle": "off",
|
"no-underscore-dangle": "off",
|
||||||
// Override airbnb config to allow leading underscore
|
|
||||||
// https://github.com/iamturns/eslint-config-airbnb-typescript/blob/master/lib/shared.js#L35
|
|
||||||
"@typescript-eslint/naming-convention": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
selector: "class",
|
|
||||||
format: ["PascalCase"],
|
|
||||||
leadingUnderscore: "allow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "function",
|
|
||||||
format: ["camelCase", "PascalCase"],
|
|
||||||
leadingUnderscore: "allowSingleOrDouble",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "typeLike",
|
|
||||||
format: ["PascalCase"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "variable",
|
|
||||||
format: ["camelCase", "PascalCase", "UPPER_CASE"],
|
|
||||||
leadingUnderscore: "allowSingleOrDouble",
|
|
||||||
trailingUnderscore: "allowDouble",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"implicit-arrow-linebreak": "off",
|
"implicit-arrow-linebreak": "off",
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
||||||
@@ -88,7 +54,6 @@ module.exports = {
|
|||||||
"object-curly-newline": ["error", { consistent: true }],
|
"object-curly-newline": ["error", { consistent: true }],
|
||||||
"react/prop-types": [0],
|
"react/prop-types": [0],
|
||||||
"space-before-function-paren": "off",
|
"space-before-function-paren": "off",
|
||||||
"@typescript-eslint/space-before-function-paren": "off",
|
|
||||||
"function-paren-newline": "off",
|
"function-paren-newline": "off",
|
||||||
"prefer-destructuring": ["error", { object: true, array: false }],
|
"prefer-destructuring": ["error", { object: true, array: false }],
|
||||||
"import/prefer-default-export": "off",
|
"import/prefer-default-export": "off",
|
||||||
@@ -107,9 +72,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["**/*.test.ts"],
|
files: ["**/*.test.js"],
|
||||||
env: {
|
env: {
|
||||||
jest: true, // now **/*.test.ts files' env has both es6 *and* jest
|
jest: true, // now **/*.test.js files' env has both es6 *and* jest
|
||||||
},
|
},
|
||||||
// Can't extend in overrides: https://github.com/eslint/eslint/issues/8813
|
// Can't extend in overrides: https://github.com/eslint/eslint/issues/8813
|
||||||
// "extends": ["plugin:jest/recommended"]
|
// "extends": ["plugin:jest/recommended"]
|
||||||
@@ -124,4 +89,3 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
/* eslint-enable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"*.{js,ts,jsx,tsx}": "eslint --fix",
|
"*.js": "eslint --fix",
|
||||||
"**/*": "prettier --write --ignore-unknown",
|
"**/*": "prettier --write --ignore-unknown",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
/* eslint-disable import/no-extraneous-dependencies -- this file is a devDependency*/
|
/* eslint-disable import/no-extraneous-dependencies -- this file is a devDependency*/
|
||||||
/* eslint-disable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const cheerio = require("cheerio");
|
const cheerio = require("cheerio");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
const digest = (str) => {
|
const digest = (str) => {
|
||||||
@@ -54,5 +50,4 @@ class CspHashPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = CspHashPlugin;
|
module.exports = CspHashPlugin;
|
||||||
/* eslint-enable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
/* eslint-enable import/no-extraneous-dependencies -- enable*/
|
/* eslint-enable import/no-extraneous-dependencies -- enable*/
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/cellxgene-logo.png"
|
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/cellxgene-logo.png"
|
||||||
style="width: 320px"
|
style="width: 320px;"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style="
|
style="
|
||||||
@@ -37,34 +37,36 @@
|
|||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div style="margin-bottom: 0; font-weight: bolder; font-size: 1.2em">
|
<div style="margin-bottom: 0; font-weight: bolder; font-size: 1.2em;">
|
||||||
Unsupported Browser
|
Unsupported Browser
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 0">
|
<div style="margin-top: 0;">
|
||||||
cellxgene is currently supported on the following browsers
|
cellxgene is currently supported on the following browsers
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-around; margin-top: 16px">
|
<div
|
||||||
|
style="display: flex; justify-content: space-around; margin-top: 16px;"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.google.com/chrome/?hl=en%22"
|
href="https://www.google.com/chrome/?hl=en%22"
|
||||||
aria-label="Download Google Chrome"
|
aria-label="Download Google Chrome"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/chrome.png"
|
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/chrome.png"
|
||||||
style="width: 80px; height: 80px"
|
style="width: 80px; height: 80px;"
|
||||||
/>
|
/>
|
||||||
<div>Chrome > 60</div>
|
<div>Chrome > 60</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.mozilla.com/firefox/" aria-label="Download Firefox">
|
<a href="https://www.mozilla.com/firefox/" aria-label="Download Firefox">
|
||||||
<img
|
<img
|
||||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/firefox.png"
|
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/firefox.png"
|
||||||
style="width: 80px; height: 80px"
|
style="width: 80px; height: 80px;"
|
||||||
/>
|
/>
|
||||||
<div>Firefox ≥ 60</div>
|
<div>Firefox ≥ 60</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="//www.microsoft.com/edge" aria-label="Download Edge">
|
<a href="//www.microsoft.com/edge" aria-label="Download Edge">
|
||||||
<img
|
<img
|
||||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
|
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
|
||||||
style="width: 80px; height: 80px"
|
style="width: 80px; height: 80px;"
|
||||||
/>
|
/>
|
||||||
<div>Edge ≥ 79</div>
|
<div>Edge ≥ 79</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
/* eslint-disable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const { merge } = require("webpack-merge");
|
const { merge } = require("webpack-merge");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const sharedConfig = require("./webpack.config.shared");
|
const sharedConfig = require("./webpack.config.shared");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const babelOptions = require("../babel/babel.dev");
|
const babelOptions = require("../babel/babel.dev");
|
||||||
|
|
||||||
const fonts = path.resolve("src/fonts");
|
const fonts = path.resolve("src/fonts");
|
||||||
@@ -33,7 +23,7 @@ const devConfig = {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(ts|js)x?$/,
|
test: /\.jsx?$/,
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
options: babelOptions,
|
options: babelOptions,
|
||||||
},
|
},
|
||||||
@@ -93,4 +83,3 @@ const devConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, devConfig);
|
module.exports = merge(sharedConfig, devConfig);
|
||||||
/* eslint-enable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const TerserJSPlugin = require("terser-webpack-plugin");
|
const TerserJSPlugin = require("terser-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const CleanCss = require("clean-css");
|
const CleanCss = require("clean-css");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const { merge } = require("webpack-merge");
|
const { merge } = require("webpack-merge");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const babelOptions = require("../babel/babel.prod");
|
const babelOptions = require("../babel/babel.prod");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const CspHashPlugin = require("./cspHashPlugin");
|
const CspHashPlugin = require("./cspHashPlugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const sharedConfig = require("./webpack.config.shared");
|
const sharedConfig = require("./webpack.config.shared");
|
||||||
|
|
||||||
const fonts = path.resolve("src/fonts");
|
const fonts = path.resolve("src/fonts");
|
||||||
@@ -51,7 +38,7 @@ const prodConfig = {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(ts|js)x?$/,
|
test: /\.jsx?$/,
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
options: babelOptions,
|
options: babelOptions,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
/* eslint-disable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const ObsoleteWebpackPlugin = require("obsolete-webpack-plugin");
|
const ObsoleteWebpackPlugin = require("obsolete-webpack-plugin");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires --- FIXME: disabled temporarily on migrate to TS.
|
// eslint-disable-next-line @blueprintjs/classes-constants -- incorrect match
|
||||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||||
|
|
||||||
const src = path.resolve("src");
|
const src = path.resolve("src");
|
||||||
@@ -34,9 +29,6 @@ module.exports = {
|
|||||||
path: path.resolve("build"),
|
path: path.resolve("build"),
|
||||||
publicPath,
|
publicPath,
|
||||||
},
|
},
|
||||||
resolve: {
|
|
||||||
extensions: [".ts", ".tsx", "..."],
|
|
||||||
},
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -80,4 +72,3 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
/* eslint-enable @blueprintjs/classes-constants -- we don't import blueprint here */
|
|
||||||
|
|||||||
Generated
+1389
-2545
File diff suppressed because it is too large
Load Diff
+15
-40
@@ -8,14 +8,13 @@
|
|||||||
"build": "npm run clean && webpack --config",
|
"build": "npm run clean && webpack --config",
|
||||||
"clean": "rimraf build",
|
"clean": "rimraf build",
|
||||||
"dev": "npm run build -- configuration/webpack/webpack.config.dev.js",
|
"dev": "npm run build -- configuration/webpack/webpack.config.dev.js",
|
||||||
"e2e": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.ts",
|
"e2e": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
|
||||||
"e2e-annotations": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2eAnnotations.test.ts",
|
"e2e-annotations": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2eAnnotations.test.js",
|
||||||
"e2e-prod": "CXG_URL_BASE='https://cellxgene.cziscience.com/d/pbmc3k.cxg/' jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.ts",
|
"e2e-prod": "CXG_URL_BASE='https://cellxgene.cziscience.com/d/pbmc3k.cxg/' jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
|
||||||
"fmt": "eslint --fix src __tests__",
|
"fmt": "eslint --fix src __tests__",
|
||||||
"lint": "eslint src __tests__ & npm run type-check",
|
"lint": "eslint --fix src __tests__",
|
||||||
"prod": "npm run build -- configuration/webpack/webpack.config.prod.js",
|
"prod": "npm run build -- configuration/webpack/webpack.config.prod.js",
|
||||||
"test": "jest --testPathIgnorePatterns e2e",
|
"test": "jest --testPathIgnorePatterns e2e"
|
||||||
"type-check": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -89,38 +88,10 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||||
"@babel/preset-env": "^7.13.15",
|
"@babel/preset-env": "^7.13.15",
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
"@babel/preset-typescript": "^7.14.5",
|
|
||||||
"@babel/register": "^7.13.16",
|
"@babel/register": "^7.13.16",
|
||||||
"@babel/runtime": "^7.13.16",
|
"@babel/runtime": "^7.13.16",
|
||||||
"@blueprintjs/eslint-plugin": "^0.3.0",
|
"@blueprintjs/eslint-plugin": "^0.3.0",
|
||||||
"@sentry/webpack-plugin": "^1.15.0",
|
"@sentry/webpack-plugin": "^1.15.0",
|
||||||
"@types/d3": "^7.0.0",
|
|
||||||
"@types/d3-scale-chromatic": "^3.0.0",
|
|
||||||
"@types/expect-puppeteer": "^4.4.6",
|
|
||||||
"@types/flatbuffers": "^1.10.0",
|
|
||||||
"@types/is-number": "^7.0.1",
|
|
||||||
"@types/jest": "^26.0.24",
|
|
||||||
"@types/jest-environment-puppeteer": "^4.4.1",
|
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
|
||||||
"@types/lodash.difference": "^4.5.6",
|
|
||||||
"@types/lodash.every": "^4.6.6",
|
|
||||||
"@types/lodash.filter": "^4.6.6",
|
|
||||||
"@types/lodash.foreach": "^4.5.6",
|
|
||||||
"@types/lodash.isnumber": "^3.0.6",
|
|
||||||
"@types/lodash.map": "^4.6.13",
|
|
||||||
"@types/lodash.pull": "^4.1.6",
|
|
||||||
"@types/lodash.sortby": "^4.7.6",
|
|
||||||
"@types/lodash.uniq": "^4.5.6",
|
|
||||||
"@types/lodash.zip": "^4.2.6",
|
|
||||||
"@types/pako": "^1.0.2",
|
|
||||||
"@types/puppeteer": "^5.4.4",
|
|
||||||
"@types/react": "^17.0.14",
|
|
||||||
"@types/react-dom": "^17.0.9",
|
|
||||||
"@types/react-helmet": "^6.1.2",
|
|
||||||
"@types/react-redux": "^7.1.18",
|
|
||||||
"@types/sha1": "^1.1.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.4",
|
|
||||||
"@typescript-eslint/parser": "^4.28.4",
|
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.1.0",
|
"babel-jest": "^26.1.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
@@ -132,7 +103,7 @@
|
|||||||
"codecov": "^3.7.1",
|
"codecov": "^3.7.1",
|
||||||
"css-loader": "^5.2.4",
|
"css-loader": "^5.2.4",
|
||||||
"eslint": "^7.24.0",
|
"eslint": "^7.24.0",
|
||||||
"eslint-config-airbnb-typescript": "^12.3.1",
|
"eslint-config-airbnb": "^18.2.0",
|
||||||
"eslint-config-prettier": "^8.2.0",
|
"eslint-config-prettier": "^8.2.0",
|
||||||
"eslint-loader": "^4.0.2",
|
"eslint-loader": "^4.0.2",
|
||||||
"eslint-plugin-compat": "^3.8.0",
|
"eslint-plugin-compat": "^3.8.0",
|
||||||
@@ -172,7 +143,6 @@
|
|||||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"terser-webpack-plugin": "^5.1.1",
|
"terser-webpack-plugin": "^5.1.1",
|
||||||
"typescript": "^4.3.5",
|
|
||||||
"webpack": "^5.34.0",
|
"webpack": "^5.34.0",
|
||||||
"webpack-cli": "^4.6.0",
|
"webpack-cli": "^4.6.0",
|
||||||
"webpack-dev-middleware": "^4.1.0",
|
"webpack-dev-middleware": "^4.1.0",
|
||||||
@@ -180,10 +150,10 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"**/__tests__/**/?(*.)(spec|test).ts?(x)"
|
"**/__tests__/**/?(*.)(spec|test).js?(x)"
|
||||||
],
|
],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"./__tests__/setupMissingGlobals.ts"
|
"./__tests__/setupMissingGlobals.js"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "./coverage/",
|
"coverageDirectory": "./coverage/",
|
||||||
"collectCoverage": true
|
"collectCoverage": true
|
||||||
@@ -193,8 +163,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"presets": [
|
"presets": [
|
||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
"@babel/preset-react",
|
"@babel/preset-react"
|
||||||
"@babel/preset-typescript"
|
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@babel/plugin-proposal-function-bind",
|
"@babel/plugin-proposal-function-bind",
|
||||||
@@ -216,6 +185,12 @@
|
|||||||
"loose": true
|
"loose": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"@babel/plugin-proposal-private-property-in-object",
|
||||||
|
{
|
||||||
|
"loose": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
"@babel/plugin-proposal-export-namespace-from",
|
||||||
"@babel/plugin-transform-react-constant-elements",
|
"@babel/plugin-transform-react-constant-elements",
|
||||||
"@babel/plugin-transform-runtime",
|
"@babel/plugin-transform-runtime",
|
||||||
|
|||||||
@@ -0,0 +1,455 @@
|
|||||||
|
/*
|
||||||
|
Action creators for user annotation
|
||||||
|
*/
|
||||||
|
import difference from "lodash.difference";
|
||||||
|
import pako from "pako";
|
||||||
|
import * as globals from "../globals";
|
||||||
|
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
|
||||||
|
|
||||||
|
const { isUserAnnotation } = AnnotationsHelpers;
|
||||||
|
|
||||||
|
export const annotationCreateCategoryAction =
|
||||||
|
(newCategoryName, categoryToDuplicate) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
Add a new user-created category to the obs annotations.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
newCategoryName - string name for the category.
|
||||||
|
categoryToDuplicate - obs category to use for initial values, or null.
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
const { schema } = prevAnnoMatrix;
|
||||||
|
|
||||||
|
/* name must be a string, non-zero length */
|
||||||
|
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
|
||||||
|
throw new Error("user annotations require string name");
|
||||||
|
|
||||||
|
/* ensure the name isn't already in use! */
|
||||||
|
if (schema.annotations.obsByName[newCategoryName])
|
||||||
|
throw new Error("name collision on annotation category create");
|
||||||
|
|
||||||
|
let initialValue;
|
||||||
|
let newSchema;
|
||||||
|
let ctor;
|
||||||
|
if (categoryToDuplicate) {
|
||||||
|
/* if we are duplicating a category, retrieve it */
|
||||||
|
const catDupSchema = schema.annotations.obsByName[categoryToDuplicate];
|
||||||
|
const catDupType = catDupSchema?.type;
|
||||||
|
if (catDupType !== "string" && catDupType !== "categorical")
|
||||||
|
throw new Error(
|
||||||
|
"categoryToDuplicate does not exist or has invalid type"
|
||||||
|
);
|
||||||
|
|
||||||
|
const catToDupDf = await prevAnnoMatrix
|
||||||
|
.base()
|
||||||
|
.fetch("obs", categoryToDuplicate);
|
||||||
|
const col = catToDupDf.col(categoryToDuplicate);
|
||||||
|
initialValue = col.asArray();
|
||||||
|
const { categories } = col.summarizeCategorical();
|
||||||
|
// all user-created annotations must have the unassigned category
|
||||||
|
if (!categories.includes(globals.unassignedCategoryLabel)) {
|
||||||
|
categories.push(globals.unassignedCategoryLabel);
|
||||||
|
}
|
||||||
|
ctor = initialValue.constructor;
|
||||||
|
newSchema = {
|
||||||
|
...catDupSchema,
|
||||||
|
name: newCategoryName,
|
||||||
|
categories,
|
||||||
|
writable: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
/* else assign to the standard default value */
|
||||||
|
initialValue = globals.unassignedCategoryLabel;
|
||||||
|
ctor = Array;
|
||||||
|
newSchema = {
|
||||||
|
name: newCategoryName,
|
||||||
|
type: "categorical",
|
||||||
|
categories: [globals.unassignedCategoryLabel],
|
||||||
|
writable: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const obsCrossfilter = prevObsCrossfilter.addObsColumn(
|
||||||
|
newSchema,
|
||||||
|
ctor,
|
||||||
|
initialValue
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: create category",
|
||||||
|
data: newCategoryName,
|
||||||
|
categoryToDuplicate,
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationRenameCategoryAction =
|
||||||
|
(oldCategoryName, newCategoryName) => (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
Rename a user-created annotation category
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, oldCategoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
/* name must be a string, non-zero length */
|
||||||
|
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
|
||||||
|
throw new Error("user annotations require string name");
|
||||||
|
|
||||||
|
if (oldCategoryName === newCategoryName) return;
|
||||||
|
|
||||||
|
const obsCrossfilter = prevObsCrossfilter.renameObsColumn(
|
||||||
|
oldCategoryName,
|
||||||
|
newCategoryName
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: category edited",
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
metadataField: oldCategoryName,
|
||||||
|
newCategoryText: newCategoryName,
|
||||||
|
data: newCategoryName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationDeleteCategoryAction =
|
||||||
|
(categoryName) => (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
Delete a user-created category
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
const obsCrossfilter = prevObsCrossfilter.dropObsColumn(categoryName);
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: delete category",
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
metadataField: categoryName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationCreateLabelInCategory =
|
||||||
|
(categoryName, labelName, assignSelected) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
Add a new label to a user-defined category. If assignSelected is true, assign
|
||||||
|
the label to all currently selected cells.
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
let obsCrossfilter = prevObsCrossfilter.addObsAnnoCategory(
|
||||||
|
categoryName,
|
||||||
|
labelName
|
||||||
|
);
|
||||||
|
if (assignSelected) {
|
||||||
|
obsCrossfilter = await obsCrossfilter.setObsColumnValues(
|
||||||
|
categoryName,
|
||||||
|
prevObsCrossfilter.allSelectedLabels(),
|
||||||
|
labelName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: add new label to category",
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
metadataField: categoryName,
|
||||||
|
newLabelText: labelName,
|
||||||
|
assignSelectedCells: assignSelected,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationDeleteLabelFromCategory =
|
||||||
|
(categoryName, labelName) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
delete a label from a user-defined category
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.removeObsAnnoCategory(
|
||||||
|
categoryName,
|
||||||
|
labelName,
|
||||||
|
globals.unassignedCategoryLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: delete label",
|
||||||
|
metadataField: categoryName,
|
||||||
|
label: labelName,
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationRenameLabelInCategory =
|
||||||
|
(categoryName, oldLabelName, newLabelName) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
label name change
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
let obsCrossfilter = await prevObsCrossfilter.resetObsColumnValues(
|
||||||
|
categoryName,
|
||||||
|
oldLabelName,
|
||||||
|
newLabelName
|
||||||
|
);
|
||||||
|
obsCrossfilter = await obsCrossfilter.removeObsAnnoCategory(
|
||||||
|
categoryName,
|
||||||
|
oldLabelName,
|
||||||
|
globals.unassignedCategoryLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: label edited",
|
||||||
|
editedLabel: newLabelName,
|
||||||
|
metadataField: categoryName,
|
||||||
|
label: oldLabelName,
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
obsCrossfilter,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const annotationLabelCurrentSelection =
|
||||||
|
(categoryName, labelName) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
set the label on all currently selected
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
|
getState();
|
||||||
|
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
||||||
|
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
||||||
|
throw new Error("not a user annotation");
|
||||||
|
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.setObsColumnValues(
|
||||||
|
categoryName,
|
||||||
|
prevObsCrossfilter.allSelectedLabels(),
|
||||||
|
labelName
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "annotation: label current cell selection",
|
||||||
|
metadataField: categoryName,
|
||||||
|
label: labelName,
|
||||||
|
obsCrossfilter,
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function writableAnnotations(annoMatrix) {
|
||||||
|
return annoMatrix.schema.annotations.obs.columns
|
||||||
|
.filter((s) => s.writable)
|
||||||
|
.map((s) => s.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const needToSaveObsAnnotations = (annoMatrix, lastSavedAnnoMatrix) => {
|
||||||
|
/*
|
||||||
|
Return true if there are LIKELY user-defined annotation modifications between the two
|
||||||
|
annoMatrices. Technically not an action creator, but intimately intertwined
|
||||||
|
with the save process.
|
||||||
|
|
||||||
|
Two conditions will trigger a need to save:
|
||||||
|
* the collection of user-defined columns have changed
|
||||||
|
* the contents of the user-defined columns have change
|
||||||
|
*/
|
||||||
|
|
||||||
|
annoMatrix = annoMatrix.base();
|
||||||
|
|
||||||
|
// if the annoMatrix hasn't changed, we are guaranteed no changes to the matrix schema or contents.
|
||||||
|
if (annoMatrix === lastSavedAnnoMatrix) return false;
|
||||||
|
|
||||||
|
// if the schema has changed, we need to save
|
||||||
|
const currentWritable = writableAnnotations(annoMatrix);
|
||||||
|
if (difference(currentWritable, writableAnnotations(lastSavedAnnoMatrix))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no schema changes; check for change in contents
|
||||||
|
return currentWritable.some(
|
||||||
|
(col) => annoMatrix.col(col) !== lastSavedAnnoMatrix.col(col)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
Save the user-created obs annotations IF any have changed.
|
||||||
|
*/
|
||||||
|
const state = getState();
|
||||||
|
const { annotations, autosave } = state;
|
||||||
|
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
|
||||||
|
const { lastSavedAnnoMatrix, saveInProgress } = autosave;
|
||||||
|
|
||||||
|
const annoMatrix = state.annoMatrix.base();
|
||||||
|
|
||||||
|
if (saveInProgress || annoMatrix === lastSavedAnnoMatrix) return;
|
||||||
|
if (!needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix)) {
|
||||||
|
dispatch({
|
||||||
|
type: "writable obs annotations - save complete",
|
||||||
|
lastSavedAnnoMatrix: annoMatrix,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Else, we really do need to save
|
||||||
|
*/
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "writable obs annotations - save started",
|
||||||
|
});
|
||||||
|
|
||||||
|
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
|
||||||
|
const matrix = MatrixFBS.encodeMatrixFBS(df);
|
||||||
|
const compressedMatrix = pako.deflate(matrix);
|
||||||
|
try {
|
||||||
|
const queryString =
|
||||||
|
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
||||||
|
? `?annotation-collection-name=${encodeURIComponent(
|
||||||
|
dataCollectionName
|
||||||
|
)}`
|
||||||
|
: "";
|
||||||
|
const res = await fetch(
|
||||||
|
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
body: compressedMatrix,
|
||||||
|
headers: new Headers({
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
}),
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
dispatch({
|
||||||
|
type: "writable obs annotations - save complete",
|
||||||
|
lastSavedAnnoMatrix: annoMatrix,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: "writable obs annotations - save error",
|
||||||
|
message: `HTTP error ${res.status} - ${res.statusText}`,
|
||||||
|
res,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: "writable obs annotations - save error",
|
||||||
|
message: error.toString(),
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveGenesetsAction = () => async (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
// bail if gene sets not available, or in readonly mode.
|
||||||
|
const { config } = state;
|
||||||
|
const { lastTid, genesets } = state.genesets;
|
||||||
|
|
||||||
|
const genesetsAreAvailable =
|
||||||
|
config?.parameters?.annotations_genesets ?? false;
|
||||||
|
const genesetsReadonly =
|
||||||
|
config?.parameters?.annotations_genesets_readonly ?? true;
|
||||||
|
if (!genesetsAreAvailable || genesetsReadonly) {
|
||||||
|
// our non-save was completed!
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets complete",
|
||||||
|
lastSavedGenesets: genesets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "autosave: genesets started",
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Create the JSON OTA data structure */
|
||||||
|
const tid = (lastTid ?? 0) + 1;
|
||||||
|
const genesetsOTA = [];
|
||||||
|
for (const [name, gs] of genesets) {
|
||||||
|
const genes = [];
|
||||||
|
for (const g of gs.genes.values()) {
|
||||||
|
genes.push({
|
||||||
|
gene_symbol: g.geneSymbol,
|
||||||
|
gene_description: g.geneDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
genesetsOTA.push({
|
||||||
|
geneset_name: name,
|
||||||
|
geneset_description: gs.genesetDescription,
|
||||||
|
genes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const ota = {
|
||||||
|
tid,
|
||||||
|
genesets: genesetsOTA,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Save to server */
|
||||||
|
try {
|
||||||
|
const { dataCollectionNameIsReadOnly, dataCollectionName } =
|
||||||
|
state.annotations;
|
||||||
|
const queryString =
|
||||||
|
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
||||||
|
? `?annotation-collection-name=${encodeURIComponent(
|
||||||
|
dataCollectionName
|
||||||
|
)}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${globals.API.prefix}${globals.API.version}genesets${queryString}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(ota),
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets error",
|
||||||
|
message: `HTTP error ${res.status} - ${res.statusText}`,
|
||||||
|
res,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.all([
|
||||||
|
dispatch({
|
||||||
|
type: "autosave: genesets complete",
|
||||||
|
lastSavedGenesets: genesets,
|
||||||
|
}),
|
||||||
|
dispatch({
|
||||||
|
type: "geneset: set tid",
|
||||||
|
tid,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
return dispatch({
|
||||||
|
type: "autosave: genesets error",
|
||||||
|
message: error.toString(),
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,530 +0,0 @@
|
|||||||
/*
|
|
||||||
Action creators for user annotation
|
|
||||||
*/
|
|
||||||
import difference from "lodash.difference";
|
|
||||||
import pako from "pako";
|
|
||||||
import * as globals from "../globals";
|
|
||||||
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
|
|
||||||
|
|
||||||
const { isUserAnnotation } = AnnotationsHelpers;
|
|
||||||
|
|
||||||
export const annotationCreateCategoryAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newCategoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryToDuplicate: any
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
Add a new user-created category to the obs annotations.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
newCategoryName - string name for the category.
|
|
||||||
categoryToDuplicate - obs category to use for initial values, or null.
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
const { schema } = prevAnnoMatrix;
|
|
||||||
|
|
||||||
/* name must be a string, non-zero length */
|
|
||||||
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
|
|
||||||
throw new Error("user annotations require string name");
|
|
||||||
|
|
||||||
/* ensure the name isn't already in use! */
|
|
||||||
if (schema.annotations.obsByName[newCategoryName])
|
|
||||||
throw new Error("name collision on annotation category create");
|
|
||||||
|
|
||||||
let initialValue;
|
|
||||||
let newSchema;
|
|
||||||
let ctor;
|
|
||||||
if (categoryToDuplicate) {
|
|
||||||
/* if we are duplicating a category, retrieve it */
|
|
||||||
const catDupSchema = schema.annotations.obsByName[categoryToDuplicate];
|
|
||||||
const catDupType = catDupSchema?.type;
|
|
||||||
if (catDupType !== "string" && catDupType !== "categorical")
|
|
||||||
throw new Error("categoryToDuplicate does not exist or has invalid type");
|
|
||||||
|
|
||||||
const catToDupDf = await prevAnnoMatrix
|
|
||||||
.base()
|
|
||||||
.fetch("obs", categoryToDuplicate);
|
|
||||||
const col = catToDupDf.col(categoryToDuplicate);
|
|
||||||
initialValue = col.asArray();
|
|
||||||
const { categories } = col.summarizeCategorical();
|
|
||||||
// all user-created annotations must have the unassigned category
|
|
||||||
if (!categories.includes(globals.unassignedCategoryLabel)) {
|
|
||||||
categories.push(globals.unassignedCategoryLabel);
|
|
||||||
}
|
|
||||||
ctor = initialValue.constructor;
|
|
||||||
newSchema = {
|
|
||||||
...catDupSchema,
|
|
||||||
name: newCategoryName,
|
|
||||||
categories,
|
|
||||||
writable: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
/* else assign to the standard default value */
|
|
||||||
initialValue = globals.unassignedCategoryLabel;
|
|
||||||
ctor = Array;
|
|
||||||
newSchema = {
|
|
||||||
name: newCategoryName,
|
|
||||||
type: "categorical",
|
|
||||||
categories: [globals.unassignedCategoryLabel],
|
|
||||||
writable: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const obsCrossfilter = prevObsCrossfilter.addObsColumn(
|
|
||||||
newSchema,
|
|
||||||
ctor,
|
|
||||||
initialValue
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: create category",
|
|
||||||
data: newCategoryName,
|
|
||||||
categoryToDuplicate,
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationRenameCategoryAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
oldCategoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newCategoryName: any
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
Rename a user-created annotation category
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, oldCategoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
/* name must be a string, non-zero length */
|
|
||||||
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
|
|
||||||
throw new Error("user annotations require string name");
|
|
||||||
|
|
||||||
if (oldCategoryName === newCategoryName) return;
|
|
||||||
|
|
||||||
const obsCrossfilter = prevObsCrossfilter.renameObsColumn(
|
|
||||||
oldCategoryName,
|
|
||||||
newCategoryName
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: category edited",
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
metadataField: oldCategoryName,
|
|
||||||
newCategoryText: newCategoryName,
|
|
||||||
data: newCategoryName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const annotationDeleteCategoryAction = (categoryName: any) => (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
/*
|
|
||||||
Delete a user-created category
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
const obsCrossfilter = prevObsCrossfilter.dropObsColumn(categoryName);
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: delete category",
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
metadataField: categoryName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationCreateLabelInCategory = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labelName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
assignSelected: any // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
Add a new label to a user-defined category. If assignSelected is true, assign
|
|
||||||
the label to all currently selected cells.
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
let obsCrossfilter = prevObsCrossfilter.addObsAnnoCategory(
|
|
||||||
categoryName,
|
|
||||||
labelName
|
|
||||||
);
|
|
||||||
if (assignSelected) {
|
|
||||||
obsCrossfilter = await obsCrossfilter.setObsColumnValues(
|
|
||||||
categoryName,
|
|
||||||
prevObsCrossfilter.allSelectedLabels(),
|
|
||||||
labelName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: add new label to category",
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
metadataField: categoryName,
|
|
||||||
newLabelText: labelName,
|
|
||||||
assignSelectedCells: assignSelected,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationDeleteLabelFromCategory = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labelName: any // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
delete a label from a user-defined category
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.removeObsAnnoCategory(
|
|
||||||
categoryName,
|
|
||||||
labelName,
|
|
||||||
globals.unassignedCategoryLabel
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: delete label",
|
|
||||||
metadataField: categoryName,
|
|
||||||
label: labelName,
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationRenameLabelInCategory = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
oldLabelName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newLabelName: any
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
label name change
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
let obsCrossfilter = await prevObsCrossfilter.resetObsColumnValues(
|
|
||||||
categoryName,
|
|
||||||
oldLabelName,
|
|
||||||
newLabelName
|
|
||||||
);
|
|
||||||
obsCrossfilter = await obsCrossfilter.removeObsAnnoCategory(
|
|
||||||
categoryName,
|
|
||||||
oldLabelName,
|
|
||||||
globals.unassignedCategoryLabel
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: label edited",
|
|
||||||
editedLabel: newLabelName,
|
|
||||||
metadataField: categoryName,
|
|
||||||
label: oldLabelName,
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
obsCrossfilter,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationLabelCurrentSelection = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
categoryName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labelName: any
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
/*
|
|
||||||
set the label on all currently selected
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
|
|
||||||
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
|
|
||||||
throw new Error("not a user annotation");
|
|
||||||
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.setObsColumnValues(
|
|
||||||
categoryName,
|
|
||||||
prevObsCrossfilter.allSelectedLabels(),
|
|
||||||
labelName
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "annotation: label current cell selection",
|
|
||||||
metadataField: categoryName,
|
|
||||||
label: labelName,
|
|
||||||
obsCrossfilter,
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
function writableAnnotations(annoMatrix: any) {
|
|
||||||
return (
|
|
||||||
annoMatrix.schema.annotations.obs.columns
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
.filter((s: any) => s.writable)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
.map((s: any) => s.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const needToSaveObsAnnotations = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
annoMatrix: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
lastSavedAnnoMatrix: any
|
|
||||||
) => {
|
|
||||||
/*
|
|
||||||
Return true if there are LIKELY user-defined annotation modifications between the two
|
|
||||||
annoMatrices. Technically not an action creator, but intimately intertwined
|
|
||||||
with the save process.
|
|
||||||
|
|
||||||
Two conditions will trigger a need to save:
|
|
||||||
* the collection of user-defined columns have changed
|
|
||||||
* the contents of the user-defined columns have change
|
|
||||||
*/
|
|
||||||
|
|
||||||
annoMatrix = annoMatrix.base();
|
|
||||||
|
|
||||||
// if the annoMatrix hasn't changed, we are guaranteed no changes to the matrix schema or contents.
|
|
||||||
if (annoMatrix === lastSavedAnnoMatrix) return false;
|
|
||||||
|
|
||||||
// if the schema has changed, we need to save
|
|
||||||
const currentWritable = writableAnnotations(annoMatrix);
|
|
||||||
if (difference(currentWritable, writableAnnotations(lastSavedAnnoMatrix))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no schema changes; check for change in contents
|
|
||||||
return currentWritable.some(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(col: any) => annoMatrix.col(col) !== lastSavedAnnoMatrix.col(col)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const saveObsAnnotationsAction = () => async (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
/*
|
|
||||||
Save the user-created obs annotations IF any have changed.
|
|
||||||
*/
|
|
||||||
const state = getState();
|
|
||||||
const { annotations, autosave } = state;
|
|
||||||
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
|
|
||||||
const { lastSavedAnnoMatrix, saveInProgress } = autosave;
|
|
||||||
|
|
||||||
const annoMatrix = state.annoMatrix.base();
|
|
||||||
|
|
||||||
if (saveInProgress || annoMatrix === lastSavedAnnoMatrix) return;
|
|
||||||
if (!needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix)) {
|
|
||||||
dispatch({
|
|
||||||
type: "writable obs annotations - save complete",
|
|
||||||
lastSavedAnnoMatrix: annoMatrix,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Else, we really do need to save
|
|
||||||
*/
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "writable obs annotations - save started",
|
|
||||||
});
|
|
||||||
|
|
||||||
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
|
|
||||||
const matrix = MatrixFBS.encodeMatrixFBS(df);
|
|
||||||
const compressedMatrix = pako.deflate(matrix);
|
|
||||||
try {
|
|
||||||
const queryString =
|
|
||||||
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
|
||||||
? `?annotation-collection-name=${encodeURIComponent(
|
|
||||||
dataCollectionName
|
|
||||||
)}`
|
|
||||||
: "";
|
|
||||||
const res = await fetch(
|
|
||||||
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: compressedMatrix,
|
|
||||||
headers: new Headers({
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
}),
|
|
||||||
credentials: "include",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (res.ok) {
|
|
||||||
dispatch({
|
|
||||||
type: "writable obs annotations - save complete",
|
|
||||||
lastSavedAnnoMatrix: annoMatrix,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "writable obs annotations - save error",
|
|
||||||
message: `HTTP error ${res.status} - ${res.statusText}`,
|
|
||||||
res,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: "writable obs annotations - save error",
|
|
||||||
message: error.toString(),
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const saveGenesetsAction = () => async (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
// bail if gene sets not available, or in readonly mode.
|
|
||||||
const { config } = state;
|
|
||||||
const { lastTid, genesets } = state.genesets;
|
|
||||||
|
|
||||||
const genesetsAreAvailable =
|
|
||||||
config?.parameters?.annotations_genesets ?? false;
|
|
||||||
const genesetsReadonly =
|
|
||||||
config?.parameters?.annotations_genesets_readonly ?? true;
|
|
||||||
if (!genesetsAreAvailable || genesetsReadonly) {
|
|
||||||
// our non-save was completed!
|
|
||||||
return dispatch({
|
|
||||||
type: "autosave: genesets complete",
|
|
||||||
lastSavedGenesets: genesets,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "autosave: genesets started",
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Create the JSON OTA data structure */
|
|
||||||
const tid = (lastTid ?? 0) + 1;
|
|
||||||
const genesetsOTA = [];
|
|
||||||
for (const [name, gs] of genesets) {
|
|
||||||
const genes = [];
|
|
||||||
for (const g of gs.genes.values()) {
|
|
||||||
genes.push({
|
|
||||||
gene_symbol: g.geneSymbol,
|
|
||||||
gene_description: g.geneDescription,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
genesetsOTA.push({
|
|
||||||
geneset_name: name,
|
|
||||||
geneset_description: gs.genesetDescription,
|
|
||||||
genes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ota = {
|
|
||||||
tid,
|
|
||||||
genesets: genesetsOTA,
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Save to server */
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
dataCollectionNameIsReadOnly,
|
|
||||||
dataCollectionName,
|
|
||||||
} = state.annotations;
|
|
||||||
const queryString =
|
|
||||||
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
|
||||||
? `?annotation-collection-name=${encodeURIComponent(
|
|
||||||
dataCollectionName
|
|
||||||
)}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${globals.API.prefix}${globals.API.version}genesets${queryString}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: new Headers({
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}),
|
|
||||||
body: JSON.stringify(ota),
|
|
||||||
credentials: "include",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) {
|
|
||||||
return dispatch({
|
|
||||||
type: "autosave: genesets error",
|
|
||||||
message: `HTTP error ${res.status} - ${res.statusText}`,
|
|
||||||
res,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await Promise.all([
|
|
||||||
dispatch({
|
|
||||||
type: "autosave: genesets complete",
|
|
||||||
lastSavedGenesets: genesets,
|
|
||||||
}),
|
|
||||||
dispatch({
|
|
||||||
type: "geneset: set tid",
|
|
||||||
tid,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
return dispatch({
|
|
||||||
type: "autosave: genesets error",
|
|
||||||
message: error.toString(),
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
action creators related to embeddings choice
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||||
|
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
|
||||||
|
|
||||||
|
export async function _switchEmbedding(
|
||||||
|
prevAnnoMatrix,
|
||||||
|
prevCrossfilter,
|
||||||
|
newEmbeddingName
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
DRY helper used by embedding action creators
|
||||||
|
*/
|
||||||
|
const base = prevAnnoMatrix.base();
|
||||||
|
const embeddingDf = await base.fetch("emb", newEmbeddingName);
|
||||||
|
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
|
||||||
|
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(
|
||||||
|
annoMatrix,
|
||||||
|
prevCrossfilter.obsCrossfilter
|
||||||
|
).select("emb", newEmbeddingName, {
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
return [annoMatrix, obsCrossfilter];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layoutChoiceAction =
|
||||||
|
(newLayoutChoice) => async (dispatch, getState) => {
|
||||||
|
/*
|
||||||
|
On layout choice, make sure we have selected all on the previous layout, AND the new
|
||||||
|
layout.
|
||||||
|
*/
|
||||||
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } =
|
||||||
|
getState();
|
||||||
|
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
||||||
|
prevAnnoMatrix,
|
||||||
|
prevCrossfilter,
|
||||||
|
newLayoutChoice
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: "set layout choice",
|
||||||
|
layoutChoice: newLayoutChoice,
|
||||||
|
obsCrossfilter,
|
||||||
|
annoMatrix,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
action creators related to embeddings choice
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Action, ActionCreator } from "redux";
|
|
||||||
import { ThunkAction } from "redux-thunk";
|
|
||||||
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
|
||||||
import type { AppDispatch, RootState } from "../reducers";
|
|
||||||
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
|
|
||||||
import { Field } from "../common/types/schema";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export async function _switchEmbedding(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
prevAnnoMatrix: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
prevCrossfilter: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
newEmbeddingName: any
|
|
||||||
) {
|
|
||||||
/*
|
|
||||||
DRY helper used by embedding action creators
|
|
||||||
*/
|
|
||||||
const base = prevAnnoMatrix.base();
|
|
||||||
const embeddingDf = await base.fetch("emb", newEmbeddingName);
|
|
||||||
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
|
|
||||||
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(
|
|
||||||
annoMatrix,
|
|
||||||
prevCrossfilter.obsCrossfilter
|
|
||||||
).select(Field.emb, newEmbeddingName, {
|
|
||||||
mode: "all",
|
|
||||||
});
|
|
||||||
return [annoMatrix, obsCrossfilter];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const layoutChoiceAction: ActionCreator<
|
|
||||||
ThunkAction<Promise<void>, RootState, never, Action<"set layout choice">>
|
|
||||||
> =
|
|
||||||
(newLayoutChoice: string) =>
|
|
||||||
async (dispatch: AppDispatch, getState: () => RootState): Promise<void> => {
|
|
||||||
/*
|
|
||||||
On layout choice, make sure we have selected all on the previous layout, AND the new
|
|
||||||
layout.
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
annoMatrix: prevAnnoMatrix,
|
|
||||||
obsCrossfilter: prevCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
|
||||||
prevAnnoMatrix,
|
|
||||||
prevCrossfilter,
|
|
||||||
newLayoutChoice
|
|
||||||
);
|
|
||||||
dispatch({
|
|
||||||
type: "set layout choice",
|
|
||||||
layoutChoice: newLayoutChoice,
|
|
||||||
obsCrossfilter,
|
|
||||||
annoMatrix,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -21,45 +21,34 @@ The behavior manifest in these action creators:
|
|||||||
Note that crossfilter indices are lazy created, as needed.
|
Note that crossfilter indices are lazy created, as needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dataframe } from "../util/dataframe";
|
export const genesetDelete = (genesetName) => (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
export const genesetDelete =
|
const { genesets } = state;
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
const gs = genesets?.genesets?.get(genesetName) ?? {};
|
||||||
(genesetName: any) => (dispatch: any, getState: any) => {
|
const geneSymbols = Array.from(gs.genes.keys());
|
||||||
const state = getState();
|
const obsCrossfilter = dropGeneset(dispatch, state, genesetName, geneSymbols);
|
||||||
const { genesets } = state;
|
if (genesetName === state.colors.colorAccessor) {
|
||||||
const gs = genesets?.genesets?.get(genesetName) ?? {};
|
|
||||||
const geneSymbols = Array.from(gs.genes.keys());
|
|
||||||
const obsCrossfilter = dropGeneset(
|
|
||||||
dispatch,
|
|
||||||
state,
|
|
||||||
genesetName,
|
|
||||||
geneSymbols
|
|
||||||
);
|
|
||||||
if (genesetName === state.colors.colorAccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "reset colorscale",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "geneset: delete",
|
type: "reset colorscale",
|
||||||
genesetName,
|
|
||||||
obsCrossfilter,
|
|
||||||
annoMatrix: obsCrossfilter.annoMatrix,
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
dispatch({
|
||||||
|
type: "geneset: delete",
|
||||||
|
genesetName,
|
||||||
|
obsCrossfilter,
|
||||||
|
annoMatrix: obsCrossfilter.annoMatrix,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const genesetAddGenes =
|
export const genesetAddGenes =
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
(genesetName, genes) => async (dispatch, getState) => {
|
||||||
(genesetName: any, genes: any) => async (dispatch: any, getState: any) => {
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { obsCrossfilter: prevObsCrossfilter, annoMatrix } = state;
|
const { obsCrossfilter: prevObsCrossfilter, annoMatrix } = state;
|
||||||
const { schema } = annoMatrix;
|
const { schema } = annoMatrix;
|
||||||
const varIndex = schema.annotations.var.index;
|
const varIndex = schema.annotations.var.index;
|
||||||
const df: Dataframe = await annoMatrix.fetch("var", varIndex);
|
const df = await annoMatrix.fetch("var", varIndex);
|
||||||
const geneNames = df.col(varIndex).asArray();
|
const geneNames = df.col(varIndex).asArray();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
genes = genes.reduce((acc, gene) => {
|
||||||
genes = genes.reduce((acc: any, gene: any) => {
|
|
||||||
if (geneNames.indexOf(gene.geneSymbol) === -1) {
|
if (geneNames.indexOf(gene.geneSymbol) === -1) {
|
||||||
postUserErrorToast(
|
postUserErrorToast(
|
||||||
`${gene.geneSymbol} doesn't appear to be a valid gene name.`
|
`${gene.geneSymbol} doesn't appear to be a valid gene name.`
|
||||||
@@ -88,8 +77,7 @@ export const genesetAddGenes =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const genesetDeleteGenes =
|
export const genesetDeleteGenes =
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
(genesetName, geneSymbols) => (dispatch, getState) => {
|
||||||
(genesetName: any, geneSymbols: any) => (dispatch: any, getState: any) => {
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const obsCrossfilter = dropGeneset(
|
const obsCrossfilter = dropGeneset(
|
||||||
dispatch,
|
dispatch,
|
||||||
@@ -110,14 +98,7 @@ export const genesetDeleteGenes =
|
|||||||
Private
|
Private
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function dropGenesetSummaryDimension(
|
function dropGenesetSummaryDimension(obsCrossfilter, state, genesetName) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
obsCrossfilter: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
state: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
genesetName: any
|
|
||||||
) {
|
|
||||||
const { annoMatrix, genesets } = state;
|
const { annoMatrix, genesets } = state;
|
||||||
const varIndex = annoMatrix.schema.annotations?.var?.index;
|
const varIndex = annoMatrix.schema.annotations?.var?.index;
|
||||||
const gs = genesets?.genesets?.get(genesetName) ?? {};
|
const gs = genesets?.genesets?.get(genesetName) ?? {};
|
||||||
@@ -133,8 +114,7 @@ function dropGenesetSummaryDimension(
|
|||||||
return obsCrossfilter.dropDimension("X", query);
|
return obsCrossfilter.dropDimension("X", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function dropGeneDimension(obsCrossfilter, state, gene) {
|
||||||
function dropGeneDimension(obsCrossfilter: any, state: any, gene: any) {
|
|
||||||
const { annoMatrix } = state;
|
const { annoMatrix } = state;
|
||||||
const varIndex = annoMatrix.schema.annotations?.var?.index;
|
const varIndex = annoMatrix.schema.annotations?.var?.index;
|
||||||
const query = {
|
const query = {
|
||||||
@@ -147,21 +127,10 @@ function dropGeneDimension(obsCrossfilter: any, state: any, gene: any) {
|
|||||||
return obsCrossfilter.dropDimension("X", query);
|
return obsCrossfilter.dropDimension("X", query);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropGeneset(
|
function dropGeneset(dispatch, state, genesetName, geneSymbols) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
state: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
genesetName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
geneSymbols: any
|
|
||||||
) {
|
|
||||||
const { obsCrossfilter: prevObsCrossfilter } = state;
|
const { obsCrossfilter: prevObsCrossfilter } = state;
|
||||||
const obsCrossfilter = geneSymbols.reduce(
|
const obsCrossfilter = geneSymbols.reduce(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
(crossfilter, gene) => dropGeneDimension(crossfilter, state, gene),
|
||||||
(crossfilter: any, gene: any) =>
|
|
||||||
dropGeneDimension(crossfilter, state, gene),
|
|
||||||
dropGenesetSummaryDimension(prevObsCrossfilter, state, genesetName)
|
dropGenesetSummaryDimension(prevObsCrossfilter, state, genesetName)
|
||||||
);
|
);
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -169,8 +138,7 @@ function dropGeneset(
|
|||||||
continuousNamespace: { isGeneSetSummary: true },
|
continuousNamespace: { isGeneSetSummary: true },
|
||||||
selection: genesetName,
|
selection: genesetName,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
geneSymbols.forEach((g) =>
|
||||||
geneSymbols.forEach((g: any) =>
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "continuous metadata histogram cancel",
|
type: "continuous metadata histogram cancel",
|
||||||
continuousNamespace: { isUserDefined: true },
|
continuousNamespace: { isUserDefined: true },
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Config } from "../globals";
|
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
import { AnnoMatrixLoader, AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
import { AnnoMatrixLoader, AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||||
import {
|
import {
|
||||||
@@ -12,12 +11,8 @@ import * as annoActions from "./annotation";
|
|||||||
import * as viewActions from "./viewStack";
|
import * as viewActions from "./viewStack";
|
||||||
import * as embActions from "./embedding";
|
import * as embActions from "./embedding";
|
||||||
import * as genesetActions from "./geneset";
|
import * as genesetActions from "./geneset";
|
||||||
import { AppDispatch, RootState } from "../reducers";
|
|
||||||
import { EmbeddingSchema, Schema } from "../common/types/schema";
|
|
||||||
import { UserInfoPayload } from "../reducers/userInfo";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function setGlobalConfig(config) {
|
||||||
function setGlobalConfig(config: any) {
|
|
||||||
/**
|
/**
|
||||||
* Set any global run-time config not _exclusively_ managed by the config reducer.
|
* Set any global run-time config not _exclusively_ managed by the config reducer.
|
||||||
* This should only set fields defined in globals.globalConfig.
|
* This should only set fields defined in globals.globalConfig.
|
||||||
@@ -30,8 +25,7 @@ function setGlobalConfig(config: any) {
|
|||||||
/*
|
/*
|
||||||
return promise fetching user-configured colors
|
return promise fetching user-configured colors
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function userColorsFetchAndLoad(dispatch) {
|
||||||
async function userColorsFetchAndLoad(dispatch: any) {
|
|
||||||
return fetchJson("colors").then((response) =>
|
return fetchJson("colors").then((response) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "universe: user color load success",
|
type: "universe: user color load success",
|
||||||
@@ -40,38 +34,36 @@ async function userColorsFetchAndLoad(dispatch: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function schemaFetch(): Promise<{ schema: Schema }> {
|
async function schemaFetch() {
|
||||||
return fetchJson<{ schema: Schema }>("schema");
|
return fetchJson("schema");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configFetch(dispatch: AppDispatch): Promise<Config> {
|
async function configFetch(dispatch) {
|
||||||
const response = await fetchJson<{ config: globals.Config }>("config");
|
return fetchJson("config").then((response) => {
|
||||||
const config = { ...globals.configDefaults, ...response.config };
|
const config = { ...globals.configDefaults, ...response.config };
|
||||||
|
|
||||||
setGlobalConfig(config);
|
setGlobalConfig(config);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "configuration load complete",
|
type: "configuration load complete",
|
||||||
config,
|
config,
|
||||||
|
});
|
||||||
|
return config;
|
||||||
});
|
});
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function userInfoFetch(dispatch: AppDispatch): Promise<UserInfoPayload> {
|
async function userInfoFetch(dispatch) {
|
||||||
return fetchJson<{ userinfo: UserInfoPayload }>("userinfo").then(
|
return fetchJson("userinfo").then((response) => {
|
||||||
(response) => {
|
const { userinfo: userInfo } = response || {};
|
||||||
const { userinfo: userInfo } = response || {};
|
dispatch({
|
||||||
dispatch({
|
type: "userInfo load complete",
|
||||||
type: "userInfo load complete",
|
userInfo,
|
||||||
userInfo,
|
});
|
||||||
});
|
return userInfo;
|
||||||
return userInfo;
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
async function genesetsFetch(dispatch, config) {
|
||||||
async function genesetsFetch(dispatch: any, config: any) {
|
|
||||||
/* request genesets ONLY if the backend supports the feature */
|
/* request genesets ONLY if the backend supports the feature */
|
||||||
const defaultResponse = {
|
const defaultResponse = {
|
||||||
genesets: [],
|
genesets: [],
|
||||||
@@ -92,32 +84,26 @@ async function genesetsFetch(dispatch: any, config: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
function prefetchEmbeddings(annoMatrix) {
|
||||||
function prefetchEmbeddings(annoMatrix: any) {
|
|
||||||
/*
|
/*
|
||||||
prefetch requests for all embeddings
|
prefetch requests for all embeddings
|
||||||
*/
|
*/
|
||||||
const { schema } = annoMatrix;
|
const { schema } = annoMatrix;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const available = schema.layout.obs.map((v) => v.name);
|
||||||
const available = schema.layout.obs.map((v: any) => v.name);
|
available.forEach((embName) => annoMatrix.prefetch("emb", embName));
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
available.forEach((embName: any) => annoMatrix.prefetch("emb", embName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Application bootstrap
|
Application bootstrap
|
||||||
*/
|
*/
|
||||||
const doInitialDataLoad = (): ((
|
const doInitialDataLoad = () =>
|
||||||
dispatch: AppDispatch,
|
catchErrorsWrap(async (dispatch) => {
|
||||||
getState: () => RootState
|
|
||||||
) => void) =>
|
|
||||||
catchErrorsWrap(async (dispatch: AppDispatch) => {
|
|
||||||
dispatch({ type: "initial data load start" });
|
dispatch({ type: "initial data load start" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [config, schema] = await Promise.all([
|
const [config, schema] = await Promise.all([
|
||||||
configFetch(dispatch),
|
configFetch(dispatch),
|
||||||
schemaFetch(),
|
schemaFetch(dispatch),
|
||||||
userColorsFetchAndLoad(dispatch),
|
userColorsFetchAndLoad(dispatch),
|
||||||
userInfoFetch(dispatch),
|
userInfoFetch(dispatch),
|
||||||
]);
|
]);
|
||||||
@@ -140,7 +126,7 @@ const doInitialDataLoad = (): ((
|
|||||||
const layoutSchema = schema?.schema?.layout?.obs ?? [];
|
const layoutSchema = schema?.schema?.layout?.obs ?? [];
|
||||||
if (
|
if (
|
||||||
defaultEmbedding &&
|
defaultEmbedding &&
|
||||||
layoutSchema.some((s: EmbeddingSchema) => s.name === defaultEmbedding)
|
layoutSchema.some((s) => s.name === defaultEmbedding)
|
||||||
) {
|
) {
|
||||||
dispatch(embActions.layoutChoiceAction(defaultEmbedding));
|
dispatch(embActions.layoutChoiceAction(defaultEmbedding));
|
||||||
}
|
}
|
||||||
@@ -149,25 +135,21 @@ const doInitialDataLoad = (): ((
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
||||||
function requestSingleGeneExpressionCountsForColoringPOST(gene: any) {
|
|
||||||
return {
|
return {
|
||||||
type: "color by expression",
|
type: "color by expression",
|
||||||
gene,
|
gene,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
const requestUserDefinedGene = (gene) => ({
|
||||||
const requestUserDefinedGene = (gene: any) => ({
|
|
||||||
type: "request user defined gene success",
|
type: "request user defined gene success",
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
genes: [gene],
|
genes: [gene],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
const dispatchDiffExpErrors = (dispatch, response) => {
|
||||||
const dispatchDiffExpErrors = (dispatch: any, response: any) => {
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 403:
|
case 403:
|
||||||
dispatchNetworkErrorMessageToUser(
|
dispatchNetworkErrorMessageToUser(
|
||||||
@@ -191,16 +173,8 @@ const dispatchDiffExpErrors = (dispatch: any, response: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requestDifferentialExpression =
|
const requestDifferentialExpression =
|
||||||
(
|
(set1, set2, num_genes = 50) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
async (dispatch, getState) => {
|
||||||
set1: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
set2: any,
|
|
||||||
num_genes = 50
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
async (dispatch: any, getState: any) => {
|
|
||||||
dispatch({ type: "request differential expression started" });
|
dispatch({ type: "request differential expression started" });
|
||||||
try {
|
try {
|
||||||
/*
|
/*
|
||||||
@@ -248,9 +222,7 @@ const requestDifferentialExpression =
|
|||||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||||
const diffexpLists = { negative: [], positive: [] };
|
const diffexpLists = { negative: [], positive: [] };
|
||||||
for (const polarity of Object.keys(diffexpLists)) {
|
for (const polarity of Object.keys(diffexpLists)) {
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
diffexpLists[polarity] = response[polarity].map((v) => [
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
diffexpLists[polarity] = response[polarity].map((v: any) => [
|
|
||||||
varIndex.at(v[0], varIndexName),
|
varIndex.at(v[0], varIndexName),
|
||||||
...v.slice(1),
|
...v.slice(1),
|
||||||
]);
|
]);
|
||||||
@@ -269,10 +241,10 @@ const requestDifferentialExpression =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchJson<T>(pathAndQuery: string): Promise<T> {
|
function fetchJson(pathAndQuery) {
|
||||||
return doJsonRequest<T>(
|
return doJsonRequest(
|
||||||
`${globals.API.prefix}${globals.API.version}${pathAndQuery}`
|
`${globals.API.prefix}${globals.API.version}${pathAndQuery}`
|
||||||
) as Promise<T>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
Action creators for selection
|
||||||
|
*/
|
||||||
|
export const selectContinuousMetadataAction =
|
||||||
|
(type, query, range, oldProps = {}) =>
|
||||||
|
async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
||||||
|
|
||||||
|
const selection = range
|
||||||
|
? {
|
||||||
|
mode: "range",
|
||||||
|
lo: range[0],
|
||||||
|
hi: range[1],
|
||||||
|
inclusive: true, // [lo, hi] incluisve selection
|
||||||
|
}
|
||||||
|
: { mode: "all" };
|
||||||
|
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select(...query, selection);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
obsCrossfilter,
|
||||||
|
range,
|
||||||
|
...oldProps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectCategoricalMetadataAction =
|
||||||
|
(
|
||||||
|
type, // action type
|
||||||
|
metadataField, // annotation category name
|
||||||
|
labels,
|
||||||
|
label, // the label being selected/deselected
|
||||||
|
isSelected, // bool
|
||||||
|
oldProps = {}
|
||||||
|
) =>
|
||||||
|
async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter, categoricalSelection } =
|
||||||
|
getState();
|
||||||
|
|
||||||
|
const labelSelectionState = new Map(categoricalSelection[metadataField]);
|
||||||
|
labels.forEach(
|
||||||
|
(l) => labelSelectionState.has(l) || labelSelectionState.set(l, true)
|
||||||
|
);
|
||||||
|
labelSelectionState.set(label, isSelected);
|
||||||
|
|
||||||
|
const values = Array.from(labelSelectionState.keys()).filter((k) =>
|
||||||
|
labelSelectionState.get(k)
|
||||||
|
);
|
||||||
|
const selection = {
|
||||||
|
mode: "exact",
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select(
|
||||||
|
"obs",
|
||||||
|
metadataField,
|
||||||
|
selection
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
obsCrossfilter,
|
||||||
|
metadataField,
|
||||||
|
labelSelectionState,
|
||||||
|
...oldProps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectCategoricalAllMetadataAction =
|
||||||
|
(
|
||||||
|
type, // action type
|
||||||
|
metadataField, // annotation category name
|
||||||
|
labels,
|
||||||
|
isSelected, // bool, select all or none
|
||||||
|
oldProps = {}
|
||||||
|
) =>
|
||||||
|
async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter, categoricalSelection } =
|
||||||
|
getState();
|
||||||
|
|
||||||
|
const labelSelectionState = new Map(categoricalSelection[metadataField]);
|
||||||
|
labels.forEach((label) => labelSelectionState.set(label, isSelected));
|
||||||
|
|
||||||
|
const selection = { mode: isSelected ? "all" : "none" };
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select(
|
||||||
|
"obs",
|
||||||
|
metadataField,
|
||||||
|
selection
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
obsCrossfilter,
|
||||||
|
metadataField,
|
||||||
|
labelSelectionState,
|
||||||
|
...oldProps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
** Graph selection-related actions
|
||||||
|
**/
|
||||||
|
|
||||||
|
export const graphBrushStartAction = () =>
|
||||||
|
/* no change to crossfilter until a change fires */
|
||||||
|
({ type: "graph brush start" });
|
||||||
|
|
||||||
|
const _graphBrushWithinRectAction =
|
||||||
|
(type, embName, brushCoords) => async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
||||||
|
|
||||||
|
const selection = { mode: "within-rect", ...brushCoords };
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select(
|
||||||
|
"emb",
|
||||||
|
embName,
|
||||||
|
selection
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
obsCrossfilter,
|
||||||
|
brushCoords,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _graphAllAction = (type, embName) => async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
||||||
|
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select("emb", embName, {
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
obsCrossfilter,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const graphBrushChangeAction = (embName, brushCoords) =>
|
||||||
|
_graphBrushWithinRectAction("graph brush change", embName, brushCoords);
|
||||||
|
|
||||||
|
export const graphBrushEndAction = (embName, brushCoords) =>
|
||||||
|
_graphBrushWithinRectAction("graph brush end", embName, brushCoords);
|
||||||
|
|
||||||
|
export const graphBrushCancelAction = (embName) =>
|
||||||
|
_graphAllAction("graph brush cancel", embName);
|
||||||
|
export const graphBrushDeselectAction = (embName) =>
|
||||||
|
_graphAllAction("graph brush deselect", embName);
|
||||||
|
|
||||||
|
export const graphLassoStartAction = () =>
|
||||||
|
/* no change to crossfilter until a change fires */
|
||||||
|
({ type: "graph lasso start" });
|
||||||
|
|
||||||
|
export const graphLassoCancelAction = (embName) =>
|
||||||
|
_graphAllAction("graph lasso cancel", embName);
|
||||||
|
|
||||||
|
export const graphLassoDeselectAction = (embName) =>
|
||||||
|
_graphAllAction("graph lasso cancel", embName);
|
||||||
|
|
||||||
|
export const graphLassoEndAction =
|
||||||
|
(embName, polygon) => async (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
mode: "within-polygon",
|
||||||
|
polygon,
|
||||||
|
};
|
||||||
|
const obsCrossfilter = await prevObsCrossfilter.select(
|
||||||
|
"emb",
|
||||||
|
embName,
|
||||||
|
selection
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "graph lasso end",
|
||||||
|
obsCrossfilter,
|
||||||
|
polygon,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Differential expression set selection
|
||||||
|
*/
|
||||||
|
export const setCellSetFromSelection = (cellSetId) => (dispatch, getState) => {
|
||||||
|
const { obsCrossfilter } = getState();
|
||||||
|
const selected = obsCrossfilter.allSelectedLabels();
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: `store current cell selection as differential set ${cellSetId}`,
|
||||||
|
data: selected.length > 0 ? selected : null,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
Action creators for selection
|
|
||||||
*/
|
|
||||||
export const selectContinuousMetadataAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
query: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
range: any,
|
|
||||||
oldProps = {} // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
|
||||||
|
|
||||||
const selection = range
|
|
||||||
? {
|
|
||||||
mode: "range",
|
|
||||||
lo: range[0],
|
|
||||||
hi: range[1],
|
|
||||||
inclusive: true, // [lo, hi] incluisve selection
|
|
||||||
}
|
|
||||||
: { mode: "all" };
|
|
||||||
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select(...query, selection);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
obsCrossfilter,
|
|
||||||
range,
|
|
||||||
...oldProps,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectCategoricalMetadataAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type: any, // action type
|
|
||||||
// annotation category name
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
metadataField: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labels: any,
|
|
||||||
// the label being selected/deselected
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
label: any,
|
|
||||||
// bool
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
isSelected: any,
|
|
||||||
oldProps = {}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
const {
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
categoricalSelection,
|
|
||||||
} = getState();
|
|
||||||
|
|
||||||
const labelSelectionState = new Map(categoricalSelection[metadataField]);
|
|
||||||
labels.forEach(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(l: any) => labelSelectionState.has(l) || labelSelectionState.set(l, true)
|
|
||||||
);
|
|
||||||
labelSelectionState.set(label, isSelected);
|
|
||||||
|
|
||||||
const values = Array.from(labelSelectionState.keys()).filter((k) =>
|
|
||||||
labelSelectionState.get(k)
|
|
||||||
);
|
|
||||||
const selection = {
|
|
||||||
mode: "exact",
|
|
||||||
values,
|
|
||||||
};
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select(
|
|
||||||
"obs",
|
|
||||||
metadataField,
|
|
||||||
selection
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
obsCrossfilter,
|
|
||||||
metadataField,
|
|
||||||
labelSelectionState,
|
|
||||||
...oldProps,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectCategoricalAllMetadataAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type: any, // action type
|
|
||||||
// annotation category name
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
metadataField: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labels: any,
|
|
||||||
// bool, select all or none
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
isSelected: any,
|
|
||||||
oldProps = {}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
const {
|
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
categoricalSelection,
|
|
||||||
} = getState();
|
|
||||||
|
|
||||||
const labelSelectionState = new Map(categoricalSelection[metadataField]);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
labels.forEach((label: any) => labelSelectionState.set(label, isSelected));
|
|
||||||
|
|
||||||
const selection = { mode: isSelected ? "all" : "none" };
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select(
|
|
||||||
"obs",
|
|
||||||
metadataField,
|
|
||||||
selection
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
obsCrossfilter,
|
|
||||||
metadataField,
|
|
||||||
labelSelectionState,
|
|
||||||
...oldProps,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
** Graph selection-related actions
|
|
||||||
**/
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphBrushStartAction = () =>
|
|
||||||
/* no change to crossfilter until a change fires */
|
|
||||||
({ type: "graph brush start" });
|
|
||||||
|
|
||||||
const _graphBrushWithinRectAction = (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
embName: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
brushCoords: any
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
) => async (dispatch: any, getState: any) => {
|
|
||||||
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
|
||||||
|
|
||||||
const selection = { mode: "within-rect", ...brushCoords };
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select(
|
|
||||||
"emb",
|
|
||||||
embName,
|
|
||||||
selection
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
obsCrossfilter,
|
|
||||||
brushCoords,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const _graphAllAction = (type: any, embName: any) => async (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
|
||||||
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select("emb", embName, {
|
|
||||||
mode: "all",
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
obsCrossfilter,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphBrushChangeAction = (embName: any, brushCoords: any) =>
|
|
||||||
_graphBrushWithinRectAction("graph brush change", embName, brushCoords);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphBrushEndAction = (embName: any, brushCoords: any) =>
|
|
||||||
_graphBrushWithinRectAction("graph brush end", embName, brushCoords);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphBrushCancelAction = (embName: any) =>
|
|
||||||
_graphAllAction("graph brush cancel", embName);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphBrushDeselectAction = (embName: any) =>
|
|
||||||
_graphAllAction("graph brush deselect", embName);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphLassoStartAction = () =>
|
|
||||||
/* no change to crossfilter until a change fires */
|
|
||||||
({ type: "graph lasso start" });
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphLassoCancelAction = (embName: any) =>
|
|
||||||
_graphAllAction("graph lasso cancel", embName);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphLassoDeselectAction = (embName: any) =>
|
|
||||||
_graphAllAction("graph lasso cancel", embName);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const graphLassoEndAction = (embName: any, polygon: any) => async (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
const { obsCrossfilter: prevObsCrossfilter } = getState();
|
|
||||||
|
|
||||||
const selection = {
|
|
||||||
mode: "within-polygon",
|
|
||||||
polygon,
|
|
||||||
};
|
|
||||||
const obsCrossfilter = await prevObsCrossfilter.select(
|
|
||||||
"emb",
|
|
||||||
embName,
|
|
||||||
selection
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "graph lasso end",
|
|
||||||
obsCrossfilter,
|
|
||||||
polygon,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Differential expression set selection
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export const setCellSetFromSelection = (cellSetId: any) => (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
const { obsCrossfilter } = getState();
|
|
||||||
const selected = obsCrossfilter.allSelectedLabels();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: `store current cell selection as differential set ${cellSetId}`,
|
|
||||||
data: selected.length > 0 ? selected : null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -18,13 +18,7 @@ import {
|
|||||||
_userResetSubsetAnnoMatrix,
|
_userResetSubsetAnnoMatrix,
|
||||||
} from "../util/stateManager/viewStackHelpers";
|
} from "../util/stateManager/viewStackHelpers";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export const clipAction = (min, max) => (dispatch, getState) => {
|
||||||
export const clipAction = (min: any, max: any) => (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dispatch: any,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
getState: any
|
|
||||||
) => {
|
|
||||||
/*
|
/*
|
||||||
apply a clip to the current annoMatrix. By convention, the clip
|
apply a clip to the current annoMatrix. By convention, the clip
|
||||||
view is ALWAYS the top view.
|
view is ALWAYS the top view.
|
||||||
@@ -40,8 +34,7 @@ export const clipAction = (min: any, max: any) => (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export const subsetAction = () => (dispatch, getState) => {
|
||||||
export const subsetAction = () => (dispatch: any, getState: any) => {
|
|
||||||
/*
|
/*
|
||||||
Subset the annoMatrix to the current crossfilter selection by pushing a
|
Subset the annoMatrix to the current crossfilter selection by pushing a
|
||||||
subset view.
|
subset view.
|
||||||
@@ -49,10 +42,8 @@ export const subsetAction = () => (dispatch: any, getState: any) => {
|
|||||||
By convention, a clip view is ALWAYS the top view, so if present, pop
|
By convention, a clip view is ALWAYS the top view, so if present, pop
|
||||||
off and re-apply
|
off and re-apply
|
||||||
*/
|
*/
|
||||||
const {
|
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
|
||||||
annoMatrix: prevAnnoMatrix,
|
getState();
|
||||||
obsCrossfilter: prevObsCrossfilter,
|
|
||||||
} = getState();
|
|
||||||
const annoMatrix = _userSubsetAnnoMatrix(
|
const annoMatrix = _userSubsetAnnoMatrix(
|
||||||
prevAnnoMatrix,
|
prevAnnoMatrix,
|
||||||
prevObsCrossfilter.allSelectedMask()
|
prevObsCrossfilter.allSelectedMask()
|
||||||
@@ -65,8 +56,7 @@ export const subsetAction = () => (dispatch: any, getState: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
export const resetSubsetAction = () => (dispatch, getState) => {
|
||||||
export const resetSubsetAction = () => (dispatch: any, getState: any) => {
|
|
||||||
/*
|
/*
|
||||||
Reset the annoMatrix to all data. Because we may have multiple views
|
Reset the annoMatrix to all data. Because we may have multiple views
|
||||||
stacked, we pop them all. By convention, any clip transformation will
|
stacked, we pop them all. By convention, any clip transformation will
|
||||||
@@ -2,9 +2,6 @@ import {
|
|||||||
Dataframe,
|
Dataframe,
|
||||||
IdentityInt32Index,
|
IdentityInt32Index,
|
||||||
dataframeMemo,
|
dataframeMemo,
|
||||||
LabelType,
|
|
||||||
DataframeValue,
|
|
||||||
DataframeValueArray,
|
|
||||||
} from "../util/dataframe";
|
} from "../util/dataframe";
|
||||||
import {
|
import {
|
||||||
_getColumnDimensionNames,
|
_getColumnDimensionNames,
|
||||||
@@ -13,71 +10,13 @@ import {
|
|||||||
_getWritableColumns,
|
_getWritableColumns,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
import { indexEntireSchema } from "../util/stateManager/schemaHelpers";
|
import { indexEntireSchema } from "../util/stateManager/schemaHelpers";
|
||||||
import {
|
import { _whereCacheGet, _whereCacheMerge } from "./whereCache";
|
||||||
_whereCacheGet,
|
|
||||||
_whereCacheMerge,
|
|
||||||
WhereCache,
|
|
||||||
WhereCacheColumnLabels,
|
|
||||||
} from "./whereCache";
|
|
||||||
import _shallowClone from "./clone";
|
import _shallowClone from "./clone";
|
||||||
import { _queryValidate, _queryCacheKey, Query } from "./query";
|
import { _queryValidate, _queryCacheKey } from "./query";
|
||||||
import { GCHints } from "../common/types/entities";
|
|
||||||
import {
|
|
||||||
AnnotationColumnSchema,
|
|
||||||
Category,
|
|
||||||
Field,
|
|
||||||
EmbeddingSchema,
|
|
||||||
Schema,
|
|
||||||
ArraySchema,
|
|
||||||
RawSchema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
import { LabelArray } from "../util/dataframe/types";
|
|
||||||
import { LabelIndexBase } from "../util/dataframe/labelIndex";
|
|
||||||
|
|
||||||
const _dataframeCache = dataframeMemo(128);
|
const _dataframeCache = dataframeMemo(128);
|
||||||
|
|
||||||
interface Cache {
|
export default class AnnoMatrix {
|
||||||
[Field.obs]: Dataframe;
|
|
||||||
[Field.var]: Dataframe;
|
|
||||||
[Field.emb]: Dataframe;
|
|
||||||
[Field.X]: Dataframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PendingLoad {
|
|
||||||
[Field.obs]: { [key: string]: Promise<void> };
|
|
||||||
[Field.var]: { [key: string]: Promise<void> };
|
|
||||||
[Field.emb]: { [key: string]: Promise<void> };
|
|
||||||
[Field.X]: { [key: string]: Promise<void> };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserFlags {
|
|
||||||
isUserSubsetView?: boolean;
|
|
||||||
isEmbSubsetView?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default abstract class AnnoMatrix {
|
|
||||||
public isView: boolean;
|
|
||||||
|
|
||||||
public nObs: number;
|
|
||||||
|
|
||||||
public nVar: number;
|
|
||||||
|
|
||||||
public rowIndex: LabelIndexBase;
|
|
||||||
|
|
||||||
public schema: Schema;
|
|
||||||
|
|
||||||
public userFlags: UserFlags;
|
|
||||||
|
|
||||||
public viewOf: AnnoMatrix;
|
|
||||||
|
|
||||||
public _cache: Cache;
|
|
||||||
|
|
||||||
private _pendingLoad: PendingLoad;
|
|
||||||
|
|
||||||
private _whereCache: WhereCache;
|
|
||||||
|
|
||||||
private _gcInfo: Map<string, number>;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Abstract base class for all AnnoMatrix objects. This class provides a proxy
|
Abstract base class for all AnnoMatrix objects. This class provides a proxy
|
||||||
to the annotated matrix data authoritatively served by the server/back-end.
|
to the annotated matrix data authoritatively served by the server/back-end.
|
||||||
@@ -108,19 +47,14 @@ export default abstract class AnnoMatrix {
|
|||||||
subset(annoMatrix, rowLabels) -> annoMatrix
|
subset(annoMatrix, rowLabels) -> annoMatrix
|
||||||
etc.
|
etc.
|
||||||
*/
|
*/
|
||||||
static fields(): Field[] {
|
static fields() {
|
||||||
/*
|
/*
|
||||||
return the fields present in the AnnoMatrix instance.
|
return the fields present in the AnnoMatrix instance.
|
||||||
*/
|
*/
|
||||||
return [Field.obs, Field.var, Field.emb, Field.X];
|
return ["obs", "var", "emb", "X"];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(schema, nObs, nVar, rowIndex = null) {
|
||||||
schema: RawSchema,
|
|
||||||
nObs: number,
|
|
||||||
nVar: number,
|
|
||||||
rowIndex: LabelIndexBase | null = null
|
|
||||||
) {
|
|
||||||
/*
|
/*
|
||||||
Private constructor - this is an abstract base class. Do not use.
|
Private constructor - this is an abstract base class. Do not use.
|
||||||
*/
|
*/
|
||||||
@@ -136,7 +70,7 @@ export default abstract class AnnoMatrix {
|
|||||||
* rowIndex - a rowIndex shared by all data on this view (ie, the list of cells).
|
* rowIndex - a rowIndex shared by all data on this view (ie, the list of cells).
|
||||||
The row index labels are as defined by the base dataset from the server.
|
The row index labels are as defined by the base dataset from the server.
|
||||||
* isView - true if this is a view, false if not.
|
* isView - true if this is a view, false if not.
|
||||||
* viewOf - pointer to parent annomatrix if a view, self if not a view.
|
* viewOf - pointer to parent annomatrix if a view, undefined/null if not a view.
|
||||||
* userFlags - container for any additional state a user of this API wants to hang
|
* userFlags - container for any additional state a user of this API wants to hang
|
||||||
off of an annoMatrix, and have propagated by the (shallow) cloning protocol.
|
off of an annoMatrix, and have propagated by the (shallow) cloning protocol.
|
||||||
*/
|
*/
|
||||||
@@ -145,17 +79,17 @@ export default abstract class AnnoMatrix {
|
|||||||
this.nVar = nVar;
|
this.nVar = nVar;
|
||||||
this.rowIndex = rowIndex || new IdentityInt32Index(nObs);
|
this.rowIndex = rowIndex || new IdentityInt32Index(nObs);
|
||||||
this.isView = false;
|
this.isView = false;
|
||||||
this.viewOf = this;
|
this.viewOf = undefined;
|
||||||
this.userFlags = {};
|
this.userFlags = {};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Private instance variables.
|
Private instance variables.
|
||||||
|
|
||||||
These are caches - lazily loaded. The only guarantee is that if they
|
These are caches - lazily loaded. The only guarantee is that if they
|
||||||
are loaded, they will conform to the schema & dimensionality constraints.
|
are loaded, they will conform to the schema & dimensionality constraints.
|
||||||
|
|
||||||
Do NOT use directly - instead, use the fetch() and preload() API.
|
Do NOT use directly - instead, use the fetch() and preload() API.
|
||||||
*/
|
*/
|
||||||
this._cache = {
|
this._cache = {
|
||||||
obs: Dataframe.empty(this.rowIndex),
|
obs: Dataframe.empty(this.rowIndex),
|
||||||
var: Dataframe.empty(this.rowIndex),
|
var: Dataframe.empty(this.rowIndex),
|
||||||
@@ -168,14 +102,14 @@ export default abstract class AnnoMatrix {
|
|||||||
emb: {},
|
emb: {},
|
||||||
X: {},
|
X: {},
|
||||||
};
|
};
|
||||||
this._whereCache = {} as WhereCache;
|
this._whereCache = {};
|
||||||
this._gcInfo = new Map();
|
this._gcInfo = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Schema helper/accessors
|
** Schema helper/accessors
|
||||||
**/
|
**/
|
||||||
getMatrixColumns(field: Field): string[] {
|
getMatrixColumns(field) {
|
||||||
/*
|
/*
|
||||||
Return array of column names in the field. ONLY supported on the
|
Return array of column names in the field. ONLY supported on the
|
||||||
obs, var and emb fields. X currently unimplemented and will throw.
|
obs, var and emb fields. X currently unimplemented and will throw.
|
||||||
@@ -188,7 +122,7 @@ export default abstract class AnnoMatrix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this -- need to be able to call this on instances
|
// eslint-disable-next-line class-methods-use-this -- need to be able to call this on instances
|
||||||
getMatrixFields(): Field[] {
|
getMatrixFields() {
|
||||||
/*
|
/*
|
||||||
Return array of fields in this annoMatrix. Currently hard-wired to
|
Return array of fields in this annoMatrix. Currently hard-wired to
|
||||||
return: ["X", "obs", "var", "emb"].
|
return: ["X", "obs", "var", "emb"].
|
||||||
@@ -198,7 +132,7 @@ export default abstract class AnnoMatrix {
|
|||||||
return AnnoMatrix.fields();
|
return AnnoMatrix.fields();
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnSchema(field: Field, col: LabelType): ArraySchema {
|
getColumnSchema(field, col) {
|
||||||
/*
|
/*
|
||||||
Return the schema for the field & column ,eg,
|
Return the schema for the field & column ,eg,
|
||||||
|
|
||||||
@@ -210,7 +144,7 @@ export default abstract class AnnoMatrix {
|
|||||||
return _getColumnSchema(this.schema, field, col);
|
return _getColumnSchema(this.schema, field, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnDimensions(field: Field, col: LabelType): LabelArray | undefined {
|
getColumnDimensions(field, col) {
|
||||||
/*
|
/*
|
||||||
Return the dimensions on this field / column. For most fields, which are 1D,
|
Return the dimensions on this field / column. For most fields, which are 1D,
|
||||||
this just return the column name. Multi-dimensional columns, such as embeddings,
|
this just return the column name. Multi-dimensional columns, such as embeddings,
|
||||||
@@ -228,19 +162,19 @@ export default abstract class AnnoMatrix {
|
|||||||
/**
|
/**
|
||||||
** General utility methods
|
** General utility methods
|
||||||
**/
|
**/
|
||||||
base(): AnnoMatrix {
|
base() {
|
||||||
/*
|
/*
|
||||||
return the base of view, or `this` if not a view.
|
return the base of view, or `this` if not a view.
|
||||||
*/
|
*/
|
||||||
let annoMatrix = this._getViewOf();
|
let annoMatrix = this;
|
||||||
while (annoMatrix.isView) annoMatrix = annoMatrix._getViewOf();
|
while (annoMatrix.isView) annoMatrix = annoMatrix.viewOf;
|
||||||
return annoMatrix;
|
return annoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Load / read interfaces
|
** Load / read interfaces
|
||||||
**/
|
**/
|
||||||
fetch(field: Field, q: Query | Query[]): Promise<Dataframe> {
|
fetch(field, q) {
|
||||||
/*
|
/*
|
||||||
Return the given query on a single matrix field as a single dataframe.
|
Return the given query on a single matrix field as a single dataframe.
|
||||||
Currently supports ONLY full column query.
|
Currently supports ONLY full column query.
|
||||||
@@ -269,7 +203,7 @@ export default abstract class AnnoMatrix {
|
|||||||
1. Fetch the "n_genes" column the "obs":
|
1. Fetch the "n_genes" column the "obs":
|
||||||
|
|
||||||
const df = await fetch("obs", "n_genes")
|
const df = await fetch("obs", "n_genes")
|
||||||
console.log("Largest number of genes is: ", df.summarizeContinuous().max);
|
console.log("Largest number of genes is: ", df.summarize().max);
|
||||||
|
|
||||||
2. Fetch two separate columns from obs. Returns a single dataframe containing
|
2. Fetch two separate columns from obs. Returns a single dataframe containing
|
||||||
the columns:
|
the columns:
|
||||||
@@ -297,7 +231,7 @@ export default abstract class AnnoMatrix {
|
|||||||
return this._fetch(field, q);
|
return this._fetch(field, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetch(field: Field, q: Query): void {
|
prefetch(field, q) {
|
||||||
/*
|
/*
|
||||||
Start a data fetch & cache fill. Identical to fetch() except it does
|
Start a data fetch & cache fill. Identical to fetch() except it does
|
||||||
not return a value.
|
not return a value.
|
||||||
@@ -306,6 +240,7 @@ export default abstract class AnnoMatrix {
|
|||||||
overall component rendering latency.
|
overall component rendering latency.
|
||||||
*/
|
*/
|
||||||
this._fetch(field, q);
|
this._fetch(field, q);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,172 +261,176 @@ export default abstract class AnnoMatrix {
|
|||||||
** The actual implementation is in the sub-classes, which MUST override these.
|
** The actual implementation is in the sub-classes, which MUST override these.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
/*
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
Add a new category value (aka "label") to a writable obs column, and return the new AnnoMatrix.
|
addObsAnnoCategory(col, category) {
|
||||||
Typical use is to add a new user-created label to a user-created obs categorical
|
/*
|
||||||
annotation.
|
Add a new category value (aka "label") to a writable obs column, and return the new AnnoMatrix.
|
||||||
|
Typical use is to add a new user-created label to a user-created obs categorical
|
||||||
|
annotation.
|
||||||
|
|
||||||
Will throw column does not exist or is not writable.
|
Will throw column does not exist or is not writable.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
addObsAnnoCategory("my cell type", "left toenail") -> AnnoMatrix
|
addObsAnnoCategory("my cell type", "left toenail") -> AnnoMatrix
|
||||||
|
|
||||||
*/
|
|
||||||
abstract addObsAnnoCategory(col: LabelType, category: string): AnnoMatrix;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Remove a category value from an obs column, reassign any obs having that value
|
|
||||||
to the 'unassignedCategory' value, and return a promise for a new AnnoMatrix.
|
|
||||||
Typical use is to remove a user-created label from a user-created obs categorical
|
|
||||||
annotation.
|
|
||||||
|
|
||||||
Will throw column does not exist or is not writable.
|
|
||||||
|
|
||||||
An `unassignedCategory` value must be provided, for assignment to any obs/cells
|
|
||||||
that had the now-delete category label as their value.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
await removeObsAnnoCategory("my-tissue-type", "right earlobe", "unassigned") -> AnnoMatrix
|
|
||||||
|
|
||||||
NOTE: method is async as it may need to fetch data to provide the reassignment.
|
|
||||||
*/
|
|
||||||
abstract removeObsAnnoCategory(
|
|
||||||
col: LabelType,
|
|
||||||
category: Category,
|
|
||||||
unassignedCategory: string
|
|
||||||
): Promise<AnnoMatrix>;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Drop an entire writable column, eg a user-created obs annotation. Typical use
|
|
||||||
is to provide the "Delete Category" implementation. Returns the new AnnoMatrix.
|
|
||||||
Will throw if not a writable annotation.
|
|
||||||
|
|
||||||
Will throw column does not exist or is not writable.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
dropObsColumn("old annotations") -> AnnoMatrix
|
|
||||||
*/
|
|
||||||
abstract dropObsColumn(col: LabelType): AnnoMatrix;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Add a new writable OBS annotation column, with the caller-specified schema, initial value
|
|
||||||
type and value.
|
|
||||||
|
|
||||||
Value may be any one of:
|
|
||||||
* an array of values
|
|
||||||
* a primitive type, including null or undefined.
|
|
||||||
If an array, length must be the same as 'this.nObs', and constructor must equal 'Ctor'.
|
|
||||||
If a primitive, 'Ctor' will be used to create the initial value, which will be filled
|
|
||||||
with 'value'.
|
|
||||||
|
|
||||||
Throws if the name specified in 'colSchema' duplicates an existing obs column.
|
|
||||||
|
|
||||||
Returns a new AnnoMatrix.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
addObsColumn(
|
|
||||||
{ name: "foo", type: "categorical", categories: "unassigned" },
|
|
||||||
Array,
|
|
||||||
"unassigned"
|
|
||||||
) -> AnnoMatrix
|
|
||||||
|
|
||||||
*/
|
|
||||||
abstract addObsColumn<T extends DataframeValueArray>(
|
|
||||||
colSchema: AnnotationColumnSchema,
|
|
||||||
Ctor: new (n: number) => T,
|
|
||||||
value: T
|
|
||||||
): AnnoMatrix;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Rename the obs column 'oldCol' to have name 'newCol' and returns new AnnoMatrix.
|
|
||||||
|
|
||||||
Will throw column does not exist or is not writable, or if 'newCol' is not unique.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
renameObsColumn('cell type', 'old cell type') -> AnnoMatrix.
|
|
||||||
|
|
||||||
*/
|
|
||||||
abstract renameObsColumn(oldCol: LabelType, newCol: LabelType): AnnoMatrix;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Set all obs with label in array 'obsLabels' to have 'value'. Typical use would be
|
|
||||||
to set a group of cells to have a label on a user-created categorical annotation
|
|
||||||
(eg set all selected cells to have a label).
|
|
||||||
|
|
||||||
NOTE: async method, as it may need to fetch.
|
|
||||||
|
|
||||||
Will throw column does not exist or is not writable.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
await setObsColmnValues("flavor", [383, 400], "tasty") -> AnnoMtarix
|
|
||||||
*/
|
|
||||||
abstract setObsColumnValues(
|
|
||||||
col: LabelType,
|
|
||||||
obsLabels: Int32Array,
|
|
||||||
value: DataframeValue
|
|
||||||
): Promise<AnnoMatrix>;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Set by value - all elements in the column with value 'oldValue' are set to 'newValue'.
|
|
||||||
Async method - returns a promise for a new AnnoMatrix.
|
|
||||||
|
|
||||||
Typical use would be to set all labels of one value to another.
|
|
||||||
|
|
||||||
Will throw column does not exist or is not writable.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
await resetObsColumnValues("my notes", "good", "not-good") -> AnnoMatrix
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
abstract resetObsColumnValues<T extends DataframeValue>(
|
_subclassResponsibility();
|
||||||
col: LabelType,
|
}
|
||||||
oldValue: T,
|
|
||||||
newValue: T
|
|
||||||
): Promise<AnnoMatrix>;
|
|
||||||
|
|
||||||
/*
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
Add a new obs embedding to the AnnoMatrix, with provided schema.
|
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||||
Returns a new annomatrix.
|
|
||||||
|
|
||||||
Typical use will be to add a re-embedding that the server has calculated.
|
|
||||||
|
|
||||||
Will throw if the column schema is invalid (eg, duplicate name).
|
|
||||||
*/
|
|
||||||
abstract addEmbedding(colSchema: EmbeddingSchema): AnnoMatrix;
|
|
||||||
|
|
||||||
getCacheKeys(
|
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): WhereCacheColumnLabels | [undefined] {
|
|
||||||
/*
|
/*
|
||||||
Return cache keys for columns associated with this query. May return
|
Remove a category value from an obs column, reassign any obs having that value
|
||||||
[unknown] if no keys are known (ie, nothing is or was cached).
|
to the 'unassignedCategory' value, and return a promise for a new AnnoMatrix.
|
||||||
*/
|
Typical use is to remove a user-created label from a user-created obs categorical
|
||||||
|
annotation.
|
||||||
|
|
||||||
|
Will throw column does not exist or is not writable.
|
||||||
|
|
||||||
|
An `unassignedCategory` value must be provided, for assignment to any obs/cells
|
||||||
|
that had the now-delete category label as their value.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
await removeObsAnnoCategory("my-tissue-type", "right earlobe", "unassigned") -> AnnoMatrix
|
||||||
|
|
||||||
|
NOTE: method is async as it may need to fetch data to provide the reassignment.
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
dropObsColumn(col) {
|
||||||
|
/*
|
||||||
|
Drop an entire writable column, eg a user-created obs annotation. Typical use
|
||||||
|
is to provide the "Delete Category" implementation. Returns the new AnnoMatrix.
|
||||||
|
Will throw if not a writable annotation.
|
||||||
|
|
||||||
|
Will throw column does not exist or is not writable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
dropObsColumn("old annotations") -> AnnoMatrix
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
addObsColumn(colSchema, Ctor, value) {
|
||||||
|
/*
|
||||||
|
Add a new writable OBS annotation column, with the caller-specified schema, initial value
|
||||||
|
type and value.
|
||||||
|
|
||||||
|
Value may be any one of:
|
||||||
|
* an array of values
|
||||||
|
* a primitive type, including null or undefined.
|
||||||
|
If an array, length must be the same as 'this.nObs', and constructor must equal 'Ctor'.
|
||||||
|
If a primitive, 'Ctor' will be used to create the initial value, which will be filled
|
||||||
|
with 'value'.
|
||||||
|
|
||||||
|
Throws if the name specified in 'colSchema' duplicates an existing obs column.
|
||||||
|
|
||||||
|
Returns a new AnnoMatrix.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
addObsColumn(
|
||||||
|
{ name: "foo", type: "categorical", categories: "unassigned" },
|
||||||
|
Array,
|
||||||
|
"unassigned"
|
||||||
|
) -> AnnoMatrix
|
||||||
|
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
renameObsColumn(oldCol, newCol) {
|
||||||
|
/*
|
||||||
|
Rename the obs column 'oldCol' to have name 'newCol' and returns new AnnoMatrix.
|
||||||
|
|
||||||
|
Will throw column does not exist or is not writable, or if 'newCol' is not unique.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
renameObsColumn('cell type', 'old cell type') -> AnnoMatrix.
|
||||||
|
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
async setObsColumnValues(col, obsLabels, value) {
|
||||||
|
/*
|
||||||
|
Set all obs with label in array 'obsLabels' to have 'value'. Typical use would be
|
||||||
|
to set a group of cells to have a label on a user-created categorical anntoation
|
||||||
|
(eg set all selected cells to have a label).
|
||||||
|
|
||||||
|
NOTE: async method, as it may need to fetch.
|
||||||
|
|
||||||
|
Will throw column does not exist or is not writable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
await setObsColmnValues("flavor", [383, 400], "tasty") -> AnnoMtarix
|
||||||
|
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
async resetObsColumnValues(col, oldValue, newValue) {
|
||||||
|
/*
|
||||||
|
Set by value - all elements in the column with value 'oldValue' are set to 'newValue'.
|
||||||
|
Async method - returns a promise for a new AnnoMatrix.
|
||||||
|
|
||||||
|
Typical use would be to set all labels of one value to another.
|
||||||
|
|
||||||
|
Will throw column does not exist or is not writable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
await resetObsColumnValues("my notes", "good", "not-good") -> AnnoMatrix
|
||||||
|
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||||
|
addEmbedding(colSchema) {
|
||||||
|
/*
|
||||||
|
Add a new obs embedding to the AnnoMatrix, with provided schema.
|
||||||
|
Returns a new annomatrix.
|
||||||
|
|
||||||
|
Typical use will be to add a re-embedding that the server has calculated.
|
||||||
|
|
||||||
|
Will throw if the column schema is invalid (eg, duplicate name).
|
||||||
|
*/
|
||||||
|
_subclassResponsibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCacheKeys(field, query) {
|
||||||
|
/*
|
||||||
|
Return cache keys for columns associated with this query. May return
|
||||||
|
[unknown] if no keys are known (ie, nothing is or was cached).
|
||||||
|
*/
|
||||||
return _whereCacheGet(this._whereCache, this.schema, field, query);
|
return _whereCacheGet(this._whereCache, this.schema, field, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Private interfaces below.
|
** Private interfaces below.
|
||||||
**/
|
**/
|
||||||
_resolveCachedQueries(field: Field, queries: Query[]): LabelArray {
|
_resolveCachedQueries(field, queries) {
|
||||||
return queries
|
return queries
|
||||||
.map((query: Query) =>
|
.map((query) =>
|
||||||
// @ts-expect-error --- TODO revisit:
|
|
||||||
// `filter`: This expression is not callable.
|
|
||||||
_whereCacheGet(this._whereCache, this.schema, field, query).filter(
|
_whereCacheGet(this._whereCache, this.schema, field, query).filter(
|
||||||
(cacheKey?: LabelType) =>
|
(cacheKey) =>
|
||||||
cacheKey !== undefined && this._cache[field].hasCol(cacheKey)
|
cacheKey !== undefined && this._cache[field].hasCol(cacheKey)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fetch(field: Field, q: Query | Query[]): Promise<Dataframe> {
|
async _fetch(field, q) {
|
||||||
if (!AnnoMatrix.fields().includes(field)) return Dataframe.empty();
|
if (!AnnoMatrix.fields().includes(field)) return undefined;
|
||||||
const queries = Array.isArray(q) ? q : [q];
|
const queries = Array.isArray(q) ? q : [q];
|
||||||
queries.forEach(_queryValidate);
|
queries.forEach(_queryValidate);
|
||||||
|
|
||||||
@@ -502,7 +441,7 @@ Return cache keys for columns associated with this query. May return
|
|||||||
/* find any query not already cached */
|
/* find any query not already cached */
|
||||||
const uncachedQueries = queries.filter((query) =>
|
const uncachedQueries = queries.filter((query) =>
|
||||||
_whereCacheGet(this._whereCache, this.schema, field, query).some(
|
_whereCacheGet(this._whereCache, this.schema, field, query).some(
|
||||||
(cacheKey?: LabelType) =>
|
(cacheKey) =>
|
||||||
cacheKey === undefined || !this._cache[field].hasCol(cacheKey)
|
cacheKey === undefined || !this._cache[field].hasCol(cacheKey)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -511,19 +450,15 @@ Return cache keys for columns associated with this query. May return
|
|||||||
if (uncachedQueries.length > 0) {
|
if (uncachedQueries.length > 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
uncachedQueries.map((query) =>
|
uncachedQueries.map((query) =>
|
||||||
this._getPendingLoad(
|
this._getPendingLoad(field, query, async (_field, _query) => {
|
||||||
field,
|
/* fetch, then index. _doLoad is subclass interface */
|
||||||
query,
|
const [whereCacheUpdate, df] = await this._doLoad(_field, _query);
|
||||||
async (_field: Field, _query: Query): Promise<void> => {
|
this._cache[_field] = this._cache[_field].withColsFrom(df);
|
||||||
/* fetch, then index. _doLoad is subclass interface */
|
this._whereCache = _whereCacheMerge(
|
||||||
const [whereCacheUpdate, df] = await this._doLoad(_field, _query);
|
this._whereCache,
|
||||||
this._cache[_field] = this._cache[_field].withColsFrom(df);
|
whereCacheUpdate
|
||||||
this._whereCache = _whereCacheMerge(
|
);
|
||||||
this._whereCache,
|
})
|
||||||
whereCacheUpdate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -537,11 +472,7 @@ Return cache keys for columns associated with this query. May return
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getPendingLoad(
|
async _getPendingLoad(field, query, fetchFn) {
|
||||||
field: Field,
|
|
||||||
query: Query,
|
|
||||||
fetchFn: (_field: Field, _query: Query) => Promise<void>
|
|
||||||
): Promise<void> {
|
|
||||||
/*
|
/*
|
||||||
Given a query on a field, ensure that we only have a single outstanding
|
Given a query on a field, ensure that we only have a single outstanding
|
||||||
fetch at any given time. If multiple requests occur while a fetch is
|
fetch at any given time. If multiple requests occur while a fetch is
|
||||||
@@ -562,22 +493,9 @@ Return cache keys for columns associated with this query. May return
|
|||||||
return this._pendingLoad[field][key];
|
return this._pendingLoad[field][key];
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract _doLoad(
|
// eslint-disable-next-line class-methods-use-this -- make sure subclass implements
|
||||||
field: Field,
|
async _doLoad() {
|
||||||
query: Query
|
_subclassResponsibility();
|
||||||
): Promise<[WhereCache | null, Dataframe]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines viewOf for this annoMatrix.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @returns - parent annoMatrix if this annoMatrix is a view, otherwise this annoMatrix if it's not a view.
|
|
||||||
*/
|
|
||||||
_getViewOf(): AnnoMatrix {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.viewOf;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -609,21 +527,20 @@ Return cache keys for columns associated with this query. May return
|
|||||||
To be effective, the GC callback needs to be invoked from the undo/redo code,
|
To be effective, the GC callback needs to be invoked from the undo/redo code,
|
||||||
as much of the cache is pinned by that data structure.
|
as much of the cache is pinned by that data structure.
|
||||||
*/
|
*/
|
||||||
_gcField(field: Field, isHot: boolean, pinnedColumns: LabelArray): void {
|
_gcField(field, isHot, pinnedColumns) {
|
||||||
const maxColumns = isHot ? 256 : 10;
|
const maxColumns = isHot ? 256 : 10; // maybe to aggressive?
|
||||||
|
|
||||||
const cache = this._cache[field];
|
const cache = this._cache[field];
|
||||||
if (cache.colIndex.size() < maxColumns) return; // trivial rejection
|
if (cache.colIndex.size() < maxColumns) return; // trivial rejection
|
||||||
|
|
||||||
const candidates = cache.colIndex
|
const candidates = cache.colIndex
|
||||||
.labels()
|
.labels()
|
||||||
// @ts-expect-error --- TODO revisit:
|
.filter((col) => !pinnedColumns.includes(col));
|
||||||
// `col`: Argument of type 'LabelType' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.
|
|
||||||
.filter((col: LabelType) => !pinnedColumns.includes(col));
|
|
||||||
|
|
||||||
const excessCount = candidates.length + pinnedColumns.length - maxColumns;
|
const excessCount = candidates.length + pinnedColumns.length - maxColumns;
|
||||||
if (excessCount > 0) {
|
if (excessCount > 0) {
|
||||||
const { _gcInfo } = this;
|
const { _gcInfo } = this;
|
||||||
candidates.sort((a: LabelType, b: LabelType) => {
|
candidates.sort((a, b) => {
|
||||||
let atime = _gcInfo.get(_columnCacheKey(field, a));
|
let atime = _gcInfo.get(_columnCacheKey(field, a));
|
||||||
if (atime === undefined) atime = 0;
|
if (atime === undefined) atime = 0;
|
||||||
|
|
||||||
@@ -640,49 +557,41 @@ Return cache keys for columns associated with this query. May return
|
|||||||
// ", "
|
// ", "
|
||||||
// )}]`
|
// )}]`
|
||||||
// );
|
// );
|
||||||
// @ts-expect-error --- TODO revisit:
|
|
||||||
// `reduce`: This expression is not callable.
|
|
||||||
this._cache[field] = toDrop.reduce(
|
this._cache[field] = toDrop.reduce(
|
||||||
(df: Dataframe, col: LabelType) => df.dropCol(col),
|
(df, col) => df.dropCol(col),
|
||||||
this._cache[field]
|
this._cache[field]
|
||||||
);
|
);
|
||||||
toDrop.forEach((col: LabelType) =>
|
toDrop.forEach((col) => _gcInfo.delete(_columnCacheKey(field, col)));
|
||||||
_gcInfo.delete(_columnCacheKey(field, col))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_gcFetchCleanup(field: Field, pinnedColumns: LabelArray): void {
|
_gcFetchCleanup(field, pinnedColumns) {
|
||||||
/*
|
/*
|
||||||
Called during data load/fetch. By definition, this is 'hot', so we
|
Called during data load/fetch. By definition, this is 'hot', so we
|
||||||
only want to gc X.
|
only want to gc X.
|
||||||
*/
|
*/
|
||||||
if (field === Field.X) {
|
if (field === "X") {
|
||||||
this._gcField(
|
this._gcField(
|
||||||
field,
|
field,
|
||||||
true,
|
true,
|
||||||
// @ts-expect-error --- TODO revisit:
|
|
||||||
// Property 'concat' does not exist on type 'LabelArray'.
|
|
||||||
pinnedColumns.concat(_getWritableColumns(this.schema, field))
|
pinnedColumns.concat(_getWritableColumns(this.schema, field))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_gc(hints: GCHints): void {
|
_gc(hints) {
|
||||||
/*
|
/*
|
||||||
Called from middleware, or elsewhere. isHot is true if we are in the active store,
|
Called from middleware, or elsewhere. isHot is true if we are in the active store,
|
||||||
or false if we are in some other context (eg, history state).
|
or false if we are in some other context (eg, history state).
|
||||||
*/
|
*/
|
||||||
const { isHot } = hints;
|
const { isHot } = hints;
|
||||||
const candidateFields = isHot
|
const candidateFields = isHot ? ["X"] : ["X", "emb", "var", "obs"];
|
||||||
? [Field.X]
|
|
||||||
: [Field.X, Field.emb, Field.var, Field.obs];
|
|
||||||
candidateFields.forEach((field) =>
|
candidateFields.forEach((field) =>
|
||||||
this._gcField(field, isHot, _getWritableColumns(this.schema, field))
|
this._gcField(field, isHot, _getWritableColumns(this.schema, field))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_gcUpdateStats(field: Field, dataframe: Dataframe): void {
|
_gcUpdateStats(field, dataframe) {
|
||||||
/*
|
/*
|
||||||
called each time a query is performed, allowing the gc to update any bookkeeping
|
called each time a query is performed, allowing the gc to update any bookkeeping
|
||||||
information. Currently, this is just a simple last-fetched timestamp, stored
|
information. Currently, this is just a simple last-fetched timestamp, stored
|
||||||
@@ -691,7 +600,7 @@ Return cache keys for columns associated with this query. May return
|
|||||||
const cols = dataframe.colIndex.labels();
|
const cols = dataframe.colIndex.labels();
|
||||||
const { _gcInfo } = this;
|
const { _gcInfo } = this;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
cols.forEach((c: LabelType) => {
|
cols.forEach((c) => {
|
||||||
_gcInfo.set(_columnCacheKey(field, c), now);
|
_gcInfo.set(_columnCacheKey(field, c), now);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -708,7 +617,7 @@ Return cache keys for columns associated with this query. May return
|
|||||||
|
|
||||||
Do not override _clone();
|
Do not override _clone();
|
||||||
**/
|
**/
|
||||||
_cloneDeeper(clone: AnnoMatrix): AnnoMatrix {
|
_cloneDeeper(clone) {
|
||||||
clone._cache = _shallowClone(this._cache);
|
clone._cache = _shallowClone(this._cache);
|
||||||
clone._gcInfo = new Map();
|
clone._gcInfo = new Map();
|
||||||
clone._pendingLoad = {
|
clone._pendingLoad = {
|
||||||
@@ -720,7 +629,7 @@ Return cache keys for columns associated with this query. May return
|
|||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
_clone(): AnnoMatrix {
|
_clone() {
|
||||||
const clone = _shallowClone(this);
|
const clone = _shallowClone(this);
|
||||||
this._cloneDeeper(clone);
|
this._cloneDeeper(clone);
|
||||||
Object.seal(clone);
|
Object.seal(clone);
|
||||||
@@ -731,6 +640,11 @@ Return cache keys for columns associated with this query. May return
|
|||||||
/*
|
/*
|
||||||
private utility functions below
|
private utility functions below
|
||||||
*/
|
*/
|
||||||
function _columnCacheKey(field: Field, column: LabelType): string {
|
function _columnCacheKey(field, column) {
|
||||||
return `${field}/${column}`;
|
return `${field}/${column}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _subclassResponsibility() {
|
||||||
|
/* protect against bugs in subclass */
|
||||||
|
throw new Error("subclass failed to implement required method");
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Shallow clone an object, correctly handling prototype
|
Shallow clone an object, correctly handling prototype
|
||||||
*/
|
*/
|
||||||
|
export default function _shallowClone(orig) {
|
||||||
export default function _shallowClone<T>(orig: T): T {
|
|
||||||
return Object.assign(Object.create(Object.getPrototypeOf(orig)), orig);
|
return Object.assign(Object.create(Object.getPrototypeOf(orig)), orig);
|
||||||
}
|
}
|
||||||
@@ -9,57 +9,26 @@ AnnoMatrix stay in sync:
|
|||||||
*/
|
*/
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
import Crossfilter from "../util/typedCrossfilter";
|
||||||
import { _getColumnSchema } from "./schema";
|
import { _getColumnSchema } from "./schema";
|
||||||
import {
|
|
||||||
AnnotationColumnSchema,
|
|
||||||
Field,
|
|
||||||
EmbeddingSchema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
import AnnoMatrix from "./annoMatrix";
|
|
||||||
import {
|
|
||||||
Dataframe,
|
|
||||||
DataframeValue,
|
|
||||||
DataframeValueArray,
|
|
||||||
LabelType,
|
|
||||||
} from "../util/dataframe";
|
|
||||||
import { Query } from "./query";
|
|
||||||
import { TypedArray } from "../common/types/arraytypes";
|
|
||||||
import { LabelArray } from "../util/dataframe/types";
|
|
||||||
|
|
||||||
type ObsDimensionParams =
|
function _dimensionNameFromDf(field, df) {
|
||||||
| [string, DataframeValueArray, DataframeValueArray]
|
|
||||||
| [string, DataframeValueArray]
|
|
||||||
| [string, DataframeValueArray, Int32ArrayConstructor]
|
|
||||||
| [string, DataframeValueArray, Float32ArrayConstructor];
|
|
||||||
|
|
||||||
function _dimensionNameFromDf(field: Field, df: Dataframe): string {
|
|
||||||
const colNames = df.colIndex.labels();
|
const colNames = df.colIndex.labels();
|
||||||
return _dimensionName(field, colNames);
|
return _dimensionName(field, colNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _dimensionName(
|
function _dimensionName(field, colNames) {
|
||||||
field: Field,
|
|
||||||
colNames: LabelType | LabelArray
|
|
||||||
): string {
|
|
||||||
if (!Array.isArray(colNames)) return `${field}/${colNames}`;
|
if (!Array.isArray(colNames)) return `${field}/${colNames}`;
|
||||||
return `${field}/${colNames.join(":")}`;
|
return `${field}/${colNames.join(":")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AnnoMatrixObsCrossfilter {
|
export default class AnnoMatrixObsCrossfilter {
|
||||||
annoMatrix: AnnoMatrix;
|
constructor(annoMatrix, _obsCrossfilter = null) {
|
||||||
|
|
||||||
obsCrossfilter: Crossfilter;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
_obsCrossfilter: Crossfilter | null = null
|
|
||||||
) {
|
|
||||||
this.annoMatrix = annoMatrix;
|
this.annoMatrix = annoMatrix;
|
||||||
this.obsCrossfilter =
|
this.obsCrossfilter =
|
||||||
_obsCrossfilter || new Crossfilter(annoMatrix._cache.obs);
|
_obsCrossfilter || new Crossfilter(annoMatrix._cache.obs);
|
||||||
this.obsCrossfilter = this.obsCrossfilter.setData(annoMatrix._cache.obs);
|
this.obsCrossfilter = this.obsCrossfilter.setData(annoMatrix._cache.obs);
|
||||||
}
|
}
|
||||||
|
|
||||||
size(): number {
|
size() {
|
||||||
return this.obsCrossfilter.size();
|
return this.obsCrossfilter.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,33 +39,26 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
|
|
||||||
See API documentation in annoMatrix.js.
|
See API documentation in annoMatrix.js.
|
||||||
**/
|
**/
|
||||||
addObsColumn<T extends DataframeValueArray>(
|
addObsColumn(colSchema, Ctor, value) {
|
||||||
colSchema: AnnotationColumnSchema,
|
|
||||||
Ctor: new (n: number) => T,
|
|
||||||
value: T
|
|
||||||
): AnnoMatrixObsCrossfilter {
|
|
||||||
const annoMatrix = this.annoMatrix.addObsColumn(colSchema, Ctor, value);
|
const annoMatrix = this.annoMatrix.addObsColumn(colSchema, Ctor, value);
|
||||||
const obsCrossfilter = this.obsCrossfilter.setData(annoMatrix._cache.obs);
|
const obsCrossfilter = this.obsCrossfilter.setData(annoMatrix._cache.obs);
|
||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
dropObsColumn(col: LabelType): AnnoMatrixObsCrossfilter {
|
dropObsColumn(col) {
|
||||||
const annoMatrix = this.annoMatrix.dropObsColumn(col);
|
const annoMatrix = this.annoMatrix.dropObsColumn(col);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
const dimName = _dimensionName(Field.obs, col);
|
const dimName = _dimensionName("obs", col);
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
}
|
}
|
||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
renameObsColumn(
|
renameObsColumn(oldCol, newCol) {
|
||||||
oldCol: LabelType,
|
|
||||||
newCol: LabelType
|
|
||||||
): AnnoMatrixObsCrossfilter {
|
|
||||||
const annoMatrix = this.annoMatrix.renameObsColumn(oldCol, newCol);
|
const annoMatrix = this.annoMatrix.renameObsColumn(oldCol, newCol);
|
||||||
const oldDimName = _dimensionName(Field.obs, oldCol);
|
const oldDimName = _dimensionName("obs", oldCol);
|
||||||
const newDimName = _dimensionName(Field.obs, newCol);
|
const newDimName = _dimensionName("obs", newCol);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
if (obsCrossfilter.hasDimension(oldDimName)) {
|
if (obsCrossfilter.hasDimension(oldDimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.renameDimension(oldDimName, newDimName);
|
obsCrossfilter = obsCrossfilter.renameDimension(oldDimName, newDimName);
|
||||||
@@ -104,12 +66,9 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
addObsAnnoCategory(
|
addObsAnnoCategory(col, category) {
|
||||||
col: LabelType,
|
|
||||||
category: string
|
|
||||||
): AnnoMatrixObsCrossfilter {
|
|
||||||
const annoMatrix = this.annoMatrix.addObsAnnoCategory(col, category);
|
const annoMatrix = this.annoMatrix.addObsAnnoCategory(col, category);
|
||||||
const dimName = _dimensionName(Field.obs, col);
|
const dimName = _dimensionName("obs", col);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
@@ -117,17 +76,13 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeObsAnnoCategory(
|
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||||
col: LabelType,
|
|
||||||
category: string,
|
|
||||||
unassignedCategory: string
|
|
||||||
): Promise<AnnoMatrixObsCrossfilter> {
|
|
||||||
const annoMatrix = await this.annoMatrix.removeObsAnnoCategory(
|
const annoMatrix = await this.annoMatrix.removeObsAnnoCategory(
|
||||||
col,
|
col,
|
||||||
category,
|
category,
|
||||||
unassignedCategory
|
unassignedCategory
|
||||||
);
|
);
|
||||||
const dimName = _dimensionName(Field.obs, col);
|
const dimName = _dimensionName("obs", col);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
@@ -135,17 +90,13 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setObsColumnValues(
|
async setObsColumnValues(col, rowLabels, value) {
|
||||||
col: LabelType,
|
|
||||||
rowLabels: Int32Array,
|
|
||||||
value: DataframeValue
|
|
||||||
): Promise<AnnoMatrixObsCrossfilter> {
|
|
||||||
const annoMatrix = await this.annoMatrix.setObsColumnValues(
|
const annoMatrix = await this.annoMatrix.setObsColumnValues(
|
||||||
col,
|
col,
|
||||||
rowLabels,
|
rowLabels,
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
const dimName = _dimensionName(Field.obs, col);
|
const dimName = _dimensionName("obs", col);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
@@ -153,17 +104,13 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetObsColumnValues<T extends DataframeValue>(
|
async resetObsColumnValues(col, oldValue, newValue) {
|
||||||
col: LabelType,
|
|
||||||
oldValue: T,
|
|
||||||
newValue: T
|
|
||||||
): Promise<AnnoMatrixObsCrossfilter> {
|
|
||||||
const annoMatrix = await this.annoMatrix.resetObsColumnValues(
|
const annoMatrix = await this.annoMatrix.resetObsColumnValues(
|
||||||
col,
|
col,
|
||||||
oldValue,
|
oldValue,
|
||||||
newValue
|
newValue
|
||||||
);
|
);
|
||||||
const dimName = _dimensionName(Field.obs, col);
|
const dimName = _dimensionName("obs", col);
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
@@ -171,25 +118,23 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmbedding(colSchema: EmbeddingSchema): AnnoMatrixObsCrossfilter {
|
addEmbedding(colSchema) {
|
||||||
const annoMatrix = this.annoMatrix.addEmbedding(colSchema);
|
const annoMatrix = this.annoMatrix.addEmbedding(colSchema);
|
||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, this.obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, this.obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop the crossfilter dimension. Do not change the annoMatrix. Useful when we
|
* Drop the crossfilter dimension. Do not change the annoMatrix. Useful when we
|
||||||
* want to stop tracking the selection state, but aren't sure we want to blow the
|
* want to stop trackin the selection state, but aren't sure we want to blow the
|
||||||
* annomatrix cache.
|
* annomatrix cache.
|
||||||
*/
|
*/
|
||||||
dropDimension(field: Field, query: Query): AnnoMatrixObsCrossfilter {
|
dropDimension(field, query) {
|
||||||
const { annoMatrix } = this;
|
const { annoMatrix } = this;
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
const keys = annoMatrix
|
const keys = annoMatrix
|
||||||
.getCacheKeys(field, query)
|
.getCacheKeys(field, query)
|
||||||
// @ts-expect-error ts-migrate --- suppressing TS defect (https://github.com/microsoft/TypeScript/issues/44373).
|
.filter((k) => k !== undefined);
|
||||||
// Compiler is complaining that expression is not callable on array union types. Remove suppression once fixed.
|
const dimName = _dimensionName(field, keys);
|
||||||
.filter((k?: string | number) => k !== undefined);
|
|
||||||
const dimName = _dimensionName(field, keys as string[]);
|
|
||||||
if (obsCrossfilter.hasDimension(dimName)) {
|
if (obsCrossfilter.hasDimension(dimName)) {
|
||||||
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
obsCrossfilter = obsCrossfilter.delDimension(dimName);
|
||||||
}
|
}
|
||||||
@@ -201,12 +146,7 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
are just wrappers to lazy create indices.
|
are just wrappers to lazy create indices.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
async select(
|
async select(field, query, spec) {
|
||||||
field: Field,
|
|
||||||
query: Query,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- TODO revisit: waiting for typings from util/typedCrossfilter
|
|
||||||
spec: any
|
|
||||||
): Promise<AnnoMatrixObsCrossfilter> {
|
|
||||||
const { annoMatrix } = this;
|
const { annoMatrix } = this;
|
||||||
let { obsCrossfilter } = this;
|
let { obsCrossfilter } = this;
|
||||||
|
|
||||||
@@ -219,9 +159,7 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
|
|
||||||
// grab the data, so we can grab the index.
|
// grab the data, so we can grab the index.
|
||||||
const df = await annoMatrix.fetch(field, query);
|
const df = await annoMatrix.fetch(field, query);
|
||||||
if (!df) {
|
|
||||||
throw new Error("Dataframe cannot be `undefined`");
|
|
||||||
}
|
|
||||||
const dimName = _dimensionNameFromDf(field, df);
|
const dimName = _dimensionNameFromDf(field, df);
|
||||||
if (!obsCrossfilter.hasDimension(dimName)) {
|
if (!obsCrossfilter.hasDimension(dimName)) {
|
||||||
// lazy index generation - add dimension when first used
|
// lazy index generation - add dimension when first used
|
||||||
@@ -238,26 +176,23 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAll(): AnnoMatrixObsCrossfilter {
|
selectAll() {
|
||||||
/*
|
/*
|
||||||
Select all on any dimension in this field.
|
Select all on any dimension in this field.
|
||||||
*/
|
*/
|
||||||
const { annoMatrix } = this;
|
const { annoMatrix } = this;
|
||||||
const currentDims = this.obsCrossfilter.dimensionNames();
|
const currentDims = this.obsCrossfilter.dimensionNames();
|
||||||
const obsCrossfilter = currentDims.reduce(
|
const obsCrossfilter = currentDims.reduce((xfltr, dim) => xfltr.select(dim, { mode: "all" }), this.obsCrossfilter);
|
||||||
(xfltr, dim) => xfltr.select(dim, { mode: "all" }),
|
|
||||||
this.obsCrossfilter
|
|
||||||
);
|
|
||||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
countSelected(): number {
|
countSelected() {
|
||||||
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
||||||
if (this.obsCrossfilter.size() === 0) return this.annoMatrix.nObs;
|
if (this.obsCrossfilter.size() === 0) return this.annoMatrix.nObs;
|
||||||
return this.obsCrossfilter.countSelected();
|
return this.obsCrossfilter.countSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
allSelectedMask(): Uint8Array {
|
allSelectedMask() {
|
||||||
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
||||||
if (
|
if (
|
||||||
this.obsCrossfilter.size() === 0 ||
|
this.obsCrossfilter.size() === 0 ||
|
||||||
@@ -269,7 +204,7 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return this.obsCrossfilter.allSelectedMask();
|
return this.obsCrossfilter.allSelectedMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
allSelectedLabels(): LabelArray {
|
allSelectedLabels() {
|
||||||
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
||||||
if (
|
if (
|
||||||
this.obsCrossfilter.size() === 0 ||
|
this.obsCrossfilter.size() === 0 ||
|
||||||
@@ -283,18 +218,12 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
return index.labels();
|
return index.labels();
|
||||||
}
|
}
|
||||||
|
|
||||||
fillByIsSelected<A extends TypedArray>(
|
fillByIsSelected(array, selectedValue, deselectedValue) {
|
||||||
array: A,
|
|
||||||
selectedValue: A[0],
|
|
||||||
deselectedValue: A[0]
|
|
||||||
): A {
|
|
||||||
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
/* if no data yet indexed in the crossfilter, just say everything is selected */
|
||||||
if (
|
if (
|
||||||
this.obsCrossfilter.size() === 0 ||
|
this.obsCrossfilter.size() === 0 ||
|
||||||
this.obsCrossfilter.dimensionNames().length === 0
|
this.obsCrossfilter.dimensionNames().length === 0
|
||||||
) {
|
) {
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// Type 'Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array' is not assignable to type 'A'...
|
|
||||||
return array.fill(selectedValue);
|
return array.fill(selectedValue);
|
||||||
}
|
}
|
||||||
return this.obsCrossfilter.fillByIsSelected(
|
return this.obsCrossfilter.fillByIsSelected(
|
||||||
@@ -308,35 +237,25 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
** Private below
|
** Private below
|
||||||
**/
|
**/
|
||||||
|
|
||||||
_addObsCrossfilterDimension(
|
_addObsCrossfilterDimension(annoMatrix, obsCrossfilter, field, df) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
obsCrossfilter: Crossfilter,
|
|
||||||
field: Field,
|
|
||||||
df: Dataframe
|
|
||||||
): Crossfilter {
|
|
||||||
if (field === "var") return obsCrossfilter;
|
if (field === "var") return obsCrossfilter;
|
||||||
const dimName = _dimensionNameFromDf(field, df);
|
const dimName = _dimensionNameFromDf(field, df);
|
||||||
const dimParams = this._getObsDimensionParams(field, df);
|
const dimParams = this._getObsDimensionParams(field, df);
|
||||||
obsCrossfilter = obsCrossfilter.setData(annoMatrix._cache.obs);
|
obsCrossfilter = obsCrossfilter.setData(annoMatrix._cache.obs);
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `...dimParams`: A spread argument must either have a tuple type or be passed to a rest parameter.
|
|
||||||
obsCrossfilter = obsCrossfilter.addDimension(dimName, ...dimParams);
|
obsCrossfilter = obsCrossfilter.addDimension(dimName, ...dimParams);
|
||||||
return obsCrossfilter;
|
return obsCrossfilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getColumnBaseType(field: Field, col: LabelType): string {
|
_getColumnBaseType(field, col) {
|
||||||
/* Look up the primitive type for this field/col */
|
/* Look up the primitive type for this field/col */
|
||||||
const colSchema = _getColumnSchema(this.annoMatrix.schema, field, col);
|
const colSchema = _getColumnSchema(this.annoMatrix.schema, field, col);
|
||||||
return colSchema.type;
|
return colSchema.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getObsDimensionParams(
|
_getObsDimensionParams(field, df) {
|
||||||
field: Field,
|
|
||||||
df: Dataframe
|
|
||||||
): ObsDimensionParams | undefined {
|
|
||||||
/* return the crossfilter dimensiontype type and params for this field/dataframe */
|
/* return the crossfilter dimensiontype type and params for this field/dataframe */
|
||||||
|
|
||||||
if (field === Field.emb) {
|
if (field === "emb") {
|
||||||
/* assumed to be 2D */
|
/* assumed to be 2D */
|
||||||
return ["spatial", df.icol(0).asArray(), df.icol(1).asArray()];
|
return ["spatial", df.icol(0).asArray(), df.icol(1).asArray()];
|
||||||
}
|
}
|
||||||
@@ -344,8 +263,6 @@ export default class AnnoMatrixObsCrossfilter {
|
|||||||
/* assumed to be 1D */
|
/* assumed to be 1D */
|
||||||
const col = df.icol(0);
|
const col = df.icol(0);
|
||||||
const colName = df.colIndex.getLabel(0);
|
const colName = df.colIndex.getLabel(0);
|
||||||
// @ts-expect-error --- TODO revisit:
|
|
||||||
// `colName` Argument of type 'LabelType | undefined' is not assignable to parameter of type 'LabelType'. Type 'undefined' is not assignable to type 'LabelType'.
|
|
||||||
const type = this._getColumnBaseType(field, colName);
|
const type = this._getColumnBaseType(field, colName);
|
||||||
if (type === "string" || type === "categorical" || type === "boolean") {
|
if (type === "string" || type === "categorical" || type === "boolean") {
|
||||||
return ["enum", col.asArray()];
|
return ["enum", col.asArray()];
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { doBinaryRequest, doFetch } from "../util/actionHelpers";
|
||||||
|
|
||||||
|
/* double URI encode - needed for query-param filters */
|
||||||
|
export function _dubEncURIComp(s) {
|
||||||
|
return encodeURIComponent(encodeURIComponent(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* currently unused, consider deleting */
|
||||||
|
export function _fetchResult(promise) {
|
||||||
|
let _status = "pending";
|
||||||
|
const res = promise.then(
|
||||||
|
(r) => {
|
||||||
|
_status = "success";
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
_status = "error";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status = () => _status;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
export { doBinaryRequest, doFetch } from "../util/actionHelpers";
|
|
||||||
|
|
||||||
/* double URI encode - needed for query-param filters */
|
|
||||||
export function _dubEncURIComp(s: string | number | boolean): string {
|
|
||||||
return encodeURIComponent(encodeURIComponent(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* currently unused, consider deleting */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
export function _fetchResult(promise: any) {
|
|
||||||
let _status = "pending";
|
|
||||||
const res = promise.then(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(r: any) => {
|
|
||||||
_status = "success";
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(e: any) => {
|
|
||||||
_status = "error";
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status = () => _status;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
@@ -2,47 +2,31 @@ import { doBinaryRequest, doFetch } from "./fetchHelpers";
|
|||||||
import { matrixFBSToDataframe } from "../util/stateManager/matrix";
|
import { matrixFBSToDataframe } from "../util/stateManager/matrix";
|
||||||
import { _getColumnSchema } from "./schema";
|
import { _getColumnSchema } from "./schema";
|
||||||
import {
|
import {
|
||||||
addObsAnnoCategory,
|
|
||||||
addObsAnnoColumn,
|
addObsAnnoColumn,
|
||||||
addObsLayout,
|
|
||||||
removeObsAnnoCategory,
|
|
||||||
removeObsAnnoColumn,
|
removeObsAnnoColumn,
|
||||||
|
addObsAnnoCategory,
|
||||||
|
removeObsAnnoCategory,
|
||||||
|
addObsLayout,
|
||||||
} from "../util/stateManager/schemaHelpers";
|
} from "../util/stateManager/schemaHelpers";
|
||||||
import { isAnyArray } from "../common/types/arraytypes";
|
import { isArrayOrTypedArray } from "../util/typeHelpers";
|
||||||
import { _whereCacheCreate, WhereCache } from "./whereCache";
|
import { _whereCacheCreate } from "./whereCache";
|
||||||
import AnnoMatrix from "./annoMatrix";
|
import AnnoMatrix from "./annoMatrix";
|
||||||
import PromiseLimit from "../util/promiseLimit";
|
import PromiseLimit from "../util/promiseLimit";
|
||||||
import {
|
import {
|
||||||
_expectComplexQuery,
|
|
||||||
_expectSimpleQuery,
|
_expectSimpleQuery,
|
||||||
_hashStringValues,
|
_expectComplexQuery,
|
||||||
_urlEncodeComplexQuery,
|
|
||||||
_urlEncodeLabelQuery,
|
_urlEncodeLabelQuery,
|
||||||
ComplexQuery,
|
_urlEncodeComplexQuery,
|
||||||
Query,
|
_hashStringValues,
|
||||||
} from "./query";
|
} from "./query";
|
||||||
import {
|
import {
|
||||||
normalizeResponse,
|
normalizeResponse,
|
||||||
normalizeWritableCategoricalSchema,
|
normalizeWritableCategoricalSchema,
|
||||||
} from "./normalize";
|
} from "./normalize";
|
||||||
import {
|
|
||||||
AnnotationColumnSchema,
|
|
||||||
Field,
|
|
||||||
EmbeddingSchema,
|
|
||||||
RawSchema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
import {
|
|
||||||
Dataframe,
|
|
||||||
DataframeValue,
|
|
||||||
DataframeValueArray,
|
|
||||||
LabelType,
|
|
||||||
} from "../util/dataframe";
|
|
||||||
|
|
||||||
const promiseThrottle = new PromiseLimit(5);
|
const promiseThrottle = new PromiseLimit(5);
|
||||||
|
|
||||||
export default class AnnoMatrixLoader extends AnnoMatrix {
|
export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||||
baseURL: string;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
AnnoMatrix implementation which proxies to HTTP server using the CXG REST API.
|
AnnoMatrix implementation which proxies to HTTP server using the CXG REST API.
|
||||||
Used as the base (non-view) instance.
|
Used as the base (non-view) instance.
|
||||||
@@ -53,7 +37,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
new AnnoMatrixLoader(serverBaseURL, schema) -> instance
|
new AnnoMatrixLoader(serverBaseURL, schema) -> instance
|
||||||
|
|
||||||
*/
|
*/
|
||||||
constructor(baseURL: string, schema: RawSchema) {
|
constructor(baseURL, schema) {
|
||||||
const { nObs, nVar } = schema.dataframe;
|
const { nObs, nVar } = schema.dataframe;
|
||||||
super(schema, nObs, nVar);
|
super(schema, nObs, nVar);
|
||||||
|
|
||||||
@@ -68,36 +52,24 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
/**
|
/**
|
||||||
** Public. API described in base class.
|
** Public. API described in base class.
|
||||||
**/
|
**/
|
||||||
addObsAnnoCategory(col: LabelType, category: string): AnnoMatrix {
|
addObsAnnoCategory(col, category) {
|
||||||
/*
|
/*
|
||||||
Add a new category (aka label) to the schema for an obs column.
|
Add a new category (aka label) to the schema for an obs column.
|
||||||
*/
|
*/
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||||
this.schema,
|
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
col
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCategoryTypeCheck(colSchema); // throws on error
|
|
||||||
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, category);
|
newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, category);
|
||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeObsAnnoCategory(
|
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||||
col: LabelType,
|
|
||||||
category: string,
|
|
||||||
unassignedCategory: string
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
/*
|
/*
|
||||||
Remove a single "category" (aka "label") from the data & schema of an obs column.
|
Remove a single "category" (aka "label") from the data & schema of an obs column.
|
||||||
*/
|
*/
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||||
this.schema,
|
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
col
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCategoryTypeCheck(colSchema); // throws on error
|
|
||||||
|
|
||||||
const newAnnoMatrix = await this.resetObsColumnValues(
|
const newAnnoMatrix = await this.resetObsColumnValues(
|
||||||
col,
|
col,
|
||||||
@@ -112,16 +84,12 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
dropObsColumn(col: LabelType): AnnoMatrix {
|
dropObsColumn(col) {
|
||||||
/*
|
/*
|
||||||
drop column from field
|
drop column from field
|
||||||
*/
|
*/
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||||
this.schema,
|
_writableCheck(colSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
col
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCheck(colSchema); // throws on error
|
|
||||||
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||||
@@ -129,11 +97,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
addObsColumn<T extends DataframeValueArray>(
|
addObsColumn(colSchema, Ctor, value) {
|
||||||
colSchema: AnnotationColumnSchema,
|
|
||||||
Ctor: new (n: number) => T,
|
|
||||||
value: T
|
|
||||||
): AnnoMatrix {
|
|
||||||
/*
|
/*
|
||||||
add a column to field, initializing with value. Value may
|
add a column to field, initializing with value. Value may
|
||||||
be one of:
|
be one of:
|
||||||
@@ -144,7 +108,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
colSchema.writable = true;
|
colSchema.writable = true;
|
||||||
const colName = colSchema.name;
|
const colName = colSchema.name;
|
||||||
if (
|
if (
|
||||||
_getColumnSchema(this.schema, Field.obs, colName) ||
|
_getColumnSchema(this.schema, "obs", colName) ||
|
||||||
this._cache.obs.hasCol(colName)
|
this._cache.obs.hasCol(colName)
|
||||||
) {
|
) {
|
||||||
throw new Error("column already exists");
|
throw new Error("column already exists");
|
||||||
@@ -152,7 +116,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
let data;
|
let data;
|
||||||
if (isAnyArray(value)) {
|
if (isArrayOrTypedArray(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)
|
||||||
@@ -170,50 +134,35 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
renameObsColumn(oldCol: LabelType, newCol: LabelType): AnnoMatrix {
|
renameObsColumn(oldCol, newCol) {
|
||||||
/*
|
/*
|
||||||
Rename the obs oldColName to newColName. oldCol must be writable.
|
Rename the obs oldColName to newColName. oldCol must be writable.
|
||||||
*/
|
*/
|
||||||
const oldColSchema = _getColumnSchema(
|
const oldColSchema = _getColumnSchema(this.schema, "obs", oldCol);
|
||||||
this.schema,
|
_writableCheck(oldColSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
oldCol
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCheck(oldColSchema);
|
|
||||||
const value = this._cache.obs.hasCol(oldCol)
|
const value = this._cache.obs.hasCol(oldCol)
|
||||||
? this._cache.obs.col(oldCol).asArray()
|
? this._cache.obs.col(oldCol).asArray()
|
||||||
: undefined;
|
: undefined;
|
||||||
return this.dropObsColumn(oldCol).addObsColumn(
|
return this.dropObsColumn(oldCol).addObsColumn(
|
||||||
{
|
{
|
||||||
...oldColSchema,
|
...oldColSchema,
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `name`: Type 'LabelType' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'.
|
|
||||||
name: newCol,
|
name: newCol,
|
||||||
},
|
},
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `value`: Object is possibly 'undefined'.
|
|
||||||
value.constructor,
|
value.constructor,
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setObsColumnValues(
|
async setObsColumnValues(col, rowLabels, value) {
|
||||||
col: LabelType,
|
|
||||||
rowLabels: Int32Array,
|
|
||||||
value: DataframeValue
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
/*
|
/*
|
||||||
Set all rows identified by rowLabels to value.
|
Set all rows identified by rowLabels to value.
|
||||||
*/
|
*/
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||||
this.schema,
|
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
col
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCategoryTypeCheck(colSchema); // throws on error
|
|
||||||
|
|
||||||
// ensure that we have the data in cache before we manipulate it
|
// ensure that we have the data in cache before we manipulate it
|
||||||
await this.fetch(Field.obs, col);
|
await this.fetch("obs", col);
|
||||||
if (!this._cache.obs.hasCol(col))
|
if (!this._cache.obs.hasCol(col))
|
||||||
throw new Error("Internal error - user annotation data missing");
|
throw new Error("Internal error - user annotation data missing");
|
||||||
|
|
||||||
@@ -221,7 +170,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
const data = this._cache.obs.col(col).asArray().slice();
|
const data = this._cache.obs.col(col).asArray().slice();
|
||||||
for (let i = 0, len = rowIndices.length; i < len; i += 1) {
|
for (let i = 0, len = rowIndices.length; i < len; i += 1) {
|
||||||
const idx = rowIndices[i];
|
const idx = rowIndices[i];
|
||||||
if (idx === -1) throw new Error("Unknown row label");
|
if (idx === undefined) throw new Error("Unknown row label");
|
||||||
data[idx] = value;
|
data[idx] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,29 +183,19 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetObsColumnValues<T extends DataframeValue>(
|
async resetObsColumnValues(col, oldValue, newValue) {
|
||||||
col: LabelType,
|
|
||||||
oldValue: T,
|
|
||||||
newValue: T
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
/*
|
/*
|
||||||
Set all rows with value 'oldValue' to 'newValue'.
|
Set all rows with value 'oldValue' to 'newValue'.
|
||||||
*/
|
*/
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||||
this.schema,
|
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||||
Field.obs,
|
|
||||||
col
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
_writableObsCategoryTypeCheck(colSchema); // throws on error
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `colSchema.categories`: Object is possibly 'undefined'.
|
|
||||||
if (!colSchema.categories.includes(oldValue)) {
|
if (!colSchema.categories.includes(oldValue)) {
|
||||||
throw new Error("unknown category");
|
throw new Error("unknown category");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that we have the data in cache before we manipulate it
|
// ensure that we have the data in cache before we manipulate it
|
||||||
await this.fetch(Field.obs, col);
|
await this.fetch("obs", col);
|
||||||
if (!this._cache.obs.hasCol(col))
|
if (!this._cache.obs.hasCol(col))
|
||||||
throw new Error("Internal error - user annotation data missing");
|
throw new Error("Internal error - user annotation data missing");
|
||||||
|
|
||||||
@@ -274,12 +213,12 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmbedding(colSchema: EmbeddingSchema): AnnoMatrix {
|
addEmbedding(colSchema) {
|
||||||
/*
|
/*
|
||||||
add new layout to the obs embeddings
|
add new layout to the obs embeddings
|
||||||
*/
|
*/
|
||||||
const { name: colName } = colSchema;
|
const { name: colName } = colSchema;
|
||||||
if (_getColumnSchema(this.schema, Field.emb, colName)) {
|
if (_getColumnSchema(this.schema, "emb", colName)) {
|
||||||
throw new Error("column already exists");
|
throw new Error("column already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,10 +230,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
/**
|
/**
|
||||||
** Private below
|
** Private below
|
||||||
**/
|
**/
|
||||||
async _doLoad(
|
async _doLoad(field, query) {
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): Promise<[WhereCache | null, Dataframe]> {
|
|
||||||
/*
|
/*
|
||||||
_doLoad - evaluates the query against the field. Returns:
|
_doLoad - evaluates the query against the field. Returns:
|
||||||
* whereCache update: column query map mapping the query to the column labels
|
* whereCache update: column query map mapping the query to the column labels
|
||||||
@@ -321,9 +257,8 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
default:
|
default:
|
||||||
throw new Error("Unknown field name");
|
throw new Error("Unknown field name");
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = await promiseThrottle.priorityAdd(priority, doRequest);
|
const buffer = await promiseThrottle.priorityAdd(priority, doRequest);
|
||||||
// @ts-expect-error --- TODO revisit:
|
|
||||||
// `buffer`: Argument of type 'unknown' is not assignable to parameter of type 'ArrayBuffer | ArrayBuffer[]'. Type 'unknown' is not assignable to type 'ArrayBuffer[]'.
|
|
||||||
let result = matrixFBSToDataframe(buffer);
|
let result = matrixFBSToDataframe(buffer);
|
||||||
if (!result || result.isEmpty()) throw Error("Unknown field/col");
|
if (!result || result.isEmpty()) throw Error("Unknown field/col");
|
||||||
|
|
||||||
@@ -333,7 +268,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
result.colIndex.labels()
|
result.colIndex.labels()
|
||||||
);
|
);
|
||||||
|
|
||||||
result = normalizeResponse(field, this.schema, result);
|
result = normalizeResponse(field, query, this.schema, result);
|
||||||
|
|
||||||
return [whereCacheUpdate, result];
|
return [whereCacheUpdate, result];
|
||||||
}
|
}
|
||||||
@@ -343,26 +278,20 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
|||||||
Utility functions below
|
Utility functions below
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _writableObsCheck(obsColSchema: AnnotationColumnSchema): void {
|
function _writableCheck(colSchema) {
|
||||||
if (!obsColSchema?.writable) {
|
if (!colSchema?.writable) {
|
||||||
throw new Error("Unknown or readonly obs column");
|
throw new Error("Unknown or readonly obs column");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _writableObsCategoryTypeCheck(
|
function _writableCategoryTypeCheck(colSchema) {
|
||||||
obsColSchema: AnnotationColumnSchema
|
_writableCheck(colSchema);
|
||||||
): void {
|
if (colSchema.type !== "categorical") {
|
||||||
_writableObsCheck(obsColSchema);
|
|
||||||
if (obsColSchema.type !== "categorical") {
|
|
||||||
throw new Error("column must be categorical");
|
throw new Error("column must be categorical");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _embLoader(
|
function _embLoader(baseURL, _field, query) {
|
||||||
baseURL: string,
|
|
||||||
_field: Field,
|
|
||||||
query: Query
|
|
||||||
): () => Promise<ArrayBuffer> {
|
|
||||||
_expectSimpleQuery(query);
|
_expectSimpleQuery(query);
|
||||||
|
|
||||||
const urlBase = `${baseURL}layout/obs`;
|
const urlBase = `${baseURL}layout/obs`;
|
||||||
@@ -371,11 +300,7 @@ function _embLoader(
|
|||||||
return () => doBinaryRequest(url);
|
return () => doBinaryRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _obsOrVarLoader(
|
function _obsOrVarLoader(baseURL, field, query) {
|
||||||
baseURL: string,
|
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): () => Promise<ArrayBuffer> {
|
|
||||||
_expectSimpleQuery(query);
|
_expectSimpleQuery(query);
|
||||||
|
|
||||||
const urlBase = `${baseURL}annotations/${field}`;
|
const urlBase = `${baseURL}annotations/${field}`;
|
||||||
@@ -384,26 +309,19 @@ function _obsOrVarLoader(
|
|||||||
return () => doBinaryRequest(url);
|
return () => doBinaryRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _XLoader(
|
function _XLoader(baseURL, field, query) {
|
||||||
baseURL: string,
|
|
||||||
_field: Field,
|
|
||||||
query: Query
|
|
||||||
): () => Promise<ArrayBuffer> {
|
|
||||||
_expectComplexQuery(query);
|
_expectComplexQuery(query);
|
||||||
|
|
||||||
// Casting here as query is validated to be complex in _expectComplexQuery above.
|
if (query.where) {
|
||||||
const complexQuery = query as ComplexQuery;
|
|
||||||
|
|
||||||
if ("where" in complexQuery) {
|
|
||||||
const urlBase = `${baseURL}data/var`;
|
const urlBase = `${baseURL}data/var`;
|
||||||
const urlQuery = _urlEncodeComplexQuery(complexQuery);
|
const urlQuery = _urlEncodeComplexQuery(query);
|
||||||
const url = `${urlBase}?${urlQuery}`;
|
const url = `${urlBase}?${urlQuery}`;
|
||||||
return () => doBinaryRequest(url);
|
return () => doBinaryRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("summarize" in complexQuery) {
|
if (query.summarize) {
|
||||||
const urlBase = `${baseURL}summarize/var`;
|
const urlBase = `${baseURL}summarize/var`;
|
||||||
const urlQuery = _urlEncodeComplexQuery(complexQuery);
|
const urlQuery = _urlEncodeComplexQuery(query);
|
||||||
|
|
||||||
if (urlBase.length + urlQuery.length < 2000) {
|
if (urlBase.length + urlQuery.length < 2000) {
|
||||||
const url = `${urlBase}?${urlQuery}`;
|
const url = `${urlBase}?${urlQuery}`;
|
||||||
@@ -11,24 +11,16 @@ Undoable metareducer and the AnnoMatrix private API. It would be helpful
|
|||||||
to make the Undoable interface better factored.
|
to make the Undoable interface better factored.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Action, Dispatch, MiddlewareAPI } from "redux";
|
const annoMatrixGC = (store) => (next) => (action) => {
|
||||||
import AnnoMatrix from "./annoMatrix";
|
if (_itIsTimeForGC()) {
|
||||||
import { GCHints } from "../common/types/entities";
|
_doGC(store);
|
||||||
|
}
|
||||||
const annoMatrixGC =
|
return next(action);
|
||||||
(store: MiddlewareAPI) =>
|
};
|
||||||
// GC middleware doesn't add any extra types to dispatch; it just executes GC and continues.
|
|
||||||
(next: Dispatch) =>
|
|
||||||
(action: Action): Action => {
|
|
||||||
if (_itIsTimeForGC()) {
|
|
||||||
_doGC(store);
|
|
||||||
}
|
|
||||||
return next(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastGCTime = 0;
|
let lastGCTime = 0;
|
||||||
const InterGCDelayMS = 30 * 1000; // 30 seconds
|
const InterGCDelayMS = 30 * 1000; // 30 seconds
|
||||||
function _itIsTimeForGC(): boolean {
|
function _itIsTimeForGC() {
|
||||||
/*
|
/*
|
||||||
we don't want to run GC on every dispatch, so throttle it a bit.
|
we don't want to run GC on every dispatch, so throttle it a bit.
|
||||||
|
|
||||||
@@ -42,22 +34,17 @@ function _itIsTimeForGC(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _doGC(store: MiddlewareAPI): void {
|
function _doGC(store) {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
|
||||||
// these should probably be a function imported from undoable.js, etc, as
|
// these should probably be a function imported from undoable.js, etc, as
|
||||||
// they have overly intimate knowledge of our reducers.
|
// they have overly intimiate knowledge of our reducers.
|
||||||
const undoablePast = state["@@undoable/past"];
|
const undoablePast = state["@@undoable/past"];
|
||||||
const undoableFuture = state["@@undoable/future"];
|
const undoableFuture = state["@@undoable/future"];
|
||||||
const undoableStack = undoablePast
|
const undoableStack = undoablePast
|
||||||
.concat(undoableFuture)
|
.concat(undoableFuture)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit: waiting for typings from /reducers
|
.flatMap((snapshot) =>
|
||||||
.flatMap((snapshot: any) =>
|
snapshot.filter((v) => v[0] === "annoMatrix").map((v) => v[1])
|
||||||
snapshot
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit: waiting for typings from /reducers
|
|
||||||
.filter((v: any) => v[0] === "annoMatrix")
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- TODO revisit: waiting for typings from /reducers
|
|
||||||
.map((v: any) => v[1])
|
|
||||||
);
|
);
|
||||||
const currentAnnoMatrix = state.annoMatrix;
|
const currentAnnoMatrix = state.annoMatrix;
|
||||||
|
|
||||||
@@ -65,17 +52,15 @@ function _doGC(store: MiddlewareAPI): void {
|
|||||||
We want to identify those matrixes currently "hot", ie, linked from the current annoMatrix,
|
We want to identify those matrixes currently "hot", ie, linked from the current annoMatrix,
|
||||||
as our current gc algo is more aggressive with those not hot.
|
as our current gc algo is more aggressive with those not hot.
|
||||||
*/
|
*/
|
||||||
const allAnnoMatrices = new Map<AnnoMatrix, GCHints>(
|
const allAnnoMatrices = new Map(
|
||||||
undoableStack.map((m: AnnoMatrix) => [m, { isHot: false }])
|
undoableStack.map((m) => [m, { isHot: false }])
|
||||||
);
|
);
|
||||||
let am = currentAnnoMatrix;
|
let am = currentAnnoMatrix;
|
||||||
while (am?.isView) {
|
while (am) {
|
||||||
allAnnoMatrices.set(am, { isHot: true });
|
allAnnoMatrices.set(am, { isHot: true });
|
||||||
am = am.viewOf;
|
am = am.viewOf;
|
||||||
}
|
}
|
||||||
allAnnoMatrices.forEach((hints, annoMatrix: AnnoMatrix) =>
|
allAnnoMatrices.forEach((hints, annoMatrix) => annoMatrix._gc(hints));
|
||||||
annoMatrix._gc(hints)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default annoMatrixGC;
|
export default annoMatrixGC;
|
||||||
@@ -5,19 +5,8 @@ import {
|
|||||||
overflowCategoryLabel,
|
overflowCategoryLabel,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
} from "../globals";
|
} from "../globals";
|
||||||
import { Dataframe, LabelType, DataframeColumn } from "../util/dataframe";
|
|
||||||
import {
|
|
||||||
AnnotationColumnSchema,
|
|
||||||
ArraySchema,
|
|
||||||
Field,
|
|
||||||
Schema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
|
|
||||||
export function normalizeResponse(
|
export function normalizeResponse(field, query, schema, response) {
|
||||||
field: Field,
|
|
||||||
schema: Schema,
|
|
||||||
response: Dataframe
|
|
||||||
): Dataframe {
|
|
||||||
/**
|
/**
|
||||||
* There are a number of assumptions in the front-end about data typing and data
|
* There are a number of assumptions in the front-end about data typing and data
|
||||||
* characteristics. This routine will normalize a server response dataframe
|
* characteristics. This routine will normalize a server response dataframe
|
||||||
@@ -42,15 +31,11 @@ export function normalizeResponse(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// currently no data or schema normalization necessary for X or emb
|
// currently no data or schema normalization necessary for X or emb
|
||||||
if (field !== Field.obs && field !== Field.var) return response;
|
if (field !== "obs" && field !== "var") return response;
|
||||||
|
|
||||||
const colLabels = response.colIndex.labels();
|
const colLabels = response.colIndex.labels();
|
||||||
for (const colLabel of colLabels) {
|
for (const colLabel of colLabels) {
|
||||||
const colSchema = _getColumnSchema(
|
const colSchema = _getColumnSchema(schema, field, colLabel);
|
||||||
schema,
|
|
||||||
field,
|
|
||||||
colLabel
|
|
||||||
) as AnnotationColumnSchema;
|
|
||||||
const isIndex = _isIndex(schema, field, colLabel);
|
const isIndex = _isIndex(schema, field, colLabel);
|
||||||
const { type, writable } = colSchema;
|
const { type, writable } = colSchema;
|
||||||
|
|
||||||
@@ -74,7 +59,7 @@ export function normalizeResponse(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function castColumnToBoolean(df: Dataframe, label: LabelType): Dataframe {
|
function castColumnToBoolean(df, label) {
|
||||||
const colData = df.col(label).asArray();
|
const colData = df.col(label).asArray();
|
||||||
const newColData = new Array(colData.length);
|
const newColData = new Array(colData.length);
|
||||||
for (let i = 0; i < colData.length; i += 1) newColData[i] = !!colData[i];
|
for (let i = 0; i < colData.length; i += 1) newColData[i] = !!colData[i];
|
||||||
@@ -82,16 +67,13 @@ function castColumnToBoolean(df: Dataframe, label: LabelType): Dataframe {
|
|||||||
return df;
|
return df;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeWritableCategoricalSchema(
|
export function normalizeWritableCategoricalSchema(colSchema, col) {
|
||||||
colSchema: AnnotationColumnSchema, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
col: DataframeColumn
|
|
||||||
): ArraySchema {
|
|
||||||
/*
|
/*
|
||||||
Ensure all enum writable / categorical schema have a categories array, that
|
Ensure all enum writable / categorical schema have a categories array, that
|
||||||
the categories array contains all unique values in the data array, AND that
|
the categories array contains all unique values in the data array, AND that
|
||||||
the array is UI sorted.
|
the array is UI sorted.
|
||||||
*/
|
*/
|
||||||
const categorySet = new Set<string>(
|
const categorySet = new Set(
|
||||||
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
|
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
|
||||||
);
|
);
|
||||||
if (!categorySet.has(unassignedCategoryLabel)) {
|
if (!categorySet.has(unassignedCategoryLabel)) {
|
||||||
@@ -101,19 +83,15 @@ export function normalizeWritableCategoricalSchema(
|
|||||||
return colSchema;
|
return colSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCategorical(
|
export function normalizeCategorical(df, colLabel, colSchema) {
|
||||||
df: Dataframe,
|
|
||||||
colLabel: LabelType,
|
|
||||||
colSchema: AnnotationColumnSchema
|
|
||||||
): Dataframe {
|
|
||||||
/*
|
/*
|
||||||
If writable, ensure schema matches data and we have an unassigned label
|
If writable, ensure schema matches data and we have an unassigned label
|
||||||
|
|
||||||
If not writable, ensure schema matches data and that we consolidate labels in excess
|
If not writable, ensure schema matches data and that we consolidate labels in excess
|
||||||
of "top N" into an overflow labels.
|
of "top N" into an overflow labels.
|
||||||
*/
|
*/
|
||||||
const { writable } = colSchema;
|
const { writable } = colSchema;
|
||||||
const col = df.col(colLabel);
|
const col = df.col(colLabel);
|
||||||
|
|
||||||
if (writable) {
|
if (writable) {
|
||||||
// writable (aka user) annotations
|
// writable (aka user) annotations
|
||||||
normalizeWritableCategoricalSchema(colSchema, col);
|
normalizeWritableCategoricalSchema(colSchema, col);
|
||||||
@@ -125,7 +103,7 @@ export function normalizeCategorical(
|
|||||||
|
|
||||||
// consolidate all categories from data and schema into a single list
|
// consolidate all categories from data and schema into a single list
|
||||||
const colDataSummary = col.summarizeCategorical();
|
const colDataSummary = col.summarizeCategorical();
|
||||||
const allCategories = new Set<string>(
|
const allCategories = new Set(
|
||||||
colDataSummary.categories.concat(colSchema.categories ?? [])
|
colDataSummary.categories.concat(colSchema.categories ?? [])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1,52 +1,21 @@
|
|||||||
import sha1 from "sha1";
|
import sha1 from "sha1";
|
||||||
import { _dubEncURIComp } from "./fetchHelpers";
|
import { _dubEncURIComp } from "./fetchHelpers";
|
||||||
import { Field } from "../common/types/schema";
|
|
||||||
import { LabelType } from "../util/dataframe";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query utilities, mostly for debugging support and validation.
|
* Query utilities, mostly for debugging support and validation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ComplexQuery = SummarizeQuery | WhereQuery;
|
|
||||||
|
|
||||||
export type Query = LabelType | ComplexQuery;
|
|
||||||
|
|
||||||
interface SummarizeQuery {
|
|
||||||
summarize: SummarizeQueryTerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SummarizeQueryTerm {
|
|
||||||
column: string;
|
|
||||||
field: string;
|
|
||||||
method: string;
|
|
||||||
values: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WhereQuery {
|
|
||||||
where: WhereQueryTerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WhereQueryTerm {
|
|
||||||
column: string;
|
|
||||||
field: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _expectSimpleQuery(query: Query): void {
|
|
||||||
if (typeof query === "object") throw new Error("expected simple query");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize & error check the query.
|
* Normalize & error check the query.
|
||||||
* @param {Query} query - the query
|
* @param {object | string} query - the query
|
||||||
* @returns {Query} - the normalized query
|
* @returns {object | string} - the normalized query
|
||||||
*/
|
*/
|
||||||
export function _queryValidate(query: Query): Query {
|
export function _queryValidate(query) {
|
||||||
if (typeof query !== "object") return query;
|
if (typeof query !== "object") return query;
|
||||||
|
|
||||||
if ("where" in query && "summarize" in query)
|
if (query.where && query.summarize)
|
||||||
throw new Error("query may not specify both where and summarize");
|
throw new Error("query may not specify both where and summarize");
|
||||||
if ("where" in query) {
|
if (query.where) {
|
||||||
const {
|
const {
|
||||||
field: queryField,
|
field: queryField,
|
||||||
column: queryColumn,
|
column: queryColumn,
|
||||||
@@ -56,7 +25,7 @@ export function _queryValidate(query: Query): Query {
|
|||||||
throw new Error("Incomplete where query");
|
throw new Error("Incomplete where query");
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
if ("summarize" in query) {
|
if (query.summarize) {
|
||||||
const {
|
const {
|
||||||
field: queryField,
|
field: queryField,
|
||||||
column: queryColumn,
|
column: queryColumn,
|
||||||
@@ -71,7 +40,11 @@ export function _queryValidate(query: Query): Query {
|
|||||||
throw new Error("query must specify one of where or summarize");
|
throw new Error("query must specify one of where or summarize");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _expectComplexQuery(query: Query): void {
|
export function _expectSimpleQuery(query) {
|
||||||
|
if (typeof query === "object") throw new Error("expected simple query");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _expectComplexQuery(query) {
|
||||||
if (typeof query !== "object") throw new Error("expected complex query");
|
if (typeof query !== "object") throw new Error("expected complex query");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +53,12 @@ export function _expectComplexQuery(query: Query): void {
|
|||||||
*
|
*
|
||||||
* @param {string} field
|
* @param {string} field
|
||||||
* @param {string|object} query
|
* @param {string|object} query
|
||||||
* @returns {string} the key
|
* @returns the key
|
||||||
*/
|
*/
|
||||||
export function _queryCacheKey(field: Field, query: Query): string {
|
export function _queryCacheKey(field, query) {
|
||||||
if (typeof query === "object") {
|
if (typeof query === "object") {
|
||||||
// complex query
|
// complex query
|
||||||
if ("where" in query) {
|
if (query.where) {
|
||||||
const {
|
const {
|
||||||
field: queryField,
|
field: queryField,
|
||||||
column: queryColumn,
|
column: queryColumn,
|
||||||
@@ -93,7 +66,7 @@ export function _queryCacheKey(field: Field, query: Query): string {
|
|||||||
} = query.where;
|
} = query.where;
|
||||||
return `${field}/${queryField}/${queryColumn}/${queryValue}`;
|
return `${field}/${queryField}/${queryColumn}/${queryValue}`;
|
||||||
}
|
}
|
||||||
if ("summarize" in query) {
|
if (query.summarize) {
|
||||||
const {
|
const {
|
||||||
method,
|
method,
|
||||||
field: queryField,
|
field: queryField,
|
||||||
@@ -111,34 +84,34 @@ export function _queryCacheKey(field: Field, query: Query): string {
|
|||||||
return `${field}/${query}`;
|
return `${field}/${query}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _urlEncodeWhereQuery(q: WhereQueryTerm): string {
|
function _urlEncodeWhereQuery(q) {
|
||||||
const { field: queryField, column: queryColumn, value: queryValue } = q;
|
const { field: queryField, column: queryColumn, value: queryValue } = q;
|
||||||
return `${_dubEncURIComp(queryField)}:${_dubEncURIComp(
|
return `${_dubEncURIComp(queryField)}:${_dubEncURIComp(
|
||||||
queryColumn
|
queryColumn
|
||||||
)}=${_dubEncURIComp(queryValue)}`;
|
)}=${_dubEncURIComp(queryValue)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _urlEncodeSummarizeQuery(q: SummarizeQueryTerm): string {
|
function _urlEncodeSummarizeQuery(q) {
|
||||||
const { method, field, column, values } = q;
|
const { method, field, column, values } = q;
|
||||||
const filter = values
|
const filter = values
|
||||||
.map((value: string) => _urlEncodeWhereQuery({ field, column, value }))
|
.map((value) => _urlEncodeWhereQuery({ field, column, value }))
|
||||||
.join("&");
|
.join("&");
|
||||||
return `method=${method}&${filter}`;
|
return `method=${method}&${filter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _urlEncodeComplexQuery(q: ComplexQuery): string {
|
export function _urlEncodeComplexQuery(q) {
|
||||||
if (typeof q === "object") {
|
if (typeof q === "object") {
|
||||||
if ("where" in q) {
|
if (q.where) {
|
||||||
return _urlEncodeWhereQuery(q.where);
|
return _urlEncodeWhereQuery(q.where);
|
||||||
}
|
}
|
||||||
if ("summarize" in q) {
|
if (q.summarize) {
|
||||||
return _urlEncodeSummarizeQuery(q.summarize);
|
return _urlEncodeSummarizeQuery(q.summarize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("Unrecognized complex query type");
|
throw new Error("Unrecognized complex query type");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _urlEncodeLabelQuery(colKey: string, q: Query): string {
|
export function _urlEncodeLabelQuery(colKey, q) {
|
||||||
if (!colKey) throw new Error("Unsupported query by name");
|
if (!colKey) throw new Error("Unsupported query by name");
|
||||||
if (typeof q !== "string") throw new Error("Query must be a simple label.");
|
if (typeof q !== "string") throw new Error("Query must be a simple label.");
|
||||||
return `${colKey}=${encodeURIComponent(q)}`;
|
return `${colKey}=${encodeURIComponent(q)}`;
|
||||||
@@ -147,6 +120,7 @@ export function _urlEncodeLabelQuery(colKey: string, q: Query): string {
|
|||||||
/**
|
/**
|
||||||
* Generate the column key the server will send us for this query.
|
* Generate the column key the server will send us for this query.
|
||||||
*/
|
*/
|
||||||
export function _hashStringValues(arrayOfString: string[]): string {
|
export function _hashStringValues(arrayOfString) {
|
||||||
return sha1(arrayOfString.join(""));
|
const hash = sha1(arrayOfString.join(""));
|
||||||
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -1,54 +1,34 @@
|
|||||||
/*
|
/*
|
||||||
Private helper functions related to schema
|
Private helper functions related to schema
|
||||||
*/
|
*/
|
||||||
import {
|
export function _getColumnSchema(schema, field, col) {
|
||||||
AnnotationColumnSchema,
|
|
||||||
ArraySchema,
|
|
||||||
Field,
|
|
||||||
Schema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
import { LabelArray, LabelType } from "../util/dataframe/types";
|
|
||||||
|
|
||||||
export function _getColumnSchema(
|
|
||||||
schema: Schema,
|
|
||||||
field: Field,
|
|
||||||
col: LabelType
|
|
||||||
): ArraySchema {
|
|
||||||
/* look up the column definition */
|
/* look up the column definition */
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case Field.obs:
|
case "obs":
|
||||||
if (typeof col === "object")
|
if (typeof col === "object")
|
||||||
throw new Error("unable to get column schema by query");
|
throw new Error("unable to get column schema by query");
|
||||||
return schema.annotations.obsByName[col];
|
return schema.annotations.obsByName[col];
|
||||||
case Field.var:
|
case "var":
|
||||||
if (typeof col === "object")
|
if (typeof col === "object")
|
||||||
throw new Error("unable to get column schema by query");
|
throw new Error("unable to get column schema by query");
|
||||||
return schema.annotations.varByName[col];
|
return schema.annotations.varByName[col];
|
||||||
case Field.emb:
|
case "emb":
|
||||||
if (typeof col === "object")
|
if (typeof col === "object")
|
||||||
throw new Error("unable to get column schema by query");
|
throw new Error("unable to get column schema by query");
|
||||||
return schema.layout.obsByName[col];
|
return schema.layout.obsByName[col];
|
||||||
case Field.X:
|
case "X":
|
||||||
return schema.dataframe;
|
return schema.dataframe;
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown field name: ${field}`);
|
throw new Error(`unknown field name: ${field}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _isIndex(
|
export function _isIndex(schema, field, col) {
|
||||||
schema: Schema,
|
|
||||||
field: Field.obs | Field.var,
|
|
||||||
col: LabelType
|
|
||||||
): boolean {
|
|
||||||
const index = schema.annotations?.[field].index;
|
const index = schema.annotations?.[field].index;
|
||||||
return !!(index && index === col);
|
return index && index === col;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _getColumnDimensionNames(
|
export function _getColumnDimensionNames(schema, field, col) {
|
||||||
schema: Schema,
|
|
||||||
field: Field,
|
|
||||||
col: LabelType
|
|
||||||
): LabelArray | undefined {
|
|
||||||
/*
|
/*
|
||||||
field/col may be an alias for multiple columns. Currently used to map ND
|
field/col may be an alias for multiple columns. Currently used to map ND
|
||||||
values to 1D dataframe columns for embeddings/layout. Signified by the presence
|
values to 1D dataframe columns for embeddings/layout. Signified by the presence
|
||||||
@@ -58,33 +38,30 @@ export function _getColumnDimensionNames(
|
|||||||
if (!colSchema) {
|
if (!colSchema) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if ("dims" in colSchema) {
|
return colSchema.dims || [col];
|
||||||
return colSchema.dims;
|
|
||||||
}
|
|
||||||
return [col];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _schemaColumns(schema: Schema, field: Field): string[] {
|
export function _schemaColumns(schema, field) {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case Field.obs:
|
case "obs":
|
||||||
return Object.keys(schema.annotations.obsByName);
|
return Object.keys(schema.annotations.obsByName);
|
||||||
case Field.var:
|
case "var":
|
||||||
return Object.keys(schema.annotations.varByName);
|
return Object.keys(schema.annotations.varByName);
|
||||||
case Field.emb:
|
case "emb":
|
||||||
return Object.keys(schema.layout.obsByName);
|
return Object.keys(schema.layout.obsByName);
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown field name: ${field}`);
|
throw new Error(`unknown field name: ${field}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _getWritableColumns(schema: Schema, field: Field): string[] {
|
export function _getWritableColumns(schema, field) {
|
||||||
if (field !== Field.obs) return [];
|
if (field !== "obs") return [];
|
||||||
return schema.annotations.obs.columns
|
return schema.annotations.obs.columns
|
||||||
.filter((v: AnnotationColumnSchema) => v.writable)
|
.filter((v) => v.writable)
|
||||||
.map((v: AnnotationColumnSchema) => v.name);
|
.map((v) => v.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _isContinuousType(schema: ArraySchema): boolean {
|
export function _isContinuousType(schema) {
|
||||||
const { type } = schema;
|
const { type } = schema;
|
||||||
return !(type === "string" || type === "boolean" || type === "categorical");
|
return !(type === "string" || type === "boolean" || type === "categorical");
|
||||||
}
|
}
|
||||||
@@ -4,18 +4,8 @@ instances of AnnoMatrix, implementing common UI functions.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnnoMatrixRowSubsetView, AnnoMatrixClipView } from "./views";
|
import { AnnoMatrixRowSubsetView, AnnoMatrixClipView } from "./views";
|
||||||
import AnnoMatrix from "./annoMatrix";
|
|
||||||
import {
|
|
||||||
DenseInt32Index,
|
|
||||||
IdentityInt32Index,
|
|
||||||
KeyIndex,
|
|
||||||
} from "../util/dataframe";
|
|
||||||
import { OffsetArray } from "../util/dataframe/types";
|
|
||||||
|
|
||||||
export function isubsetMask(
|
export function isubsetMask(annoMatrix, obsMask) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
obsMask: Uint8Array
|
|
||||||
): AnnoMatrixRowSubsetView {
|
|
||||||
/*
|
/*
|
||||||
Subset annomatrix to contain the rows which have truish value in the mask.
|
Subset annomatrix to contain the rows which have truish value in the mask.
|
||||||
Maks length must equal annoMatrix.nObs (row count).
|
Maks length must equal annoMatrix.nObs (row count).
|
||||||
@@ -23,10 +13,7 @@ export function isubsetMask(
|
|||||||
return isubset(annoMatrix, _maskToList(obsMask));
|
return isubset(annoMatrix, _maskToList(obsMask));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isubset(
|
export function isubset(annoMatrix, obsOffsets) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
obsOffsets: OffsetArray
|
|
||||||
): AnnoMatrixRowSubsetView {
|
|
||||||
/*
|
/*
|
||||||
Subset annomatrix to contain the positions contained in the obsOffsets array
|
Subset annomatrix to contain the positions contained in the obsOffsets array
|
||||||
|
|
||||||
@@ -38,10 +25,7 @@ export function isubset(
|
|||||||
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subset(
|
export function subset(annoMatrix, obsLabels) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
obsLabels: Int32Array
|
|
||||||
): AnnoMatrixRowSubsetView {
|
|
||||||
/*
|
/*
|
||||||
subset based on labels
|
subset based on labels
|
||||||
*/
|
*/
|
||||||
@@ -49,21 +33,14 @@ export function subset(
|
|||||||
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subsetByIndex(
|
export function subsetByIndex(annoMatrix, obsIndex) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
obsIndex: DenseInt32Index | IdentityInt32Index | KeyIndex
|
|
||||||
): AnnoMatrixRowSubsetView {
|
|
||||||
/*
|
/*
|
||||||
subset based upon the new obs index.
|
subset based upon the new obs index.
|
||||||
*/
|
*/
|
||||||
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clip(
|
export function clip(annoMatrix, qmin, qmax) {
|
||||||
annoMatrix: AnnoMatrix,
|
|
||||||
qmin: number,
|
|
||||||
qmax: number
|
|
||||||
): AnnoMatrix {
|
|
||||||
/*
|
/*
|
||||||
Create a view that clips all continuous data to the [min, max] range.
|
Create a view that clips all continuous data to the [min, max] range.
|
||||||
The matrix shape does not change, but the continuous values outside the
|
The matrix shape does not change, but the continuous values outside the
|
||||||
@@ -76,8 +53,11 @@ export function clip(
|
|||||||
Private utility functions below
|
Private utility functions below
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _maskToList(mask: Uint8Array): OffsetArray {
|
function _maskToList(mask) {
|
||||||
/* convert masks to lists - method wastes space, but is fast */
|
/* convert masks to lists - method wastes space, but is fast */
|
||||||
|
if (!mask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const list = new Int32Array(mask.length);
|
const list = new Int32Array(mask.length);
|
||||||
let elems = 0;
|
let elems = 0;
|
||||||
for (let i = 0, l = mask.length; i < l; i += 1) {
|
for (let i = 0, l = mask.length; i < l; i += 1) {
|
||||||
@@ -5,51 +5,25 @@ Views on the annomatrix. all API here is defined in viewCreators.js and annoMat
|
|||||||
*/
|
*/
|
||||||
import clip from "../util/clip";
|
import clip from "../util/clip";
|
||||||
import AnnoMatrix from "./annoMatrix";
|
import AnnoMatrix from "./annoMatrix";
|
||||||
import { _whereCacheCreate, WhereCache } from "./whereCache";
|
import { _whereCacheCreate } from "./whereCache";
|
||||||
import { _isContinuousType, _getColumnSchema } from "./schema";
|
import { _isContinuousType, _getColumnSchema } from "./schema";
|
||||||
import {
|
|
||||||
Dataframe,
|
|
||||||
DataframeValue,
|
|
||||||
DataframeValueArray,
|
|
||||||
LabelType,
|
|
||||||
} from "../util/dataframe";
|
|
||||||
import { Query } from "./query";
|
|
||||||
import {
|
|
||||||
AnnotationColumnSchema,
|
|
||||||
ArraySchema,
|
|
||||||
Field,
|
|
||||||
EmbeddingSchema,
|
|
||||||
} from "../common/types/schema";
|
|
||||||
import { LabelIndexBase } from "../util/dataframe/labelIndex";
|
|
||||||
|
|
||||||
type MapFn = (
|
class AnnoMatrixView extends AnnoMatrix {
|
||||||
field: Field,
|
constructor(viewOf, rowIndex = null) {
|
||||||
colLabel: LabelType,
|
|
||||||
colSchema: ArraySchema,
|
|
||||||
colData: DataframeValueArray,
|
|
||||||
df: Dataframe
|
|
||||||
) => DataframeValueArray;
|
|
||||||
|
|
||||||
abstract class AnnoMatrixView extends AnnoMatrix {
|
|
||||||
constructor(viewOf: AnnoMatrix, rowIndex: LabelIndexBase | null = null) {
|
|
||||||
const nObs = rowIndex ? rowIndex.size() : viewOf.nObs;
|
const nObs = rowIndex ? rowIndex.size() : viewOf.nObs;
|
||||||
super(viewOf.schema, nObs, viewOf.nVar, rowIndex || viewOf.rowIndex);
|
super(viewOf.schema, nObs, viewOf.nVar, rowIndex || viewOf.rowIndex);
|
||||||
this.viewOf = viewOf;
|
this.viewOf = viewOf;
|
||||||
this.isView = true;
|
this.isView = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
addObsAnnoCategory(col: LabelType, category: string): AnnoMatrix {
|
addObsAnnoCategory(col, category) {
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = this.viewOf.addObsAnnoCategory(col, category);
|
newAnnoMatrix.viewOf = this.viewOf.addObsAnnoCategory(col, category);
|
||||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeObsAnnoCategory(
|
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||||
col: LabelType,
|
|
||||||
category: string,
|
|
||||||
unassignedCategory: string
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = await this.viewOf.removeObsAnnoCategory(
|
newAnnoMatrix.viewOf = await this.viewOf.removeObsAnnoCategory(
|
||||||
col,
|
col,
|
||||||
@@ -60,7 +34,7 @@ abstract class AnnoMatrixView extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
dropObsColumn(col: LabelType): AnnoMatrix {
|
dropObsColumn(col) {
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = this.viewOf.dropObsColumn(col);
|
newAnnoMatrix.viewOf = this.viewOf.dropObsColumn(col);
|
||||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||||
@@ -68,29 +42,21 @@ abstract class AnnoMatrixView extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
addObsColumn<T extends DataframeValueArray>(
|
addObsColumn(colSchema, Ctor, value) {
|
||||||
colSchema: AnnotationColumnSchema,
|
|
||||||
Ctor: new (n: number) => T,
|
|
||||||
value: T
|
|
||||||
): AnnoMatrix {
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
|
newAnnoMatrix.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
|
||||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
renameObsColumn(oldCol: LabelType, newCol: LabelType): AnnoMatrix {
|
renameObsColumn(oldCol, newCol) {
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
|
newAnnoMatrix.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
|
||||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setObsColumnValues(
|
async setObsColumnValues(col, rowLabels, value) {
|
||||||
col: LabelType,
|
|
||||||
rowLabels: Int32Array,
|
|
||||||
value: DataframeValue
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = await this.viewOf.setObsColumnValues(
|
newAnnoMatrix.viewOf = await this.viewOf.setObsColumnValues(
|
||||||
col,
|
col,
|
||||||
@@ -102,11 +68,7 @@ abstract class AnnoMatrixView extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetObsColumnValues<T extends DataframeValue>(
|
async resetObsColumnValues(col, oldValue, newValue) {
|
||||||
col: LabelType,
|
|
||||||
oldValue: T,
|
|
||||||
newValue: T
|
|
||||||
): Promise<AnnoMatrix> {
|
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = await this.viewOf.resetObsColumnValues(
|
newAnnoMatrix.viewOf = await this.viewOf.resetObsColumnValues(
|
||||||
col,
|
col,
|
||||||
@@ -118,7 +80,7 @@ abstract class AnnoMatrixView extends AnnoMatrix {
|
|||||||
return newAnnoMatrix;
|
return newAnnoMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmbedding(colSchema: EmbeddingSchema): AnnoMatrix {
|
addEmbedding(colSchema) {
|
||||||
const newAnnoMatrix = this._clone();
|
const newAnnoMatrix = this._clone();
|
||||||
newAnnoMatrix.viewOf = this.viewOf.addEmbedding(colSchema);
|
newAnnoMatrix.viewOf = this.viewOf.addEmbedding(colSchema);
|
||||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||||
@@ -127,32 +89,21 @@ abstract class AnnoMatrixView extends AnnoMatrix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AnnoMatrixMapView extends AnnoMatrixView {
|
class AnnoMatrixMapView extends AnnoMatrixView {
|
||||||
mapFn: MapFn;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A view which knows how to transform its data.
|
A view which knows how to transform its data.
|
||||||
*/
|
*/
|
||||||
constructor(viewOf: AnnoMatrix, mapFn: MapFn) {
|
constructor(viewOf, mapFn) {
|
||||||
super(viewOf);
|
super(viewOf);
|
||||||
this.mapFn = mapFn;
|
this.mapFn = mapFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doLoad(
|
async _doLoad(field, query) {
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): Promise<[WhereCache | null, Dataframe]> {
|
|
||||||
const df = await this.viewOf._fetch(field, query);
|
const df = await this.viewOf._fetch(field, query);
|
||||||
const dfMapped = df.mapColumns(
|
const dfMapped = df.mapColumns((colData, colIdx) => {
|
||||||
(colData: DataframeValueArray, colIdx: number) => {
|
const colLabel = df.colIndex.getLabel(colIdx);
|
||||||
const colLabel = df.colIndex.getLabel(colIdx);
|
const colSchema = _getColumnSchema(this.schema, field, colLabel);
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
return this.mapFn(field, colLabel, colSchema, colData, df);
|
||||||
// `colLabel`: Argument of type 'LabelType | undefined' is not assignable to parameter of type 'LabelType'.
|
});
|
||||||
const colSchema = _getColumnSchema(this.schema, field, colLabel);
|
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `colLabel`: Argument of type 'LabelType | undefined' is not assignable to parameter of type 'LabelType'.
|
|
||||||
return this.mapFn(field, colLabel, colSchema, colData, df);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const whereCacheUpdate = _whereCacheCreate(
|
const whereCacheUpdate = _whereCacheCreate(
|
||||||
field,
|
field,
|
||||||
query,
|
query,
|
||||||
@@ -163,23 +114,12 @@ class AnnoMatrixMapView extends AnnoMatrixView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AnnoMatrixClipView extends AnnoMatrixMapView {
|
export class AnnoMatrixClipView extends AnnoMatrixMapView {
|
||||||
clipRange: [number, number];
|
|
||||||
|
|
||||||
isClipped: boolean;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A view which is a clipped transformation of its parent
|
A view which is a clipped transformation of its parent
|
||||||
*/
|
*/
|
||||||
constructor(viewOf: AnnoMatrix, qmin: number, qmax: number) {
|
constructor(viewOf, qmin, qmax) {
|
||||||
super(
|
super(viewOf, (field, colLabel, colSchema, colData, df) =>
|
||||||
viewOf,
|
_clipAnnoMatrix(field, colLabel, colSchema, colData, df, qmin, qmax)
|
||||||
(
|
|
||||||
field: Field,
|
|
||||||
colLabel: LabelType,
|
|
||||||
colSchema: ArraySchema,
|
|
||||||
colData: DataframeValueArray,
|
|
||||||
df: Dataframe
|
|
||||||
) => _clipAnnoMatrix(field, colLabel, colSchema, colData, df, qmin, qmax)
|
|
||||||
);
|
);
|
||||||
this.isClipped = true;
|
this.isClipped = true;
|
||||||
this.clipRange = [qmin, qmax];
|
this.clipRange = [qmin, qmax];
|
||||||
@@ -189,21 +129,18 @@ export class AnnoMatrixClipView extends AnnoMatrixMapView {
|
|||||||
|
|
||||||
export class AnnoMatrixRowSubsetView extends AnnoMatrixView {
|
export class AnnoMatrixRowSubsetView extends AnnoMatrixView {
|
||||||
/*
|
/*
|
||||||
A view which is a subset of total rows.
|
A view which is a subset of total rows.
|
||||||
*/
|
*/
|
||||||
constructor(viewOf: AnnoMatrix, rowIndex: LabelIndexBase) {
|
constructor(viewOf, rowIndex) {
|
||||||
super(viewOf, rowIndex);
|
super(viewOf, rowIndex);
|
||||||
Object.seal(this);
|
Object.seal(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doLoad(
|
async _doLoad(field, query) {
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): Promise<[WhereCache | null, Dataframe]> {
|
|
||||||
const df = await this.viewOf._fetch(field, query);
|
const df = await this.viewOf._fetch(field, query);
|
||||||
|
|
||||||
// don't try to row-subset the var dimension.
|
// don't try to row-subset the var dimension.
|
||||||
if (field === Field.var) {
|
if (field === "var") {
|
||||||
return [null, df];
|
return [null, df];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,23 +158,15 @@ export class AnnoMatrixRowSubsetView extends AnnoMatrixView {
|
|||||||
Utility functions below
|
Utility functions below
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _clipAnnoMatrix(
|
function _clipAnnoMatrix(field, colLabel, colSchema, colData, df, qmin, qmax) {
|
||||||
field: Field,
|
|
||||||
colLabel: LabelType,
|
|
||||||
colSchema: ArraySchema,
|
|
||||||
colData: DataframeValueArray,
|
|
||||||
df: Dataframe,
|
|
||||||
qmin: number,
|
|
||||||
qmax: number
|
|
||||||
): DataframeValueArray {
|
|
||||||
/* only clip obs and var scalar columns */
|
/* only clip obs and var scalar columns */
|
||||||
if (field !== Field.obs && field !== Field.X) return colData;
|
if (field !== "obs" && field !== "X") return colData;
|
||||||
if (!_isContinuousType(colSchema)) return colData;
|
if (!_isContinuousType(colSchema)) return colData;
|
||||||
if (qmin < 0) qmin = 0;
|
if (qmin < 0) qmin = 0;
|
||||||
if (qmax > 1) qmax = 1;
|
if (qmax > 1) qmax = 1;
|
||||||
if (qmin === 0 && qmax === 1) return colData;
|
if (qmin === 0 && qmax === 1) return colData;
|
||||||
|
|
||||||
const quantiles = df.col(colLabel).summarizeContinuous().percentiles;
|
const quantiles = df.col(colLabel).summarize().percentiles;
|
||||||
const lower = quantiles[100 * qmin];
|
const lower = quantiles[100 * qmin];
|
||||||
const upper = quantiles[100 * qmax];
|
const upper = quantiles[100 * qmax];
|
||||||
const clippedData = clip(colData.slice(), lower, upper, Number.NaN);
|
const clippedData = clip(colData.slice(), lower, upper, Number.NaN);
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Private support functions.
|
Private support functions.
|
||||||
|
|
||||||
This implements a query resolver cache, mapping a query onto the column labels
|
This implements a query resolver cache, mapping a query onto the column labels
|
||||||
resolved by that query. These labels are then used to manage the actual data cache,
|
resolved by that query. These labels are then used to manage the acutal data cache,
|
||||||
which stores data by the resolved label.
|
which stores data by the resolved label.
|
||||||
|
|
||||||
There are three query forms:
|
There are three query forms:
|
||||||
@@ -49,33 +49,9 @@ creates a cache entry of:
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
import { _getColumnDimensionNames } from "./schema";
|
import { _getColumnDimensionNames } from "./schema";
|
||||||
import { _hashStringValues, Query } from "./query";
|
import { _hashStringValues } from "./query";
|
||||||
import { Field, Schema } from "../common/types/schema";
|
|
||||||
import { LabelArray } from "../util/dataframe/types";
|
|
||||||
|
|
||||||
export interface WhereCache {
|
export function _whereCacheGet(whereCache, schema, field, query) {
|
||||||
summarize?: {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: WhereCacheTerms;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
where?: {
|
|
||||||
[key: string]: WhereCacheTerms;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WhereCacheColumnLabels = LabelArray;
|
|
||||||
|
|
||||||
interface WhereCacheTerms {
|
|
||||||
[key: string]: Map<string, Map<string, WhereCacheColumnLabels>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _whereCacheGet(
|
|
||||||
whereCache: WhereCache,
|
|
||||||
schema: Schema,
|
|
||||||
field: Field,
|
|
||||||
query: Query
|
|
||||||
): WhereCacheColumnLabels | [undefined] {
|
|
||||||
/*
|
/*
|
||||||
query will either be an where query (object) or a column name (string).
|
query will either be an where query (object) or a column name (string).
|
||||||
|
|
||||||
@@ -83,7 +59,7 @@ export function _whereCacheGet(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if (typeof query === "object") {
|
if (typeof query === "object") {
|
||||||
if ("where" in query) {
|
if (query.where) {
|
||||||
const {
|
const {
|
||||||
field: queryField,
|
field: queryField,
|
||||||
column: queryColumn,
|
column: queryColumn,
|
||||||
@@ -92,7 +68,7 @@ export function _whereCacheGet(
|
|||||||
const columnMap = whereCache?.where?.[field]?.[queryField];
|
const columnMap = whereCache?.where?.[field]?.[queryField];
|
||||||
return columnMap?.get(queryColumn)?.get(queryValue) ?? [undefined];
|
return columnMap?.get(queryColumn)?.get(queryValue) ?? [undefined];
|
||||||
}
|
}
|
||||||
if ("summarize" in query) {
|
if (query.summarize) {
|
||||||
const {
|
const {
|
||||||
method,
|
method,
|
||||||
field: queryField,
|
field: queryField,
|
||||||
@@ -109,17 +85,13 @@ export function _whereCacheGet(
|
|||||||
return _getColumnDimensionNames(schema, field, query) ?? [undefined];
|
return _getColumnDimensionNames(schema, field, query) ?? [undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _whereCacheCreate(
|
export function _whereCacheCreate(field, query, columnLabels) {
|
||||||
field: Field,
|
|
||||||
query: Query,
|
|
||||||
columnLabels: LabelArray
|
|
||||||
): WhereCache | null {
|
|
||||||
/*
|
/*
|
||||||
Create a new whereCache
|
Create a new whereCache
|
||||||
*/
|
*/
|
||||||
if (typeof query !== "object") return null;
|
if (typeof query !== "object") return null;
|
||||||
|
|
||||||
if ("where" in query) {
|
if (query.where) {
|
||||||
const {
|
const {
|
||||||
field: queryField,
|
field: queryField,
|
||||||
column: queryColumn,
|
column: queryColumn,
|
||||||
@@ -135,7 +107,7 @@ export function _whereCacheCreate(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if ("summarize" in query) {
|
if (query.summarize) {
|
||||||
const {
|
const {
|
||||||
method,
|
method,
|
||||||
field: queryField,
|
field: queryField,
|
||||||
@@ -159,25 +131,20 @@ export function _whereCacheCreate(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function __mergeQueries(dst: WhereCacheTerms, src: WhereCacheTerms) {
|
function __mergeQueries(dst, src) {
|
||||||
for (const [queryField, columnMap] of Object.entries(src)) {
|
for (const [queryField, columnMap] of Object.entries(src)) {
|
||||||
dst[queryField] = dst[queryField] || new Map();
|
dst[queryField] = dst[queryField] || new Map();
|
||||||
for (const [queryColumn, valueMap] of columnMap) {
|
for (const [queryColumn, valueMap] of columnMap) {
|
||||||
if (!dst[queryField].has(queryColumn))
|
if (!dst[queryField].has(queryColumn))
|
||||||
dst[queryField].set(queryColumn, new Map());
|
dst[queryField].set(queryColumn, new Map());
|
||||||
for (const [queryValue, columnLabels] of valueMap) {
|
for (const [queryValue, columnLabels] of valueMap) {
|
||||||
// @ts-expect-error ts-migrate --- TODO revisit:
|
|
||||||
// `dst[queryField].get(queryColumn)` Object is possibly 'undefined'.
|
|
||||||
dst[queryField].get(queryColumn).set(queryValue, columnLabels);
|
dst[queryField].get(queryColumn).set(queryValue, columnLabels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __whereCacheMerge(
|
function __whereCacheMerge(dst, src) {
|
||||||
dst: WhereCache,
|
|
||||||
src: WhereCache | null
|
|
||||||
): WhereCache {
|
|
||||||
/*
|
/*
|
||||||
merge src into dst (modifies dst)
|
merge src into dst (modifies dst)
|
||||||
*/
|
*/
|
||||||
@@ -204,6 +171,6 @@ function __whereCacheMerge(
|
|||||||
return dst;
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _whereCacheMerge(...caches: (WhereCache | null)[]): WhereCache {
|
export function _whereCacheMerge(...caches) {
|
||||||
return caches.reduce(__whereCacheMerge, {} as WhereCache);
|
return caches.reduce(__whereCacheMerge, {});
|
||||||
}
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility type and interface definitions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypedArrays that can be assigned to a number.
|
|
||||||
*/
|
|
||||||
export type TypedArray =
|
|
||||||
| Int8Array
|
|
||||||
| Uint8Array
|
|
||||||
| Uint8ClampedArray
|
|
||||||
| 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 interface GenericArrayConstructor<T extends AnyArray> {
|
|
||||||
new (
|
|
||||||
...args: ConstructorParameters<
|
|
||||||
typeof Int8Array &
|
|
||||||
typeof Uint8Array &
|
|
||||||
typeof Int16Array &
|
|
||||||
typeof Uint16Array &
|
|
||||||
typeof Int32Array &
|
|
||||||
typeof Uint32Array &
|
|
||||||
typeof Float32Array &
|
|
||||||
typeof Float64Array &
|
|
||||||
typeof Array
|
|
||||||
>
|
|
||||||
): T;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,8 +0,0 @@
|
|||||||
// If a globally shared type or interface doesn't have a clear owner, put it here
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flags informing garbage collection-related logic.
|
|
||||||
*/
|
|
||||||
export interface GCHints {
|
|
||||||
isHot: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
export type Category = number | string | boolean;
|
|
||||||
|
|
||||||
export interface AnnotationColumnSchema {
|
|
||||||
categories?: Category[];
|
|
||||||
name: string;
|
|
||||||
type: "string" | "float32" | "int32" | "categorical" | "boolean";
|
|
||||||
writable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sub-schema objects describing the schema for a primitive Array or Matrix in one of the fields.
|
|
||||||
*/
|
|
||||||
export type ArraySchema =
|
|
||||||
| AnnotationColumnSchema
|
|
||||||
| EmbeddingSchema
|
|
||||||
| XMatrixSchema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set of data / metadata objects that must be specified in a CXG.
|
|
||||||
*/
|
|
||||||
export enum Field {
|
|
||||||
"obs" = "obs",
|
|
||||||
"var" = "var",
|
|
||||||
"emb" = "emb",
|
|
||||||
"X" = "X",
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
|
||||||
|
|
||||||
|
class AnnoDialog extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isActive,
|
||||||
|
text,
|
||||||
|
title,
|
||||||
|
instruction,
|
||||||
|
cancelTooltipContent,
|
||||||
|
errorMessage,
|
||||||
|
validationError,
|
||||||
|
annoSelect,
|
||||||
|
annoInput,
|
||||||
|
secondaryInstructions,
|
||||||
|
secondaryInput,
|
||||||
|
handleCancel,
|
||||||
|
handleSubmit,
|
||||||
|
primaryButtonText,
|
||||||
|
secondaryButtonText,
|
||||||
|
handleSecondaryButtonSubmit,
|
||||||
|
primaryButtonProps,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog icon="tag" title={title} isOpen={isActive} onClose={handleCancel}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<p>{instruction}</p>
|
||||||
|
{annoInput || null}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: 7,
|
||||||
|
visibility: validationError ? "visible" : "hidden",
|
||||||
|
color: Colors.ORANGE3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
{/* we might rename, secondary button and secondary input are not related */}
|
||||||
|
{secondaryInstructions && (
|
||||||
|
<p style={{ marginTop: secondaryInstructions ? 20 : 0 }}>
|
||||||
|
{secondaryInstructions}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{secondaryInput || null}
|
||||||
|
</div>
|
||||||
|
{annoSelect || null}
|
||||||
|
</div>
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Tooltip content={cancelTooltipContent}>
|
||||||
|
<Button onClick={handleCancel}>Cancel</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{/* we might rename, secondary button and secondary input are not related */}
|
||||||
|
{handleSecondaryButtonSubmit && secondaryButtonText ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleSecondaryButtonSubmit}
|
||||||
|
disabled={!text || validationError}
|
||||||
|
intent="none"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{secondaryButtonText}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
{...primaryButtonProps} // eslint-disable-line react/jsx-props-no-spreading -- Spreading props allows for modularity
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!text || validationError}
|
||||||
|
intent="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{primaryButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnoDialog;
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type State = any;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
class AnnoDialog extends React.PureComponent<{}, State> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isActive' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
isActive,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Readonly<{... Remove this comment to see the full error message
|
|
||||||
text,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
title,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'instruction' does not exist on type 'Rea... Remove this comment to see the full error message
|
|
||||||
instruction,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'cancelTooltipContent' does not exist on ... Remove this comment to see the full error message
|
|
||||||
cancelTooltipContent,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'errorMessage' does not exist on type 'Re... Remove this comment to see the full error message
|
|
||||||
errorMessage,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'validationError' does not exist on type ... Remove this comment to see the full error message
|
|
||||||
validationError,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoSelect' does not exist on type 'Read... Remove this comment to see the full error message
|
|
||||||
annoSelect,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoInput' does not exist on type 'Reado... Remove this comment to see the full error message
|
|
||||||
annoInput,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'secondaryInstructions' does not exist on... Remove this comment to see the full error message
|
|
||||||
secondaryInstructions,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'secondaryInput' does not exist on type '... Remove this comment to see the full error message
|
|
||||||
secondaryInput,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'handleCancel' does not exist on type 'Re... Remove this comment to see the full error message
|
|
||||||
handleCancel,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'handleSubmit' does not exist on type 'Re... Remove this comment to see the full error message
|
|
||||||
handleSubmit,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'primaryButtonText' does not exist on typ... Remove this comment to see the full error message
|
|
||||||
primaryButtonText,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'secondaryButtonText' does not exist on t... Remove this comment to see the full error message
|
|
||||||
secondaryButtonText,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'handleSecondaryButtonSubmit' does not ex... Remove this comment to see the full error message
|
|
||||||
handleSecondaryButtonSubmit,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'primaryButtonProps' does not exist on ty... Remove this comment to see the full error message
|
|
||||||
primaryButtonProps,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog icon="tag" title={title} isOpen={isActive} onClose={handleCancel}>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={Classes.DIALOG_BODY}>
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<p>{instruction}</p>
|
|
||||||
{annoInput || null}
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
marginTop: 7,
|
|
||||||
visibility: validationError ? "visible" : "hidden",
|
|
||||||
color: Colors.ORANGE3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{errorMessage}
|
|
||||||
</p>
|
|
||||||
{/* we might rename, secondary button and secondary input are not related */}
|
|
||||||
{secondaryInstructions && (
|
|
||||||
<p style={{ marginTop: secondaryInstructions ? 20 : 0 }}>
|
|
||||||
{secondaryInstructions}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{secondaryInput || null}
|
|
||||||
</div>
|
|
||||||
{annoSelect || null}
|
|
||||||
</div>
|
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
||||||
<Tooltip content={cancelTooltipContent}>
|
|
||||||
<Button onClick={handleCancel}>Cancel</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{/* we might rename, secondary button and secondary input are not related */}
|
|
||||||
{handleSecondaryButtonSubmit && secondaryButtonText ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleSecondaryButtonSubmit}
|
|
||||||
disabled={!text || validationError}
|
|
||||||
intent="none"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{secondaryButtonText}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<Button
|
|
||||||
{...primaryButtonProps} // eslint-disable-line react/jsx-props-no-spreading -- Spreading props allows for modularity
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!text || validationError}
|
|
||||||
intent="primary"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{primaryButtonText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnnoDialog;
|
|
||||||
@@ -15,38 +15,30 @@ import TermsOfServicePrompt from "./termsPrompt";
|
|||||||
|
|
||||||
import actions from "../actions";
|
import actions from "../actions";
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
loading: state.controls.loading,
|
||||||
loading: (state as any).controls.loading,
|
error: state.controls.error,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
graphRenderCounter: state.controls.graphRenderCounter,
|
||||||
error: (state as any).controls.error,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
graphRenderCounter: (state as any).controls.graphRenderCounter,
|
|
||||||
}))
|
}))
|
||||||
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.
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
/* listen for url changes, fire one when we start the app up */
|
/* listen for url changes, fire one when we start the app up */
|
||||||
window.addEventListener("popstate", this._onURLChanged);
|
window.addEventListener("popstate", this._onURLChanged);
|
||||||
this._onURLChanged();
|
this._onURLChanged();
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
|
||||||
dispatch(actions.doInitialDataLoad(window.location.search));
|
dispatch(actions.doInitialDataLoad(window.location.search));
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
_onURLChanged() {
|
_onURLChanged() {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch({ type: "url changed", url: document.location.href });
|
dispatch({ type: "url changed", url: document.location.href });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
const { loading, error, graphRenderCounter } = this.props;
|
const { loading, error, graphRenderCounter } = this.props;
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -78,16 +70,13 @@ class App extends React.Component {
|
|||||||
{loading || error ? null : (
|
{loading || error ? null : (
|
||||||
<Layout>
|
<Layout>
|
||||||
<LeftSideBar />
|
<LeftSideBar />
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. */}
|
{(viewportRef) => (
|
||||||
{(viewportRef: any) => (
|
|
||||||
<>
|
<>
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
<Embedding />
|
<Embedding />
|
||||||
<Autosave />
|
<Autosave />
|
||||||
<TermsOfServicePrompt />
|
<TermsOfServicePrompt />
|
||||||
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
|
|
||||||
<Legend viewportRef={viewportRef} />
|
<Legend viewportRef={viewportRef} />
|
||||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ key: any; viewportRef: any; }' is not assi... Remove this comment to see the full error message */}
|
|
||||||
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
|
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
+18
-46
@@ -11,78 +11,60 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type State = any;
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
idhash:
|
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
annotations: state.annotations,
|
||||||
(state as any).config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
auth: state.config?.authentication,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
userInfo: state.userInfo,
|
||||||
annotations: (state as any).annotations,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
auth: (state as any).config?.authentication,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
userInfo: (state as any).userInfo,
|
|
||||||
writableCategoriesEnabled:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).config?.parameters?.annotations ?? false,
|
|
||||||
writableGenesetsEnabled: !(
|
writableGenesetsEnabled: !(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
state.config?.parameters?.annotations_genesets_readonly ?? true
|
||||||
((state as any).config?.parameters?.annotations_genesets_readonly ?? true)
|
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
class FilenameDialog extends React.Component {
|
||||||
class FilenameDialog extends React.Component<{}, State> {
|
constructor(props) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
filenameText: "",
|
filenameText: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
dismissFilenameDialog = () => {};
|
dismissFilenameDialog = () => {};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
handleCreateFilename = () => {
|
handleCreateFilename = () => {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { filenameText } = this.state;
|
const { filenameText } = this.state;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "set annotations collection name",
|
type: "set annotations collection name",
|
||||||
data: filenameText,
|
data: filenameText,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
filenameError = () => {
|
filenameError = () => {
|
||||||
const legalNames = /^\w+$/;
|
const legalNames = /^\w+$/;
|
||||||
const { filenameText } = this.state;
|
const { filenameText } = this.state;
|
||||||
let err = false;
|
let err = false;
|
||||||
|
|
||||||
if (filenameText === "") {
|
if (filenameText === "") {
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'boolean'.
|
|
||||||
err = "empty_string";
|
err = "empty_string";
|
||||||
} else if (!legalNames.test(filenameText)) {
|
} else if (!legalNames.test(filenameText)) {
|
||||||
/*
|
/*
|
||||||
IMPORTANT: this test must ultimately match the test applied by the
|
IMPORTANT: this test must ultimately match the test applied by the
|
||||||
backend, which is designed to ensure a safe file name can be created
|
backend, which is designed to ensure a safe file name can be created
|
||||||
from the data collection name. If you change this, you will also need
|
from the data collection name. If you change this, you will also need
|
||||||
to change the validation code in the backend, or it will have no effect.
|
to change the validation code in the backend, or it will have no effect.
|
||||||
*/
|
*/
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'boolean'.
|
|
||||||
err = "characters";
|
err = "characters";
|
||||||
}
|
}
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
filenameErrorMessage = () => {
|
filenameErrorMessage = () => {
|
||||||
const err = this.filenameError();
|
const err = this.filenameError();
|
||||||
let markup = null;
|
let markup = null;
|
||||||
// @ts-expect-error ts-migrate(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
|
||||||
if (err === "empty_string") {
|
if (err === "empty_string") {
|
||||||
markup = (
|
markup = (
|
||||||
<span
|
<span
|
||||||
@@ -96,7 +78,6 @@ class FilenameDialog extends React.Component<{}, State> {
|
|||||||
Name cannot be blank
|
Name cannot be blank
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
// @ts-expect-error ts-migrate(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
|
||||||
} else if (err === "characters") {
|
} else if (err === "characters") {
|
||||||
markup = (
|
markup = (
|
||||||
<span
|
<span
|
||||||
@@ -114,21 +95,16 @@ class FilenameDialog extends React.Component<{}, State> {
|
|||||||
return markup;
|
return markup;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'writableCategoriesEnabled' does not exis... Remove this comment to see the full error message
|
|
||||||
writableCategoriesEnabled,
|
writableCategoriesEnabled,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'writableGenesetsEnabled' does not exist ... Remove this comment to see the full error message
|
|
||||||
writableGenesetsEnabled,
|
writableGenesetsEnabled,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annotations' does not exist on type 'Rea... Remove this comment to see the full error message
|
|
||||||
annotations,
|
annotations,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'idhash' does not exist on type 'Readonly... Remove this comment to see the full error message
|
|
||||||
idhash,
|
idhash,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'userInfo' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
userInfo,
|
userInfo,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { filenameText } = this.state;
|
const { filenameText } = this.state;
|
||||||
|
|
||||||
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
|
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
|
||||||
annotations.promptForFilename &&
|
annotations.promptForFilename &&
|
||||||
!annotations.dataCollectionNameIsReadOnly &&
|
!annotations.dataCollectionNameIsReadOnly &&
|
||||||
@@ -152,7 +128,6 @@ class FilenameDialog extends React.Component<{}, State> {
|
|||||||
<InputGroup
|
<InputGroup
|
||||||
autoFocus
|
autoFocus
|
||||||
value={filenameText}
|
value={filenameText}
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
|
||||||
intent={this.filenameError(filenameText) ? "warning" : "none"}
|
intent={this.filenameError(filenameText) ? "warning" : "none"}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
this.setState({ filenameText: e.target.value })
|
this.setState({ filenameText: e.target.value })
|
||||||
@@ -163,14 +138,12 @@ class FilenameDialog extends React.Component<{}, State> {
|
|||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
marginTop: 7,
|
marginTop: 7,
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
|
||||||
visibility: this.filenameError(filenameText)
|
visibility: this.filenameError(filenameText)
|
||||||
? "visible"
|
? "visible"
|
||||||
: "hidden",
|
: "hidden",
|
||||||
color: Colors.ORANGE3,
|
color: Colors.ORANGE3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. */}
|
|
||||||
{this.filenameErrorMessage(filenameText)}
|
{this.filenameErrorMessage(filenameText)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +171,6 @@ class FilenameDialog extends React.Component<{}, State> {
|
|||||||
<Button onClick={this.dismissFilenameDialog}>Cancel</Button>
|
<Button onClick={this.dismissFilenameDialog}>Cancel</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
|
||||||
disabled={!filenameText || this.filenameError(filenameText)}
|
disabled={!filenameText || this.filenameError(filenameText)}
|
||||||
onClick={this.handleCreateFilename}
|
onClick={this.handleCreateFilename}
|
||||||
intent="primary"
|
intent="primary"
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import actions from "../../actions";
|
||||||
|
import FilenameDialog from "./filenameDialog";
|
||||||
|
|
||||||
|
@connect((state) => ({
|
||||||
|
annotations: state.annotations,
|
||||||
|
obsAnnotationSaveInProgress:
|
||||||
|
state.autosave?.obsAnnotationSaveInProgress ?? false,
|
||||||
|
genesetSaveInProgress: state.autosave?.genesetSaveInProgress ?? false,
|
||||||
|
error: state.autosave?.error,
|
||||||
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
|
writableGenesetsEnabled: !(
|
||||||
|
state.config?.parameters?.annotations_genesets_readonly ?? true
|
||||||
|
),
|
||||||
|
annoMatrix: state.annoMatrix,
|
||||||
|
genesets: state.genesets,
|
||||||
|
lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix,
|
||||||
|
lastSavedGenesets: state.autosave?.lastSavedGenesets,
|
||||||
|
}))
|
||||||
|
class Autosave extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
timer: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { writableCategoriesEnabled, writableGenesetsEnabled } = this.props;
|
||||||
|
|
||||||
|
let { timer } = this.state;
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
if (writableCategoriesEnabled || writableGenesetsEnabled) {
|
||||||
|
timer = setInterval(this.tick, 2500);
|
||||||
|
} else {
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
this.setState({ timer });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const { timer } = this.state;
|
||||||
|
if (timer) this.clearInterval(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick = () => {
|
||||||
|
const { dispatch, obsAnnotationSaveInProgress, genesetSaveInProgress } =
|
||||||
|
this.props;
|
||||||
|
if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) {
|
||||||
|
dispatch(actions.saveObsAnnotationsAction());
|
||||||
|
}
|
||||||
|
if (!genesetSaveInProgress && this.needToSaveGenesets()) {
|
||||||
|
dispatch(actions.saveGenesetsAction());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
needToSaveObsAnnotations = () => {
|
||||||
|
/* return true if we need to save obs cell labels, false if we don't */
|
||||||
|
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
|
||||||
|
return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix);
|
||||||
|
};
|
||||||
|
|
||||||
|
needToSaveGenesets = () => {
|
||||||
|
/* return true if we need to save gene ses, false if we do not */
|
||||||
|
const { genesets, lastSavedGenesets } = this.props;
|
||||||
|
return genesets.initialized && genesets.genesets !== lastSavedGenesets;
|
||||||
|
};
|
||||||
|
|
||||||
|
needToSave() {
|
||||||
|
return this.needToSaveGenesets() || this.needToSaveObsAnnotations();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveInProgress() {
|
||||||
|
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
|
||||||
|
return obsAnnotationSaveInProgress || genesetSaveInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusMessage() {
|
||||||
|
const { error } = this.props;
|
||||||
|
if (error) {
|
||||||
|
return `Autosave error: ${error}`;
|
||||||
|
}
|
||||||
|
return this.needToSave() ? "Unsaved" : "All saved";
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
writableCategoriesEnabled,
|
||||||
|
writableGenesetsEnabled,
|
||||||
|
lastSavedAnnoMatrix,
|
||||||
|
} = this.props;
|
||||||
|
const initialDataLoadComplete = lastSavedAnnoMatrix;
|
||||||
|
|
||||||
|
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="autosave"
|
||||||
|
data-testclass={
|
||||||
|
!initialDataLoadComplete
|
||||||
|
? "autosave-init"
|
||||||
|
: this.saveInProgress() || this.needToSave()
|
||||||
|
? "autosave-incomplete"
|
||||||
|
: "autosave-complete"
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
display: "inherit",
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.statusMessage()}
|
||||||
|
<FilenameDialog />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Autosave;
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import FilenameDialog from "./filenameDialog";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
type State = any;
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
|
||||||
@connect((state) => ({
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
annotations: (state as any).annotations,
|
|
||||||
obsAnnotationSaveInProgress:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).autosave?.obsAnnotationSaveInProgress ?? false,
|
|
||||||
genesetSaveInProgress:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).autosave?.genesetSaveInProgress ?? false,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
error: (state as any).autosave?.error,
|
|
||||||
writableCategoriesEnabled:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).config?.parameters?.annotations ?? false,
|
|
||||||
writableGenesetsEnabled: !(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
((state as any).config?.parameters?.annotations_genesets_readonly ?? true)
|
|
||||||
),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
annoMatrix: (state as any).annoMatrix,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
genesets: (state as any).genesets,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
lastSavedAnnoMatrix: (state as any).autosave?.lastSavedAnnoMatrix,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
lastSavedGenesets: (state as any).autosave?.lastSavedGenesets,
|
|
||||||
}))
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
class Autosave extends React.Component<{}, State> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
timer: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
componentDidMount() {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'writableCategoriesEnabled' does not exis... Remove this comment to see the full error message
|
|
||||||
const { writableCategoriesEnabled, writableGenesetsEnabled } = this.props;
|
|
||||||
let { timer } = this.state;
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
if (writableCategoriesEnabled || writableGenesetsEnabled) {
|
|
||||||
timer = setInterval(this.tick, 2500);
|
|
||||||
} else {
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
this.setState({ timer });
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
componentWillUnmount() {
|
|
||||||
const { timer } = this.state;
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
tick = () => {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
dispatch,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'obsAnnotationSaveInProgress' does not ex... Remove this comment to see the full error message
|
|
||||||
obsAnnotationSaveInProgress,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'genesetSaveInProgress' does not exist on... Remove this comment to see the full error message
|
|
||||||
genesetSaveInProgress,
|
|
||||||
} = this.props;
|
|
||||||
if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) {
|
|
||||||
dispatch(actions.saveObsAnnotationsAction());
|
|
||||||
}
|
|
||||||
if (!genesetSaveInProgress && this.needToSaveGenesets()) {
|
|
||||||
dispatch(actions.saveGenesetsAction());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
needToSaveObsAnnotations = () => {
|
|
||||||
/* return true if we need to save obs cell labels, false if we don't */
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
|
||||||
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
|
|
||||||
return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
needToSaveGenesets = () => {
|
|
||||||
/* return true if we need to save gene ses, false if we do not */
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'genesets' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { genesets, lastSavedGenesets } = this.props;
|
|
||||||
return genesets.initialized && genesets.genesets !== lastSavedGenesets;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
needToSave() {
|
|
||||||
return this.needToSaveGenesets() || this.needToSaveObsAnnotations();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
saveInProgress() {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'obsAnnotationSaveInProgress' does not ex... Remove this comment to see the full error message
|
|
||||||
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
|
|
||||||
return obsAnnotationSaveInProgress || genesetSaveInProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
statusMessage() {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'error' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
const { error } = this.props;
|
|
||||||
if (error) {
|
|
||||||
return `Autosave error: ${error}`;
|
|
||||||
}
|
|
||||||
return this.needToSave() ? "Unsaved" : "All saved";
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'writableCategoriesEnabled' does not exis... Remove this comment to see the full error message
|
|
||||||
writableCategoriesEnabled,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'writableGenesetsEnabled' does not exist ... Remove this comment to see the full error message
|
|
||||||
writableGenesetsEnabled,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastSavedAnnoMatrix' does not exist on t... Remove this comment to see the full error message
|
|
||||||
lastSavedAnnoMatrix,
|
|
||||||
} = this.props;
|
|
||||||
const initialDataLoadComplete = lastSavedAnnoMatrix;
|
|
||||||
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="autosave"
|
|
||||||
data-testclass={
|
|
||||||
!initialDataLoadComplete
|
|
||||||
? "autosave-init"
|
|
||||||
: this.saveInProgress() || this.needToSave()
|
|
||||||
? "autosave-incomplete"
|
|
||||||
: "autosave-complete"
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
display: "inherit",
|
|
||||||
right: 8,
|
|
||||||
bottom: 8,
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.statusMessage()}
|
|
||||||
<FilenameDialog />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Autosave;
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
|
||||||
|
const ErrorLoading = ({ displayName, zebra }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: zebra ? globals.lightestGrey : "white",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{`Failure loading ${displayName}`}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ErrorLoading;
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const ErrorLoading = ({ displayName, zebra }: any) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white",
|
|
||||||
fontStyle: "italic",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{`Failure loading ${displayName}`}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ErrorLoading;
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const HistogramFooter = React.memo(
|
||||||
|
({
|
||||||
|
displayName,
|
||||||
|
hideRanges,
|
||||||
|
rangeMin,
|
||||||
|
rangeMax,
|
||||||
|
rangeColorMin,
|
||||||
|
rangeColorMax,
|
||||||
|
isObs,
|
||||||
|
isGeneSetSummary,
|
||||||
|
}) =>
|
||||||
|
/*
|
||||||
|
Footer of each histogram. Will render range and title.
|
||||||
|
|
||||||
|
Required props:
|
||||||
|
* displayName - the displayName, aka "n_genes", "FOXP2", etc.
|
||||||
|
* hideRanges - true/false, enables/disable rendering of ranges
|
||||||
|
* range - length two array, [min, max], containing the range values to display
|
||||||
|
* rangeColor - length two array, [mincolor, maxcolor], each a CSS color
|
||||||
|
*/
|
||||||
|
(
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: hideRanges ? "center" : "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: rangeColorMin,
|
||||||
|
display: hideRanges ? "none" : "block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
min {rangeMin.toPrecision(4)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
data-testclass="brushable-histogram-field-name"
|
||||||
|
style={{ fontStyle: "italic" }}
|
||||||
|
>
|
||||||
|
{isObs && displayName}
|
||||||
|
{isGeneSetSummary && "gene set mean expression"}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: hideRanges ? "block" : "none" }}>
|
||||||
|
: {rangeMin}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: rangeColorMax,
|
||||||
|
display: hideRanges ? "none" : "block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
max {rangeMax.toPrecision(4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HistogramFooter;
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const HistogramFooter = React.memo(
|
|
||||||
({
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'displayName' does not exist on type '{ c... Remove this comment to see the full error message
|
|
||||||
displayName,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'hideRanges' does not exist on type '{ ch... Remove this comment to see the full error message
|
|
||||||
hideRanges,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'rangeMin' does not exist on type '{ chil... Remove this comment to see the full error message
|
|
||||||
rangeMin,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'rangeMax' does not exist on type '{ chil... Remove this comment to see the full error message
|
|
||||||
rangeMax,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'rangeColorMin' does not exist on type '{... Remove this comment to see the full error message
|
|
||||||
rangeColorMin,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'rangeColorMax' does not exist on type '{... Remove this comment to see the full error message
|
|
||||||
rangeColorMax,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type '{ childre... Remove this comment to see the full error message
|
|
||||||
isObs,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isGeneSetSummary' does not exist on type... Remove this comment to see the full error message
|
|
||||||
isGeneSetSummary,
|
|
||||||
}) => (
|
|
||||||
/*
|
|
||||||
Footer of each histogram. Will render range and title.
|
|
||||||
|
|
||||||
Required props:
|
|
||||||
* displayName - the displayName, aka "n_genes", "FOXP2", etc.
|
|
||||||
* hideRanges - true/false, enables/disable rendering of ranges
|
|
||||||
* range - length two array, [min, max], containing the range values to display
|
|
||||||
* rangeColor - length two array, [mincolor, maxcolor], each a CSS color
|
|
||||||
*/
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: hideRanges ? "center" : "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: rangeColorMin,
|
|
||||||
display: hideRanges ? "none" : "block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
min {rangeMin.toPrecision(4)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-testclass="brushable-histogram-field-name"
|
|
||||||
style={{ fontStyle: "italic" }}
|
|
||||||
>
|
|
||||||
{isObs && displayName}
|
|
||||||
{isGeneSetSummary && "gene set mean expression"}
|
|
||||||
</span>
|
|
||||||
<div style={{ display: hideRanges ? "block" : "none" }}>
|
|
||||||
: {rangeMin}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: rangeColorMax,
|
|
||||||
display: hideRanges ? "none" : "block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
max {rangeMax.toPrecision(4)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HistogramFooter;
|
|
||||||
-9
@@ -5,23 +5,14 @@ import * as globals from "../../globals";
|
|||||||
|
|
||||||
const HistogramHeader = React.memo(
|
const HistogramHeader = React.memo(
|
||||||
({
|
({
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'fieldId' does not exist on type '{ child... Remove this comment to see the full error message
|
|
||||||
fieldId,
|
fieldId,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorBy' does not exist on type '{ chi... Remove this comment to see the full error message
|
|
||||||
isColorBy,
|
isColorBy,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onColorByClick' does not exist on type '... Remove this comment to see the full error message
|
|
||||||
onColorByClick,
|
onColorByClick,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onRemoveClick' does not exist on type '{... Remove this comment to see the full error message
|
|
||||||
onRemoveClick,
|
onRemoveClick,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterPlotX' does not exist on type '... Remove this comment to see the full error message
|
|
||||||
isScatterPlotX,
|
isScatterPlotX,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterPlotY' does not exist on type '... Remove this comment to see the full error message
|
|
||||||
isScatterPlotY,
|
isScatterPlotY,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onScatterPlotXClick' does not exist on t... Remove this comment to see the full error message
|
|
||||||
onScatterPlotXClick,
|
onScatterPlotXClick,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onScatterPlotYClick' does not exist on t... Remove this comment to see the full error message
|
|
||||||
onScatterPlotYClick,
|
onScatterPlotYClick,
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type '{ childre... Remove this comment to see the full error message
|
|
||||||
isObs,
|
isObs,
|
||||||
}) => {
|
}) => {
|
||||||
/*
|
/*
|
||||||
+5
-24
@@ -2,11 +2,9 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import { interpolateCool } from "d3-scale-chromatic";
|
import { interpolateCool } from "d3-scale-chromatic";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import { AxisDomain } from "d3";
|
|
||||||
import maybeScientific from "../../util/maybeScientific";
|
import maybeScientific from "../../util/maybeScientific";
|
||||||
import clamp from "../../util/clamp";
|
import clamp from "../../util/clamp";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const Histogram = ({
|
const Histogram = ({
|
||||||
field,
|
field,
|
||||||
fieldForId,
|
fieldForId,
|
||||||
@@ -19,8 +17,8 @@ const Histogram = ({
|
|||||||
margin,
|
margin,
|
||||||
isColorBy,
|
isColorBy,
|
||||||
selectionRange,
|
selectionRange,
|
||||||
mini, // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
mini,
|
||||||
}: any) => {
|
}) => {
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const [brush, setBrush] = useState(null);
|
const [brush, setBrush] = useState(null);
|
||||||
|
|
||||||
@@ -71,19 +69,14 @@ const Histogram = ({
|
|||||||
.data(bins)
|
.data(bins)
|
||||||
.enter()
|
.enter()
|
||||||
.append("rect")
|
.append("rect")
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'd' is declared but its value is never read.
|
|
||||||
.attr("x", (d, i) => x(binStart(i)) + 1)
|
.attr("x", (d, i) => x(binStart(i)) + 1)
|
||||||
.attr("y", (d) => y(d))
|
.attr("y", (d) => y(d))
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'd' is declared but its value is never read.
|
|
||||||
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - binPadding)
|
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - binPadding)
|
||||||
.attr("height", (d) => y(0) - y(d))
|
.attr("height", (d) => y(0) - y(d))
|
||||||
.style(
|
.style(
|
||||||
"fill",
|
"fill",
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
isColorBy
|
isColorBy
|
||||||
? // @ts-expect-error ts-migrate(6133) FIXME: 'd' is declared but its value is never read.
|
? (d, i) => colorScale(histogramScale(binStart(i)))
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(d: any, i: any) => colorScale(histogramScale(binStart(i)))
|
|
||||||
: defaultBarColor
|
: defaultBarColor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -108,7 +101,6 @@ const Histogram = ({
|
|||||||
const brushXselection = container
|
const brushXselection = container
|
||||||
.insert("g")
|
.insert("g")
|
||||||
.attr("class", "brush")
|
.attr("class", "brush")
|
||||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
||||||
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
|
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
|
||||||
.call(brushX);
|
.call(brushX);
|
||||||
|
|
||||||
@@ -121,12 +113,7 @@ const Histogram = ({
|
|||||||
d3
|
d3
|
||||||
.axisBottom(x)
|
.axisBottom(x)
|
||||||
.ticks(4)
|
.ticks(4)
|
||||||
.tickFormat(
|
.tickFormat(d3.format(maybeScientific(x)))
|
||||||
d3.format(maybeScientific(x)) as (
|
|
||||||
dv: AxisDomain,
|
|
||||||
i: number
|
|
||||||
) => string
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Y AXIS */
|
/* Y AXIS */
|
||||||
@@ -139,10 +126,8 @@ const Histogram = ({
|
|||||||
.axisRight(y)
|
.axisRight(y)
|
||||||
.ticks(3)
|
.ticks(3)
|
||||||
.tickFormat(
|
.tickFormat(
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
|
||||||
d3.format(
|
d3.format(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
|
||||||
y.domain().some((n: any) => Math.abs(n) >= 10000) ? ".0e" : ","
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -152,7 +137,6 @@ const Histogram = ({
|
|||||||
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
|
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
|
||||||
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
|
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ brushX: d3.BrushBehavior<unkno... Remove this comment to see the full error message
|
|
||||||
setBrush({ brushX, brushXselection });
|
setBrush({ brushX, brushXselection });
|
||||||
}
|
}
|
||||||
}, [histogram, isColorBy]);
|
}, [histogram, isColorBy]);
|
||||||
@@ -162,7 +146,6 @@ const Histogram = ({
|
|||||||
paint/update selection brush
|
paint/update selection brush
|
||||||
*/
|
*/
|
||||||
if (!brush) return;
|
if (!brush) return;
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'brushX' does not exist on type 'null'.
|
|
||||||
const { brushX, brushXselection } = brush;
|
const { brushX, brushXselection } = brush;
|
||||||
const selection = d3.brushSelection(brushXselection.node());
|
const selection = d3.brushSelection(brushXselection.node());
|
||||||
if (!selectionRange && selection) {
|
if (!selectionRange && selection) {
|
||||||
@@ -179,9 +162,7 @@ const Histogram = ({
|
|||||||
} else {
|
} else {
|
||||||
/* there is an active selection and a brush - make sure they match */
|
/* there is an active selection and a brush - make sure they match */
|
||||||
const moveDeltaThreshold = 1;
|
const moveDeltaThreshold = 1;
|
||||||
// @ts-expect-error ts-migrate(2363) FIXME: The right-hand side of an arithmetic operation mus... Remove this comment to see the full error message
|
|
||||||
const dX0 = Math.abs(x0 - selection[0]);
|
const dX0 = Math.abs(x0 - selection[0]);
|
||||||
// @ts-expect-error ts-migrate(2363) FIXME: The right-hand side of an arithmetic operation mus... Remove this comment to see the full error message
|
|
||||||
const dX1 = Math.abs(x1 - selection[1]);
|
const dX1 = Math.abs(x1 - selection[1]);
|
||||||
/*
|
/*
|
||||||
only update the brush if it is grossly incorrect,
|
only update the brush if it is grossly incorrect,
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect, shallowEqual } from "react-redux";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import Async from "react-async";
|
||||||
|
import memoize from "memoize-one";
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
import actions from "../../actions";
|
||||||
|
import { makeContinuousDimensionName } from "../../util/nameCreators";
|
||||||
|
import HistogramHeader from "./header";
|
||||||
|
import Histogram from "./histogram";
|
||||||
|
import HistogramFooter from "./footer";
|
||||||
|
import StillLoading from "./loading";
|
||||||
|
import ErrorLoading from "./error";
|
||||||
|
|
||||||
|
const MARGIN = {
|
||||||
|
LEFT: 10, // Space for 0 tick label on X axis
|
||||||
|
RIGHT: 54, // space for Y axis & labels
|
||||||
|
BOTTOM: 25, // space for X axis & labels
|
||||||
|
TOP: 3,
|
||||||
|
};
|
||||||
|
const WIDTH = 340 - MARGIN.LEFT - MARGIN.RIGHT;
|
||||||
|
const HEIGHT = 135 - MARGIN.TOP - MARGIN.BOTTOM;
|
||||||
|
const MARGIN_MINI = {
|
||||||
|
LEFT: 0, // Space for 0 tick label on X axis
|
||||||
|
RIGHT: 0, // space for Y axis & labels
|
||||||
|
BOTTOM: 0, // space for X axis & labels
|
||||||
|
TOP: 0,
|
||||||
|
};
|
||||||
|
const WIDTH_MINI = 120 - MARGIN_MINI.LEFT - MARGIN_MINI.RIGHT;
|
||||||
|
const HEIGHT_MINI = 15 - MARGIN_MINI.TOP - MARGIN_MINI.BOTTOM;
|
||||||
|
|
||||||
|
@connect((state, ownProps) => {
|
||||||
|
const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps;
|
||||||
|
const myName = makeContinuousDimensionName(
|
||||||
|
{ isObs, isUserDefined, isGeneSetSummary },
|
||||||
|
field
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
annoMatrix: state.annoMatrix,
|
||||||
|
isScatterplotXXaccessor: state.controls.scatterplotXXaccessor === field,
|
||||||
|
isScatterplotYYaccessor: state.controls.scatterplotYYaccessor === field,
|
||||||
|
continuousSelectionRange: state.continuousSelection[myName],
|
||||||
|
isColorAccessor: state.colors.colorAccessor === field,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
class HistogramBrush extends React.PureComponent {
|
||||||
|
static watchAsync(props, prevProps) {
|
||||||
|
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* memoized closure to prevent HistogramHeader unecessary repaint */
|
||||||
|
handleColorAction = memoize((dispatch) => (field, isObs) => {
|
||||||
|
if (isObs) {
|
||||||
|
dispatch({
|
||||||
|
type: "color by continuous metadata",
|
||||||
|
colorAccessor: field,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBrush = (selection, x, eventType) => {
|
||||||
|
const type = `continuous metadata histogram ${eventType}`;
|
||||||
|
return () => {
|
||||||
|
const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
|
// ignore programmatically generated events
|
||||||
|
if (!d3.event.sourceEvent) return;
|
||||||
|
// ignore cascading events, which are programmatically generated
|
||||||
|
if (d3.event.sourceEvent.sourceEvent) return;
|
||||||
|
|
||||||
|
const query = this.createQuery();
|
||||||
|
const range = d3.event.selection
|
||||||
|
? [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
||||||
|
: null;
|
||||||
|
const otherProps = {
|
||||||
|
selection: field,
|
||||||
|
continuousNamespace: {
|
||||||
|
isObs,
|
||||||
|
isUserDefined,
|
||||||
|
isGeneSetSummary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dispatch(
|
||||||
|
actions.selectContinuousMetadataAction(type, query, range, otherProps)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onBrushEnd = (selection, x) => () => {
|
||||||
|
const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } =
|
||||||
|
this.props;
|
||||||
|
const minAllowedBrushSize = 10;
|
||||||
|
const smallAmountToAvoidInfiniteLoop = 0.1;
|
||||||
|
|
||||||
|
// ignore programmatically generated events
|
||||||
|
if (!d3.event.sourceEvent) return;
|
||||||
|
// ignore cascading events, which are programmatically generated
|
||||||
|
if (d3.event.sourceEvent.sourceEvent) return;
|
||||||
|
|
||||||
|
let type;
|
||||||
|
let range = null;
|
||||||
|
if (d3.event.selection) {
|
||||||
|
type = "continuous metadata histogram end";
|
||||||
|
if (
|
||||||
|
d3.event.selection[1] - d3.event.selection[0] >
|
||||||
|
minAllowedBrushSize
|
||||||
|
) {
|
||||||
|
range = [x(d3.event.selection[0]), x(d3.event.selection[1])];
|
||||||
|
} else {
|
||||||
|
/* the user selected range is too small and will be hidden #587, so take control of it procedurally */
|
||||||
|
/* https://stackoverflow.com/questions/12354729/d3-js-limit-size-of-brush */
|
||||||
|
|
||||||
|
const procedurallyResizedBrushWidth =
|
||||||
|
d3.event.selection[0] +
|
||||||
|
minAllowedBrushSize +
|
||||||
|
smallAmountToAvoidInfiniteLoop; //
|
||||||
|
|
||||||
|
range = [x(d3.event.selection[0]), x(procedurallyResizedBrushWidth)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
type = "continuous metadata histogram cancel";
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.createQuery();
|
||||||
|
const otherProps = {
|
||||||
|
selection: field,
|
||||||
|
continuousNamespace: {
|
||||||
|
isObs,
|
||||||
|
isUserDefined,
|
||||||
|
isGeneSetSummary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dispatch(
|
||||||
|
actions.selectContinuousMetadataAction(type, query, range, otherProps)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetGeneAsScatterplotX = () => {
|
||||||
|
const { dispatch, field } = this.props;
|
||||||
|
dispatch({
|
||||||
|
type: "set scatterplot x",
|
||||||
|
data: field,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetGeneAsScatterplotY = () => {
|
||||||
|
const { dispatch, field } = this.props;
|
||||||
|
dispatch({
|
||||||
|
type: "set scatterplot y",
|
||||||
|
data: field,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
removeHistogram = () => {
|
||||||
|
const {
|
||||||
|
dispatch,
|
||||||
|
field,
|
||||||
|
isColorAccessor,
|
||||||
|
isScatterplotXXaccessor,
|
||||||
|
isScatterplotYYaccessor,
|
||||||
|
} = this.props;
|
||||||
|
dispatch({
|
||||||
|
type: "clear user defined gene",
|
||||||
|
data: field,
|
||||||
|
});
|
||||||
|
if (isColorAccessor) {
|
||||||
|
dispatch({
|
||||||
|
type: "reset colorscale",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isScatterplotXXaccessor) {
|
||||||
|
dispatch({
|
||||||
|
type: "set scatterplot x",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isScatterplotYYaccessor) {
|
||||||
|
dispatch({
|
||||||
|
type: "set scatterplot y",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAsyncProps = async () => {
|
||||||
|
const { annoMatrix, width } = this.props;
|
||||||
|
|
||||||
|
const { isClipped } = annoMatrix;
|
||||||
|
|
||||||
|
const query = this.createQuery();
|
||||||
|
const df = await annoMatrix.fetch(...query);
|
||||||
|
const column = df.icol(0);
|
||||||
|
|
||||||
|
// if we are clipped, fetch both our value and our unclipped value,
|
||||||
|
// as we need the absolute min/max range, not just the clipped min/max.
|
||||||
|
const summary = column.summarize();
|
||||||
|
const range = [summary.min, summary.max];
|
||||||
|
|
||||||
|
let unclippedRange = [...range];
|
||||||
|
if (isClipped) {
|
||||||
|
const parent = await annoMatrix.viewOf.fetch(...query);
|
||||||
|
const { min, max } = parent.icol(0).summarize();
|
||||||
|
unclippedRange = [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
const unclippedRangeColor = [
|
||||||
|
!annoMatrix.isClipped || annoMatrix.clipRange[0] === 0
|
||||||
|
? "#bbb"
|
||||||
|
: globals.blue,
|
||||||
|
!annoMatrix.isClipped || annoMatrix.clipRange[1] === 1
|
||||||
|
? "#bbb"
|
||||||
|
: globals.blue,
|
||||||
|
];
|
||||||
|
|
||||||
|
const histogram = this.calcHistogramCache(
|
||||||
|
column,
|
||||||
|
MARGIN,
|
||||||
|
width || WIDTH,
|
||||||
|
HEIGHT
|
||||||
|
);
|
||||||
|
const miniHistogram = this.calcHistogramCache(
|
||||||
|
column,
|
||||||
|
MARGIN_MINI,
|
||||||
|
width || WIDTH_MINI,
|
||||||
|
HEIGHT_MINI
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSingleValue = summary.min === summary.max;
|
||||||
|
const nonFiniteExtent =
|
||||||
|
summary.min === undefined ||
|
||||||
|
summary.max === undefined ||
|
||||||
|
Number.isNaN(summary.min) ||
|
||||||
|
Number.isNaN(summary.max);
|
||||||
|
|
||||||
|
const OK2Render = !summary.categorical && !nonFiniteExtent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
histogram,
|
||||||
|
miniHistogram,
|
||||||
|
range,
|
||||||
|
unclippedRange,
|
||||||
|
unclippedRangeColor,
|
||||||
|
isSingleValue,
|
||||||
|
OK2Render,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this -- instance method allows for memoization per annotation
|
||||||
|
calcHistogramCache(col, newMargin, newWidth, newHeight) {
|
||||||
|
/*
|
||||||
|
recalculate expensive stuff, notably bins, summaries, etc.
|
||||||
|
*/
|
||||||
|
const histogramCache = {}; /* maybe change this so that it computes ... */
|
||||||
|
const summary =
|
||||||
|
col.summarize(); /* this is memoized, so it's free the second time you call it */
|
||||||
|
const { min: domainMin, max: domainMax } = summary;
|
||||||
|
const numBins = 40;
|
||||||
|
const { TOP: topMargin, LEFT: leftMargin } =
|
||||||
|
newMargin; /* changes with mini */
|
||||||
|
|
||||||
|
histogramCache.domain = [
|
||||||
|
domainMin,
|
||||||
|
domainMax,
|
||||||
|
]; /* doesn't change with mini */
|
||||||
|
|
||||||
|
histogramCache.x = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([domainMin, domainMax])
|
||||||
|
.range([leftMargin, leftMargin + newWidth]);
|
||||||
|
|
||||||
|
histogramCache.bins = col.histogram(numBins, [
|
||||||
|
domainMin,
|
||||||
|
domainMax,
|
||||||
|
]); /* memoized */
|
||||||
|
|
||||||
|
histogramCache.binWidth = (domainMax - domainMin) / numBins;
|
||||||
|
|
||||||
|
histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
|
||||||
|
histogramCache.binEnd = (i) =>
|
||||||
|
domainMin + (i + 1) * histogramCache.binWidth;
|
||||||
|
|
||||||
|
const yMax = histogramCache.bins.reduce((l, r) => (l > r ? l : r));
|
||||||
|
|
||||||
|
histogramCache.y = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.domain([0, yMax])
|
||||||
|
.range([topMargin + newHeight, topMargin]);
|
||||||
|
|
||||||
|
return histogramCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
createQuery() {
|
||||||
|
const { isObs, isGeneSetSummary, field, setGenes, annoMatrix } = this.props;
|
||||||
|
const { schema } = annoMatrix;
|
||||||
|
if (isObs) {
|
||||||
|
return ["obs", field];
|
||||||
|
}
|
||||||
|
const varIndex = schema?.annotations?.var?.index;
|
||||||
|
if (!varIndex) return null;
|
||||||
|
|
||||||
|
if (isGeneSetSummary) {
|
||||||
|
return [
|
||||||
|
"X",
|
||||||
|
{
|
||||||
|
summarize: {
|
||||||
|
method: "mean",
|
||||||
|
field: "var",
|
||||||
|
column: varIndex,
|
||||||
|
values: [...setGenes.keys()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// else, we assume it is a gene expression
|
||||||
|
return [
|
||||||
|
"X",
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
field: "var",
|
||||||
|
column: varIndex,
|
||||||
|
value: field,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatch,
|
||||||
|
annoMatrix,
|
||||||
|
field,
|
||||||
|
isColorAccessor,
|
||||||
|
isUserDefined,
|
||||||
|
isGeneSetSummary,
|
||||||
|
isScatterplotXXaccessor,
|
||||||
|
isScatterplotYYaccessor,
|
||||||
|
zebra,
|
||||||
|
continuousSelectionRange,
|
||||||
|
isObs,
|
||||||
|
mini,
|
||||||
|
setGenes,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let { width } = this.props;
|
||||||
|
if (!width) {
|
||||||
|
width = mini ? WIDTH_MINI : WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldForId = field.replace(/\s/g, "_");
|
||||||
|
const showScatterPlot = isUserDefined;
|
||||||
|
|
||||||
|
let testClass = "histogram-continuous-metadata";
|
||||||
|
if (isUserDefined) testClass = "histogram-user-gene";
|
||||||
|
else if (isGeneSetSummary) testClass = "histogram-gene-set-summary";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Async
|
||||||
|
watchFn={HistogramBrush.watchAsync}
|
||||||
|
promiseFn={this.fetchAsyncProps}
|
||||||
|
watchProps={{ annoMatrix, setGenes }}
|
||||||
|
>
|
||||||
|
<Async.Pending initial>
|
||||||
|
<StillLoading displayName={field} zebra={zebra} />
|
||||||
|
</Async.Pending>
|
||||||
|
<Async.Rejected>
|
||||||
|
{(error) => (
|
||||||
|
<ErrorLoading zebra={zebra} error={error} displayName={field} />
|
||||||
|
)}
|
||||||
|
</Async.Rejected>
|
||||||
|
<Async.Fulfilled>
|
||||||
|
{(asyncProps) =>
|
||||||
|
asyncProps.OK2Render ? (
|
||||||
|
<div
|
||||||
|
id={`histogram_${fieldForId}`}
|
||||||
|
data-testid={`histogram-${field}`}
|
||||||
|
data-testclass={testClass}
|
||||||
|
style={{
|
||||||
|
padding: mini ? 0 : globals.leftSidebarSectionPadding,
|
||||||
|
backgroundColor: zebra ? globals.lightestGrey : "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!mini && isObs ? (
|
||||||
|
<HistogramHeader
|
||||||
|
fieldId={field}
|
||||||
|
isColorBy={isColorAccessor}
|
||||||
|
isObs={isObs}
|
||||||
|
onColorByClick={this.handleColorAction(dispatch)}
|
||||||
|
onRemoveClick={isUserDefined ? this.removeHistogram : null}
|
||||||
|
isScatterPlotX={isScatterplotXXaccessor}
|
||||||
|
isScatterPlotY={isScatterplotYYaccessor}
|
||||||
|
onScatterPlotXClick={
|
||||||
|
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
|
||||||
|
}
|
||||||
|
onScatterPlotYClick={
|
||||||
|
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Histogram
|
||||||
|
field={field}
|
||||||
|
fieldForId={fieldForId}
|
||||||
|
display={asyncProps.isSingleValue ? "none" : "block"}
|
||||||
|
histogram={
|
||||||
|
mini ? asyncProps.miniHistogram : asyncProps.histogram
|
||||||
|
}
|
||||||
|
width={width}
|
||||||
|
height={mini ? HEIGHT_MINI : HEIGHT}
|
||||||
|
onBrush={this.onBrush}
|
||||||
|
onBrushEnd={this.onBrushEnd}
|
||||||
|
margin={mini ? MARGIN_MINI : MARGIN}
|
||||||
|
isColorBy={isColorAccessor}
|
||||||
|
selectionRange={continuousSelectionRange}
|
||||||
|
mini={mini}
|
||||||
|
/>
|
||||||
|
{!mini && (
|
||||||
|
<HistogramFooter
|
||||||
|
isGeneSetSummary={isGeneSetSummary}
|
||||||
|
isObs={isObs}
|
||||||
|
displayName={field}
|
||||||
|
hideRanges={asyncProps.isSingleValue}
|
||||||
|
rangeMin={asyncProps.unclippedRange[0]}
|
||||||
|
rangeMax={asyncProps.unclippedRange[1]}
|
||||||
|
rangeColorMin={asyncProps.unclippedRangeColor[0]}
|
||||||
|
rangeColorMax={asyncProps.unclippedRangeColor[1]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</Async.Fulfilled>
|
||||||
|
</Async>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramBrush;
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { connect, shallowEqual } from "react-redux";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import Async from "react-async";
|
|
||||||
import memoize from "memoize-one";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import { makeContinuousDimensionName } from "../../util/nameCreators";
|
|
||||||
import HistogramHeader from "./header";
|
|
||||||
import Histogram from "./histogram";
|
|
||||||
import HistogramFooter from "./footer";
|
|
||||||
import StillLoading from "./loading";
|
|
||||||
import ErrorLoading from "./error";
|
|
||||||
import { Dataframe } from "../../util/dataframe";
|
|
||||||
|
|
||||||
const MARGIN = {
|
|
||||||
LEFT: 10, // Space for 0 tick label on X axis
|
|
||||||
RIGHT: 54, // space for Y axis & labels
|
|
||||||
BOTTOM: 25, // space for X axis & labels
|
|
||||||
TOP: 3,
|
|
||||||
};
|
|
||||||
const WIDTH = 340 - MARGIN.LEFT - MARGIN.RIGHT;
|
|
||||||
const HEIGHT = 135 - MARGIN.TOP - MARGIN.BOTTOM;
|
|
||||||
const MARGIN_MINI = {
|
|
||||||
LEFT: 0, // Space for 0 tick label on X axis
|
|
||||||
RIGHT: 0, // space for Y axis & labels
|
|
||||||
BOTTOM: 0, // space for X axis & labels
|
|
||||||
TOP: 0,
|
|
||||||
};
|
|
||||||
const WIDTH_MINI = 120 - MARGIN_MINI.LEFT - MARGIN_MINI.RIGHT;
|
|
||||||
const HEIGHT_MINI = 15 - MARGIN_MINI.TOP - MARGIN_MINI.BOTTOM;
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
|
||||||
@connect((state, ownProps) => {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type '{}'.
|
|
||||||
const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps;
|
|
||||||
const myName = makeContinuousDimensionName(
|
|
||||||
{ isObs, isUserDefined, isGeneSetSummary },
|
|
||||||
field
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
annoMatrix: (state as any).annoMatrix,
|
|
||||||
isScatterplotXXaccessor:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).controls.scatterplotXXaccessor === field,
|
|
||||||
isScatterplotYYaccessor:
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(state as any).controls.scatterplotYYaccessor === field,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
continuousSelectionRange: (state as any).continuousSelection[myName],
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
isColorAccessor: (state as any).colors.colorAccessor === field,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
class HistogramBrush extends React.PureComponent {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
static watchAsync(props: any, prevProps: any) {
|
|
||||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* memoized closure to prevent HistogramHeader unecessary repaint */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
handleColorAction = memoize((dispatch) => (field: any, isObs: any) => {
|
|
||||||
if (isObs) {
|
|
||||||
dispatch({
|
|
||||||
type: "color by continuous metadata",
|
|
||||||
colorAccessor: field,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(6133) FIXME: 'selection' is declared but its value is never rea... Remove this comment to see the full error message
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
onBrush = (selection: any, x: any, eventType: any) => {
|
|
||||||
const type = `continuous metadata histogram ${eventType}`;
|
|
||||||
return () => {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
dispatch,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'field' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
field,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
isObs,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserDefined' does not exist on type 'R... Remove this comment to see the full error message
|
|
||||||
isUserDefined,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isGeneSetSummary' does not exist on type... Remove this comment to see the full error message
|
|
||||||
isGeneSetSummary,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// ignore programmatically generated events
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
if (!(d3 as any).event.sourceEvent) return;
|
|
||||||
// ignore cascading events, which are programmatically generated
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
if ((d3 as any).event.sourceEvent.sourceEvent) return;
|
|
||||||
|
|
||||||
const query = this.createQuery();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const range = (d3 as any).event.selection
|
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
[x((d3 as any).event.selection[0]), x((d3 as any).event.selection[1])]
|
|
||||||
: null;
|
|
||||||
const otherProps = {
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isGeneSetSummary,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
dispatch(
|
|
||||||
actions.selectContinuousMetadataAction(type, query, range, otherProps)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
onBrushEnd =
|
|
||||||
(
|
|
||||||
_selection: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
x: any // eslint-disable-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 --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
() => {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
dispatch,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'field' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
field,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
isObs,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserDefined' does not exist on type 'R... Remove this comment to see the full error message
|
|
||||||
isUserDefined,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isGeneSetSummary' does not exist on type... Remove this comment to see the full error message
|
|
||||||
isGeneSetSummary,
|
|
||||||
} = this.props;
|
|
||||||
const minAllowedBrushSize = 10;
|
|
||||||
const smallAmountToAvoidInfiniteLoop = 0.1;
|
|
||||||
|
|
||||||
// ignore programmatically generated events
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
if (!(d3 as any).event.sourceEvent) return;
|
|
||||||
// ignore cascading events, which are programmatically generated
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
if ((d3 as any).event.sourceEvent.sourceEvent) return;
|
|
||||||
|
|
||||||
let type;
|
|
||||||
let range = null;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
if ((d3 as any).event.selection) {
|
|
||||||
type = "continuous metadata histogram end";
|
|
||||||
if (
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(d3 as any).event.selection[1] - (d3 as any).event.selection[0] >
|
|
||||||
minAllowedBrushSize
|
|
||||||
) {
|
|
||||||
range = [
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
x((d3 as any).event.selection[0]),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
x((d3 as any).event.selection[1]),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
/* the user selected range is too small and will be hidden #587, so take control of it procedurally */
|
|
||||||
/* https://stackoverflow.com/questions/12354729/d3-js-limit-size-of-brush */
|
|
||||||
const procedurallyResizedBrushWidth =
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(d3 as any).event.selection[0] +
|
|
||||||
minAllowedBrushSize +
|
|
||||||
smallAmountToAvoidInfiniteLoop; //
|
|
||||||
range = [
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
x((d3 as any).event.selection[0]),
|
|
||||||
x(procedurallyResizedBrushWidth),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
type = "continuous metadata histogram cancel";
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = this.createQuery();
|
|
||||||
const otherProps = {
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isGeneSetSummary,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
dispatch(
|
|
||||||
actions.selectContinuousMetadataAction(type, query, range, otherProps)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
handleSetGeneAsScatterplotX = () => {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { dispatch, field } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot x",
|
|
||||||
data: field,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
handleSetGeneAsScatterplotY = () => {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
const { dispatch, field } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot y",
|
|
||||||
data: field,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
removeHistogram = () => {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
dispatch,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'field' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
field,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
|
||||||
isColorAccessor,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterplotXXaccessor' does not exist ... Remove this comment to see the full error message
|
|
||||||
isScatterplotXXaccessor,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterplotYYaccessor' does not exist ... Remove this comment to see the full error message
|
|
||||||
isScatterplotYYaccessor,
|
|
||||||
} = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "clear user defined gene",
|
|
||||||
data: field,
|
|
||||||
});
|
|
||||||
if (isColorAccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "reset colorscale",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isScatterplotXXaccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot x",
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isScatterplotYYaccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot y",
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
fetchAsyncProps = async () => {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
|
||||||
const { annoMatrix, width } = this.props;
|
|
||||||
|
|
||||||
const { isClipped } = annoMatrix;
|
|
||||||
|
|
||||||
const query = this.createQuery();
|
|
||||||
// @ts-expect-error ts-migrate(2488) FIXME: Type 'any[] | null' must have a '[Symbol.iterator]... Remove this comment to see the full error message
|
|
||||||
const df: Dataframe = await annoMatrix.fetch(...query);
|
|
||||||
const column = df.icol(0);
|
|
||||||
|
|
||||||
// if we are clipped, fetch both our value and our unclipped value,
|
|
||||||
// as we need the absolute min/max range, not just the clipped min/max.
|
|
||||||
const summary = column.summarizeContinuous();
|
|
||||||
const range = [summary.min, summary.max];
|
|
||||||
|
|
||||||
let unclippedRange = [...range];
|
|
||||||
if (isClipped) {
|
|
||||||
const parent: Dataframe = await annoMatrix.viewOf.fetch(...query);
|
|
||||||
const { min, max } = parent.icol(0).summarizeContinuous();
|
|
||||||
unclippedRange = [min, max];
|
|
||||||
}
|
|
||||||
|
|
||||||
const unclippedRangeColor = [
|
|
||||||
!annoMatrix.isClipped || annoMatrix.clipRange[0] === 0
|
|
||||||
? "#bbb"
|
|
||||||
: globals.blue,
|
|
||||||
!annoMatrix.isClipped || annoMatrix.clipRange[1] === 1
|
|
||||||
? "#bbb"
|
|
||||||
: globals.blue,
|
|
||||||
];
|
|
||||||
|
|
||||||
const histogram = this.calcHistogramCache(
|
|
||||||
column,
|
|
||||||
MARGIN,
|
|
||||||
width || WIDTH,
|
|
||||||
HEIGHT
|
|
||||||
);
|
|
||||||
const miniHistogram = this.calcHistogramCache(
|
|
||||||
column,
|
|
||||||
MARGIN_MINI,
|
|
||||||
width || WIDTH_MINI,
|
|
||||||
HEIGHT_MINI
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSingleValue = summary.min === summary.max;
|
|
||||||
const nonFiniteExtent =
|
|
||||||
summary.min === undefined ||
|
|
||||||
summary.max === undefined ||
|
|
||||||
Number.isNaN(summary.min) ||
|
|
||||||
Number.isNaN(summary.max);
|
|
||||||
|
|
||||||
const OK2Render = !summary.categorical && !nonFiniteExtent;
|
|
||||||
|
|
||||||
return {
|
|
||||||
histogram,
|
|
||||||
miniHistogram,
|
|
||||||
range,
|
|
||||||
unclippedRange,
|
|
||||||
unclippedRangeColor,
|
|
||||||
isSingleValue,
|
|
||||||
OK2Render,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- instance method allows for memoization per annotation
|
|
||||||
calcHistogramCache(col: any, newMargin: any, newWidth: any, newHeight: any) {
|
|
||||||
/*
|
|
||||||
recalculate expensive stuff, notably bins, summaries, etc.
|
|
||||||
*/
|
|
||||||
const histogramCache = {}; /* maybe change this so that it computes ... */
|
|
||||||
const summary =
|
|
||||||
col.summarizeContinuous(); /* this is memoized, so it's free the second time you call it */
|
|
||||||
const { min: domainMin, max: domainMax } = summary;
|
|
||||||
const numBins = 40;
|
|
||||||
const { TOP: topMargin, LEFT: leftMargin } = newMargin;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(histogramCache as any).domain = [domainMin, domainMax];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
/* doesn't change with mini */ (histogramCache as any).x = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain([domainMin, domainMax])
|
|
||||||
.range([leftMargin, leftMargin + newWidth]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(histogramCache as any).bins = col.histogramContinuous(numBins, [
|
|
||||||
domainMin,
|
|
||||||
domainMax,
|
|
||||||
]);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
/* memoized */ (histogramCache as any).binWidth =
|
|
||||||
(domainMax - domainMin) / numBins;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(histogramCache as any).binStart = (i: any) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
domainMin + i * (histogramCache as any).binWidth;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(histogramCache as any).binEnd = (i: any) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
domainMin + (i + 1) * (histogramCache as any).binWidth;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const yMax = (histogramCache as any).bins.reduce((l: any, r: any) =>
|
|
||||||
l > r ? l : r
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(histogramCache as any).y = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain([0, yMax])
|
|
||||||
.range([topMargin + newHeight, topMargin]);
|
|
||||||
|
|
||||||
return histogramCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
createQuery() {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
const { isObs, isGeneSetSummary, field, setGenes, annoMatrix } = this.props;
|
|
||||||
const { schema } = annoMatrix;
|
|
||||||
if (isObs) {
|
|
||||||
return ["obs", field];
|
|
||||||
}
|
|
||||||
const varIndex = schema?.annotations?.var?.index;
|
|
||||||
if (!varIndex) return null;
|
|
||||||
|
|
||||||
if (isGeneSetSummary) {
|
|
||||||
return [
|
|
||||||
"X",
|
|
||||||
{
|
|
||||||
summarize: {
|
|
||||||
method: "mean",
|
|
||||||
field: "var",
|
|
||||||
column: varIndex,
|
|
||||||
values: [...setGenes.keys()],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// else, we assume it is a gene expression
|
|
||||||
return [
|
|
||||||
"X",
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
field: "var",
|
|
||||||
column: varIndex,
|
|
||||||
value: field,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
dispatch,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
|
||||||
annoMatrix,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'field' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
field,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isColorAccessor' does not exist on type ... Remove this comment to see the full error message
|
|
||||||
isColorAccessor,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserDefined' does not exist on type 'R... Remove this comment to see the full error message
|
|
||||||
isUserDefined,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isGeneSetSummary' does not exist on type... Remove this comment to see the full error message
|
|
||||||
isGeneSetSummary,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterplotXXaccessor' does not exist ... Remove this comment to see the full error message
|
|
||||||
isScatterplotXXaccessor,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isScatterplotYYaccessor' does not exist ... Remove this comment to see the full error message
|
|
||||||
isScatterplotYYaccessor,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'zebra' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
zebra,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'continuousSelectionRange' does not exist... Remove this comment to see the full error message
|
|
||||||
continuousSelectionRange,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isObs' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
isObs,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mini' does not exist on type 'Readonly<{... Remove this comment to see the full error message
|
|
||||||
mini,
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'setGenes' does not exist on type 'Readon... Remove this comment to see the full error message
|
|
||||||
setGenes,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'width' does not exist on type 'Readonly<... Remove this comment to see the full error message
|
|
||||||
let { width } = this.props;
|
|
||||||
if (!width) {
|
|
||||||
width = mini ? WIDTH_MINI : WIDTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldForId = field.replace(/\s/g, "_");
|
|
||||||
const showScatterPlot = isUserDefined;
|
|
||||||
|
|
||||||
let testClass = "histogram-continuous-metadata";
|
|
||||||
if (isUserDefined) testClass = "histogram-user-gene";
|
|
||||||
else if (isGeneSetSummary) testClass = "histogram-gene-set-summary";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Async
|
|
||||||
watchFn={HistogramBrush.watchAsync}
|
|
||||||
promiseFn={this.fetchAsyncProps}
|
|
||||||
watchProps={{ annoMatrix, setGenes }}
|
|
||||||
>
|
|
||||||
<Async.Pending initial>
|
|
||||||
<StillLoading displayName={field} zebra={zebra} />
|
|
||||||
</Async.Pending>
|
|
||||||
<Async.Rejected>
|
|
||||||
{(error) => (
|
|
||||||
<ErrorLoading zebra={zebra} error={error} displayName={field} />
|
|
||||||
)}
|
|
||||||
</Async.Rejected>
|
|
||||||
<Async.Fulfilled>
|
|
||||||
{(asyncProps) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(asyncProps as any).OK2Render ? (
|
|
||||||
<div
|
|
||||||
id={`histogram_${fieldForId}`}
|
|
||||||
data-testid={`histogram-${field}`}
|
|
||||||
data-testclass={testClass}
|
|
||||||
style={{
|
|
||||||
padding: mini ? 0 : globals.leftSidebarSectionPadding,
|
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!mini && isObs ? (
|
|
||||||
<HistogramHeader
|
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ fieldId: any; isColorBy: any; isObs: any; ... Remove this comment to see the full error message
|
|
||||||
fieldId={field}
|
|
||||||
isColorBy={isColorAccessor}
|
|
||||||
isObs={isObs}
|
|
||||||
onColorByClick={this.handleColorAction(dispatch)}
|
|
||||||
onRemoveClick={isUserDefined ? this.removeHistogram : null}
|
|
||||||
isScatterPlotX={isScatterplotXXaccessor}
|
|
||||||
isScatterPlotY={isScatterplotYYaccessor}
|
|
||||||
onScatterPlotXClick={
|
|
||||||
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
|
|
||||||
}
|
|
||||||
onScatterPlotYClick={
|
|
||||||
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Histogram
|
|
||||||
field={field}
|
|
||||||
fieldForId={fieldForId}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
display={(asyncProps as any).isSingleValue ? "none" : "block"}
|
|
||||||
histogram={
|
|
||||||
mini
|
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(asyncProps as any).miniHistogram
|
|
||||||
: // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
(asyncProps as any).histogram
|
|
||||||
}
|
|
||||||
width={width}
|
|
||||||
height={mini ? HEIGHT_MINI : HEIGHT}
|
|
||||||
onBrush={this.onBrush}
|
|
||||||
onBrushEnd={this.onBrushEnd}
|
|
||||||
margin={mini ? MARGIN_MINI : MARGIN}
|
|
||||||
isColorBy={isColorAccessor}
|
|
||||||
selectionRange={continuousSelectionRange}
|
|
||||||
mini={mini}
|
|
||||||
/>
|
|
||||||
{!mini && (
|
|
||||||
<HistogramFooter
|
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ isGeneSetSummary: any; isObs: any; display... Remove this comment to see the full error message
|
|
||||||
isGeneSetSummary={isGeneSetSummary}
|
|
||||||
isObs={isObs}
|
|
||||||
displayName={field}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
hideRanges={(asyncProps as any).isSingleValue}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
rangeMin={(asyncProps as any).unclippedRange[0]}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
rangeMax={(asyncProps as any).unclippedRange[1]}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
rangeColorMin={(asyncProps as any).unclippedRangeColor[0]}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
|
|
||||||
rangeColorMax={(asyncProps as any).unclippedRangeColor[1]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</Async.Fulfilled>
|
|
||||||
</Async>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramBrush;
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@blueprintjs/core";
|
||||||
|
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
|
||||||
|
const StillLoading = ({ zebra, displayName }) =>
|
||||||
|
/*
|
||||||
|
Render a loading indicator for the field.
|
||||||
|
*/
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
data-testclass="gene-loading-spinner"
|
||||||
|
style={{
|
||||||
|
padding: globals.leftSidebarSectionPadding,
|
||||||
|
backgroundColor: zebra ? globals.lightestGrey : "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
justifyItems: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 30 }} />
|
||||||
|
<div style={{ display: "flex", alignSelf: "center" }}>
|
||||||
|
<span style={{ fontStyle: "italic" }}>{displayName}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button minimal loading intent="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
export default StillLoading;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS.
|
|
||||||
const StillLoading = ({ zebra, displayName }: any) => (
|
|
||||||
/*
|
|
||||||
Render a loading indicator for the field.
|
|
||||||
*/
|
|
||||||
<div
|
|
||||||
data-testclass="gene-loading-spinner"
|
|
||||||
style={{
|
|
||||||
padding: globals.leftSidebarSectionPadding,
|
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
justifyItems: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ minWidth: 30 }} />
|
|
||||||
<div style={{ display: "flex", alignSelf: "center" }}>
|
|
||||||
<span style={{ fontStyle: "italic" }}>{displayName}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button minimal loading intent="primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
export default StillLoading;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user