Skip to content

Commit 2038cb3

Browse files
jkancheLTLA
andauthored
VS Mode (#141)
* add ui elements filter select or flip cluster selection in vs-mode * Added worker code for the versus mode. (#143) * cleaning up css * bump app version Co-authored-by: LTLA <[email protected]>
1 parent ccd70cd commit 2038cb3

File tree

5 files changed

+201
-37
lines changed

5 files changed

+201
-37
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "kana",
33
"description": "Single cell data analysis in the browser",
4-
"version": "2.2.4",
4+
"version": "2.3.0",
55
"private": true,
66
"author": {
77
"name": "Jayaram Kancherla",

src/App.js

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ const App = () => {
105105
const [selectedClusterIndex, setSelectedClusterIndex] = useState([]);
106106
// set Cluster rank-type
107107
const [clusterRank, setClusterRank] = useState("cohen-min-rank");
108+
// which cluster is selected for vsmode
109+
const [selectedVSCluster, setSelectedVSCluster] = useState(null);
108110

109111
// Cluster Analysis
110112
// cluster assignments
@@ -185,23 +187,40 @@ const App = () => {
185187

186188
// request worker for new markers
187189
// if either the cluster or the ranking changes
190+
// VS mode
188191
useEffect(() => {
189-
if (selectedCluster !== null && selectedModality != null) {
190-
191-
let type = String(selectedCluster).startsWith("cs") ?
192-
"getMarkersForSelection" : "getMarkersForCluster";
193-
scranWorker.postMessage({
194-
"type": type,
195-
"payload": {
196-
"modality": selectedModality,
197-
"cluster": selectedCluster,
198-
"rank_type": clusterRank,
199-
}
200-
});
192+
if (selectedModality !== null && clusterRank !== null) {
193+
if (selectedVSCluster !== null && selectedCluster !== null) {
194+
let type = String(selectedCluster).startsWith("cs") ?
195+
"computeVersusSelections" : "computeVersusClusters";
196+
scranWorker.postMessage({
197+
"type": type,
198+
"payload": {
199+
"modality": selectedModality,
200+
"left": selectedCluster,
201+
"right": selectedVSCluster,
202+
"rank_type": clusterRank,
203+
}
204+
});
205+
206+
add_to_logs("info", `--- ${type} sent ---`);
207+
} else if (selectedCluster !== null) {
201208

202-
add_to_logs("info", `--- ${type} sent ---`);
209+
let type = String(selectedCluster).startsWith("cs") ?
210+
"getMarkersForSelection" : "getMarkersForCluster";
211+
scranWorker.postMessage({
212+
"type": type,
213+
"payload": {
214+
"modality": selectedModality,
215+
"cluster": selectedCluster,
216+
"rank_type": clusterRank,
217+
}
218+
});
219+
220+
add_to_logs("info", `--- ${type} sent ---`);
221+
}
203222
}
204-
}, [selectedCluster, clusterRank, selectedModality]);
223+
}, [selectedCluster, selectedVSCluster, clusterRank, selectedModality]);
205224

206225
// compute markers in the worker
207226
// when a new custom selection of cells is made through the UI
@@ -618,7 +637,9 @@ const App = () => {
618637
setTriggerAnimation(false);
619638
setShowDimPlotLoader(false);
620639
} else if (payload.type === "setMarkersForCluster"
621-
|| payload.type === "setMarkersForCustomSelection") {
640+
|| payload.type === "setMarkersForCustomSelection"
641+
|| payload.type === "computeVersusSelections"
642+
|| payload.type === "computeVersusClusters" ) {
622643
const { resp } = payload;
623644
let records = [];
624645
let index = Array(resp.ordering.length);
@@ -849,6 +870,8 @@ const App = () => {
849870
selectedClusterIndex={selectedClusterIndex}
850871
selectedCluster={selectedCluster}
851872
setSelectedCluster={setSelectedCluster}
873+
selectedVSCluster={selectedVSCluster}
874+
setSelectedVSCluster={setSelectedVSCluster}
852875
setClusterRank={setClusterRank}
853876
clusterData={clusterData}
854877
customSelection={customSelection}

src/components/Markers/index.js

Lines changed: 110 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useContext, useState, useMemo } from 'react';
22
import {
3-
Button, H4, H5, Icon, Collapse, InputGroup, Text,
3+
Button, H4, H5, Icon, Collapse, InputGroup, Text, Switch,
44
RangeSlider, Tag, HTMLSelect, Classes, Card, Elevation, Label
55
} from "@blueprintjs/core";
66
import { Popover2, Tooltip2 } from "@blueprintjs/popover2";
@@ -40,6 +40,9 @@ const MarkerPlot = (props) => {
4040
// records to show after filtering
4141
const [prosRecords, setProsRecords] = useState(null);
4242

43+
// toggle for vs mode
44+
const [vsmode, setVsmode] = useState(false);
45+
4346
// scale to use for detected on expression bar
4447
const detectedScale = d3.interpolateRdYlBu; //d3.interpolateRdBu;
4548
// d3.scaleSequential()
@@ -207,7 +210,7 @@ const MarkerPlot = (props) => {
207210
</Popover2>
208211
{
209212
props?.modality ?
210-
<Label style={{textAlign: "left"}}>
213+
<Label style={{textAlign: "left", marginBottom:"5px"}}>
211214
Select Modality
212215
<HTMLSelect
213216
onChange={(x) => {
@@ -223,30 +226,116 @@ const MarkerPlot = (props) => {
223226
</Label>
224227
: ""
225228
}
229+
{
230+
<div className='marker-cluster-header'>
231+
<Label style={{marginBottom:"0"}}>Select Cluster</Label>
232+
<div className='marker-vsmode'>
233+
<Popover2
234+
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
235+
hasBackdrop={false}
236+
interactionKind="hover"
237+
placement='left'
238+
hoverOpenDelay={500}
239+
modifiers={{
240+
arrow: { enabled: true },
241+
flip: { enabled: true },
242+
preventOverflow: { enabled: true },
243+
}}
244+
content={
245+
<Card style={{
246+
width: '450px'
247+
}} elevation={Elevation.ZERO}
248+
>
249+
<p>
250+
By default, the <strong>general</strong> mode will rank markers for a cluster or custom selection based on the comparison to all other clusters or cells.
251+
<br /><br />Users can instead enable <strong>versus</strong> mode to compare markers between two clusters or between two custom selections.
252+
This is useful for identifying subtle differences between closely related groups of cells.
253+
</p>
254+
</Card>
255+
}
256+
>
257+
<Icon intent="warning" icon="comparison" style={{ paddingRight: '5px' }}></Icon>
258+
</Popover2>
259+
<Switch large={false} checked={vsmode}
260+
innerLabelChecked="versus" innerLabel="general"
261+
onChange={(e) => {
262+
if (e.target.checked === false) {
263+
props?.setSelectedVSCluster(null);
264+
}
265+
setVsmode(e.target.checked)
266+
}} />
267+
</div>
268+
</div>
269+
}
226270
{
227271
clusSel ?
228-
<Label style={{textAlign: "left"}}>
229-
Select Cluster
230-
<HTMLSelect
231-
onChange={(x) => {
232-
let tmpselection = x.currentTarget?.value;
233-
if (tmpselection.startsWith("Cluster")) {
234-
tmpselection = parseInt(tmpselection.replace("Cluster ", "")) - 1
235-
} else if (tmpselection.startsWith("Custom")) {
236-
tmpselection = tmpselection.replace("Custom Selection ", "")
237-
}
238-
props?.setSelectedCluster(tmpselection);
272+
<div className='marker-cluster-selection'>
273+
<HTMLSelect
274+
className='marker-cluster-selection-width'
275+
onChange={(x) => {
276+
let tmpselection = x.currentTarget?.value;
277+
if (tmpselection.startsWith("Cluster")) {
278+
tmpselection = parseInt(tmpselection.replace("Cluster ", "")) - 1
279+
} else if (tmpselection.startsWith("Custom")) {
280+
tmpselection = tmpselection.replace("Custom Selection ", "")
281+
}
282+
props?.setSelectedCluster(tmpselection);
283+
284+
setMarkerFilter({});
285+
props?.setGene(null);
286+
props?.setSelectedVSCluster(null);
287+
}}>
288+
{
289+
clusSel.map((x, i) => (
290+
<option
291+
selected={String(props?.selectedCluster).startsWith("cs") ? x == props?.selectedCluster : parseInt(x) - 1 == parseInt(props?.selectedCluster)}
292+
key={i}>{String(x).startsWith("cs") ? "Custom Selection" : "Cluster"} {x}</option>
293+
))
294+
}
295+
</HTMLSelect>
296+
{
297+
vsmode &&
298+
<>
299+
<Button style={{margin: "0 3px"}} onClick={() => {
300+
let mid = props?.selectedVSCluster;
301+
props?.setSelectedVSCluster(props?.selectedCluster)
302+
props?.setSelectedCluster(mid);
239303

240304
setMarkerFilter({});
241305
props?.setGene(null);
242-
}}>
243-
{
244-
clusSel.map((x, i) => (
245-
<option key={i}>{String(x).startsWith("cs") ? "Custom Selection" : "Cluster"} {x}</option>
246-
))
247-
}
248-
</HTMLSelect>
249-
</Label>
306+
}} icon="exchange" disabled={props?.selectedVSCluster == null} outlined={true} intent="primary"></Button>
307+
<HTMLSelect
308+
className='marker-cluster-selection-width'
309+
onChange={(x) => {
310+
let tmpselection = x.currentTarget?.value;
311+
if (tmpselection.startsWith("Cluster")) {
312+
tmpselection = parseInt(tmpselection.replace("Cluster ", "")) - 1
313+
} else if (tmpselection.startsWith("Custom")) {
314+
tmpselection = tmpselection.replace("Custom Selection ", "")
315+
}
316+
props?.setSelectedVSCluster(tmpselection);
317+
318+
setMarkerFilter({});
319+
props?.setGene(null);
320+
}}>
321+
{
322+
props?.selectedVSCluster == null && <option selected={true}>Choose a Cluster</option>
323+
}
324+
{
325+
clusSel.filter((x,i) => String(props?.selectedCluster).startsWith("cs") ?
326+
String(x).startsWith("cs") && String(x) !== String(props?.selectedCluster) :
327+
!String(x).startsWith("cs") && parseInt(x) - 1 !== parseInt(props?.selectedCluster))
328+
// .filter((x,i) => String(props?.selectedCluster) == String(x) )
329+
.map((x, i) => (
330+
<option
331+
selected={String(props?.selectedVSCluster).startsWith("cs") ? x == props?.selectedVSCluster : parseInt(x) - 1 == parseInt(props?.selectedVSCluster)}
332+
key={i}>{String(x).startsWith("cs") ? "Custom Selection" : "Cluster"} {x}</option>
333+
))
334+
}
335+
</HTMLSelect>
336+
</>
337+
}
338+
</div>
250339
: ""
251340
}
252341
{

src/components/Markers/markers.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,26 @@
125125
font-weight: bold;
126126
overflow-wrap: break-word;
127127
}
128+
129+
.marker-cluster-header {
130+
display: flex;
131+
flex-direction: row;
132+
justify-content: space-between;
133+
height: 22px;
134+
}
135+
136+
.marker-vsmode {
137+
display: flex;
138+
flex-direction: row;
139+
}
140+
141+
.marker-cluster-selection {
142+
display: flex;
143+
flex-direction: row;
144+
justify-content: stretch;
145+
}
146+
147+
.marker-cluster-selection-width {
148+
width: 100%;
149+
margin: 0 2px;
150+
}

src/workers/scran.worker.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,35 @@ onmessage = function (msg) {
316316
postError(type, err, fatal)
317317
});
318318

319+
/**************** VERSUS MODE *******************/
320+
} else if (type == "computeVersusClusters") {
321+
loaded.then(x => {
322+
let rank_type = payload.rank_type.replace(/-.*/, ""); // summary type doesn't matter for pairwise comparisons.
323+
let res = superstate.marker_detection.computeVersus(payload.left, payload.right, rank_type, payload.modality);
324+
postMessage({
325+
type: "computeVersusClusters",
326+
resp: res,
327+
msg: "Success: COMPUTE_VERSUS_CLUSTERS done"
328+
});
329+
}).catch(err => {
330+
console.error(err);
331+
postError(type, err, fatal)
332+
});
333+
334+
} else if (type == "computeVersusSelections") {
335+
loaded.then(x => {
336+
let rank_type = payload.rank_type.replace(/-.*/, ""); // summary type doesn't matter for pairwise comparisons.
337+
let res = superstate.custom_selections.computeVersus(payload.left, payload.right, rank_type, payload.modality);
338+
postMessage({
339+
type: "computeVersusSelections",
340+
resp: res,
341+
msg: "Success: COMPUTE_VERSUS_SELECTIONS done"
342+
});
343+
}).catch(err => {
344+
console.error(err);
345+
postError(type, err, fatal)
346+
});
347+
319348
/**************** OTHER EVENTS FROM UI *******************/
320349
} else if (type == "getMarkersForCluster") {
321350
loaded.then(x => {

0 commit comments

Comments
 (0)