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 RightSideBar from "./rightSidebar";
import Legend from "./continuousLegend"; import Legend from "./continuousLegend";
import Graph from "./graph/graph"; import Graph from "./graph/graph";
import Dotplot from "./dotplot";
import MenuBar from "./menubar"; import MenuBar from "./menubar";
import Autosave from "./autosave"; import Autosave from "./autosave";
import Embedding from "./embedding"; import Embedding from "./embedding";
@@ -23,6 +24,7 @@ import actions from "../actions";
error: (state as any).controls.error, error: (state as any).controls.error,
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
graphRenderCounter: (state as any).controls.graphRenderCounter, graphRenderCounter: (state as any).controls.graphRenderCounter,
layoutChoice: (state as any).layoutChoice,
})) }))
class App extends React.Component { class App extends React.Component {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. // 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. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
render() { 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 // @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 ( return (
<Container> <Container>
<Helmet title="cellxgene" /> <Helmet title="cellxgene" />
@@ -87,8 +89,14 @@ class App extends React.Component {
<TermsOfServicePrompt /> <TermsOfServicePrompt />
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */} {/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<Legend viewportRef={viewportRef} /> <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 */} {/* @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 /> <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, schema: (state as any).annoMatrix?.schema,
// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS.
crossfilter: (state as any).obsCrossfilter, 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. // eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS.
class Embedding extends React.PureComponent<{}, EmbeddingState> { 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. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
render() { 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 // @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; const { annoMatrix } = crossfilter;
return ( return (
<ButtonGroup <ButtonGroup
style={{ style={{
position: "absolute", position: "absolute",
display: "inherit", display: dotplot ? "none" : "inherit",
left: 8, left: 8,
bottom: 8, bottom: 8,
zIndex: 9999, zIndex: 9999,
+2
View File
@@ -914,6 +914,7 @@ class Graph extends React.Component<{}, GraphState> {
pointDilation, pointDilation,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message // @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on ... Remove this comment to see the full error message
crossfilter, crossfilter,
dotplotMode,
} = this.props; } = this.props;
const { modelTF, projectionTF, camera, viewport, regl } = this.state; const { modelTF, projectionTF, camera, viewport, regl } = this.state;
const cameraTF = camera?.view()?.slice(); const cameraTF = camera?.view()?.slice();
@@ -924,6 +925,7 @@ class Graph extends React.Component<{}, GraphState> {
position: "relative", position: "relative",
top: 0, top: 0,
left: 0, left: 0,
display: dotplotMode ? "none" : "inherit",
}} }}
> >
<GraphOverlayLayer <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, height,
borderBottom: "solid rgb(230, 230, 230) 0.25px", borderBottom: "solid rgb(230, 230, 230) 0.25px",
}} }}
className="mini-histo"
width={width} width={width}
height={height} height={height}
ref={this.canvasRef} ref={this.canvasRef}
@@ -81,6 +81,7 @@ export default class MiniStackedBar extends React.PureComponent {
width, width,
height, height,
}} }}
className="mini-stacked-bar-canvas"
width={width} width={width}
height={height} height={height}
ref={this.canvasRef} ref={this.canvasRef}
@@ -566,6 +566,7 @@ class Scatterplot extends React.PureComponent<{}, State> {
width={width} width={width}
height={height} height={height}
data-testid="scatterplot" data-testid="scatterplot"
className="scatterplot-canvas"
style={{ style={{
marginLeft: margin.left, marginLeft: margin.left,
marginTop: margin.top, 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. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS.
const ColorsReducer = ( const ColorsReducer = (
state = { state = {
/* TODO(colinmegill) #632 remove hardcode for dev */
colorMode: null /* by continuous, by expression */, colorMode: null /* by continuous, by expression */,
colorAccessor: null /* tissue, Apod */, 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 categorical metadata":
case "color by continuous metadata": { case "color by continuous metadata": {
/* toggle between this mode and reset */ /* 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 annotations from "./annotations";
import genesets from "./genesets"; import genesets from "./genesets";
import genesetsUI from "./genesetsUI"; import genesetsUI from "./genesetsUI";
import dotplot from "./dotplot";
import autosave from "./autosave"; import autosave from "./autosave";
import centroidLabels from "./centroidLabels"; import centroidLabels from "./centroidLabels";
import pointDialation from "./pointDilation"; import pointDialation from "./pointDilation";
@@ -32,6 +33,7 @@ const Reducer = undoable(
["annotations", annotations], ["annotations", annotations],
["genesets", genesets], ["genesets", genesets],
["genesetsUI", genesetsUI], ["genesetsUI", genesetsUI],
["dotplot", dotplot],
["layoutChoice", layoutChoice], ["layoutChoice", layoutChoice],
["categoricalSelection", categoricalSelection], ["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection], ["continuousSelection", continuousSelection],
+8
View File
@@ -28,6 +28,7 @@ function setToDefaultLayout(schema: any) {
const LayoutChoice = ( const LayoutChoice = (
state = { state = {
available: [], // all available choices available: [], // all available choices
dotplot: false, // is the dotplot toggled on, or not
current: undefined, // name of the current layout, eg, 'umap' current: undefined, // name of the current layout, eg, 'umap'
currentDimNames: [], // dimension name currentDimNames: [], // dimension name
}, },
@@ -46,6 +47,13 @@ const LayoutChoice = (
}; };
} }
case "toggle dotplot": {
return {
...state,
dotplot: !state.dotplot,
};
}
case "set layout choice": { case "set layout choice": {
const { schema } = nextSharedState.annoMatrix; const { schema } = nextSharedState.annoMatrix;
const current = action.layoutChoice; const current = action.layoutChoice;
+4
View File
@@ -88,6 +88,10 @@ const saveOnActions = new Set<string>([
"color by expression", "color by expression",
"color by geneset mean expression", "color by geneset mean expression",
"set dotplot row",
"set dotplot column",
"toggle dotplot",
"show centroid labels for category", "show centroid labels for category",
"set scatterplot x", "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: { default: {
return null; return null;
} }