From e6e358ddc81d3c5f42e9fc98a9ade4a1db62ad9b Mon Sep 17 00:00:00 2001
From: Colin Megill
Date: Wed, 24 Mar 2021 16:33:26 -0400
Subject: [PATCH] Gene sets UI, right sidebar refactor (#2097)
* prototyping
* render histos on open gene set
* prototyping
* render histos on open gene set
* factor out add genes to own component
* remove unused import
* mock reducer
* color by geneset stub
* menus and buttons
* geneset dialogue stub
* remove heatmap mock
* componetize histogram
* reenable add genes
* re-add isuserdefined
* test data
* remove have fetched
* add isExpanded state to gene, and pass to histogram
* expand button
* toggleable
* mini
* bump number of genes to 50
* don't clear diffexp on subset
* move create category to top
* render diffexp as geneset
* geneset show mean expression
* gene set reducer
* add geneset UI reducer
* wire e2e gene set loading prototype
* fix sniffing bug
* fix typo
* add gene modals
* client/src/actions/
* add autosave
* rename data-dir cli param
* add geneset, add gene, delete set
* prototype: remove csv upload placeholder
* handle delete gene from set
* prepopulate geneset with genes from modal
* add geneset: rename action
* icons, language consistency
* chevron after
* handle empty string case on genes for create geneset
* edit geneset
* fix language on create
* copy correction
* add popper2
upgrade react popper
upgrade react popper
adding popover2 package
* truncate uses tooltip2
* gene set button text typo
* remove logging
* moving server over
* remove test imports
* don't try to destructure map, use array.from
* fix add gene map datastructure error
* Revert "fix add gene map datastructure error"
This reverts commit b0eed459529d9c39a7849ce157a3dc0ed2b2b8b9.
* name --> genesetName, genes --> geneSymbols
* add gene to geneset, temporary format
* handle empty case, clear form input
* lint -- genesets wasn't passed via props
* userinfo
* move genes string to object conversion to action
* remove tmp gene description
* emptystring default for description
* remove empty string
* remove top level package json
* remove package lock as well
* remove flag for feature toggle
* remove comments in geneset
* comment cleanup
* remove comment
* revert diffexp genes to 10
* color by gene set
* disable color by gene set
* Gene menus are now inline, remove dead prototype code
* remove todo, magic number to variable
* remove jshint in rightsidebar
Co-authored-by: Severiano Badajoz
* remove unused geneset validation code
* tmp format pending geneset description
* move magic number into variable
* reorganize genesetsUI reducer pending tests
* rewire edit given new action name
* add basic validation and feedback for geneset name uniqueness
* mv annoDialog
* mv label, repair paths
* Update client/src/components/brushableHistogram/header.js
Co-authored-by: Severiano Badajoz
* add imports for icon in histo
* update jest snapshots given blueprint/tooltip2 usage of index -1
* ensure no empty paragraph
* intent from blueprint
* remove remainder of jshint references
* do not push undo when autosave fires
* fix autosave bugs
* remove todos
* clamp to util
* scient to util
* revert clearing diffexp
* rename value to be more specific stacked bar
* clean up logging and commetns
* remove gene entry tests pending rewrite
* tab index -1
* update jest snapshot, blueprint tooltip 2
* caret margin
* snapshot update
* ensure histogram is centered
* add geneset actions to config
* comment maybeScientific
* comment clamp
* comment ui reducer
* remove prototype code
* remove error log
* remove references to bl.ocks
* componetize parseBulkGeneString
* catch case where geneset rename same name
* genesetui reducer tests
* add geneset ui to index reducer config
Co-authored-by: bkmartinjr
Co-authored-by: Severiano Badajoz
---
.../e2e/__snapshots__/e2e.test.js.snap | 2 +-
.../__snapshots__/e2eAnnotations.test.js.snap | 16 +-
client/__tests__/e2e/e2e.test.js | 170 ------
client/__tests__/reducers/genesetsUI.test.js | 75 +++
.../util/typedCrossfilter/bitArray.test.js | 2 -
.../typedCrossfilter/positiveInterval.test.js | 2 -
client/package-lock.json | 255 +++++---
client/package.json | 3 +
client/src/actions/annotation.js | 16 +-
.../{categorical => }/annoDialog.js | 8 +
client/src/components/app.js | 1 -
.../src/components/autosave/filenameDialog.js | 6 +-
client/src/components/autosave/index.js | 2 +-
.../components/brushableHistogram/error.js | 17 +
.../components/brushableHistogram/footer.js | 90 +++
.../components/brushableHistogram/header.js | 100 +++
.../brushableHistogram/histogram.js | 185 ++++++
.../components/brushableHistogram/index.js | 570 +++---------------
.../components/brushableHistogram/loading.js | 42 ++
.../category/annoDialogAddLabel.js | 4 +-
.../category/annoDialogEditCategoryName.js | 4 +-
.../categorical/category/annoMenuCategory.js | 6 +-
client/src/components/categorical/index.js | 61 +-
.../src/components/categorical/value/index.js | 30 +-
.../components/categorical/value/occupancy.js | 1 -
.../continuous/setupParallelCoordinates.js | 1 -
client/src/components/continuous/util.js | 1 -
.../src/components/continuousLegend/index.js | 2 -
client/src/components/framework/container.js | 1 -
client/src/components/framework/layout.js | 1 -
client/src/components/geneExpression/gene.js | 188 ++++++
.../src/components/geneExpression/geneSet.js | 183 ++++++
client/src/components/geneExpression/index.js | 105 ++--
.../menus/addGeneToGenesetDialogue.js | 92 +++
.../geneExpression/{ => menus}/addGenes.js | 14 +-
.../menus/createGenesetDialogue.js | 149 +++++
.../menus/editGenesetNameDialogue.js | 95 +++
.../geneExpression/menus/genesetMenus.js | 120 ++++
client/src/components/graph/graph.js | 1 -
client/src/components/graph/setupLasso.js | 2 -
.../src/components/graph/setupSVGandBrush.js | 1 -
.../{categorical => }/labelInput.js | 0
client/src/components/leftSidebar/index.js | 3 +-
client/src/components/leftSidebar/infoMenu.js | 1 -
.../src/components/menubar/cellSetButtons.js | 1 -
client/src/components/rightSidebar/index.js | 4 +-
client/src/components/scatterplot/util.js | 1 -
client/src/components/util/truncate.js | 12 +-
client/src/index.css | 1 +
client/src/reducers/autosave.js | 2 +-
client/src/reducers/config.js | 1 -
client/src/reducers/controls.js | 2 -
client/src/reducers/differential.js | 2 -
client/src/reducers/genesets.js | 1 +
client/src/reducers/genesetsUI.js | 88 +++
client/src/reducers/index.js | 3 +
client/src/reducers/undoableConfig.js | 21 +
client/src/util/clamp.js | 13 +
client/src/util/maybeScientific.js | 27 +
client/src/util/parseBulkGeneString.js | 10 +
client/src/util/parseRGB.js | 1 -
client/src/util/scaleLinear.js | 2 -
client/src/util/scaleRGB.js | 1 -
.../typedCrossfilter/positiveIntervals.js | 2 -
client/src/util/typedCrossfilter/util.js | 2 -
package.json | 6 -
66 files changed, 1926 insertions(+), 905 deletions(-)
create mode 100644 client/__tests__/reducers/genesetsUI.test.js
rename client/src/components/{categorical => }/annoDialog.js (89%)
create mode 100644 client/src/components/brushableHistogram/error.js
create mode 100644 client/src/components/brushableHistogram/footer.js
create mode 100644 client/src/components/brushableHistogram/header.js
create mode 100644 client/src/components/brushableHistogram/histogram.js
create mode 100644 client/src/components/brushableHistogram/loading.js
create mode 100644 client/src/components/geneExpression/gene.js
create mode 100644 client/src/components/geneExpression/geneSet.js
create mode 100644 client/src/components/geneExpression/menus/addGeneToGenesetDialogue.js
rename client/src/components/geneExpression/{ => menus}/addGenes.js (96%)
create mode 100644 client/src/components/geneExpression/menus/createGenesetDialogue.js
create mode 100644 client/src/components/geneExpression/menus/editGenesetNameDialogue.js
create mode 100644 client/src/components/geneExpression/menus/genesetMenus.js
rename client/src/components/{categorical => }/labelInput.js (100%)
create mode 100644 client/src/reducers/genesetsUI.js
create mode 100644 client/src/util/clamp.js
create mode 100644 client/src/util/maybeScientific.js
create mode 100644 client/src/util/parseBulkGeneString.js
delete mode 100644 package.json
diff --git a/client/__tests__/e2e/__snapshots__/e2e.test.js.snap b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap
index d33b3262..d5c42b1b 100644
--- a/client/__tests__/e2e/__snapshots__/e2e.test.js.snap
+++ b/client/__tests__/e2e/__snapshots__/e2e.test.js.snap
@@ -2,4 +2,4 @@
exports[`did launch page launched 1`] = `"pbm c3k c3k "`;
-exports[`metadata loads categories and values from dataset appear 1`] = `"
"`;
+exports[`metadata loads categories and values from dataset appear 1`] = `"
"`;
diff --git a/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap b/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap
index 56a349b2..a82cc979 100644
--- a/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap
+++ b/client/__tests__/e2e/__snapshots__/e2eAnnotations.test.js.snap
@@ -2,22 +2,22 @@
exports[`annotations stacked bar graph renders 1`] = `
Array [
- "",
- "",
+ "",
+ "",
]
`;
exports[`annotations stacked bar graph renders 2`] = `
Array [
- "",
- "",
+ "",
+ "",
]
`;
-exports[`annotations truncate midpoint whitespace 1`] = `"123 456 456 "`;
+exports[`annotations truncate midpoint whitespace 1`] = `"123 456 456 "`;
-exports[`annotations truncate midpoint whitespace 2`] = `"123 456 456 "`;
+exports[`annotations truncate midpoint whitespace 2`] = `"123 456 456 "`;
-exports[`annotations truncate single character 1`] = `"T "`;
+exports[`annotations truncate single character 1`] = `"T "`;
-exports[`annotations truncate single character 2`] = `"T "`;
+exports[`annotations truncate single character 2`] = `"T "`;
diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js
index 427db7db..c0f7eb36 100644
--- a/client/__tests__/e2e/e2e.test.js
+++ b/client/__tests__/e2e/e2e.test.js
@@ -15,23 +15,17 @@ import {
getOneElementInnerHTML,
getTestId,
goToPage,
- typeInto,
waitByID,
clickOnUntil,
} from "./puppeteerUtils";
import {
- addGeneToSearch,
- bulkAddGenes,
calcDragCoordinates,
clip,
drag,
getAllCategoriesAndCounts,
- getAllHistograms,
getCellSetCount,
- runDiffExp,
selectCategory,
- subset,
login,
logout,
} from "./cellxgeneActions";
@@ -149,47 +143,6 @@ describe("cell selection", () => {
});
});
-describe("gene entry", () => {
- test("search for single gene", async () => {
- await goToPage(appUrlBase);
- await addGeneToSearch(data.genes.search);
- });
-
- test("bulk add genes", async () => {
- await goToPage(appUrlBase);
-
- const testGenes = data.genes.bulkadd;
-
- await bulkAddGenes(testGenes);
- const allHistograms = await getAllHistograms(
- "histogram-user-gene",
- testGenes
- );
-
- expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
- expect(allHistograms).toHaveLength(testGenes.length);
- });
-});
-
-describe("differential expression", () => {
- test("selects cells, saves them and performs diffexp", async () => {
- await goToPage(appUrlBase);
-
- await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
-
- const allHistograms = await getAllHistograms(
- "histogram-diffexp",
- data.diffexp["gene-results"]
- );
-
- expect(allHistograms).toEqual(
- expect.arrayContaining(data.diffexp["gene-results"])
- );
-
- expect(allHistograms).toHaveLength(data.diffexp["gene-results"].length);
- });
-});
-
describe("subset", () => {
test("subset - cell count matches", async () => {
await goToPage(appUrlBase);
@@ -236,94 +189,6 @@ describe("subset", () => {
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.subset.lasso.count);
});
-
- test("undo selection appends the top diff exp genes to user defined genes", async () => {
- await goToPage(appUrlBase);
-
- const userDefinedGenes = data.genes.bulkadd;
- const diffExpGenes = data.diffexp["gene-results"];
-
- await bulkAddGenes(userDefinedGenes);
- const userDefinedHistograms = await getAllHistograms(
- "histogram-user-gene",
- userDefinedGenes
- );
-
- expect(userDefinedHistograms).toEqual(
- expect.arrayContaining(userDefinedGenes)
- );
-
- await subset({ x1: 0.15, y1: 0.1, x2: 0.98, y2: 0.98 });
- await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
-
- const diffExpHistograms = await getAllHistograms(
- "histogram-diffexp",
- diffExpGenes
- );
-
- expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes));
-
- await clickOn("reset-subset-button");
- const expected = [].concat(userDefinedGenes, diffExpGenes);
- const userDefinedHistogramsAfterSubset = await getAllHistograms(
- "histogram-user-gene",
- expected
- );
-
- expect(userDefinedHistogramsAfterSubset).toEqual(
- expect.arrayContaining(expected)
- );
- });
-
- test("subset selection appends the top diff exp genes to user defined genes", async () => {
- await goToPage(appUrlBase);
-
- const userDefinedGenes = data.genes.bulkadd;
- const diffExpGenes = data.diffexp["gene-results"];
-
- await bulkAddGenes(userDefinedGenes);
- const userDefinedHistograms = await getAllHistograms(
- "histogram-user-gene",
- userDefinedGenes
- );
-
- expect(userDefinedHistograms).toEqual(
- expect.arrayContaining(userDefinedGenes)
- );
-
- await subset({ x1: 0.15, y1: 0.1, x2: 0.98, y2: 0.98 });
- await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
-
- const diffExpHistograms = await getAllHistograms(
- "histogram-diffexp",
- diffExpGenes
- );
-
- expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes));
-
- await subset({ x1: 0.16, y1: 0.11, x2: 0.97, y2: 0.97 });
-
- const expected = [].concat(userDefinedGenes, diffExpGenes);
- const userDefinedHistogramsAfterSubset = await getAllHistograms(
- "histogram-user-gene",
- expected
- );
-
- expect(userDefinedHistogramsAfterSubset).toEqual(
- expect.arrayContaining(expected)
- );
- });
-});
-
-describe("scatter plot", () => {
- test("scatter plot appears", async () => {
- await goToPage(appUrlBase);
-
- await bulkAddGenes(Object.values(data.scatter.genes));
- await clickOn(`plot-x-${data.scatter.genes.x}`);
- await clickOn(`plot-y-${data.scatter.genes.y}`);
- await waitByID("scatterplot");
- });
});
describe("clipping", () => {
@@ -340,30 +205,6 @@ describe("clipping", () => {
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.clip.count);
});
-
- test("clip gene", async () => {
- await goToPage(appUrlBase);
-
- await typeInto("gene-search", data.clip.gene);
- await page.keyboard.press("Enter");
-
- await page.waitForSelector(`[data-testid='histogram-${data.clip.gene}']`);
-
- await clip(data.clip.min, data.clip.max);
-
- const histBrushableAreaId = `histogram-${data.clip.gene}-plot-brushable-area`;
-
- const coords = await calcDragCoordinates(
- histBrushableAreaId,
- data.clip["coordinates-as-percent"]
- );
-
- await drag(histBrushableAreaId, coords.start, coords.end);
-
- const cellCount = await getCellSetCount(1);
-
- expect(cellCount).toBe(data.clip["gene-cell-count"]);
- });
});
// interact with UI elements just that they do not break
@@ -381,17 +222,6 @@ describe("ui elements don't error", () => {
}
});
- test("color by for gene", async () => {
- await goToPage(appUrlBase);
-
- await typeInto("gene-search", data.genes.search);
- await page.keyboard.press("Enter");
- await page.waitForSelector(
- `[data-testid='histogram-${data.genes.search}']`
- );
- await clickOn(`colorby-${data.genes.search}`);
- });
-
test("pan and zoom", async () => {
await goToPage(appUrlBase);
diff --git a/client/__tests__/reducers/genesetsUI.test.js b/client/__tests__/reducers/genesetsUI.test.js
new file mode 100644
index 00000000..0c0ca6a5
--- /dev/null
+++ b/client/__tests__/reducers/genesetsUI.test.js
@@ -0,0 +1,75 @@
+import genesetsUIReducer from "../../src/reducers/genesetsUI";
+
+// Format: GeneSetsUI(state,action)
+
+const initialState = {
+ createGenesetModeActive: false,
+ isEditingGenesetName: false,
+ isAddingGenesToGeneset: false,
+};
+
+/* initial */
+describe("geneset UI states", () => {
+ test("initial state, some other action", () => {
+ expect(
+ genesetsUIReducer(undefined, {
+ type: "foo",
+ })
+ ).toMatchObject(initialState);
+ });
+ test("geneset: activate add new geneset mode", () => {
+ expect(
+ genesetsUIReducer(initialState, {
+ type: "geneset: activate add new geneset mode",
+ })
+ ).toMatchObject({
+ createGenesetModeActive: true,
+ isEditingGenesetName: false,
+ isAddingGenesToGeneset: false,
+ });
+ });
+ test("geneset: disable create geneset mode", () => {
+ expect(
+ genesetsUIReducer(undefined, { isEditingGenesetName: false })
+ ).toMatchObject(initialState);
+ });
+
+ test("activate add new genes mode", () => {
+ expect(
+ genesetsUIReducer(undefined, {
+ type: "geneset: activate add new genes mode",
+ geneset: "a geneset name",
+ })
+ ).toMatchObject({
+ createGenesetModeActive: false,
+ isEditingGenesetName: false,
+ isAddingGenesToGeneset: "a geneset name",
+ });
+ });
+ test("disable create geneset mode", () => {
+ expect(
+ genesetsUIReducer(undefined, {
+ type: "geneset: disable create geneset mode",
+ })
+ ).toMatchObject(initialState);
+ });
+ test("activate rename geneset mode", () => {
+ expect(
+ genesetsUIReducer(undefined, {
+ type: "geneset: activate rename geneset mode",
+ data: "a geneset name",
+ })
+ ).toMatchObject({
+ createGenesetModeActive: false,
+ isEditingGenesetName: "a geneset name",
+ isAddingGenesToGeneset: false,
+ });
+ });
+ test("disable rename geneset mode", () => {
+ expect(
+ genesetsUIReducer(undefined, {
+ type: "geneset: disable rename geneset mode",
+ })
+ ).toMatchObject(initialState);
+ });
+});
diff --git a/client/__tests__/util/typedCrossfilter/bitArray.test.js b/client/__tests__/util/typedCrossfilter/bitArray.test.js
index 90ad4760..8878d413 100644
--- a/client/__tests__/util/typedCrossfilter/bitArray.test.js
+++ b/client/__tests__/util/typedCrossfilter/bitArray.test.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
import BitArray from "../../../src/util/typedCrossfilter/bitArray";
const defaultTestLength = 8;
diff --git a/client/__tests__/util/typedCrossfilter/positiveInterval.test.js b/client/__tests__/util/typedCrossfilter/positiveInterval.test.js
index 1e9355fa..7c43e8a5 100644
--- a/client/__tests__/util/typedCrossfilter/positiveInterval.test.js
+++ b/client/__tests__/util/typedCrossfilter/positiveInterval.test.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
// const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals";
diff --git a/client/package-lock.json b/client/package-lock.json
index 2cf552e3..3f792d31 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -4154,6 +4154,21 @@
"react-transition-group": "^2.9.0",
"resize-observer-polyfill": "^1.5.1",
"tslib": "~1.10.0"
+ },
+ "dependencies": {
+ "react-popper": {
+ "version": "1.3.10",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.10.tgz",
+ "integrity": "sha512-sZfwHtHCMst0L0G/c83/Y/K1f9fNWMEKsk/cGAor68rQBHB75WuDQ7k95tkce8QNaUHhg9uFXIJbZO0eWRvJbw==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "@hypnosphi/create-react-context": "^0.3.1",
+ "popper.js": "^1.14.4",
+ "prop-types": "^15.6.1",
+ "typed-styles": "^0.0.7",
+ "warning": "^4.0.2"
+ }
+ }
}
},
"@blueprintjs/eslint-plugin": {
@@ -4375,6 +4390,68 @@
"tslib": "~1.10.0"
}
},
+ "@blueprintjs/popover2": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@blueprintjs/popover2/-/popover2-0.3.0.tgz",
+ "integrity": "sha512-EtxKoaafkn3FRLKYh0V2CdtgPBgtSoOxIzrT7eZil5XBgEQiYap1lzyc/Rm+N+4yeW1+iOB6uY/3gdE4CRiPIg==",
+ "requires": {
+ "@blueprintjs/core": "^3.39.0",
+ "@popperjs/core": "^2.5.4",
+ "classnames": "^2.2",
+ "dom4": "^2.1.5",
+ "react-popper": "^2.2.4",
+ "tslib": "~1.13.0"
+ },
+ "dependencies": {
+ "@blueprintjs/core": {
+ "version": "3.39.0",
+ "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.39.0.tgz",
+ "integrity": "sha512-Pl92Qvfo0iowvJ81hOxfOXgBUuqHdMgtYao7hCvhF5hvz8wYRfOr1GHChVsiD5NvpNmG2rtUTYqkfft06Tyj5A==",
+ "requires": {
+ "@blueprintjs/icons": "^3.24.0",
+ "@types/dom4": "^2.0.1",
+ "classnames": "^2.2",
+ "dom4": "^2.1.5",
+ "normalize.css": "^8.0.1",
+ "popper.js": "^1.16.1",
+ "react-lifecycles-compat": "^3.0.4",
+ "react-popper": "^1.3.7",
+ "react-transition-group": "^2.9.0",
+ "resize-observer-polyfill": "^1.5.1",
+ "tslib": "~1.13.0"
+ },
+ "dependencies": {
+ "react-popper": {
+ "version": "1.3.10",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.10.tgz",
+ "integrity": "sha512-sZfwHtHCMst0L0G/c83/Y/K1f9fNWMEKsk/cGAor68rQBHB75WuDQ7k95tkce8QNaUHhg9uFXIJbZO0eWRvJbw==",
+ "requires": {
+ "@babel/runtime": "^7.1.2",
+ "@hypnosphi/create-react-context": "^0.3.1",
+ "popper.js": "^1.14.4",
+ "prop-types": "^15.6.1",
+ "typed-styles": "^0.0.7",
+ "warning": "^4.0.2"
+ }
+ }
+ }
+ },
+ "@blueprintjs/icons": {
+ "version": "3.24.0",
+ "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.24.0.tgz",
+ "integrity": "sha512-OvDDI5EUueS1Y3t594iS8LAGoHhLhYjC2GuN/01a85n+ASLSp0jf0/+uix2JeCOj41iTdRRCINbWuRwVQNNGPw==",
+ "requires": {
+ "classnames": "^2.2",
+ "tslib": "~1.13.0"
+ }
+ },
+ "tslib": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
+ }
+ }
+ },
"@blueprintjs/select": {
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-3.13.5.tgz",
@@ -4503,6 +4580,15 @@
"@hapi/hoek": "^8.3.0"
}
},
+ "@hypnosphi/create-react-context": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz",
+ "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==",
+ "requires": {
+ "gud": "^1.0.0",
+ "warning": "^4.0.3"
+ }
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -5460,6 +5546,11 @@
}
}
},
+ "@popperjs/core": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.8.4.tgz",
+ "integrity": "sha512-h0lY7g36rhjNV8KVHKS3/BEOgfsxu0AiRI8+ry5IFBGEsQFkpjxtcpVc9ndN8zrKUeMZXAWMc7eQMepfgykpxQ=="
+ },
"@sentry/cli": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.55.0.tgz",
@@ -8229,15 +8320,6 @@
"sha.js": "^2.4.8"
}
},
- "create-react-context": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
- "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
- "requires": {
- "gud": "^1.0.0",
- "warning": "^4.0.3"
- }
- },
"cross-fetch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.5.tgz",
@@ -8935,19 +9017,6 @@
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"dev": true
},
- "deep-equal": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
- "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
- "requires": {
- "is-arguments": "^1.0.4",
- "is-date-object": "^1.0.1",
- "is-regex": "^1.0.4",
- "object-is": "^1.0.1",
- "object-keys": "^1.1.1",
- "regexp.prototype.flags": "^1.2.0"
- }
- },
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -8968,6 +9037,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
"requires": {
"object-keys": "^1.0.12"
}
@@ -9454,6 +9524,7 @@
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
"integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
+ "dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@@ -9472,6 +9543,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
@@ -11303,7 +11375,8 @@
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
},
"functional-red-black-tree": {
"version": "1.0.1",
@@ -11633,6 +11706,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
"requires": {
"function-bind": "^1.1.1"
}
@@ -11645,7 +11719,8 @@
"has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
- "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
+ "dev": true
},
"has-unicode": {
"version": "2.0.1",
@@ -12276,7 +12351,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "dev": true,
"requires": {
"loose-envify": "^1.0.0"
}
@@ -12316,11 +12390,6 @@
}
}
},
- "is-arguments": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
- "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
- },
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -12344,7 +12413,8 @@
"is-callable": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
+ "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+ "dev": true
},
"is-ci": {
"version": "2.0.0",
@@ -12389,7 +12459,8 @@
"is-date-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
- "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
+ "dev": true
},
"is-descriptor": {
"version": "0.1.6",
@@ -12543,6 +12614,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+ "dev": true,
"requires": {
"has": "^1.0.3"
}
@@ -12589,6 +12661,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+ "dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@@ -14215,6 +14288,11 @@
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
"dev": true
},
+ "lodash.assign": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+ "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
+ },
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -14233,6 +14311,16 @@
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==",
"dev": true
},
+ "lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
+ },
+ "lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+ },
"lodash.kebabcase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
@@ -15180,54 +15268,14 @@
"object-inspect": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
- "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
- },
- "object-is": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
- "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.5"
- },
- "dependencies": {
- "es-abstract": {
- "version": "1.17.6",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
- "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
- "requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1",
- "is-callable": "^1.2.0",
- "is-regex": "^1.1.0",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimend": "^1.0.1",
- "string.prototype.trimstart": "^1.0.1"
- }
- },
- "is-callable": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
- "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw=="
- },
- "is-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
- "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
- "requires": {
- "has-symbols": "^1.0.1"
- }
- }
- }
+ "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
+ "dev": true
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
},
"object-visit": {
"version": "1.0.1",
@@ -15241,6 +15289,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.2",
"function-bind": "^1.1.1",
@@ -16822,6 +16871,15 @@
"scheduler": "^0.19.1"
}
},
+ "react-easy-emoji": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-easy-emoji/-/react-easy-emoji-1.4.0.tgz",
+ "integrity": "sha512-TcufijpuWKgYgzbySEBukNef+y0HI/4PAJ4gc9vb1CF7Q4CcAS2ZV8VMZk0ObtKKwJJfVgAHVt86nXWOed8QXg==",
+ "requires": {
+ "lodash.assign": "^4.0.8",
+ "string-replace-to-array": "^1.0.1"
+ }
+ },
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
@@ -16866,17 +16924,19 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-popper": {
- "version": "1.3.7",
- "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
- "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.4.tgz",
+ "integrity": "sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew==",
"requires": {
- "@babel/runtime": "^7.1.2",
- "create-react-context": "^0.3.0",
- "deep-equal": "^1.1.1",
- "popper.js": "^1.14.4",
- "prop-types": "^15.6.1",
- "typed-styles": "^0.0.7",
+ "react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
+ },
+ "dependencies": {
+ "react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ }
}
},
"react-redux": {
@@ -17090,6 +17150,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
"integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1"
@@ -18438,6 +18499,16 @@
"strip-ansi": "^6.0.0"
}
},
+ "string-replace-to-array": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
+ "integrity": "sha1-yT66mZpe4k1zGuu69auja18Y978=",
+ "requires": {
+ "invariant": "^2.2.1",
+ "lodash.flatten": "^4.2.0",
+ "lodash.isstring": "^4.0.1"
+ }
+ },
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -18484,6 +18555,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
@@ -18493,6 +18565,7 @@
"version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
+ "dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@@ -18510,12 +18583,14 @@
"is-callable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
- "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw=="
+ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
+ "dev": true
},
"is-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
"integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
+ "dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@@ -18526,6 +18601,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
"integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
@@ -18535,6 +18611,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
"integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
@@ -18544,6 +18621,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
@@ -18553,6 +18631,7 @@
"version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
+ "dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@@ -18570,12 +18649,14 @@
"is-callable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
- "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw=="
+ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
+ "dev": true
},
"is-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
"integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
+ "dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
diff --git a/client/package.json b/client/package.json
index 29561f45..ea3a1988 100644
--- a/client/package.json
+++ b/client/package.json
@@ -40,6 +40,7 @@
"dependencies": {
"@blueprintjs/core": "^3.30.0",
"@blueprintjs/icons": "^3.19.0",
+ "@blueprintjs/popover2": "^0.3.0",
"@blueprintjs/select": "^3.13.5",
"abort-controller": "^3.0.0",
"core-js": "^3.6.5",
@@ -58,9 +59,11 @@
"react": "^16.13.1",
"react-async": "^10.0.1",
"react-dom": "^16.13.1",
+ "react-easy-emoji": "^1.4.0",
"react-flip-toolkit": "^7.0.12",
"react-helmet": "^5.2.1",
"react-icons": "^3.10.0",
+ "react-popper": "^2.2.4",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
diff --git a/client/src/actions/annotation.js b/client/src/actions/annotation.js
index 3d40fb2d..684fe7cb 100644
--- a/client/src/actions/annotation.js
+++ b/client/src/actions/annotation.js
@@ -393,6 +393,8 @@ export const saveGenesetsAction = () => async (dispatch, 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 =
@@ -401,16 +403,18 @@ export const saveGenesetsAction = () => async (dispatch, getState) => {
// our non-save was completed!
return dispatch({
type: "autosave: genesets complete",
- lastSavedGenesets: state.genesets,
+ lastSavedGenesets: genesets,
});
}
- const { lastTid, genesets: lastGenesets } = state.genesets;
+ dispatch({
+ type: "autosave: genesets started",
+ });
/* Create the JSON OTA data structure */
const tid = (lastTid ?? 0) + 1;
- const genesets = [];
- for (const [name, gs] of lastGenesets) {
+ const genesetsOTA = [];
+ for (const [name, gs] of genesets) {
const genes = [];
for (const g of gs.genes.values()) {
genes.push({
@@ -418,7 +422,7 @@ export const saveGenesetsAction = () => async (dispatch, getState) => {
gene_description: g.geneDescription,
});
}
- genesets.push({
+ genesetsOTA.push({
geneset_name: name,
geneset_description: gs.genesetDescription,
genes,
@@ -426,7 +430,7 @@ export const saveGenesetsAction = () => async (dispatch, getState) => {
}
const ota = {
tid,
- genesets,
+ genesets: genesetsOTA,
};
/* Save to server */
diff --git a/client/src/components/categorical/annoDialog.js b/client/src/components/annoDialog.js
similarity index 89%
rename from client/src/components/categorical/annoDialog.js
rename to client/src/components/annoDialog.js
index fdb07e77..e3842611 100644
--- a/client/src/components/categorical/annoDialog.js
+++ b/client/src/components/annoDialog.js
@@ -18,6 +18,8 @@ class AnnoDialog extends React.PureComponent {
validationError,
annoSelect,
annoInput,
+ secondaryInstructions,
+ secondaryInput,
handleCancel,
handleSubmit,
primaryButtonText,
@@ -46,6 +48,12 @@ class AnnoDialog extends React.PureComponent {
>
{errorMessage}
+ {secondaryInstructions && (
+
+ {secondaryInstructions}
+
+ )}
+ {secondaryInput || null}
{annoSelect || null}
diff --git a/client/src/components/app.js b/client/src/components/app.js
index 4de58f1c..eb46d3a6 100644
--- a/client/src/components/app.js
+++ b/client/src/components/app.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import Helmet from "react-helmet";
import { connect } from "react-redux";
diff --git a/client/src/components/autosave/filenameDialog.js b/client/src/components/autosave/filenameDialog.js
index 3ff7e500..b1fde95b 100644
--- a/client/src/components/autosave/filenameDialog.js
+++ b/client/src/components/autosave/filenameDialog.js
@@ -17,6 +17,9 @@ import {
auth: state.config?.authentication,
userInfo: state.userInfo,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
+ writableGenesetsEnabled: !(
+ state.config?.parameters?.["annotations_genesets_readonly"] ?? true
+ ),
}))
class FilenameDialog extends React.Component {
constructor(props) {
@@ -95,13 +98,14 @@ class FilenameDialog extends React.Component {
render() {
const {
writableCategoriesEnabled,
+ writableGenesetsEnabled,
annotations,
idhash,
userInfo,
} = this.props;
const { filenameText } = this.state;
- return writableCategoriesEnabled &&
+ return (writableCategoriesEnabled || writableGenesetsEnabled) &&
annotations.promptForFilename &&
!annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName &&
diff --git a/client/src/components/autosave/index.js b/client/src/components/autosave/index.js
index 18b5f152..12cd4982 100644
--- a/client/src/components/autosave/index.js
+++ b/client/src/components/autosave/index.js
@@ -67,7 +67,7 @@ class Autosave extends React.Component {
needToSaveGenesets = () => {
/* return true if we need to save gene ses, false if we do not */
const { genesets, lastSavedGenesets } = this.props;
- return genesets.initialized && genesets !== lastSavedGenesets;
+ return genesets.initialized && genesets.genesets !== lastSavedGenesets;
};
needToSave() {
diff --git a/client/src/components/brushableHistogram/error.js b/client/src/components/brushableHistogram/error.js
new file mode 100644
index 00000000..13a015d5
--- /dev/null
+++ b/client/src/components/brushableHistogram/error.js
@@ -0,0 +1,17 @@
+import React from "react";
+import * as globals from "../../globals";
+
+const ErrorLoading = ({ displayName, zebra }) => {
+ return (
+
+ {`Failure loading ${displayName}`}
+
+ );
+};
+
+export default ErrorLoading;
diff --git a/client/src/components/brushableHistogram/footer.js b/client/src/components/brushableHistogram/footer.js
new file mode 100644
index 00000000..39e800f2
--- /dev/null
+++ b/client/src/components/brushableHistogram/footer.js
@@ -0,0 +1,90 @@
+import React from "react";
+
+const HistogramFooter = React.memo(
+ ({
+ displayName,
+ hideRanges,
+ rangeMin,
+ rangeMax,
+ rangeColorMin,
+ rangeColorMax,
+ logFoldChange,
+ pvalAdj,
+ isObs,
+ }) => {
+ /*
+ Footer of each histogram. Will render range, title, and optionally
+ differential expression info.
+
+ 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
+ * logFoldChange - lfc to display, optional.
+ * pValue - pValue to display, optional.
+ */
+ return (
+
+
+
+ min {rangeMin.toPrecision(4)}
+
+
+ {isObs ? displayName : null}
+
+
+ : {rangeMin}
+
+
+ max {rangeMax.toPrecision(4)}
+
+
+
+ {logFoldChange && pvalAdj ? (
+
+
+ log fold change:
+ {` ${logFoldChange.toPrecision(4)}`}
+
+
+ p-value (adj):
+ {pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
+
+
+ ) : null}
+
+ );
+ }
+);
+
+export default HistogramFooter;
diff --git a/client/src/components/brushableHistogram/header.js b/client/src/components/brushableHistogram/header.js
new file mode 100644
index 00000000..b53684c1
--- /dev/null
+++ b/client/src/components/brushableHistogram/header.js
@@ -0,0 +1,100 @@
+import React, { useCallback } from "react";
+import { Button, ButtonGroup, Tooltip, Icon } from "@blueprintjs/core";
+import { IconNames } from "@blueprintjs/icons";
+import * as globals from "../../globals";
+
+const HistogramHeader = React.memo(
+ ({
+ fieldId,
+ isColorBy,
+ onColorByClick,
+ onRemoveClick,
+ isScatterPlotX,
+ isScatterPlotY,
+ onScatterPlotXClick,
+ onScatterPlotYClick,
+ isObs,
+ }) => {
+ /*
+ Render the toolbar for the histogram. Props:
+ * fieldId - field identifier, used for various IDs
+ * isColorBy - true/false, is this the current color-by
+ * onColorByClick - color-by click handler
+ * onRemoveClick - optional handler for remove. Button will not render if not defined.
+ * isScatterPlotX - optional, true/false if currently the X scatterplot field
+ * isScatterPlotY - optional, true/false if currently the Y scatterplot field
+ * onScatterPlotXClick - optional, handler for scatterPlot X button.
+ * onScatterPlotYClick - optional, handler for scatterPlot X button.
+
+ Scatterplot controls will not render if either handler unspecified.
+ */
+
+ const memoizedColorByCallback = useCallback(
+ () => onColorByClick(fieldId, isObs),
+ [fieldId, isObs]
+ );
+
+ return (
+
+ {onScatterPlotXClick && onScatterPlotYClick ? (
+
+
+
+
+ plot x
+
+
+ plot y
+
+
+
+ ) : null}
+ {onRemoveClick ? (
+
+ remove
+
+ ) : null}
+
+
+
+
+ );
+ }
+);
+
+export default HistogramHeader;
diff --git a/client/src/components/brushableHistogram/histogram.js b/client/src/components/brushableHistogram/histogram.js
new file mode 100644
index 00000000..e49eb6e4
--- /dev/null
+++ b/client/src/components/brushableHistogram/histogram.js
@@ -0,0 +1,185 @@
+import React, { useEffect, useRef, useState } from "react";
+import { interpolateCool } from "d3-scale-chromatic";
+import * as d3 from "d3";
+
+import maybeScientific from "../../util/maybeScientific";
+import clamp from "../../util/clamp";
+
+const Histogram = ({
+ field,
+ fieldForId,
+ display,
+ histogram,
+ width,
+ height,
+ onBrush,
+ onBrushEnd,
+ margin,
+ isColorBy,
+ selectionRange,
+ mini,
+}) => {
+ const svgRef = useRef(null);
+ const [brush, setBrush] = useState(null);
+
+ useEffect(() => {
+ /*
+ Create the d3 histogram
+ */
+ const { marginLeft, marginRight, marginBottom, marginTop } = margin;
+ const { x, y, bins, binStart, binEnd, binWidth } = histogram;
+ const svg = d3.select(svgRef.current);
+ const binPadding = mini ? 0 : -1;
+ const defaultBarColor = mini ? "black" : "#bbb";
+
+ /* Remove everything */
+ svg.selectAll("*").remove();
+
+ /* Set margins within the SVG */
+ const container = svg
+ .attr("width", width + marginLeft + marginRight)
+ .attr("height", height + marginTop + marginBottom)
+ .append("g")
+ .attr("class", "histogram-container")
+ .attr("transform", `translate(${marginLeft},${marginTop})`);
+
+ const colorScale = d3
+ .scaleSequential(interpolateCool)
+ .domain([0, bins.length]);
+
+ const histogramScale = d3
+ .scaleLinear()
+ .domain(x.domain())
+ .range([
+ colorScale.domain()[1],
+ colorScale.domain()[0],
+ ]); /* we flip this to make colors dark if high in the color scale */
+
+ if (binWidth > 0) {
+ /* BINS */
+ container
+ .insert("g", "*")
+ .selectAll("rect")
+ .data(bins)
+ .enter()
+ .append("rect")
+ .attr("x", (d, i) => x(binStart(i)) + 1)
+ .attr("y", (d) => y(d))
+ .attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - binPadding)
+ .attr("height", (d) => y(0) - y(d))
+ .style(
+ "fill",
+ isColorBy
+ ? (d, i) => colorScale(histogramScale(binStart(i)))
+ : defaultBarColor
+ );
+ }
+
+ if (!mini) {
+ // BRUSH
+ // Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
+ const brushX = d3
+ .brushX()
+ .extent([
+ [x.range()[0], y.range()[1]],
+ [x.range()[1], marginTop + height + marginBottom],
+ ])
+ /*
+ emit start so that the Undoable history can save an undo point
+ upon drag start, and ignore the subsequent intermediate drag events.
+ */
+ .on("start", onBrush(field, x.invert, "start"))
+ .on("brush", onBrush(field, x.invert, "brush"))
+ .on("end", onBrushEnd(field, x.invert));
+
+ const brushXselection = container
+ .insert("g")
+ .attr("class", "brush")
+ .attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
+ .call(brushX);
+
+ /* X AXIS */
+ container
+ .insert("g")
+ .attr("class", "axis axis--x")
+ .attr("transform", `translate(0,${marginTop + height})`)
+ .call(
+ d3
+ .axisBottom(x)
+ .ticks(4)
+ .tickFormat(d3.format(maybeScientific(x)))
+ );
+
+ /* Y AXIS */
+ container
+ .insert("g")
+ .attr("class", "axis axis--y")
+ .attr("transform", `translate(${marginLeft + width},0)`)
+ .call(
+ d3
+ .axisRight(y)
+ .ticks(3)
+ .tickFormat(
+ d3.format(
+ y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
+ )
+ )
+ );
+
+ /* axis style */
+ svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
+ svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
+ svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
+
+ setBrush({ brushX, brushXselection });
+ }
+ }, [histogram, isColorBy]);
+
+ useEffect(() => {
+ /*
+ paint/update selection brush
+ */
+ if (!brush) return;
+ const { brushX, brushXselection } = brush;
+ const selection = d3.brushSelection(brushXselection.node());
+ if (!selectionRange && selection) {
+ /* no active selection - clear brush */
+ brushXselection.call(brushX.move, null);
+ } else if (selectionRange) {
+ const { x, domain } = histogram;
+ const [min, max] = domain;
+ const x0 = x(clamp(selectionRange[0], [min, max]));
+ const x1 = x(clamp(selectionRange[1], [min, max]));
+ if (!selection) {
+ /* there is an active selection, but no brush - set the brush */
+ brushXselection.call(brushX.move, [x0, x1]);
+ } else {
+ /* there is an active selection and a brush - make sure they match */
+ const moveDeltaThreshold = 1;
+ const dX0 = Math.abs(x0 - selection[0]);
+ const dX1 = Math.abs(x1 - selection[1]);
+ /*
+ only update the brush if it is grossly incorrect,
+ as defined by the moveDeltaThreshold
+ */
+ if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
+ brushXselection.call(brushX.move, [x0, x1]);
+ }
+ }
+ }
+ }, [brush, selectionRange]);
+
+ return (
+
+ );
+};
+
+export default Histogram;
diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js
index e86e49ff..ff0c983e 100644
--- a/client/src/components/brushableHistogram/index.js
+++ b/client/src/components/brushableHistogram/index.js
@@ -1,442 +1,17 @@
-/*
-https://bl.ocks.org/mbostock/4341954
-https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
-https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
-https://bl.ocks.org/mbostock/3019563
-*/
-import React, { useEffect, useRef, useState, useCallback } from "react";
-import { Button, ButtonGroup, Icon, Tooltip } from "@blueprintjs/core";
+import React from "react";
import { connect } from "react-redux";
import * as d3 from "d3";
-import { interpolateCool } from "d3-scale-chromatic";
import Async from "react-async";
import memoize from "memoize-one";
-import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../globals";
import actions from "../../actions";
import { histogramContinuous } from "../../util/dataframe/histogram";
import { makeContinuousDimensionName } from "../../util/nameCreators";
-import significantDigits from "../../util/significantDigits";
-
-function clamp(val, rng) {
- return Math.max(Math.min(val, rng[1]), rng[0]);
-}
-
-function maybeScientific(x) {
- let format = ",";
- const _ticks = x.ticks(4);
-
- if (x.domain().some((n) => Math.abs(n) >= 10000)) {
- /*
- heuristic: if the last tick d3 wants to render has one significant
- digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render
- 4.20e+n
- */
- format = significantDigits(_ticks[_ticks.length - 1]) === 1 ? ".0e" : ".2e";
- }
-
- return format;
-}
-
-const StillLoading = ({ zebra, displayName }) => {
- /*
- Render a loading indicator for the field.
- */
- return (
-
-
-
-
- {displayName}
-
-
-
-
-
-
- );
-};
-
-const ErrorLoading = ({ displayName, error, zebra }) => {
- console.log(error); // log to console as this is unexpected
- return (
-
- {`Failure loading ${displayName}`}
-
- );
-};
-
-const HistogramFooter = React.memo(
- ({
- displayName,
- hideRanges,
- rangeMin,
- rangeMax,
- rangeColorMin,
- rangeColorMax,
- logFoldChange,
- pvalAdj,
- }) => {
- /*
- Footer of each histogram. Will render range, title, and optionally
- differential expression info.
-
- 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
- * logFoldChange - lfc to display, optional.
- * pValue - pValue to display, optional.
- */
- return (
-
-
-
- min {rangeMin.toPrecision(4)}
-
-
- {displayName}
-
-
- : {rangeMin}
-
-
- max {rangeMax.toPrecision(4)}
-
-
-
- {logFoldChange !== undefined && pvalAdj !== undefined ? (
-
-
- log fold change:
- {` ${logFoldChange.toPrecision(4)}`}
-
-
- p-value (adj):
- {pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
-
-
- ) : null}
-
- );
- }
-);
-
-const HistogramHeader = React.memo(
- ({
- fieldId,
- isColorBy,
- onColorByClick,
- onRemoveClick,
- isScatterPlotX,
- isScatterPlotY,
- onScatterPlotXClick,
- onScatterPlotYClick,
- isObs,
- }) => {
- /*
- Render the toolbar for the histogram. Props:
- * fieldId - field identifier, used for various IDs
- * isColorBy - true/false, is this the current color-by
- * onColorByClick - color-by click handler
- * onRemoveClick - optional handler for remove. Button will not render if not defined.
- * isScatterPlotX - optional, true/false if currently the X scatterplot field
- * isScatterPlotY - optional, true/false if currently the Y scatterplot field
- * onScatterPlotXClick - optional, handler for scatterPlot X button.
- * onScatterPlotYClick - optional, handler for scatterPlot X button.
-
- Scatterplot controls will not render if either handler unspecified.
- */
-
- const memoizedColorByCallback = useCallback(
- () => onColorByClick(fieldId, isObs),
- [fieldId, isObs]
- );
-
- return (
-
- {onScatterPlotXClick && onScatterPlotYClick ? (
-
-
-
-
- plot x
-
-
- plot y
-
-
-
- ) : null}
- {onRemoveClick ? (
-
- remove
-
- ) : null}
-
-
-
-
- );
- }
-);
-
-const Histogram = ({
- field,
- fieldForId,
- display,
- histogram,
- width,
- height,
- onBrush,
- onBrushEnd,
- margin,
- isColorBy,
- selectionRange,
-}) => {
- const svgRef = useRef(null);
- const [brush, setBrush] = useState(null);
-
- useEffect(() => {
- /*
- Create the d3 histogram
- */
- const { marginLeft, marginRight, marginBottom, marginTop } = margin;
- const { x, y, bins, binStart, binEnd, binWidth } = histogram;
- const svg = d3.select(svgRef.current);
-
- /* Remove everything */
- svg.selectAll("*").remove();
-
- /* Set margins within the SVG */
- const container = svg
- .attr("width", width + marginLeft + marginRight)
- .attr("height", height + marginTop + marginBottom)
- .append("g")
- .attr("class", "histogram-container")
- .attr("transform", `translate(${marginLeft},${marginTop})`);
-
- const colorScale = d3
- .scaleSequential(interpolateCool)
- .domain([0, bins.length]);
-
- const histogramScale = d3
- .scaleLinear()
- .domain(x.domain())
- .range([
- colorScale.domain()[1],
- colorScale.domain()[0],
- ]); /* we flip this to make colors dark if high in the color scale */
-
- if (binWidth > 0) {
- /* BINS */
- container
- .insert("g", "*")
- .selectAll("rect")
- .data(bins)
- .enter()
- .append("rect")
- .attr("x", (d, i) => x(binStart(i)) + 1)
- .attr("y", (d) => y(d))
- .attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
- .attr("height", (d) => y(0) - y(d))
- .style(
- "fill",
- isColorBy ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb"
- );
- }
-
- // BRUSH
- // Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
- const brushX = d3
- .brushX()
- .extent([
- [x.range()[0], y.range()[1]],
- [x.range()[1], marginTop + height + marginBottom],
- ])
- /*
- emit start so that the Undoable history can save an undo point
- upon drag start, and ignore the subsequent intermediate drag events.
- */
- .on("start", onBrush(field, x.invert, "start"))
- .on("brush", onBrush(field, x.invert, "brush"))
- .on("end", onBrushEnd(field, x.invert));
-
- const brushXselection = container
- .insert("g")
- .attr("class", "brush")
- .attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
- .call(brushX);
-
- /* X AXIS */
- container
- .insert("g")
- .attr("class", "axis axis--x")
- .attr("transform", `translate(0,${marginTop + height})`)
- .call(
- d3
- .axisBottom(x)
- .ticks(4)
- .tickFormat(d3.format(maybeScientific(x)))
- );
-
- /* Y AXIS */
- container
- .insert("g")
- .attr("class", "axis axis--y")
- .attr("transform", `translate(${marginLeft + width},0)`)
- .call(
- d3
- .axisRight(y)
- .ticks(3)
- .tickFormat(
- d3.format(
- y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
- )
- )
- );
-
- /* axis style */
- svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
- svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
- svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
-
- setBrush({ brushX, brushXselection });
- }, [histogram, isColorBy]);
-
- useEffect(() => {
- /*
- paint/update selection brush
- */
- if (!brush) return;
- const { brushX, brushXselection } = brush;
- const selection = d3.brushSelection(brushXselection.node());
- if (!selectionRange && selection) {
- /* no active selection - clear brush */
- brushXselection.call(brushX.move, null);
- } else if (selectionRange) {
- const { x, domain } = histogram;
- const [min, max] = domain;
- const x0 = x(clamp(selectionRange[0], [min, max]));
- const x1 = x(clamp(selectionRange[1], [min, max]));
- if (!selection) {
- /* there is an active selection, but no brush - set the brush */
- brushXselection.call(brushX.move, [x0, x1]);
- } else {
- /* there is an active selection and a brush - make sure they match */
- const moveDeltaThreshold = 1;
- const dX0 = Math.abs(x0 - selection[0]);
- const dX1 = Math.abs(x1 - selection[1]);
- /*
- only update the brush if it is grossly incorrect,
- as defined by the moveDeltaThreshold
- */
- if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
- brushXselection.call(brushX.move, [x0, x1]);
- }
- }
- }
- }, [brush, selectionRange]);
-
- return (
-
- );
-};
+import HistogramHeader from "./header";
+import Histogram from "./histogram";
+import HistogramFooter from "./footer";
+import StillLoading from "./loading";
+import ErrorLoading from "./error";
@connect((state, ownProps) => {
const { isObs, isUserDefined, isDiffExp, field } = ownProps;
@@ -472,14 +47,25 @@ class HistogramBrush extends React.PureComponent {
const marginRight = 54; // space for Y axis & labels
const marginBottom = 25; // space for X axis & labels
const marginTop = 3;
- this.margin = {
- marginLeft,
- marginRight,
- marginBottom,
- marginTop,
+
+ this.state = {
+ margin: {
+ marginLeft,
+ marginRight,
+ marginBottom,
+ marginTop,
+ },
+ width: 340 - marginLeft - marginRight,
+ height: 135 - marginTop - marginBottom,
+ marginMini: {
+ marginLeft: 0, // Space for 0 tick label on X axis
+ marginRight: 0, // space for Y axis & labels
+ marginBottom: 0, // space for X axis & labels
+ marginTop: 0,
+ },
+ widthMini: 120,
+ heightMini: 15,
};
- this.width = 340 - marginLeft - marginRight;
- this.height = 135 - marginTop - marginBottom;
}
onBrush = (selection, x, eventType) => {
@@ -609,6 +195,14 @@ class HistogramBrush extends React.PureComponent {
fetchAsyncProps = async () => {
const { annoMatrix } = this.props;
+ const {
+ margin,
+ width,
+ height,
+ marginMini,
+ widthMini,
+ heightMini,
+ } = this.state;
const { isClipped } = annoMatrix;
const query = this.createQuery();
@@ -636,11 +230,12 @@ class HistogramBrush extends React.PureComponent {
: globals.blue,
];
- const histogram = this.calcHistogramCache(
+ const histogram = this.calcHistogramCache(column, margin, width, height);
+ const miniHistogram = this.calcHistogramCache(
column,
- this.margin,
- this.width,
- this.height
+ marginMini,
+ widthMini,
+ heightMini
);
const isSingleValue = summary.min === summary.max;
@@ -654,6 +249,7 @@ class HistogramBrush extends React.PureComponent {
return {
histogram,
+ miniHistogram,
range,
unclippedRange,
unclippedRangeColor,
@@ -667,13 +263,16 @@ class HistogramBrush extends React.PureComponent {
/*
recalculate expensive stuff, notably bins, summaries, etc.
*/
- const histogramCache = {};
- const summary = col.summarize();
+ 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 { marginTop, marginLeft } = margin;
+ const { marginTop, marginLeft } = margin; /* changes with mini */
- histogramCache.domain = [domainMin, domainMax];
+ histogramCache.domain = [
+ domainMin,
+ domainMax,
+ ]; /* doesn't change with mini */
histogramCache.x = d3
.scaleLinear()
@@ -683,7 +282,7 @@ class HistogramBrush extends React.PureComponent {
histogramCache.bins = histogramContinuous(col, numBins, [
domainMin,
domainMax,
- ]);
+ ]); /* memoized */
histogramCache.binWidth = (domainMax - domainMin) / numBins;
histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
@@ -733,7 +332,16 @@ class HistogramBrush extends React.PureComponent {
zebra,
continuousSelectionRange,
isObs,
+ mini,
} = this.props;
+ const {
+ margin,
+ width,
+ height,
+ marginMini,
+ widthMini,
+ heightMini,
+ } = this.state;
const fieldForId = field.replace(/\s/g, "_");
const showScatterPlot = isDiffExp || isUserDefined;
@@ -761,48 +369,56 @@ class HistogramBrush extends React.PureComponent {
: "histogram-continuous-metadata"
}
style={{
- padding: globals.leftSidebarSectionPadding,
+ padding: mini ? 0 : globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
-
+ {!mini && isObs ? (
+
+ ) : null}
-
+ {!mini ? (
+
+ ) : null}
) : null
}
diff --git a/client/src/components/brushableHistogram/loading.js b/client/src/components/brushableHistogram/loading.js
new file mode 100644
index 00000000..7a195c78
--- /dev/null
+++ b/client/src/components/brushableHistogram/loading.js
@@ -0,0 +1,42 @@
+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.
+ */
+ return (
+
+
+
+
+ {displayName}
+
+
+
+
+
+
+ );
+};
+
+export default StillLoading;
diff --git a/client/src/components/categorical/category/annoDialogAddLabel.js b/client/src/components/categorical/category/annoDialogAddLabel.js
index 7de242d0..80047f21 100644
--- a/client/src/components/categorical/category/annoDialogAddLabel.js
+++ b/client/src/components/categorical/category/annoDialogAddLabel.js
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
-import AnnoDialog from "../annoDialog";
-import LabelInput from "../labelInput";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
import { labelPrompt, isLabelErroneous } from "../labelUtil";
import actions from "../../../actions";
diff --git a/client/src/components/categorical/category/annoDialogEditCategoryName.js b/client/src/components/categorical/category/annoDialogEditCategoryName.js
index 832d5ae3..d90d7040 100644
--- a/client/src/components/categorical/category/annoDialogEditCategoryName.js
+++ b/client/src/components/categorical/category/annoDialogEditCategoryName.js
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
-import AnnoDialog from "../annoDialog";
-import LabelInput from "../labelInput";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
import { labelPrompt } from "../labelUtil";
import { AnnotationsHelpers } from "../../../util/stateManager";
diff --git a/client/src/components/categorical/category/annoMenuCategory.js b/client/src/components/categorical/category/annoMenuCategory.js
index 4606f75a..706ff63a 100644
--- a/client/src/components/categorical/category/annoMenuCategory.js
+++ b/client/src/components/categorical/category/annoMenuCategory.js
@@ -9,7 +9,9 @@ import {
Tooltip,
Icon,
PopoverInteractionKind,
+ Intent,
} from "@blueprintjs/core";
+import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../../globals";
import actions from "../../../actions";
@@ -89,8 +91,8 @@ class AnnoMenuCategory extends React.PureComponent {
text={editText}
/>
+ {writableCategoriesEnabled ? (
+
+
+
+ Create new category
+
+
+
+ ) : null}
+
{/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */}
{allCategoryNames.map((catName) =>
@@ -209,33 +237,6 @@ class Categories extends React.Component {
/>
) : null
)}
-
- {writableCategoriesEnabled ? (
-
-
- Create new category
-
-
- ) : null}
);
}
diff --git a/client/src/components/categorical/value/index.js b/client/src/components/categorical/value/index.js
index a9e682dc..628118fb 100644
--- a/client/src/components/categorical/value/index.js
+++ b/client/src/components/categorical/value/index.js
@@ -14,8 +14,8 @@ import {
} from "@blueprintjs/core";
import * as globals from "../../../globals";
import styles from "../categorical.css";
-import AnnoDialog from "../annoDialog";
-import LabelInput from "../labelInput";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
import Truncate from "../../util/truncate";
import { AnnotationsHelpers } from "../../../util/stateManager";
@@ -25,8 +25,8 @@ import MiniHistogram from "../../miniHistogram";
import MiniStackedBar from "../../miniStackedBar";
import { CategoryCrossfilterContext } from "../categoryContext";
-const VALUE_HEIGHT = 11;
-const CHART_WIDTH = 100;
+const STACKED_BAR_HEIGHT = 11;
+const STACKED_BAR_WIDTH = 100;
/* this is defined outside of the class so we can use it in connect() */
function _currentLabelAsString(ownProps) {
@@ -410,7 +410,7 @@ class CategoryValue extends React.Component {
label,
colorTable,
schema,
- CHART_WIDTH
+ STACKED_BAR_WIDTH
) ?? {};
if (!domainValues || !scale || !domain || !occupancy) {
@@ -426,8 +426,8 @@ class CategoryValue extends React.Component {
domain,
occupancy,
}}
- height={VALUE_HEIGHT}
- width={CHART_WIDTH}
+ height={STACKED_BAR_HEIGHT}
+ width={STACKED_BAR_WIDTH}
/>
);
};
@@ -460,8 +460,8 @@ class CategoryValue extends React.Component {
colorAccessor,
colorData,
label,
- CHART_WIDTH,
- VALUE_HEIGHT
+ STACKED_BAR_WIDTH,
+ STACKED_BAR_HEIGHT
) ?? {}; // if createHistogramBins returns empty object assign null to deconstructed
if (!xScale || !yScale || !bins) return null;
@@ -476,8 +476,8 @@ class CategoryValue extends React.Component {
}}
obsOrVarContinuousFieldDisplayName={colorAccessor}
domainLabel={label}
- height={VALUE_HEIGHT}
- width={CHART_WIDTH}
+ height={STACKED_BAR_HEIGHT}
+ width={STACKED_BAR_WIDTH}
/>
);
};
@@ -534,7 +534,7 @@ class CategoryValue extends React.Component {
colorAccessor && !isColorBy
? globals.leftSidebarWidth -
otherElementsWidth -
- CHART_WIDTH -
+ STACKED_BAR_WIDTH -
CHART_MARGIN
: globals.leftSidebarWidth - otherElementsWidth;
@@ -677,8 +677,8 @@ class CategoryValue extends React.Component {
display={isColorBy && categoryValueIndices ? "auto" : "none"}
style={{
marginLeft: 5,
- width: VALUE_HEIGHT,
- height: VALUE_HEIGHT,
+ width: 15,
+ height: 15,
backgroundColor:
isColorBy && categoryValueIndices
? colorScale(categoryValueIndices.get(label))
@@ -739,7 +739,7 @@ class CategoryValue extends React.Component {
) : null}
{displayString !== globals.unassignedCategoryLabel ? (
{
diff --git a/client/src/components/continuous/util.js b/client/src/components/continuous/util.js
index 40462c6c..cca3b91c 100644
--- a/client/src/components/continuous/util.js
+++ b/client/src/components/continuous/util.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import _ from "lodash";
import * as d3 from "d3";
diff --git a/client/src/components/continuousLegend/index.js b/client/src/components/continuousLegend/index.js
index 38de34f1..5087ab59 100644
--- a/client/src/components/continuousLegend/index.js
+++ b/client/src/components/continuousLegend/index.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
import * as d3 from "d3";
@@ -10,7 +9,6 @@ import {
} from "../../util/stateManager/colorHelpers";
// create continuous color legend
-// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
const continuous = (selectorId, colorScale, colorAccessor) => {
const legendHeight = 200;
const legendWidth = 80;
diff --git a/client/src/components/framework/container.js b/client/src/components/framework/container.js
index a5e161f6..db9e2e19 100644
--- a/client/src/components/framework/container.js
+++ b/client/src/components/framework/container.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
function Container(props) {
diff --git a/client/src/components/framework/layout.js b/client/src/components/framework/layout.js
index 6b0edb29..0ee27913 100644
--- a/client/src/components/framework/layout.js
+++ b/client/src/components/framework/layout.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import * as globals from "../../globals";
diff --git a/client/src/components/geneExpression/gene.js b/client/src/components/geneExpression/gene.js
new file mode 100644
index 00000000..5ea3f1c7
--- /dev/null
+++ b/client/src/components/geneExpression/gene.js
@@ -0,0 +1,188 @@
+import React from "react";
+import { connect } from "react-redux";
+
+import { AnchorButton, Icon } from "@blueprintjs/core";
+import Truncate from "../util/truncate";
+import HistogramBrush from "../brushableHistogram";
+
+import * as globals from "../../globals";
+import actions from "../../actions";
+
+@connect((state, ownProps) => {
+ const { gene } = ownProps;
+
+ return {
+ isColorAccessor: state.colors.colorAccessor === gene,
+ isScatterplotXXaccessor: state.controls.scatterplotXXaccessor === gene,
+ isScatterplotYYaccessor: state.controls.scatterplotYYaccessor === gene,
+ };
+})
+class Gene extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ geneIsExpanded: false,
+ };
+ }
+
+ onColorChangeClick = () => {
+ const { dispatch, gene } = this.props;
+ dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(gene));
+ };
+
+ handleGeneExpandClick = () => {
+ const { geneIsExpanded } = this.state;
+ this.setState({ geneIsExpanded: !geneIsExpanded });
+ };
+
+ handleSetGeneAsScatterplotX = () => {
+ const { dispatch, gene } = this.props;
+ dispatch({
+ type: "set scatterplot x",
+ data: gene,
+ });
+ };
+
+ handleSetGeneAsScatterplotY = () => {
+ const { dispatch, gene } = this.props;
+ dispatch({
+ type: "set scatterplot y",
+ data: gene,
+ });
+ };
+
+ handleDeleteGeneFromSet = () => {
+ const { dispatch, gene, geneset } = this.props;
+
+ dispatch({
+ type: "geneset: delete genes",
+ genesetName: geneset,
+ geneSymbols: [gene],
+ });
+ };
+
+ render() {
+ const {
+ gene,
+ isColorAccessor,
+ isScatterplotXXaccessor,
+ isScatterplotYYaccessor,
+ isDiffexp,
+ } = this.props;
+ const { geneIsExpanded } = this.state;
+ const genesetNameLengthVisible = 310; /* this magic number determines how much of a long geneset name we see */
+ return (
+
+
+
{}}
+ style={{
+ cursor: "pointer",
+ display: "flex",
+ justifyContent: "space-between",
+ width: "100%",
+ }}
+ >
+
+
+
+
+ {gene}
+
+
+
+ {!geneIsExpanded ? (
+
+ ) : null}
+
+
+ {!isDiffexp ? (
+
}
+ />
+ ) : null}
+
+ x
+
+
+ y
+
+
}
+ style={{ marginRight: 2 }}
+ />
+
}
+ />
+
+
+ {geneIsExpanded ?
: null}
+
+ );
+ }
+}
+
+export default Gene;
diff --git a/client/src/components/geneExpression/geneSet.js b/client/src/components/geneExpression/geneSet.js
new file mode 100644
index 00000000..deb6b3aa
--- /dev/null
+++ b/client/src/components/geneExpression/geneSet.js
@@ -0,0 +1,183 @@
+import React from "react";
+import _ from "lodash";
+import { connect } from "react-redux";
+import { Tooltip, Position, Switch } from "@blueprintjs/core";
+import { FaChevronRight, FaChevronDown } from "react-icons/fa";
+import actions from "../../actions";
+import Gene from "./gene";
+import { memoize } from "../../util/dataframe/util";
+import Truncate from "../util/truncate";
+import * as globals from "../../globals";
+import GenesetMenus from "./menus/genesetMenus";
+
+@connect((state, ownProps) => {
+ return {
+ world: state.world,
+ userDefinedGenes: state.controls.userDefinedGenes,
+ userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
+ isColorAccessor: state.colors.colorAccessor === ownProps.setName,
+ };
+})
+class GeneSet extends React.Component {
+ _memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr);
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpen: false,
+ toggleSummaryHisto: false,
+ };
+ }
+
+ _genesToUpper = (listGenes) => {
+ // Has to be a Map to preserve index
+ const upperGenes = new Map();
+ for (let i = 0, { length } = listGenes; i < length; i += 1) {
+ upperGenes.set(listGenes[i].toUpperCase(), i);
+ }
+
+ return upperGenes;
+ };
+
+ fetchGenes = () => {
+ const { world, dispatch, setGenes } = this.props;
+ const varIndexName = world.schema.annotations.var.index;
+
+ const worldGenes = world.varAnnotations.col(varIndexName).asArray();
+
+ const upperGenes = this._genesToUpper(setGenes);
+ const upperWorldGenes = this._memoGenesToUpper(worldGenes);
+
+ dispatch({ type: "bulk user defined gene start" });
+
+ Promise.all(
+ [...upperGenes.keys()].map((upperGene) => {
+ const indexOfGene = upperWorldGenes.get(upperGene);
+
+ return dispatch(
+ actions.requestUserDefinedGene(worldGenes[indexOfGene])
+ );
+ })
+ ).then(
+ () => dispatch({ type: "bulk user defined gene complete" }),
+ () => dispatch({ type: "bulk user defined gene error" })
+ );
+
+ return undefined;
+ };
+
+ onGenesetMenuClick = () => {
+ const { isOpen } = this.state;
+ this.setState({ isOpen: !isOpen });
+ };
+
+ onColorChangeClick = () => {
+ // const { dispatch, setName } = this.props;
+ // dispatch({
+ // type: "color by gene set",
+ // colorAccessor: setName,
+ // });
+ };
+
+ toggleSummaryHisto = () => {
+ const { toggleSummaryHisto } = this.state;
+ this.setState({ toggleSummaryHisto: !toggleSummaryHisto });
+ };
+
+ render() {
+ const { setName, setGenes, isDiffexp } = this.props;
+ const { isOpen, toggleSummaryHisto } = this.state;
+ const genesetNameLengthVisible = 120; /* this magic number determines how much of a long geneset name we see */
+ return (
+
+
+
{}
+ }
+ style={{
+ cursor: "pointer",
+ userSelect: "none",
+ }}
+ onClick={this.onGenesetMenuClick}
+ >
+
+
+ {setName}
+
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {isOpen ? (
+
+
+
+ ) : null}
+
+
+ {isOpen && !toggleSummaryHisto
+ ? _.map(setGenes, (gene) => {
+ return (
+
+ );
+ })
+ : null}
+
+ );
+ }
+}
+
+export default GeneSet;
diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js
index b7a9bf21..43d2fc79 100644
--- a/client/src/components/geneExpression/index.js
+++ b/client/src/components/geneExpression/index.js
@@ -1,59 +1,78 @@
-/* rc slider https://www.npmjs.com/package/rc-slider */
-
import React from "react";
-import _ from "lodash";
import { connect } from "react-redux";
-import HistogramBrush from "../brushableHistogram";
-import * as globals from "../../globals";
-import AddGenes from "./addGenes";
+import { Button } from "@blueprintjs/core";
+import GeneSet from "./geneSet";
+
+import CreateGenesetDialogue from "./menus/createGenesetDialogue";
+import EditGenesetNameDialogue from "./menus/editGenesetNameDialogue";
@connect((state) => {
return {
- userDefinedGenes: state.controls.userDefinedGenes,
differential: state.differential,
+ genesets: state.genesets.genesets,
};
})
class GeneExpression extends React.Component {
+ renderGeneSets = () => {
+ const sets = [];
+ const { genesets } = this.props;
+
+ for (const [name, genes] of genesets) {
+ sets.push(
+
+ );
+ }
+ return sets;
+ };
+
+ renderDiffexpGeneSets = () => {
+ const { differential } = this.props;
+
+ const setGenes = [];
+
+ if (differential.diffExp) {
+ differential.diffExp.forEach((diffexpGene) => {
+ setGenes.push(diffexpGene[0]);
+ });
+ }
+
+ return differential.diffExp ? (
+
+ ) : null;
+ };
+
+ handleActivateCreateGenesetMode = () => {
+ const { dispatch } = this.props;
+ dispatch({ type: "geneset: activate add new geneset mode" });
+ };
+
render() {
- const { userDefinedGenes, differential } = this.props;
return (
-
+
-
- {userDefinedGenes.length > 0
- ? _.map(userDefinedGenes, (geneName, index) => {
- return (
-
- );
- })
- : null}
-
-
- {differential.diffExp
- ? _.map(differential.diffExp, (value, index) => {
- return (
-
- );
- })
- : null}
+
+
+ Create new gene set
+
+
+
+
+
{this.renderDiffexpGeneSets()}
+
{this.renderGeneSets()}
);
}
diff --git a/client/src/components/geneExpression/menus/addGeneToGenesetDialogue.js b/client/src/components/geneExpression/menus/addGeneToGenesetDialogue.js
new file mode 100644
index 00000000..f8148e19
--- /dev/null
+++ b/client/src/components/geneExpression/menus/addGeneToGenesetDialogue.js
@@ -0,0 +1,92 @@
+import React from "react";
+import { connect } from "react-redux";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
+import parseBulkGeneString from "../../../util/parseBulkGeneString";
+
+@connect((state) => ({
+ genesetsUI: state.genesetsUI,
+}))
+class AddGeneToGenesetDialogue extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ genesToAdd: "",
+ };
+ }
+
+ disableAddGeneMode = () => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: "geneset: disable add new genes mode",
+ });
+ };
+
+ handleAddGeneToGeneSet = (e) => {
+ const { geneset, dispatch } = this.props;
+ const { genesToAdd } = this.state;
+
+ const genesTmpHardcodedFormat = [];
+
+ const genesArrayFromString = parseBulkGeneString(genesToAdd);
+
+ genesArrayFromString.forEach((_gene) => {
+ genesTmpHardcodedFormat.push({
+ geneSymbol: _gene,
+ });
+ });
+
+ dispatch({
+ type: "geneset: add genes",
+ genesetName: geneset,
+ genes: genesTmpHardcodedFormat,
+ });
+ dispatch({
+ type: "geneset: disable add new genes mode",
+ });
+ if (e) e.preventDefault();
+ };
+
+ handleChange = (e) => {
+ this.setState({ genesToAdd: e });
+ };
+
+ render() {
+ const { geneset, genesetsUI } = this.props;
+ const { genesToAdd } = this.state;
+
+ return (
+ <>
+
+ }
+ handleSubmit={this.handleAddGeneToGeneSet}
+ handleCancel={this.disableAddGeneMode}
+ />
+ >
+ );
+ }
+}
+
+export default AddGeneToGenesetDialogue;
diff --git a/client/src/components/geneExpression/addGenes.js b/client/src/components/geneExpression/menus/addGenes.js
similarity index 96%
rename from client/src/components/geneExpression/addGenes.js
rename to client/src/components/geneExpression/menus/addGenes.js
index 2d1ca1b4..f9bdda99 100644
--- a/client/src/components/geneExpression/addGenes.js
+++ b/client/src/components/geneExpression/menus/addGenes.js
@@ -1,7 +1,4 @@
-/* rc slider https://www.npmjs.com/package/rc-slider */
-
import React from "react";
-import _ from "lodash";
import fuzzysort from "fuzzysort";
import { connect } from "react-redux";
import { Suggest } from "@blueprintjs/select";
@@ -13,14 +10,15 @@ import {
Intent,
MenuItem,
} from "@blueprintjs/core";
-import * as globals from "../../globals";
-import actions from "../../actions";
+import * as globals from "../../../globals";
+import actions from "../../../actions";
import {
postUserErrorToast,
keepAroundErrorToast,
-} from "../framework/toasters";
+} from "../../framework/toasters";
-import { memoize } from "../../util/dataframe/util";
+import { memoize } from "../../../util/dataframe/util";
+import parseBulkGeneString from "../../../util/parseBulkGeneString";
const renderGene = (fuzzySortResult, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) {
@@ -101,7 +99,7 @@ class AddGenes extends React.Component {
Apod,,, Cd74,, ,,, Foo, Bar-2,,
*/
if (bulkAdd !== "") {
- const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
+ const genes = parseBulkGeneString(bulkAdd);
if (genes.length === 0) {
return keepAroundErrorToast("Must enter a gene name.");
}
diff --git a/client/src/components/geneExpression/menus/createGenesetDialogue.js b/client/src/components/geneExpression/menus/createGenesetDialogue.js
new file mode 100644
index 00000000..7fc4bd55
--- /dev/null
+++ b/client/src/components/geneExpression/menus/createGenesetDialogue.js
@@ -0,0 +1,149 @@
+import _ from "lodash";
+import React from "react";
+import { connect } from "react-redux";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
+
+@connect((state) => ({
+ annotations: state.annotations,
+ schema: state.annoMatrix?.schema,
+ ontology: state.ontology,
+ obsCrossfilter: state.obsCrossfilter,
+ genesets: state.genesets.genesets,
+ genesetsUI: state.genesetsUI,
+}))
+class CreateGenesetDialogue extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ genesetName: "",
+ genesToPopulateGeneset: "",
+ };
+ }
+
+ disableCreateGenesetMode = (e) => {
+ const { dispatch } = this.props;
+ this.setState({
+ genesetName: "",
+ genesToPopulateGeneset: "",
+ });
+ dispatch({
+ type: "geneset: disable create geneset mode",
+ });
+ if (e) e.preventDefault();
+ };
+
+ createGeneset = (e) => {
+ const { dispatch } = this.props;
+ const { genesetName, genesToPopulateGeneset } = this.state;
+
+ dispatch({
+ type: "geneset: create",
+ genesetName,
+ genesetDescription: "",
+ });
+ if (genesToPopulateGeneset) {
+ const genesTmpHardcodedFormat = [];
+
+ const genesArrayFromString = _.pull(
+ _.uniq(genesToPopulateGeneset.split(/[ ,]+/)),
+ ""
+ );
+
+ genesArrayFromString.forEach((_gene) => {
+ genesTmpHardcodedFormat.push({
+ geneSymbol: _gene,
+ });
+ });
+
+ dispatch({
+ type: "geneset: add genes",
+ genesetName,
+ genes: genesTmpHardcodedFormat,
+ });
+ }
+ dispatch({
+ type: "geneset: disable create geneset mode",
+ });
+ this.setState({
+ genesetName: "",
+ genesToPopulateGeneset: "",
+ });
+ e.preventDefault();
+ };
+
+ genesetNameError = () => {
+ return false;
+ };
+
+ handleChange = (e) => {
+ this.setState({ genesetName: e });
+ };
+
+ handleGenesetInputChange = (e) => {
+ this.setState({ genesToPopulateGeneset: e });
+ };
+
+ instruction = (genesetName, genesets) => {
+ return genesets.has(genesetName)
+ ? "Geneset name must be unique."
+ : "New, unique geneset name";
+ };
+
+ validate = (genesetName, genesets) => {
+ return genesets.has(genesetName);
+ };
+
+ render() {
+ const { genesetName } = this.state;
+ const { metadataField, genesetsUI, genesets } = this.props;
+
+ return (
+ <>
+
+ }
+ secondaryInstructions="Optionally add a list of comma separated genes to populate the gene set"
+ secondaryInput={
+
+ }
+ handleSubmit={this.createGeneset}
+ handleCancel={this.disableCreateGenesetMode}
+ />
+ >
+ );
+ }
+}
+
+export default CreateGenesetDialogue;
diff --git a/client/src/components/geneExpression/menus/editGenesetNameDialogue.js b/client/src/components/geneExpression/menus/editGenesetNameDialogue.js
new file mode 100644
index 00000000..0ac6e724
--- /dev/null
+++ b/client/src/components/geneExpression/menus/editGenesetNameDialogue.js
@@ -0,0 +1,95 @@
+import React from "react";
+import { connect } from "react-redux";
+import AnnoDialog from "../../annoDialog";
+import LabelInput from "../../labelInput";
+
+@connect((state) => ({
+ annotations: state.annotations,
+ schema: state.annoMatrix?.schema,
+ ontology: state.ontology,
+ obsCrossfilter: state.obsCrossfilter,
+ genesetsUI: state.genesetsUI,
+}))
+class RenameGeneset extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ genesetName: "",
+ };
+ }
+
+ disableEditGenesetNameMode = (e) => {
+ const { dispatch } = this.props;
+ this.setState({
+ genesetName: "",
+ });
+ dispatch({
+ type: "geneset: disable rename geneset mode",
+ });
+ if (e) e.preventDefault();
+ };
+
+ renameGeneset = (e) => {
+ const { dispatch, genesetsUI } = this.props;
+ const { genesetName } = this.state;
+
+ dispatch({
+ type: "geneset: update",
+ genesetName: genesetsUI.isEditingGenesetName,
+ update: { genesetName },
+ });
+ dispatch({
+ type: "geneset: disable rename geneset mode",
+ });
+ e.preventDefault();
+ };
+
+ genesetNameError = () => {
+ return false;
+ };
+
+ handleChange = (e) => {
+ this.setState({ genesetName: e });
+ };
+
+ render() {
+ const { genesetName } = this.state;
+ const { genesetsUI } = this.props;
+
+ return (
+ <>
+
+ }
+ handleSubmit={this.renameGeneset}
+ handleCancel={this.disableEditGenesetNameMode}
+ />
+ >
+ );
+ }
+}
+
+export default RenameGeneset;
diff --git a/client/src/components/geneExpression/menus/genesetMenus.js b/client/src/components/geneExpression/menus/genesetMenus.js
new file mode 100644
index 00000000..ccdfe9c5
--- /dev/null
+++ b/client/src/components/geneExpression/menus/genesetMenus.js
@@ -0,0 +1,120 @@
+import React from "react";
+import { connect } from "react-redux";
+import {
+ Button,
+ Menu,
+ MenuItem,
+ Popover,
+ Position,
+ Tooltip,
+ Icon,
+ PopoverInteractionKind,
+} from "@blueprintjs/core";
+
+import * as globals from "../../../globals";
+import AddGeneToGenesetDialogue from "./addGeneToGenesetDialogue";
+
+@connect((state) => {
+ return {
+ genesetsUI: state.genesetsUI,
+ };
+})
+class GenesetMenus extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ activateAddGeneToGenesetMode = () => {
+ const { dispatch, geneset } = this.props;
+ dispatch({
+ type: "geneset: activate add new genes mode",
+ geneset,
+ });
+ };
+
+ activateEditGenesetNameMode = () => {
+ const { dispatch, geneset } = this.props;
+
+ dispatch({
+ type: "geneset: activate rename geneset mode",
+ data: geneset,
+ });
+ };
+
+ handleDeleteCategory = () => {
+ const { dispatch, geneset } = this.props;
+ dispatch({ type: "geneset: delete", genesetName: geneset });
+ };
+
+ render() {
+ const { geneset, genesetsEditable, createText } = this.props;
+
+ return (
+ <>
+ {genesetsEditable ? (
+ <>
+
+ }
+ onClick={this.activateAddGeneToGenesetMode}
+ small
+ minimal
+ />
+
+
+
+
+
+
+ }
+ >
+ }
+ small
+ minimal
+ />
+
+
}
+ />
+ >
+ ) : null}
+ >
+ );
+ }
+}
+
+export default GenesetMenus;
diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js
index ceaf31c7..51c98ef9 100644
--- a/client/src/components/graph/graph.js
+++ b/client/src/components/graph/graph.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import * as d3 from "d3";
import { connect, shallowEqual } from "react-redux";
diff --git a/client/src/components/graph/setupLasso.js b/client/src/components/graph/setupLasso.js
index 6610e04c..095b4ec6 100644
--- a/client/src/components/graph/setupLasso.js
+++ b/client/src/components/graph/setupLasso.js
@@ -1,5 +1,3 @@
-// https://bl.ocks.org/pbeshai/8008075f9ce771ee8be39e8c38907570
-
import * as d3 from "d3";
import { Colors } from "@blueprintjs/core";
diff --git a/client/src/components/graph/setupSVGandBrush.js b/client/src/components/graph/setupSVGandBrush.js
index d6a77bc4..bedf96a9 100644
--- a/client/src/components/graph/setupSVGandBrush.js
+++ b/client/src/components/graph/setupSVGandBrush.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import * as d3 from "d3";
import Lasso from "./setupLasso";
diff --git a/client/src/components/categorical/labelInput.js b/client/src/components/labelInput.js
similarity index 100%
rename from client/src/components/categorical/labelInput.js
rename to client/src/components/labelInput.js
diff --git a/client/src/components/leftSidebar/index.js b/client/src/components/leftSidebar/index.js
index 4cc60658..6d72b424 100644
--- a/client/src/components/leftSidebar/index.js
+++ b/client/src/components/leftSidebar/index.js
@@ -1,10 +1,10 @@
-// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
import Categorical from "../categorical";
import * as globals from "../../globals";
import DynamicScatterplot from "../scatterplot/scatterplot";
import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
+import Continuous from "../continuous/continuous";
@connect((state) => ({
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
@@ -32,6 +32,7 @@ class LeftSideBar extends React.Component {
}}
>
+
{scatterplotXXaccessor && scatterplotYYaccessor ? (
diff --git a/client/src/components/leftSidebar/infoMenu.js b/client/src/components/leftSidebar/infoMenu.js
index 6ab9bdde..f709af2c 100644
--- a/client/src/components/leftSidebar/infoMenu.js
+++ b/client/src/components/leftSidebar/infoMenu.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
diff --git a/client/src/components/menubar/cellSetButtons.js b/client/src/components/menubar/cellSetButtons.js
index ae4bb95c..f421d5ec 100644
--- a/client/src/components/menubar/cellSetButtons.js
+++ b/client/src/components/menubar/cellSetButtons.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux";
diff --git a/client/src/components/rightSidebar/index.js b/client/src/components/rightSidebar/index.js
index 4e75ba05..43802b99 100644
--- a/client/src/components/rightSidebar/index.js
+++ b/client/src/components/rightSidebar/index.js
@@ -1,7 +1,5 @@
-// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
-import Continuous from "../continuous/continuous";
import GeneExpression from "../geneExpression";
import * as globals from "../../globals";
@@ -22,10 +20,10 @@ class RightSidebar extends React.Component {
overflowY: "inherit",
height: "inherit",
width: "inherit",
+ padding: globals.leftSidebarSectionPadding,
}}
>
-
);
}
diff --git a/client/src/components/scatterplot/util.js b/client/src/components/scatterplot/util.js
index f60216f0..54393dde 100644
--- a/client/src/components/scatterplot/util.js
+++ b/client/src/components/scatterplot/util.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
export const margin = { top: 40, right: 5, bottom: 20, left: 60 };
export const width = 340;
export const height = 340 - margin.top - margin.bottom;
diff --git a/client/src/components/util/truncate.js b/client/src/components/util/truncate.js
index 33e64dcc..80c9eaf0 100644
--- a/client/src/components/util/truncate.js
+++ b/client/src/components/util/truncate.js
@@ -1,5 +1,5 @@
import React, { cloneElement } from "react";
-import { Tooltip, Position } from "@blueprintjs/core";
+import { Tooltip2 } from "@blueprintjs/popover2";
import { tooltipHoverOpenDelayQuick } from "../../globals";
@@ -86,18 +86,12 @@ export default (props) => {
})
);
return (
-
{newChildren}
-
+
);
};
diff --git a/client/src/index.css b/client/src/index.css
index 185bb657..80218dc7 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -7,6 +7,7 @@ namespace mangling on these definitions. See webpack config for specifics.
@import "~@blueprintjs/core/lib/css/blueprint.css";
@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";
@import "~@blueprintjs/select/lib/css/blueprint-select.css";
+@import "~@blueprintjs/popover2/lib/css/blueprint-popover2.css";
@font-face {
font-family: "Roboto Condensed";
diff --git a/client/src/reducers/autosave.js b/client/src/reducers/autosave.js
index e545708e..5b03cab6 100644
--- a/client/src/reducers/autosave.js
+++ b/client/src/reducers/autosave.js
@@ -53,7 +53,7 @@ const Autosave = (
return {
...state,
genesetSaveInProgress: false,
- lastSavedGenesets: nextSharedState.genesets,
+ lastSavedGenesets: nextSharedState.genesets.genesets,
};
}
diff --git a/client/src/reducers/config.js b/client/src/reducers/config.js
index e6bfad4d..22858c8f 100644
--- a/client/src/reducers/config.js
+++ b/client/src/reducers/config.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
const Config = (
state = {
displayNames: null,
diff --git a/client/src/reducers/controls.js b/client/src/reducers/controls.js
index d5c4d406..c2e195b1 100644
--- a/client/src/reducers/controls.js
+++ b/client/src/reducers/controls.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
import _ from "lodash";
import { subsetAndResetGeneLists } from "../util/stateManager/controlsHelpers";
diff --git a/client/src/reducers/differential.js b/client/src/reducers/differential.js
index 8ef7e2b6..74bbce67 100644
--- a/client/src/reducers/differential.js
+++ b/client/src/reducers/differential.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
const Differential = (
state = {
diffExp: null,
diff --git a/client/src/reducers/genesets.js b/client/src/reducers/genesets.js
index 56458330..2e2dff42 100644
--- a/client/src/reducers/genesets.js
+++ b/client/src/reducers/genesets.js
@@ -200,6 +200,7 @@ const GeneSets = (
*/
case "geneset: add genes": {
const { genesetName, genes } = action;
+
if (!state.genesets.has(genesetName))
throw new Error("geneset: add genes -- geneset name does not exist.");
diff --git a/client/src/reducers/genesetsUI.js b/client/src/reducers/genesetsUI.js
new file mode 100644
index 00000000..a49aa9b9
--- /dev/null
+++ b/client/src/reducers/genesetsUI.js
@@ -0,0 +1,88 @@
+/*
+Reducers for geneset UI-state.
+*/
+const GeneSetsUI = (
+ state = {
+ createGenesetModeActive: false,
+ isEditingGenesetName: false,
+ isAddingGenesToGeneset: false,
+ },
+ action
+) => {
+ switch (action.type) {
+ /**
+ * Activate interface for adding a new geneset
+ * No params, if the action is fired we flip
+ * a boolean here.
+ */
+ case "geneset: activate add new geneset mode": {
+ return {
+ ...state,
+ createGenesetModeActive: true,
+ };
+ }
+ /**
+ * Disable interface for adding a new geneset
+ * No params, if the action is fired we flip
+ * a boolean here.
+ */
+ case "geneset: disable create geneset mode": {
+ return {
+ ...state,
+ createGenesetModeActive: false,
+ };
+ }
+ /**
+ * Activate the interface for adding new genes to a geneset
+ * isAddingGenesToGeneset {
+ * geneset: string, name of geneset
+ * },
+ */
+ case "geneset: activate add new genes mode": {
+ return {
+ ...state,
+ isAddingGenesToGeneset: action.geneset,
+ };
+ }
+ /**
+ * Disable the interface for adding new genes to a geneset
+ * No params, if the action is fired we flip
+ * a boolean here.
+ */
+ case "geneset: disable add new genes mode": {
+ return {
+ ...state,
+ isAddingGenesToGeneset: false,
+ };
+ }
+ /**
+ * Activate the interface for renaming a geneset
+ * isEditingGenesetName: {
+ * type: "geneset: activate rename geneset mode",
+ * data: geneset, // a string, name of geneset
+ * }
+ */
+ case "geneset: activate rename geneset mode": {
+ return {
+ ...state,
+ isEditingGenesetName: action.data,
+ };
+ }
+ /**
+ * Disable the interface for renaming a geneset
+ * No params, if the action is fired we flip
+ * a boolean here.
+ */
+ case "geneset: disable rename geneset mode": {
+ return {
+ ...state,
+ isEditingGenesetName: false,
+ };
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default GeneSetsUI;
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index e1def811..46812339 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -16,6 +16,7 @@ import layoutChoice from "./layoutChoice";
import controls from "./controls";
import annotations from "./annotations";
import genesets from "./genesets";
+import genesetsUI from "./genesetsUI";
import autosave from "./autosave";
import ontology from "./ontology";
import centroidLabels from "./centroidLabels";
@@ -33,6 +34,7 @@ const Reducer = undoable(
["ontology", ontology],
["annotations", annotations],
["genesets", genesets],
+ ["genesetsUI", genesetsUI],
["layoutChoice", layoutChoice],
["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
@@ -58,6 +60,7 @@ const Reducer = undoable(
"layoutChoice",
"centroidLabels",
"genesets",
+ "genesetsUI",
"annotations",
],
undoableConfig
diff --git a/client/src/reducers/undoableConfig.js b/client/src/reducers/undoableConfig.js
index 1f298eb1..d733bdf4 100644
--- a/client/src/reducers/undoableConfig.js
+++ b/client/src/reducers/undoableConfig.js
@@ -17,6 +17,8 @@ const skipOnActions = new Set([
"window resize",
"reset colorscale",
"reset centroid labels",
+ "geneset: initial load",
+ "geneset: set tid",
"graph brush change",
"continuous metadata histogram brush",
@@ -30,6 +32,9 @@ const skipOnActions = new Set([
"writable obs annotations - save complete",
"writable obs annotations - save started",
"writable obs annotations - save error",
+ "autosave: genesets started",
+ "autosave: genesets error",
+ "autosave: genesets complete",
/* annotation component action */
"annotation: activate add new label mode",
@@ -39,6 +44,14 @@ const skipOnActions = new Set([
"annotation: activate edit label mode",
"annotation: cancel edit label mode",
"set annotations collection name",
+
+ /* geneset component action */
+ "geneset: activate add new geneset mode",
+ "geneset: disable create geneset mode",
+ "geneset: activate add new genes mode",
+ "geneset: disable add new genes mode",
+ "geneset: activate rename geneset mode",
+ "geneset: disable rename geneset mode",
]);
/*
@@ -90,6 +103,14 @@ const saveOnActions = new Set([
"annotation: label current cell selection",
"annotation: delete label",
"annotation: category edited",
+
+ /* geneset component action */
+ "geneset: create",
+ "geneset: delete",
+ "geneset: update",
+ "geneset: add genes",
+ "geneset: delete genes",
+ "geneset: set gene description",
]);
/**
diff --git a/client/src/util/clamp.js b/client/src/util/clamp.js
new file mode 100644
index 00000000..b1ae5f1d
--- /dev/null
+++ b/client/src/util/clamp.js
@@ -0,0 +1,13 @@
+/**
+ * The function will ensure a number is above or below given thresholds
+ * @param val - a number
+ * @param rng - an array of two numbers, a min and a max
+ * Ie., in the case of a histogram brush selection:
+ * const x0 = x(clamp(selectionRange[0], [min, max]));
+ * const x1 = x(clamp(selectionRange[1], [min, max]));
+ * @returns a number
+ */
+
+export default function clamp(val, rng) {
+ return Math.max(Math.min(val, rng[1]), rng[0]);
+}
diff --git a/client/src/util/maybeScientific.js b/client/src/util/maybeScientific.js
new file mode 100644
index 00000000..234f49e1
--- /dev/null
+++ b/client/src/util/maybeScientific.js
@@ -0,0 +1,27 @@
+import significantDigits from "./significantDigits";
+
+/**
+ * The function will conditionally convert a d3 axis label to scientific notation
+ * @param x - a d3 axis, ie:
+ * d3
+ * .axisBottom(x)
+ * .ticks(4)
+ * .tickFormat(d3.format(maybeScientific(x)))
+ * @returns - the number formatted as scientific, if it's big enough
+ */
+
+export default function maybeScientific(x) {
+ let format = ",";
+ const _ticks = x.ticks(4);
+
+ if (x.domain().some((n) => Math.abs(n) >= 10000)) {
+ /*
+ heuristic: if the last tick d3 wants to render has one significant
+ digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render
+ 4.20e+n
+ */
+ format = significantDigits(_ticks[_ticks.length - 1]) === 1 ? ".0e" : ".2e";
+ }
+
+ return format;
+}
diff --git a/client/src/util/parseBulkGeneString.js b/client/src/util/parseBulkGeneString.js
new file mode 100644
index 00000000..c454857f
--- /dev/null
+++ b/client/src/util/parseBulkGeneString.js
@@ -0,0 +1,10 @@
+/**
+ * The function will split string by comma or space, return unique non-empty strings
+ * @param geneString - a string of comma delimited genes
+ * @returns an array
+ */
+import _ from "lodash";
+
+export default function parseBulkGeneString(geneString) {
+ return _.pull(_.uniq(geneString.split(/[ ,]+/)), "");
+}
diff --git a/client/src/util/parseRGB.js b/client/src/util/parseRGB.js
index 91ba5c1f..d8546d87 100644
--- a/client/src/util/parseRGB.js
+++ b/client/src/util/parseRGB.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
import scaleRGB from "./scaleRGB";
// maintain a cache of already parsed RGB names, as it is reasonably expensive
diff --git a/client/src/util/scaleLinear.js b/client/src/util/scaleLinear.js
index f64ab123..239a5dc0 100644
--- a/client/src/util/scaleLinear.js
+++ b/client/src/util/scaleLinear.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
// Substitute for a d3 linear scale - less flexible, more performant.
// Returns a function which will scale a value.
//
diff --git a/client/src/util/scaleRGB.js b/client/src/util/scaleRGB.js
index d49f7414..10b42d35 100644
--- a/client/src/util/scaleRGB.js
+++ b/client/src/util/scaleRGB.js
@@ -1,4 +1,3 @@
-// jshint esversion: 6
export default (input) => {
const outputMax = 1;
const outputMin = 0;
diff --git a/client/src/util/typedCrossfilter/positiveIntervals.js b/client/src/util/typedCrossfilter/positiveIntervals.js
index fa89e70c..8fe7e54f 100644
--- a/client/src/util/typedCrossfilter/positiveIntervals.js
+++ b/client/src/util/typedCrossfilter/positiveIntervals.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
// Interval operations - very simple version of interval set relationship
// operators. An interval is a multi-interval list of [min, max),
// where min and max are mandatory. Constraints:
diff --git a/client/src/util/typedCrossfilter/util.js b/client/src/util/typedCrossfilter/util.js
index 4bd62940..8d28d65a 100644
--- a/client/src/util/typedCrossfilter/util.js
+++ b/client/src/util/typedCrossfilter/util.js
@@ -1,5 +1,3 @@
-// jshint esversion: 6
-
import { sortIndex } from "./sort";
import { rangeFill as fillRange } from "../range";
diff --git a/package.json b/package.json
deleted file mode 100644
index e3722f75..00000000
--- a/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "cellxgene",
- "scripts": {
- "postinstall": "npm install --prefix client && npm run build --prefix client && make copy-client-assets"
- }
-}