Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

subset embedding UI improvement #1667

Merged
merged 6 commits into from Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions client/src/actions/embedding.js
@@ -0,0 +1,33 @@
/*
action creators related to embeddings choice
*/

import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";

export const layoutChoiceAction = (newLayoutChoice) => async (
dispatch,
getState
) => {
/*
On layout choice, make sure we have selected all on the previous layout, AND the new
layout.
*/
const { annoMatrix: prevAnnoMatrix } = getState();

const embeddingDf = await prevAnnoMatrix.base().fetch("emb", newLayoutChoice);
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select(
"emb",
newLayoutChoice,
{
mode: "all",
}
);
dispatch({
type: "set layout choice",
layoutChoice: newLayoutChoice,
obsCrossfilter,
annoMatrix,
});
};
14 changes: 13 additions & 1 deletion client/src/actions/index.js
Expand Up @@ -12,6 +12,7 @@ import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
import * as selnActions from "./selection";
import * as annoActions from "./annotation";
import * as viewActions from "./viewStack";
import * as embActions from "./embedding";

/*
return promise fetching user-configured colors
Expand Down Expand Up @@ -40,6 +41,15 @@ async function configFetch(dispatch) {
});
}

function prefetchEmbeddings(annoMatrix) {
/*
prefetch requests for all embeddings
*/
const { schema } = annoMatrix;
const available = schema.layout.obs.map((v) => v.name);
available.forEach((embName) => annoMatrix.prefetch("emb", embName));
}

/*
Application bootstrap
*/
Expand All @@ -57,6 +67,8 @@ const doInitialDataLoad = () =>
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
prefetchEmbeddings(annoMatrix);

dispatch({
type: "annoMatrix: init complete",
annoMatrix,
Expand Down Expand Up @@ -210,6 +222,6 @@ export default {
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
layoutChoiceAction: selnActions.layoutChoiceAction,
layoutChoiceAction: embActions.layoutChoiceAction,
setCellSetFromSelection: selnActions.setCellSetFromSelection,
};
25 changes: 0 additions & 25 deletions client/src/actions/selection.js
Expand Up @@ -185,31 +185,6 @@ export const graphLassoEndAction = (embName, polygon) => async (
});
};

export const layoutChoiceAction = (newLayoutChoice) => async (
dispatch,
getState
) => {
/*
On layout choice, make sure we have selected all on the previous layout, AND the new
layout.
*/
const { obsCrossfilter: prevObsCrossfilter, layoutChoice } = getState();

let obsCrossfilter = await prevObsCrossfilter.select(
"emb",
layoutChoice.current,
{ mode: "all" }
);
obsCrossfilter = await obsCrossfilter.select("emb", newLayoutChoice, {
mode: "all",
});
dispatch({
type: "set layout choice",
layoutChoice: newLayoutChoice,
obsCrossfilter,
});
};

/*
Differential expression set selection
*/
Expand Down
48 changes: 12 additions & 36 deletions client/src/actions/viewStack.js
Expand Up @@ -11,17 +11,20 @@ stack multiple subsets.
If these conventions change, code elsewhere (eg. menubar/clip.js) will need to
change as well.
*/
import { AnnoMatrixObsCrossfilter, clip, isubsetMask } from "../annoMatrix";
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import {
_clipAnnoMatrix,
_userSubsetAnnoMatrix,
_userResetSubsetAnnoMatrix,
} from "../util/stateManager/viewStackHelpers";

