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 b0eed45952.

* 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 <sbadajoz@chanzuckerberg.com>

* 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 <sbadajoz@chanzuckerberg.com>

* 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 <bruce@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
This commit is contained in:
Colin Megill
2021-03-24 16:33:26 -04:00
committed by GitHub
co-authored by Severiano Badajoz bkmartinjr
parent 5335c39184
commit e6e358ddc8
66 changed files with 1926 additions and 905 deletions
@@ -2,4 +2,4 @@
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"max-width: 265px;\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover2-target\\" tabindex=\\"0\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"-1\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
@@ -2,22 +2,22 @@
exports[`annotations stacked bar graph renders 1`] = `
Array [
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2133</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover2-target\\" tabindex=\\"0\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 15px; height: 15px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover2-target\\" tabindex=\\"0\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2133</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 15px; height: 15px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
]
`;
exports[`annotations stacked bar graph renders 2`] = `
Array [
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2638</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover2-target\\" tabindex=\\"0\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 15px; height: 15px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover2-target\\" tabindex=\\"0\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2638</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 15px; height: 15px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
]
`;
exports[`annotations truncate midpoint whitespace 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate midpoint whitespace 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate midpoint whitespace 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate midpoint whitespace 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate single character 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
exports[`annotations truncate single character 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
exports[`annotations truncate single character 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
exports[`annotations truncate single character 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"-1\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
-170
View File
@@ -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);
@@ -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);
});
});
@@ -1,5 +1,3 @@
// jshint esversion: 6
import BitArray from "../../../src/util/typedCrossfilter/bitArray";
const defaultTestLength = 8;
@@ -1,5 +1,3 @@
// jshint esversion: 6
// const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals";
+168 -87
View File
@@ -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"
}
+3
View File
@@ -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",
+10 -6
View File
@@ -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 */
@@ -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}
</p>
{secondaryInstructions && (
<p style={{ marginTop: secondaryInstructions ? 20 : 0 }}>
{secondaryInstructions}
</p>
)}
{secondaryInput || null}
</div>
{annoSelect || null}
</div>
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import Helmet from "react-helmet";
import { connect } from "react-redux";
@@ -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 &&
+1 -1
View File
@@ -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() {
@@ -0,0 +1,17 @@
import React from "react";
import * as globals from "../../globals";
const ErrorLoading = ({ displayName, zebra }) => {
return (
<div
style={{
backgroundColor: zebra ? globals.lightestGrey : "white",
fontStyle: "italic",
}}
>
<span>{`Failure loading ${displayName}`}</span>
</div>
);
};
export default ErrorLoading;
@@ -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 (
<div>
<div
style={{
display: "flex",
justifyContent: hideRanges ? "center" : "space-between",
}}
>
<span
style={{
color: rangeColorMin,
display: hideRanges ? "none" : "block",
}}
>
min {rangeMin.toPrecision(4)}
</span>
<span
data-testclass="brushable-histogram-field-name"
style={{ fontStyle: "italic" }}
>
{isObs ? displayName : null}
</span>
<div style={{ display: hideRanges ? "block" : "none" }}>
: {rangeMin}
</div>
<span
style={{
color: rangeColorMax,
display: hideRanges ? "none" : "block",
}}
>
max {rangeMax.toPrecision(4)}
</span>
</div>
{logFoldChange && pvalAdj ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "baseline",
}}
>
<span>
<strong>log fold change:</strong>
{` ${logFoldChange.toPrecision(4)}`}
</span>
<span
style={{
marginLeft: 7,
padding: 2,
}}
>
<strong>p-value (adj):</strong>
{pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
</span>
</div>
) : null}
</div>
);
}
);
export default HistogramFooter;
@@ -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 (
<div
style={{
display: "flex",
justifyContent: "flex-end",
paddingBottom: "8px",
}}
>
{onScatterPlotXClick && onScatterPlotYClick ? (
<span>
<Icon icon={IconNames.SCATTER_PLOT} style={{ marginRight: 7 }} />
<ButtonGroup style={{ marginRight: 7 }}>
<Button
data-testid={`plot-x-${fieldId}`}
onClick={onScatterPlotXClick}
active={isScatterPlotX}
intent={isScatterPlotX ? "primary" : "none"}
>
plot x
</Button>
<Button
data-testid={`plot-y-${fieldId}`}
onClick={onScatterPlotYClick}
active={isScatterPlotY}
intent={isScatterPlotY ? "primary" : "none"}
>
plot y
</Button>
</ButtonGroup>
</span>
) : null}
{onRemoveClick ? (
<Button
minimal
onClick={onRemoveClick}
style={{
color: globals.blue,
cursor: "pointer",
marginLeft: 7,
}}
>
remove
</Button>
) : null}
<Tooltip
content="Use as color scale"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
onClick={memoizedColorByCallback}
active={isColorBy}
intent={isColorBy ? "primary" : "none"}
data-testclass="colorby"
data-testid={`colorby-${fieldId}`}
icon="tint"
/>
</Tooltip>
</div>
);
}
);
export default HistogramHeader;
@@ -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 (
<svg
style={{ display }}
width={width}
height={height}
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef}
/>
);
};
export default Histogram;
+93 -477
View File
@@ -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 (
<div
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
justifyItems: "center",
alignItems: "center",
}}
>
<div style={{ minWidth: 30 }} />
<div style={{ display: "flex", alignSelf: "center" }}>
<span style={{ fontStyle: "italic" }}>{displayName}</span>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button minimal loading intent="primary" />
</div>
</div>
</div>
);
};
const ErrorLoading = ({ displayName, error, zebra }) => {
console.log(error); // log to console as this is unexpected
return (
<div
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<span>{`Failure loading ${displayName}`}</span>
</div>
);
};
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 (
<div>
<div
style={{
display: "flex",
justifyContent: hideRanges ? "center" : "space-between",
}}
>
<span
style={{
color: rangeColorMin,
display: hideRanges ? "none" : "block",
}}
>
min {rangeMin.toPrecision(4)}
</span>
<span
data-testclass="brushable-histogram-field-name"
style={{ fontStyle: "italic" }}
>
{displayName}
</span>
<div style={{ display: hideRanges ? "block" : "none" }}>
: {rangeMin}
</div>
<span
style={{
color: rangeColorMax,
display: hideRanges ? "none" : "block",
}}
>
max {rangeMax.toPrecision(4)}
</span>
</div>
{logFoldChange !== undefined && pvalAdj !== undefined ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "baseline",
}}
>
<span>
<strong>log fold change:</strong>
{` ${logFoldChange.toPrecision(4)}`}
</span>
<span
style={{
marginLeft: 7,
padding: 2,
}}
>
<strong>p-value (adj):</strong>
{pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
</span>
</div>
) : null}
</div>
);
}
);
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 (
<div
style={{
display: "flex",
justifyContent: "flex-end",
paddingBottom: "8px",
}}
>
{onScatterPlotXClick && onScatterPlotYClick ? (
<span>
<Icon icon={IconNames.SCATTER_PLOT} style={{ marginRight: 7 }} />
<ButtonGroup style={{ marginRight: 7 }}>
<Button
data-testid={`plot-x-${fieldId}`}
onClick={onScatterPlotXClick}
active={isScatterPlotX}
intent={isScatterPlotX ? "primary" : "none"}
>
plot x
</Button>
<Button
data-testid={`plot-y-${fieldId}`}
onClick={onScatterPlotYClick}
active={isScatterPlotY}
intent={isScatterPlotY ? "primary" : "none"}
>
plot y
</Button>
</ButtonGroup>
</span>
) : null}
{onRemoveClick ? (
<Button
minimal
onClick={onRemoveClick}
style={{
color: globals.blue,
cursor: "pointer",
marginLeft: 7,
}}
>
remove
</Button>
) : null}
<Tooltip
content="Use as color scale"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
onClick={memoizedColorByCallback}
active={isColorBy}
intent={isColorBy ? "primary" : "none"}
data-testclass="colorby"
data-testid={`colorby-${fieldId}`}
icon="tint"
/>
</Tooltip>
</div>
);
}
);
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 (
<svg
style={{ display }}
width={width}
height={height}
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef}
/>
);
};
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",
}}
>
<HistogramHeader
fieldId={field}
isColorBy={isColorAccessor}
isObs={isObs}
onColorByClick={this.handleColorAction(dispatch)}
onRemoveClick={isUserDefined ? this.removeHistogram : null}
isScatterPlotX={isScatterplotXXaccessor}
isScatterPlotY={isScatterplotYYaccessor}
onScatterPlotXClick={
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
}
onScatterPlotYClick={
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
}
/>
{!mini && isObs ? (
<HistogramHeader
fieldId={field}
isColorBy={isColorAccessor}
isObs={isObs}
onColorByClick={this.handleColorAction(dispatch)}
onRemoveClick={isUserDefined ? this.removeHistogram : null}
isScatterPlotX={isScatterplotXXaccessor}
isScatterPlotY={isScatterplotYYaccessor}
onScatterPlotXClick={
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
}
onScatterPlotYClick={
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
}
/>
) : null}
<Histogram
field={field}
fieldForId={fieldForId}
display={asyncProps.isSingleValue ? "none" : "block"}
histogram={asyncProps.histogram}
width={this.width}
height={this.height}
histogram={
mini ? asyncProps.miniHistogram : asyncProps.histogram
}
width={mini ? widthMini : width}
height={mini ? heightMini : height}
onBrush={this.onBrush}
onBrushEnd={this.onBrushEnd}
margin={this.margin}
margin={mini ? marginMini : margin}
isColorBy={isColorAccessor}
selectionRange={continuousSelectionRange}
mini={mini}
/>
<HistogramFooter
displayName={field}
hideRanges={asyncProps.isSingleValue}
rangeMin={asyncProps.unclippedRange[0]}
rangeMax={asyncProps.unclippedRange[1]}
rangeColorMin={asyncProps.unclippedRangeColor[0]}
rangeColorMax={asyncProps.unclippedRangeColor[1]}
logFoldChange={logFoldChange}
pvalAdj={pvalAdj}
/>
{!mini ? (
<HistogramFooter
isObs={isObs}
displayName={field}
hideRanges={asyncProps.isSingleValue}
rangeMin={asyncProps.unclippedRange[0]}
rangeMax={asyncProps.unclippedRange[1]}
rangeColorMin={asyncProps.unclippedRangeColor[0]}
rangeColorMax={asyncProps.unclippedRangeColor[1]}
logFoldChange={logFoldChange}
pvalAdj={pvalAdj}
/>
) : null}
</div>
) : null
}
@@ -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 (
<div
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
justifyItems: "center",
alignItems: "center",
}}
>
<div style={{ minWidth: 30 }} />
<div style={{ display: "flex", alignSelf: "center" }}>
<span style={{ fontStyle: "italic" }}>{displayName}</span>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button minimal loading intent="primary" />
</div>
</div>
</div>
);
};
export default StillLoading;
@@ -1,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";
@@ -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";
@@ -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}
/>
<MenuItem
icon="delete"
intent="danger"
icon={IconNames.TRASH}
intent={Intent.DANGER}
data-testclass="handleDeleteCategory"
data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory}
+31 -30
View File
@@ -1,13 +1,12 @@
// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip, Position } from "@blueprintjs/core";
import { connect } from "react-redux";
import * as globals from "../../globals";
import Category from "./category";
import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager";
import AnnoDialog from "./annoDialog";
import AnnoDialog from "../annoDialog";
import AnnoSelect from "./annoSelect";
import LabelInput from "./labelInput";
import LabelInput from "../labelInput";
import { labelPrompt } from "./labelUtil";
import actions from "../../actions";
@@ -182,6 +181,35 @@ class Categories extends React.Component {
}
/>
{writableCategoriesEnabled ? (
<div style={{ marginBottom: 10 }}>
<Tooltip
content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userInfo.is_authenticated}
>
Create new <strong>category</strong>
</AnchorButton>
</Tooltip>
</div>
) : 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 ? (
<Tooltip
content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userInfo.is_authenticated}
>
Create new category
</AnchorButton>
</Tooltip>
) : null}
</div>
);
}
@@ -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 ? (
<MenuItem
icon="delete"
icon="trash"
intent="danger"
data-testclass="handleDeleteValue"
data-testid={`${metadataField}:${displayString}:delete-label`}
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
import * as d3 from "d3";
@@ -3,7 +3,6 @@
Setup SVG & Canvas elements
******************************************
******************************************/
// jshint esversion: 6
import * as d3 from "d3";
const setupParallelCoordinates = (width, height, margin) => {
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import _ from "lodash";
import * as d3 from "d3";
@@ -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;
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
function Container(props) {
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import * as globals from "../../globals";
@@ -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 (
<div>
<div
style={{
marginLeft: 5,
marginRight: 0,
marginTop: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div
role="menuitem"
tabIndex="0"
data-testclass="gene-expand"
data-testid={`${gene}:gene-expand`}
onKeyPress={() => {}}
style={{
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<div>
<Icon
icon="drag-handle-horizontal"
iconSize={12}
style={{
marginRight: 7,
cursor: "grab",
position: "relative",
top: -1,
}}
/>
<Truncate>
<span
style={{
width: globals.leftSidebarWidth - genesetNameLengthVisible,
}}
data-testid={`${gene}:gene-label`}
>
{gene}
</span>
</Truncate>
</div>
{!geneIsExpanded ? (
<HistogramBrush isUserDefined field={gene} mini />
) : null}
</div>
<div style={{ flexShrink: 0, marginLeft: 2 }}>
{!isDiffexp ? (
<AnchorButton
minimal
small
data-testid={`delete-from-geneset-${gene}`}
onClick={this.handleDeleteGeneFromSet}
intent="none"
style={{ fontWeight: 700, marginRight: 2 }}
icon={<Icon icon="trash" iconSize={10} />}
/>
) : null}
<AnchorButton
minimal
small
data-testid={`plot-x-${gene}`}
onClick={this.handleSetGeneAsScatterplotX}
active={isScatterplotXXaccessor}
intent={isScatterplotXXaccessor ? "primary" : "none"}
style={{ fontWeight: 700, marginRight: 2 }}
>
x
</AnchorButton>
<AnchorButton
minimal
small
data-testid={`plot-y-${gene}`}
onClick={this.handleSetGeneAsScatterplotY}
active={isScatterplotYYaccessor}
intent={isScatterplotYYaccessor ? "primary" : "none"}
style={{ fontWeight: 700, marginRight: 2 }}
>
y
</AnchorButton>
<AnchorButton
minimal
small
data-testclass="maximize"
data-testid={`maximize-${gene}`}
onClick={this.handleGeneExpandClick}
active={geneIsExpanded}
intent="none"
icon={<Icon icon="maximize" iconSize={10} />}
style={{ marginRight: 2 }}
/>
<AnchorButton
minimal
small
data-testclass="colorby"
data-testid={`colorby-${gene}`}
onClick={this.onColorChangeClick}
active={isColorAccessor}
intent={isColorAccessor ? "primary" : "none"}
icon={<Icon icon="tint" iconSize={12} />}
/>
</div>
</div>
{geneIsExpanded ? <HistogramBrush isUserDefined field={gene} /> : null}
</div>
);
}
}
export default Gene;
@@ -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 (
<div style={{ marginBottom: 3 }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
}}
>
<span
role="menuitem"
tabIndex="0"
data-testclass="geneset-expand"
data-testid={`${setName}:geneset-expand`}
onKeyPress={
/* TODO(colinmegill): #2101: click handler on span */ () => {}
}
style={{
cursor: "pointer",
userSelect: "none",
}}
onClick={this.onGenesetMenuClick}
>
<Truncate>
<span
style={{
maxWidth: globals.leftSidebarWidth - genesetNameLengthVisible,
}}
data-testid={`${setName}:geneset-label`}
>
{setName}
</span>
</Truncate>
{isOpen ? (
<FaChevronDown
data-testclass="geneset-expand-is-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
) : (
<FaChevronRight
data-testclass="geneset-expand-is-not-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
)}
</span>
<div>
<GenesetMenus genesetsEditable geneset={setName} />
</div>
</div>
<div style={{ marginLeft: 15, marginTop: 5, marginRight: 0 }}>
{isOpen ? (
<Tooltip
content="Aggregate all genes in this geneset, and allow coloring by and selecting on the entire set."
position={Position.BOTTOM_RIGHT}
usePortal
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<Switch
style={{ fontStyle: "italic" }}
checked={toggleSummaryHisto}
label="Show mean expression across entire set"
alignIndicator="left"
innerLabel="off"
innerLabelChecked="on"
onChange={this.toggleSummaryHisto}
/>
</Tooltip>
) : null}
</div>
{isOpen && !toggleSummaryHisto
? _.map(setGenes, (gene) => {
return (
<Gene
key={gene}
gene={gene}
geneset={setName}
isDiffexp={isDiffexp}
/>
);
})
: null}
</div>
);
}
}
export default GeneSet;
+62 -43
View File
@@ -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(
<GeneSet
key={name}
setGenes={Array.from(genes.genes.keys())}
setName={name}
/>
);
}
return sets;
};
renderDiffexpGeneSets = () => {
const { differential } = this.props;
const setGenes = [];
if (differential.diffExp) {
differential.diffExp.forEach((diffexpGene) => {
setGenes.push(diffexpGene[0]);
});
}
return differential.diffExp ? (
<GeneSet
key="Temp DiffExp Set"
setGenes={setGenes}
isDiffexp
setName="Temp DiffExp Set"
/>
) : null;
};
handleActivateCreateGenesetMode = () => {
const { dispatch } = this.props;
dispatch({ type: "geneset: activate add new geneset mode" });
};
render() {
const { userDefinedGenes, differential } = this.props;
return (
<div
style={{
borderBottom: `1px solid ${globals.lighterGrey}`,
}}
>
<div>
<div>
<AddGenes />
{userDefinedGenes.length > 0
? _.map(userDefinedGenes, (geneName, index) => {
return (
<HistogramBrush
key={geneName}
field={geneName}
zebra={index % 2 === 0}
isUserDefined
/>
);
})
: null}
</div>
<div>
{differential.diffExp
? _.map(differential.diffExp, (value, index) => {
return (
<HistogramBrush
key={value[0]}
field={value[0]}
zebra={index % 2 === 0}
isDiffExp
logFoldChange={value[1]}
pval={value[2]}
pvalAdj={value[3]}
/>
);
})
: null}
<div style={{ marginBottom: 10, position: "relative", top: -2 }}>
<Button
data-testid="open-create-geneset-dialog"
onClick={this.handleActivateCreateGenesetMode}
intent="primary"
>
Create new <strong>gene set</strong>
</Button>
</div>
<CreateGenesetDialogue />
<EditGenesetNameDialogue />
</div>
<div>{this.renderDiffexpGeneSets()}</div>
<div>{this.renderGeneSets()}</div>
</div>
);
}
@@ -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 (
<>
<AnnoDialog
isActive={genesetsUI.isAddingGenesToGeneset === geneset}
inputProps={{ "data-testid": `${geneset}:create-label-dialog` }}
primaryButtonProps={{
"data-testid": `${geneset}:submit-label`,
}}
title="Add genes to gene set"
instruction={`Add gene to ${geneset}`}
cancelTooltipContent="Close this dialog without adding genes to gene set."
primaryButtonText="Add genes"
text={genesToAdd}
validationError={false}
annoInput={
<LabelInput
onChange={this.handleChange}
inputProps={{
"data-testid": "add-genes",
leftIcon: "manually-entered-data",
intent: "none",
autoFocus: true,
}}
newLabelMessage="New category"
/>
}
handleSubmit={this.handleAddGeneToGeneSet}
handleCancel={this.disableAddGeneMode}
/>
</>
);
}
}
export default AddGeneToGenesetDialogue;
@@ -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.");
}
@@ -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 (
<>
<AnnoDialog
isActive={genesetsUI.createGenesetModeActive}
inputProps={{
"data-testid": `${metadataField}:create-geneset-dialog`,
}}
primaryButtonProps={{
"data-testid": `${metadataField}:submit-geneset`,
}}
title="Create gene set"
instruction={this.instruction(genesetName, genesets)}
cancelTooltipContent="Close this dialog without creating a new gene set."
primaryButtonText="Create gene set"
text={genesetName}
validationError={this.validate(genesetName, genesets)}
annoInput={
<LabelInput
onChange={this.handleChange}
inputProps={{
"data-testid": "create-geneset-modal",
leftIcon: "manually-entered-data",
intent: "none",
autoFocus: true,
}}
newLabelMessage="Create gene set"
/>
}
secondaryInstructions="Optionally add a list of comma separated genes to populate the gene set"
secondaryInput={
<LabelInput
onChange={this.handleGenesetInputChange}
inputProps={{
"data-testid": "add-genes",
intent: "none",
autoFocus: false,
}}
newLabelMessage="populate geneset with genes"
/>
}
handleSubmit={this.createGeneset}
handleCancel={this.disableCreateGenesetMode}
/>
</>
);
}
}
export default CreateGenesetDialogue;
@@ -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 (
<>
<AnnoDialog
isActive={genesetsUI.isEditingGenesetName}
inputProps={{
"data-testid": `${genesetsUI.isEditingGenesetName}:rename-geneset-dialog`,
}}
primaryButtonProps={{
"data-testid": `${genesetsUI.isEditingGenesetName}:submit-geneset`,
}}
title="Rename gene set"
instruction={`Rename ${genesetsUI.isEditingGenesetName}`}
cancelTooltipContent="Close this dialog without renaming the gene set."
primaryButtonText="Rename gene set"
text={genesetName}
validationError={genesetsUI.isEditingGenesetName === genesetName}
annoInput={
<LabelInput
onChange={this.handleChange}
inputProps={{
"data-testid": "rename-geneset-modal",
leftIcon: "manually-entered-data",
intent: "none",
autoFocus: true,
}}
newLabelMessage="Rename gene set"
/>
}
handleSubmit={this.renameGeneset}
handleCancel={this.disableEditGenesetNameMode}
/>
</>
);
}
}
export default RenameGeneset;
@@ -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 ? (
<>
<Tooltip
content={createText}
position={Position.BOTTOM}
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
style={{ marginLeft: 0, marginRight: 2 }}
data-testclass="handleAddNewLabelToCategory"
data-testid={`${geneset}:add-new-label-to-category`}
icon={<Icon icon="plus" iconSize={10} />}
onClick={this.activateAddGeneToGenesetMode}
small
minimal
/>
</Tooltip>
<AddGeneToGenesetDialogue geneset={geneset} />
<Popover
interactionKind={PopoverInteractionKind.HOVER}
boundary="window"
position={Position.BOTTOM}
content={
<Menu>
<MenuItem
icon="edit"
data-testclass="activateEditGenesetNameMode"
data-testid={`${geneset}:edit-genesetName-mode`}
onClick={this.activateEditGenesetNameMode}
text="Edit gene set name"
/>
<MenuItem
icon="trash"
intent="danger"
data-testclass="handleDeleteCategory"
data-testid={`${geneset}:delete-category`}
onClick={this.handleDeleteCategory}
text="Delete this gene set (destructive, will remove set and collection of genes)"
/>
</Menu>
}
>
<Button
style={{ marginLeft: 0, marginRight: 5 }}
data-testclass="seeActions"
data-testid={`${geneset}:see-actions`}
icon={<Icon icon="more" iconSize={10} />}
small
minimal
/>
</Popover>
<Button
disabled
style={{ marginLeft: 0 }}
data-testclass="colorby-entire-geneset"
data-testid={`${geneset}:colorby-entire-geneset`}
icon={<Icon icon="tint" iconSize={16} />}
/>
</>
) : null}
</>
);
}
}
export default GenesetMenus;
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import * as d3 from "d3";
import { connect, shallowEqual } from "react-redux";
@@ -1,5 +1,3 @@
// https://bl.ocks.org/pbeshai/8008075f9ce771ee8be39e8c38907570
import * as d3 from "d3";
import { Colors } from "@blueprintjs/core";
@@ -1,4 +1,3 @@
// jshint esversion: 6
import * as d3 from "d3";
import Lasso from "./setupLasso";
+2 -1
View File
@@ -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 {
}}
>
<Categorical />
<Continuous />
</div>
{scatterplotXXaccessor && scatterplotYYaccessor ? (
<DynamicScatterplot />
@@ -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";
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux";
+1 -3
View File
@@ -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,
}}
>
<GeneExpression />
<Continuous />
</div>
);
}
@@ -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;
+3 -9
View File
@@ -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 (
<Tooltip
<Tooltip2
content={originalString}
hoverOpenDelay={tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
targetProps={{ style: children.props.style }}
>
{newChildren}
</Tooltip>
</Tooltip2>
);
};
+1
View File
@@ -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";
+1 -1
View File
@@ -53,7 +53,7 @@ const Autosave = (
return {
...state,
genesetSaveInProgress: false,
lastSavedGenesets: nextSharedState.genesets,
lastSavedGenesets: nextSharedState.genesets.genesets,
};
}
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
const Config = (
state = {
displayNames: null,
-2
View File
@@ -1,5 +1,3 @@
// jshint esversion: 6
import _ from "lodash";
import { subsetAndResetGeneLists } from "../util/stateManager/controlsHelpers";
-2
View File
@@ -1,5 +1,3 @@
// jshint esversion: 6
const Differential = (
state = {
diffExp: null,
+1
View File
@@ -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.");
+88
View File
@@ -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;
+3
View File
@@ -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
+21
View File
@@ -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",
]);
/**
+13
View File
@@ -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]);
}
+27
View File
@@ -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;
}
+10
View File
@@ -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(/[ ,]+/)), "");
}
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import scaleRGB from "./scaleRGB";
// maintain a cache of already parsed RGB names, as it is reasonably expensive
-2
View File
@@ -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.
//
-1
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
export default (input) => {
const outputMax = 1;
const outputMin = 0;
@@ -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:
-2
View File
@@ -1,5 +1,3 @@
// jshint esversion: 6
import { sortIndex } from "./sort";
import { rangeFill as fillRange } from "../range";
-6
View File
@@ -1,6 +0,0 @@
{
"name": "cellxgene",
"scripts": {
"postinstall": "npm install --prefix client && npm run build --prefix client && make copy-client-assets"
}
}