Compare commits

...
Author SHA1 Message Date
Colin Megill 87ba3ff870 Merge branch 'main' into colinmegill/#632 2021-08-16 12:14:34 -07:00
Colin Megill b553da0264 async 1 2021-08-05 16:39:22 -07:00
Colin Megill b67142e98f embedding to tsx 2021-08-04 16:02:52 -07:00
Colin Megill 22a0921147 Merge branch 'main' into colinmegill/#632 2021-08-04 14:43:09 -07:00
Colin Megill 020e562f5c merge typescript changes 2021-08-04 14:42:31 -07:00
Colin Megill 60d89b9478 flip scale 2021-07-30 10:59:42 -07:00
Colin Megill c2b12abe2b toggle and scale dotplot 2021-07-26 13:23:21 -07:00
Colin Megill 2462d4afb1 color scale, button 2021-07-23 13:29:27 -07:00
Colin Megill 02e79d502f add classnames to canvases 2021-07-23 13:29:27 -07:00
Colin Megill 9c2323b7bc queries 2021-07-23 13:29:27 -07:00
Colin Megill e7200ce6c3 todo comment 2021-07-23 13:29:27 -07:00
Colin Megill b7ffb2748d colorby type 2021-07-23 13:29:27 -07:00
Colin Megill 7ab8a8894d no overlap 2021-07-23 13:29:27 -07:00
Colin Megill 7b01e9e67b remove hardcoded geneset 2021-07-23 13:29:27 -07:00
Colin Megill 72ee670620 remove logs 2021-07-23 13:29:27 -07:00
Colin Megill e21cac65bf set row and column 2021-07-23 13:29:27 -07:00
Colin Megill bb5bbaac8a reducer 2021-07-23 13:29:27 -07:00
Colin Megill e29a6f72c2 d3 scale for dot size 2021-07-23 13:29:10 -07:00
Colin Megill ed97013277 dotplot button 2021-07-23 13:29:10 -07:00
Colin Megill 2b29a152b9 metadata as var, maxsize todo 2021-07-23 13:28:17 -07:00
Colin Megill f0e9b1ab91 dotplot proto full 2021-07-23 13:28:17 -07:00
Colin Megill c489221296 geneset iterate 2021-07-23 13:28:17 -07:00
Colin Megill 0a69af98c5 break out load and err 2021-07-23 13:28:17 -07:00
Colin Megill 11570273e0 dotplot 1 2021-07-23 13:28:17 -07:00
Colin Megill 5f9d0a6b34 logging out values 2021-07-23 13:28:17 -07:00
18 changed files with 936 additions and 4 deletions
+10 -2
View File
@@ -8,6 +8,7 @@ import LeftSideBar from "./leftSidebar";
import RightSideBar from "./rightSidebar";
import Legend from "./continuousLegend";
import Graph from "./graph/graph";
import Dotplot from "./dotplot";
import MenuBar from "./menubar";
import Autosave from "./autosave";
import Embedding from "./embedding";
@@ -23,6 +24,7 @@ import actions from "../actions";
error: (state as any).controls.error,
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
graphRenderCounter: (state as any).controls.graphRenderCounter,
layoutChoice: (state as any).layoutChoice,
}))
class App extends React.Component {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
@@ -47,7 +49,7 @@ class App extends React.Component {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
render() {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'loading' does not exist on type 'Readonl... Remove this comment to see the full error message
const { loading, error, graphRenderCounter } = this.props;
const { loading, error, graphRenderCounter, layoutChoice } = this.props;
return (
<Container>
<Helmet title="cellxgene" />
@@ -87,8 +89,14 @@ class App extends React.Component {
<TermsOfServicePrompt />
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<Legend viewportRef={viewportRef} />
{layoutChoice.dotplot && <Dotplot viewportRef={viewportRef} />}
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ key: any; viewportRef: any; }' is not assi... Remove this comment to see the full error message */}
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
<Graph
key={graphRenderCounter}
dotplotMode={layoutChoice.dotplot}
viewportRef={viewportRef}
/>
</>
)}
<RightSideBar />
+237
View File
@@ -0,0 +1,237 @@
import React from "react";
import { connect, shallowEqual } from "react-redux";
import * as d3 from "d3";
import memoize from "memoize-one";
import Async from "react-async";
import ErrorLoading from "./err";
import StillLoading from "./load";
import Dot from "./dot";
import { createCategorySummaryFromDfCol } from "../../util/stateManager/controlsHelpers";
import { createColorQuery } from "../../util/stateManager/colorHelpers";
@connect((state) => ({
annoMatrix: state.annoMatrix,
colors: state.colors,
genesets: state.genesets.genesets,
pointDilation: state.pointDilation,
differential: state.differential,
dotplot: state.dotplot,
}))
class Column extends React.Component {
static watchAsync(props, prevProps) {
return !shallowEqual(props.watchProps, prevProps.watchProps);
}
createCategorySummaryFromDfCol = memoize(createCategorySummaryFromDfCol);
fetchAsyncProps = async (props) => {
const {
annoMatrix,
colors,
_geneSymbol,
_geneIndex,
metadataField,
} = props.watchProps;
const [categoryData, categorySummary, colorData] = await this.fetchData(
annoMatrix,
metadataField,
colors,
_geneSymbol,
_geneIndex
);
return {
categoryData,
categorySummary,
colorData,
};
};
async fetchData(annoMatrix, metadataField, colors, _geneSymbol) {
/*
fetch our data and the color-by data if appropriate, and then build a summary
of our category and a color table for the color-by annotation.
*/
const { schema } = annoMatrix;
const { colorMode } = colors;
const { genesets, differential } = this.props;
let colorDataPromise = Promise.resolve(null);
const query = createColorQuery(
colorMode,
_geneSymbol,
schema,
genesets,
differential.diffExp
);
if (query) colorDataPromise = annoMatrix.fetch(...query);
const [categoryData, colorData] = await Promise.all([
annoMatrix.fetch("obs", metadataField),
colorDataPromise,
]);
// our data
const column = categoryData.icol(0);
const colSchema = schema.annotations.obsByName[metadataField];
const categorySummary = this.createCategorySummaryFromDfCol(
column,
colSchema
);
return [categoryData, categorySummary, colorData];
}
render() {
const {
annoMatrix,
pointDilation,
colors,
viewport,
_geneSymbol,
_geneIndex,
rowColumnSize,
metadataField,
} = this.props;
return (
<g key={_geneSymbol}>
<Async
watchFn={Column.watchAsync}
promiseFn={this.fetchAsyncProps}
watchProps={{
annoMatrix,
pointDilation,
colors,
viewport,
_geneSymbol,
_geneIndex,
metadataField,
}}
>
<Async.Pending initial>
<StillLoading width={viewport.width} height={viewport.height} />
</Async.Pending>
<Async.Rejected>
{(error) => (
<ErrorLoading
width={viewport.width}
height={viewport.height}
error={error}
/>
)}
</Async.Rejected>
<Async.Fulfilled persist>
{(asyncProps) => {
const { categoryData, categorySummary, colorData } = asyncProps;
if (!_geneSymbol || !colorData) return null;
/* TODO(colinmegill) #632 wire to dotplot */
const groupBy = categoryData.col(metadataField);
const col = colorData.icol(0);
const range = col.summarize();
const histogramMap = col.histogram(
100,
[range.min, range.max],
groupBy
);
const categories =
annoMatrix?.schema?.annotations?.obsByName[metadataField]
?.categories;
const cellCategories = groupBy.asArray();
const geneExpressions = col.asArray();
let mean;
const meanGeneExpressions = {};
for (const c of categories) {
const arr = [];
for (let i = 0; i < geneExpressions.length; i += 1) {
if (cellCategories[i] === c) {
arr.push(geneExpressions[i]);
}
}
mean = arr.reduce((a, b) => a + b) / arr.length;
meanGeneExpressions[c] = mean;
}
const columnColorScale = d3
.scaleLinear()
.domain(d3.extent(Object.values(meanGeneExpressions)))
.range([1, 0]);
return categorySummary.categoryValues.map(
(val, _categoryValueIndex) => {
return (
<Dot
key={val}
categoryValue={val}
_categoryValueIndex={_categoryValueIndex}
histogramMap={histogramMap}
_geneSymbol={_geneSymbol}
_geneIndex={_geneIndex}
colorData={colorData}
rowColumnSize={rowColumnSize}
columnColorScale={columnColorScale}
meanGeneExpression={meanGeneExpressions[val]}
/>
);
}
);
}}
</Async.Fulfilled>
</Async>
</g>
);
}
}
export default Column;
// updateColorTable(colors, colorDf) {
// const { annoMatrix } = this.props;
// const { schema } = annoMatrix;
// /* update color table state */
// if (!colors || !colorDf) {
// return createColorTable(
// null, // default mode
// null,
// null,
// schema,
// null
// );
// }
// const { colorAccessor, userColors, colorMode } = colors;
// return createColorTable(
// colorMode,
// colorAccessor /* TODO(colinmegill) #632 dotplot wiring */,
// colorDf,
// schema,
// userColors
// );
// }
// createColorByQuery(colors) {
// const { annoMatrix, genesets, differential } = this.props;
// const { schema } = annoMatrix;
// const { colorMode, colorAccessor } = colors;
// return createColorQuery(
// colorMode,
// colorAccessor /* TODO(colinmegill) #632 dotplot wiring */,
// schema,
// genesets,
// differential.diffExp
// );
// }
+76
View File
@@ -0,0 +1,76 @@
import React from "react";
import { interpolateCool } from "d3-scale-chromatic";
import * as d3 from "d3";
const Dot = (props) => {
const {
categoryValue,
_categoryValueIndex,
histogramMap,
_geneSymbol,
_geneIndex,
rowColumnSize,
columnColorScale,
meanGeneExpression,
} = props;
const bins = histogramMap.has(categoryValue)
? histogramMap.get(categoryValue)
: new Array(100).fill(0);
const totalCells = bins.reduce(
(acc, current) => acc + current
); /* SUM REMAINING ELEMENTS */
/*
TODO(colinmegill) #632 this is a heuristic —
we need to figure out what non expressing means
and use it, rather than shifting off the first bin
for prototyping
*/
bins.shift(); /* MUTATES, REMOVES FIRST ELEMENT */
const expressing = bins.reduce(
(acc, current) => acc + current
); /* SUM REMAINING ELEMENTS */
/* TODO(colinmegill) #632 scale between correct dimensions */
const paddingEquivalentToRowColumnIndexOffset = 8;
/* domain is some fraction of the cells expressing, percent as decimal */
const dotscale = d3
.scaleLinear()
.domain([0, 1])
.range([0, rowColumnSize - paddingEquivalentToRowColumnIndexOffset]);
const _radius = dotscale(expressing / totalCells);
return (
<g
id={`row_${categoryValue}_${_geneSymbol}`}
key={`${_categoryValueIndex}_${categoryValue}`}
transform={`translate(${_geneIndex * rowColumnSize}, ${
_categoryValueIndex * rowColumnSize
})`}
>
{_geneIndex === 0 && (
<text
textAnchor="end"
style={{ fill: "black", font: "12px Roboto Condensed" }}
>
{categoryValue}
</text>
)}
<circle
r={_radius}
cx="11"
cy="-3.5"
style={{
fill: interpolateCool(columnColorScale(meanGeneExpression)),
fillOpacity: 1,
stroke: "none",
}}
/>
</g>
);
};
export default Dot;
+20
View File
@@ -0,0 +1,20 @@
import React from "react";
import * as globals from "../../globals";
const ErrorLoading = ({ error, width, height }) => {
console.log(error); // log to console as this is an unepected error
return (
<div
style={{
position: "fixed",
fontWeight: 500,
top: height / 2,
left: globals.leftSidebarWidth + width / 2 - 50,
}}
>
<span>Failure loading dotplot</span>
</div>
);
};
export default ErrorLoading;
+129
View File
@@ -0,0 +1,129 @@
import React from "react";
import { connect } from "react-redux";
import Column from "./column";
@connect((state) => ({
layoutChoice: state.layoutChoice,
genesets: state.genesets.genesets,
dotplot: state.dotplot,
}))
class Dotplot extends React.Component {
constructor(props) {
super(props);
const viewport = this.getViewportDimensions();
this.dotplotTopPadding = 120;
this.dotplotLeftPadding = 170;
this.rowColumnSize = 15;
this.dotplotBrowserScalingFactor = 0.65;
this.state = {
viewport,
};
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
/* chrome only, which is support matrix as of 2021 */
document.body.style.zoom = `${this.dotplotBrowserScalingFactor * 100}%`;
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
document.body.style.zoom = "100%";
}
getViewportDimensions = () => {
const { viewportRef } = this.props;
return {
height: viewportRef.clientHeight,
width: viewportRef.clientWidth,
};
};
render() {
const { viewport } = this.state;
const { genesets, dotplot } = this.props;
let _geneset = null;
let _genes = null;
if (dotplot.column) {
_geneset = genesets.get(dotplot.column);
_genes = Array.from(_geneset.genes.keys());
}
return (
<div
id="dotplot-wrapper"
style={{
position: "relative",
top: 0,
left: 0,
zIndex: -9999,
}}
>
<svg
id="dotplot"
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
width={viewport.width * (1 + this.dotplotBrowserScalingFactor)}
height={viewport.height * (1 + this.dotplotBrowserScalingFactor)}
>
<g
id="dotplot_help_text"
transform={`translate(${this.dotplotLeftPadding},${this.dotplotTopPadding})`}
>
<text>
{!dotplot.row && "Select a row"}{" "}
{!dotplot.column && "Select a column"}
</text>
</g>
{/* ASYNC HERE */}
{dotplot.row && dotplot.column && (
<g
id="dotplot_interface_margin"
transform={`translate(${this.dotplotLeftPadding},${this.dotplotTopPadding})`}
>
{/* Acaa1b, Mal, Foxq1 ... across the top of the dotplot */}
<g id="dotplot_column_labels" transform="translate(14,-13)">
{_genes.map((_geneSymbol, _geneIndexInGeneset) => (
<text
key={_geneSymbol}
x={0}
y={0}
transform={`translate(${
_geneIndexInGeneset * this.rowColumnSize
}) rotate(270)`}
style={{ fill: "black", font: "12px Roboto Condensed" }}
>
{_geneSymbol}
</text>
))}
</g>
{/* loop over genes in the geneset, */}
<g id="dotplot_columns">
{_genes.map((_geneSymbol, _geneIndexInGeneset) => (
<Column
key={_geneSymbol}
_geneSymbol={_geneSymbol}
_geneIndex={_geneIndexInGeneset}
viewport={viewport}
rowColumnSize={this.rowColumnSize}
metadataField={dotplot.row}
/>
))}
</g>
</g>
)}
</svg>
</div>
);
}
}
export default Dotplot;
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
const StillLoading = ({ width, height }) => {
/*
Render a busy/loading indicator
*/
return (
<div
style={{
position: "fixed",
fontWeight: 500,
top: height / 2,
width,
}}
>
<div
style={{
display: "flex",
justifyContent: "center",
justifyItems: "center",
alignItems: "center",
}}
>
<span style={{ fontStyle: "italic" }}>Loading dotplot</span>
</div>
</div>
);
};
export default StillLoading;
+3 -2
View File
@@ -26,6 +26,7 @@ type EmbeddingState = any;
schema: (state as any).annoMatrix?.schema,
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
crossfilter: (state as any).obsCrossfilter,
dotplot: (state as any).layoutChoice.dotplot,
}))
// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
class Embedding extends React.PureComponent<{}, EmbeddingState> {
@@ -45,13 +46,13 @@ class Embedding extends React.PureComponent<{}, EmbeddingState> {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
render() {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'layoutChoice' does not exist on type 'Re... Remove this comment to see the full error message
const { layoutChoice, schema, crossfilter } = this.props;
const { layoutChoice, schema, crossfilter, dotplot } = this.props;
const { annoMatrix } = crossfilter;
return (
<ButtonGroup
style={{
position: "absolute",
display: "inherit",
display: dotplot ? "none" : "inherit",
left: 8,
bottom: 8,
zIndex: 9999,
+2
View File
@@ -914,6 +914,7 @@ class Graph extends React.Component<{}, GraphState> {
pointDilation,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message
crossfilter,
dotplotMode,
} = this.props;
const { modelTF, projectionTF, camera, viewport, regl } = this.state;
const cameraTF = camera?.view()?.slice();
@@ -924,6 +925,7 @@ class Graph extends React.Component<{}, GraphState> {
position: "relative",
top: 0,
left: 0,
display: dotplotMode ? "none" : "inherit",
}}
>
<GraphOverlayLayer
+348
View File
@@ -0,0 +1,348 @@
import React from "react";
import { connect } from "react-redux";
import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
import * as globals from "../../globals";
import styles from "./menubar.css";
import actions from "../../actions";
import Clip from "./clip";
import AuthButtons from "./authButtons";
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 and not none are selected
const embSubsetView = getEmbSubsetView(annoMatrix);
const subsetResetPossible = !embSubsetView
? annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs
: annoMatrix.nObs !== embSubsetView.nObs;
return {
subsetPossible,
subsetResetPossible,
graphInteractionMode: state.controls.graphInteractionMode,
clipPercentileMin: Math.round(100 * (annoMatrix?.clipRange?.[0] ?? 0)),
clipPercentileMax: Math.round(100 * (annoMatrix?.clipRange?.[1] ?? 1)),
userDefinedGenes: state.controls.userDefinedGenes,
colorAccessor: state.colors.colorAccessor,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
libraryVersions: state.config?.library_versions,
auth: state.config?.authentication,
userInfo: state.userInfo,
undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
diffexpMayBeSlow:
state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
categoricalSelection: state.categoricalSelection,
dotplotEnabled: state.layoutChoice.dotplot,
};
})
class MenuBar extends React.PureComponent {
static isValidDigitKeyEvent(e) {
/*
Return true if this event is necessary to enter a percent number input.
Return false if not.
Returns true for events with keys: backspace, control, alt, meta, [0-9],
or events that don't have a key.
*/
if (e.key === null) return true;
if (e.ctrlKey || e.altKey || e.metaKey) return true;
// concept borrowed from blueprint's numericInputUtils:
// keys that print a single character when pressed have a `key` name of
// length 1. every other key has a longer `key` name (e.g. "Backspace",
// "ArrowUp", "Shift"). since none of those keys can print a character
// to the field--and since they may have important native behaviors
// beyond printing a character--we don't want to disable their effects.
const isSingleCharKey = e.key.length === 1;
if (!isSingleCharKey) return true;
const key = e.key.charCodeAt(0) - 48; /* "0" */
return key >= 0 && key <= 9;
}
constructor(props) {
super(props);
this.state = {
pendingClipPercentiles: null,
};
}
isClipDisabled = () => {
/*
return true if clip button should be disabled.
*/
const { pendingClipPercentiles } = this.state;
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
const {
clipPercentileMin: currentClipMin,
clipPercentileMax: currentClipMax,
} = this.props;
// if you change this test, be careful with logic around
// comparisons between undefined / NaN handling.
const isDisabled =
!(clipPercentileMin < clipPercentileMax) ||
(clipPercentileMin === currentClipMin &&
clipPercentileMax === currentClipMax);
return isDisabled;
};
handleClipOnKeyPress = (e) => {
/*
allow only numbers, plus other critical keys which
may be required to make a number
*/
if (!MenuBar.isValidDigitKeyEvent(e)) {
e.preventDefault();
}
};
handleClipPercentileMinValueChange = (v) => {
/*
Ignore anything that isn't a legit number
*/
if (!Number.isFinite(v)) return;
const { pendingClipPercentiles } = this.state;
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
/*
clamp to [0, currentClipPercentileMax]
*/
if (v <= 0) v = 0;
if (v > 100) v = 100;
const clipPercentileMin = Math.round(v); // paranoia
this.setState({
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
});
};
handleClipPercentileMaxValueChange = (v) => {
/*
Ignore anything that isn't a legit number
*/
if (!Number.isFinite(v)) return;
const { pendingClipPercentiles } = this.state;
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
/*
clamp to [0, 100]
*/
if (v < 0) v = 0;
if (v > 100) v = 100;
const clipPercentileMax = Math.round(v); // paranoia
this.setState({
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
});
};
handleClipCommit = () => {
const { dispatch } = this.props;
const { pendingClipPercentiles } = this.state;
const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles;
const min = clipPercentileMin / 100;
const max = clipPercentileMax / 100;
dispatch(actions.clipAction(min, max));
};
handleClipOpening = () => {
const { clipPercentileMin, clipPercentileMax } = this.props;
this.setState({
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax },
});
};
handleClipClosing = () => {
this.setState({ pendingClipPercentiles: null });
};
handleCentroidChange = () => {
const { dispatch, showCentroidLabels } = this.props;
dispatch({
type: "show centroid labels for category",
showLabels: !showCentroidLabels,
});
};
handleSubset = () => {
const { dispatch } = this.props;
dispatch(actions.subsetAction());
};
handleSubsetReset = () => {
const { dispatch } = this.props;
dispatch(actions.resetSubsetAction());
};
handleDotplotToggle = () => {
const { dispatch } = this.props;
dispatch({ type: "toggle dotplot" });
};
render() {
const {
dispatch,
disableDiffexp,
undoDisabled,
redoDisabled,
selectionTool,
clipPercentileMin,
clipPercentileMax,
graphInteractionMode,
showCentroidLabels,
categoricalSelection,
colorAccessor,
subsetPossible,
subsetResetPossible,
userInfo,
auth,
dotplotEnabled,
} = this.props;
const { pendingClipPercentiles } = this.state;
const isColoredByCategorical = !!categoricalSelection?.[colorAccessor];
// constants used to create selection tool button
const [selectionTooltip, selectionButtonIcon] =
selectionTool === "brush"
? ["Brush selection", "Lasso selection"]
: ["select", "polygon-filter"];
return (
<div
style={{
position: "absolute",
right: 8,
top: 0,
display: "flex",
flexDirection: "row-reverse",
alignItems: "flex-start",
flexWrap: "wrap",
justifyContent: "flex-start",
zIndex: 3,
}}
>
<AuthButtons {...{ auth, userInfo }} />
<UndoRedoReset
dispatch={dispatch}
undoDisabled={undoDisabled}
redoDisabled={redoDisabled}
/>
<Clip
pendingClipPercentiles={pendingClipPercentiles}
clipPercentileMin={clipPercentileMin}
clipPercentileMax={clipPercentileMax}
handleClipOpening={this.handleClipOpening}
handleClipClosing={this.handleClipClosing}
handleClipCommit={this.handleClipCommit}
isClipDisabled={this.isClipDisabled}
handleClipOnKeyPress={this.handleClipOnKeyPress}
handleClipPercentileMaxValueChange={
this.handleClipPercentileMaxValueChange
}
handleClipPercentileMinValueChange={
this.handleClipPercentileMinValueChange
}
/>
<Tooltip
content="Enable dotplot mode (hides embedding)"
position="bottom"
>
<AnchorButton
className={styles.menubarButton}
type="button"
data-testid="dotplot-toggle"
icon="layout-grid"
onClick={this.handleDotplotToggle}
active={dotplotEnabled}
intent={dotplotEnabled ? "success" : "none"}
/>
</Tooltip>
<Tooltip
content="When a category is colored by, show labels on the graph"
position="bottom"
disabled={graphInteractionMode === "zoom"}
>
<AnchorButton
className={styles.menubarButton}
type="button"
data-testid="centroid-label-toggle"
icon="property"
onClick={this.handleCentroidChange}
active={showCentroidLabels}
intent={showCentroidLabels ? "primary" : "none"}
disabled={!isColoredByCategorical}
/>
</Tooltip>
<ButtonGroup className={styles.menubarButton}>
<Tooltip
content={selectionTooltip}
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-lasso"
icon={selectionButtonIcon}
active={graphInteractionMode === "select"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "select",
});
}}
/>
</Tooltip>
<Tooltip
content="Drag to pan, scroll to zoom"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-pan-zoom"
icon="zoom-in"
active={graphInteractionMode === "zoom"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "zoom",
});
}}
/>
</Tooltip>
</ButtonGroup>
<Subset
subsetPossible={subsetPossible}
subsetResetPossible={subsetResetPossible}
handleSubset={this.handleSubset}
handleSubsetReset={this.handleSubsetReset}
/>
{disableDiffexp ? null : <DiffexpButtons />}
</div>
);
}
}
export default MenuBar;
@@ -92,6 +92,7 @@ export default class MiniHistogram extends React.PureComponent {
height,
borderBottom: "solid rgb(230, 230, 230) 0.25px",
}}
className="mini-histo"
width={width}
height={height}
ref={this.canvasRef}
@@ -81,6 +81,7 @@ export default class MiniStackedBar extends React.PureComponent {
width,
height,
}}
className="mini-stacked-bar-canvas"
width={width}
height={height}
ref={this.canvasRef}
@@ -566,6 +566,7 @@ class Scatterplot extends React.PureComponent<{}, State> {
width={width}
height={height}
data-testid="scatterplot"
className="scatterplot-canvas"
style={{
marginLeft: margin.left,
marginTop: margin.top,
+12
View File
@@ -5,6 +5,7 @@ Color By UI state
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
const ColorsReducer = (
state = {
/* TODO(colinmegill) #632 remove hardcode for dev */
colorMode: null /* by continuous, by expression */,
colorAccessor: null /* tissue, Apod */,
},
@@ -53,6 +54,17 @@ const ColorsReducer = (
};
}
case "toggle dotplot": {
return {
...state,
colorMode:
state.colorMode === "color by dotplot columns"
? null
: "color by dotplot columns",
colorAccessor: null,
};
}
case "color by categorical metadata":
case "color by continuous metadata": {
/* toggle between this mode and reset */
+30
View File
@@ -0,0 +1,30 @@
const Dotplot = (
state = {
row: null,
column: null,
},
action
) => {
switch (action.type) {
case "set dotplot row":
return {
...state,
row: action.data,
};
case "set dotplot column":
return {
...state,
column: action.data,
};
case "toggle dotplot":
return {
...state,
row: null,
column: null,
};
default:
return state;
}
};
export default Dotplot;
+2
View File
@@ -17,6 +17,7 @@ import controls from "./controls";
import annotations from "./annotations";
import genesets from "./genesets";
import genesetsUI from "./genesetsUI";
import dotplot from "./dotplot";
import autosave from "./autosave";
import centroidLabels from "./centroidLabels";
import pointDialation from "./pointDilation";
@@ -32,6 +33,7 @@ const Reducer = undoable(
["annotations", annotations],
["genesets", genesets],
["genesetsUI", genesetsUI],
["dotplot", dotplot],
["layoutChoice", layoutChoice],
["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
+8
View File
@@ -28,6 +28,7 @@ function setToDefaultLayout(schema: any) {
const LayoutChoice = (
state = {
available: [], // all available choices
dotplot: false, // is the dotplot toggled on, or not
current: undefined, // name of the current layout, eg, 'umap'
currentDimNames: [], // dimension name
},
@@ -46,6 +47,13 @@ const LayoutChoice = (
};
}
case "toggle dotplot": {
return {
...state,
dotplot: !state.dotplot,
};
}
case "set layout choice": {
const { schema } = nextSharedState.annoMatrix;
const current = action.layoutChoice;
+4
View File
@@ -88,6 +88,10 @@ const saveOnActions = new Set<string>([
"color by expression",
"color by geneset mean expression",
"set dotplot row",
"set dotplot column",
"toggle dotplot",
"show centroid labels for category",
"set scatterplot x",
@@ -65,6 +65,28 @@ export function createColorQuery(
},
];
}
case "color by dotplot columns": {
/*
Color by COLUMNS is a mode at the UI level,
as we are going to be keeping track of many color scales —
one per column in the dotplot. The query is for
one gene at a time.
*/
const varIndex = schema?.annotations?.var?.index;
if (!varIndex) return null;
return [
"X",
{
where: {
field: "var",
column: varIndex,
value: colorByAccessor,
},
},
];
}
default: {
return null;
}