export const clipAction = (min, max) => (dispatch, getState) => {
/*
apply a clip to the current annoMatrix. By convention, the clip
view is ALWAYS the top view.
*/
const { annoMatrix: prevAnnoMatrix } = getState();
const annoMatrix = prevAnnoMatrix.isClipped
? clip(prevAnnoMatrix.viewOf, min, max)
: clip(prevAnnoMatrix, min, max);
const annoMatrix = _clipAnnoMatrix(prevAnnoMatrix, min, max);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
dispatch({
type: "set clip quantiles",
Expand All @@ -43,24 +46,10 @@ export const subsetAction = () => (dispatch, getState) => {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();

let annoMatrix;
if (prevAnnoMatrix.isClipped) {
// if there is a clip view, pop it and reapply after we subset
const { clipRange } = prevAnnoMatrix;
annoMatrix = isubsetMask(
prevAnnoMatrix.viewOf,
prevObsCrossfilter.allSelectedMask()
);
annoMatrix = clip(annoMatrix, ...clipRange);
} else {
// else just push a subset view.
annoMatrix = isubsetMask(
prevAnnoMatrix,
prevObsCrossfilter.allSelectedMask()
);
}

const annoMatrix = _userSubsetAnnoMatrix(
prevAnnoMatrix,
prevObsCrossfilter.allSelectedMask()
);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
dispatch({
type: "subset to selection",
Expand All @@ -77,20 +66,7 @@ export const resetSubsetAction = () => (dispatch, getState) => {
*/

const { annoMatrix: prevAnnoMatrix } = getState();

const clipRange = prevAnnoMatrix.isClipped ? prevAnnoMatrix.clipRange : null;

/* pop all views */
let annoMatrix = prevAnnoMatrix;
while (annoMatrix.isView) {
annoMatrix = annoMatrix.viewOf;
}

/* re-apply the clip, if any */
if (clipRange !== null) {
annoMatrix = clip(annoMatrix, ...clipRange);
}

const annoMatrix = _userResetSubsetAnnoMatrix(prevAnnoMatrix);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
dispatch({
type: "reset subset",
Expand Down
3 changes: 3 additions & 0 deletions client/src/annoMatrix/annoMatrix.js
Expand Up @@ -70,13 +70,16 @@ export default class AnnoMatrix {
The row index labels are as defined by the base dataset from the server.
* isView - true if this is a view, false if not.
* viewOf - pointer to parent annomatrix if a view, undefined/null if not a view.
* userFlags - container for any additional state a user of this API wants to hang
off of an annoMatrix, and have propagated by the (shallow) cloning protocol.
*/
this.schema = indexEntireSchema(schema);
this.nObs = nObs;
this.nVar = nVar;
this.rowIndex = rowIndex || new IdentityInt32Index(nObs);
this.isView = false;
this.viewOf = undefined;
this.userFlags = {};

/*
Private instance variables.
Expand Down
9 changes: 8 additions & 1 deletion client/src/annoMatrix/viewCreators.js
Expand Up @@ -33,6 +33,13 @@ export function subset(annoMatrix, obsLabels) {
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
}

export function subsetByIndex(annoMatrix, obsIndex) {
/*
subset based upon the new obs index.
*/
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
}

export function clip(annoMatrix, qmin, qmax) {
/*
Create a view that clips all continuous data to the [min, max] range.
Expand All @@ -59,5 +66,5 @@ function _maskToList(mask) {
elems += 1;
}
}
return new Int32Array(list.buffer, 0, elems);
return list.subarray(0, elems);
}
50 changes: 45 additions & 5 deletions client/src/components/embedding/index.js
@@ -1,5 +1,6 @@
import React from "react";
import { connect } from "react-redux";
import Async from "react-async";
import {
ButtonGroup,
Popover,
Expand All @@ -11,10 +12,11 @@ import {
} from "@blueprintjs/core";
import * as globals from "../../globals";
import actions from "../../actions";
import { getDiscreteCellEmbeddingRowIndex } from "../../util/stateManager/viewStackHelpers";

@connect((state) => {
return {
layoutChoice: state.layoutChoice,
layoutChoice: state.layoutChoice, // TODO: really should clean up naming, s/layout/embedding/g
schema: state.annoMatrix?.schema,
crossfilter: state.obsCrossfilter,
};
Expand All @@ -32,6 +34,7 @@ class Embedding extends React.PureComponent {

render() {
const { layoutChoice, schema, crossfilter } = this.props;
const { annoMatrix } = crossfilter;
return (
<ButtonGroup
style={{
Expand Down Expand Up @@ -61,7 +64,6 @@ class Embedding extends React.PureComponent {
>
{layoutChoice?.current}: {crossfilter.countSelected()} out of{" "}
{crossfilter.size()} cells
{/* BRUCE to extend 1559 */}
</Button>
</Tooltip>
}
Expand All @@ -87,9 +89,9 @@ class Embedding extends React.PureComponent {
selectedValue={layoutChoice.current}
>
{layoutChoice.available.map((name) => (
<Radio
label={`${name} ${schema?.dataframe?.nObs} cells`}
value={name}
<LayoutChoice
annoMatrix={annoMatrix}
layoutName={name}
key={name}
/>
))}
Expand All @@ -103,3 +105,41 @@ class Embedding extends React.PureComponent {
}

export default Embedding;

const loadEmbeddingCounts = async ({ annoMatrix, layoutName }) => {
const embedding = await annoMatrix.fetch("emb", layoutName);
const discreteCellIndex = getDiscreteCellEmbeddingRowIndex(embedding);
return { embedding, discreteCellIndex };
};

const LayoutChoice = ({ annoMatrix, layoutName }) => {
return (
<Async
promiseFn={loadEmbeddingCounts}
annoMatrix={annoMatrix}
layoutName={layoutName}
>
{({ data, error, isPending }) => {
if (error) {
/* log, as this is unexpected */
console.error(error);
}
if (error || isPending) {
/* still loading, or errored out - just omit counts (TODO: spinner?) */
return <Radio label={`${layoutName}`} value={layoutName} />;
}
if (data) {
const { embedding, discreteCellIndex } = data;
const isAllCells = discreteCellIndex.size() === embedding.length;
const sizeHint = `${discreteCellIndex.size()} ${
isAllCells ? "(all) " : ""
}cells`;
return (
<Radio label={`${layoutName}: ${sizeHint}`} value={layoutName} />
);
}
return null;
}}
</Async>
);
};
9 changes: 6 additions & 3 deletions client/src/components/menubar/index.js
Expand Up @@ -10,16 +10,19 @@ import InformationMenu from "./infoMenu";
import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons";
import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";

@connect((state) => {
const { annoMatrix } = state;
const crossfilter = state.obsCrossfilter;
const selectedCount = crossfilter.countSelected();

const subsetPossible =
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all are selected
const subsetResetPossible =
annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all and not none are selected
const embSubsetView = getEmbSubsetView(annoMatrix);
const subsetResetPossible = !embSubsetView
? annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs
: annoMatrix.nObs !== embSubsetView.nObs;

return {
subsetPossible,
Expand Down