mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 08:08:13 +08:00
* refactor categorical controls state * lint * fix race condition in tests * fix typo * add missing update on subset * remove obsolete code * update jest and puppeteer major version; update all minors * update when label changes * remove lint from tests; increase timeouts in e2e tests * initial refactoring to new async annomatrix * refine error handling * fix bad merge * add continuous legend * lint * fix memoization in color table creators * partial implementation of user defined annotations * add new annotations action creator file * first pass at user annotations * additional user annotation bug fixes * user annotation auto-save * unit test cleanup * lint * refactor into multiple files * cleanup * add column GC * fix several bugs in user annotations * remove debug code * no anonymous functions * undo redo cleanup * file cleanup * scatterplot * performance * cleanup * remove old code * render in parallel with load * fix race condition * simply graph rendering * render throttle DRY * fix category label order * fix typo in e2e test setup * re-fix the e2e test setup * be more tolerant of races * anno matrix unit tests * temp disable reembedding * pilot port continuous histo to react-async * name change * lint * fix repaint bug * typo fix * update snap to match new ids * world/universe name cleanup * move annoMatrix to src dir * use private underscore naming convention * fix corner case in all selected * name cleanup * add layout control * init edge case * lint * port scatterplot * fix label indexing bug and improve tests * port category to react-async * fix user annotation labelling while subset * select all of prev layout on layout switch * fix race with crossfilter update * prettier lint * fix misleading comment * fix url composition in loader * first pass at crossfilter tests * lint * lint * fix typo * improved error handling for network errors * fix memoization bug * add memo * refactor for performnce * add missing single-value handling in select exact parser * small bugs discovered by tests * lint * additional crossfilter unit tests * remove extraneous comment * add support for automatic category determination * lint * fix render bug in category * take advantage of schema categories guarantee * lint * do not clear history when resetting * enhanced annomatrix gc * lint * finish renaming to follow conventions; fix clone race bug * lint * add priority based loading to improve initial data load UX * crossfilter cache perf * perf tuning * remove timers * documentation * PR review changes * PR review changes * more PR review edits * improve clarity of comment * more PR review fixes * port centroidLabels to use react-async * remove dead code * pr review updates * oops, remove logging
218 lines
5.9 KiB
JavaScript
218 lines
5.9 KiB
JavaScript
import React, { PureComponent } from "react";
|
|
import { connect, shallowEqual } from "react-redux";
|
|
import Async from "react-async";
|
|
|
|
import { categoryLabelDisplayStringLongLength } from "../../../globals";
|
|
import calcCentroid from "../../../util/centroid";
|
|
import { createColorQuery } from "../../../util/stateManager/colorHelpers";
|
|
|
|
export default
|
|
@connect((state) => ({
|
|
annoMatrix: state.annoMatrix,
|
|
colors: state.colors,
|
|
layoutChoice: state.layoutChoice,
|
|
dilatedValue: state.pointDilation.categoryField,
|
|
categoricalSelection: state.categoricalSelection,
|
|
showLabels: state.centroidLabels?.showLabels,
|
|
}))
|
|
class CentroidLabels extends PureComponent {
|
|
static watchAsync(props, prevProps) {
|
|
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
|
}
|
|
|
|
fetchAsyncProps = async (props) => {
|
|
const {
|
|
annoMatrix,
|
|
colors,
|
|
layoutChoice,
|
|
categoricalSelection,
|
|
showLabels,
|
|
} = props.watchProps;
|
|
const { schema } = annoMatrix;
|
|
const { colorAccessor } = colors;
|
|
|
|
const [layoutDf, colorDf] = await this.fetchData();
|
|
let labels;
|
|
if (colorDf) {
|
|
labels = calcCentroid(
|
|
schema,
|
|
colorAccessor,
|
|
colorDf,
|
|
layoutChoice,
|
|
layoutDf
|
|
);
|
|
} else {
|
|
labels = new Map();
|
|
}
|
|
|
|
const { overlaySetShowing } = this.props;
|
|
overlaySetShowing("centroidLabels", showLabels && labels.size > 0);
|
|
|
|
return {
|
|
labels,
|
|
colorAccessor,
|
|
category: categoricalSelection[colorAccessor],
|
|
};
|
|
};
|
|
|
|
handleMouseEnter = (e, colorAccessor, label) => {
|
|
const { dispatch } = this.props;
|
|
dispatch({
|
|
type: "category value mouse hover start",
|
|
metadataField: colorAccessor,
|
|
categoryField: label,
|
|
});
|
|
};
|
|
|
|
handleMouseOut = (e, colorAccessor, label) => {
|
|
const { dispatch } = this.props;
|
|
dispatch({
|
|
type: "category value mouse hover end",
|
|
metadataField: colorAccessor,
|
|
categoryField: label,
|
|
});
|
|
};
|
|
|
|
colorByQuery() {
|
|
const { annoMatrix, colors } = this.props;
|
|
const { schema } = annoMatrix;
|
|
const { colorMode, colorAccessor } = colors;
|
|
return createColorQuery(colorMode, colorAccessor, schema);
|
|
}
|
|
|
|
async fetchData() {
|
|
const { annoMatrix, layoutChoice } = this.props;
|
|
// fetch all data we need: layout, category
|
|
const promises = [];
|
|
// layout
|
|
promises.push(annoMatrix.fetch("emb", layoutChoice.current));
|
|
// category to label - we ONLY label on obs, never on X, etc.
|
|
const query = this.colorByQuery();
|
|
if (query && query[0] === "obs") {
|
|
promises.push(annoMatrix.fetch(...query));
|
|
} else {
|
|
promises.push(Promise.resolve(null));
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
inverseTransform,
|
|
dilatedValue,
|
|
categoricalSelection,
|
|
showLabels,
|
|
colors,
|
|
annoMatrix,
|
|
layoutChoice,
|
|
} = this.props;
|
|
|
|
return (
|
|
<Async
|
|
watchFn={CentroidLabels.watchAsync}
|
|
promiseFn={this.fetchAsyncProps}
|
|
watchProps={{
|
|
annoMatrix,
|
|
colors,
|
|
layoutChoice,
|
|
categoricalSelection,
|
|
dilatedValue,
|
|
showLabels,
|
|
}}
|
|
>
|
|
<Async.Fulfilled>
|
|
{(asyncProps) => {
|
|
if (!showLabels) return null;
|
|
|
|
const labelSVGS = [];
|
|
const deselectOpacity = 0.375;
|
|
const { category, colorAccessor, labels } = asyncProps;
|
|
|
|
labels.forEach((coords, label) => {
|
|
const selected = category.get(label) ?? true;
|
|
|
|
// Mirror LSB middle truncation
|
|
let displayLabel = label;
|
|
if (displayLabel.length > categoryLabelDisplayStringLongLength) {
|
|
displayLabel = `${label.slice(
|
|
0,
|
|
categoryLabelDisplayStringLongLength / 2
|
|
)}…${label.slice(-categoryLabelDisplayStringLongLength / 2)}`;
|
|
}
|
|
|
|
labelSVGS.push(
|
|
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events -- the mouse actions for centroid labels do not have a screen reader alternative
|
|
<Label
|
|
key={label} // eslint-disable-line react/no-array-index-key --- label is not an index, eslint is confused
|
|
label={label}
|
|
dilatedValue={dilatedValue}
|
|
coords={coords}
|
|
inverseTransform={inverseTransform}
|
|
opactity={selected ? 1 : deselectOpacity}
|
|
colorAccessor={colorAccessor}
|
|
displayLabel={displayLabel}
|
|
onMouseEnter={this.handleMouseEnter}
|
|
onMouseOut={this.handleMouseOut}
|
|
/>
|
|
);
|
|
});
|
|
|
|
return <>{labelSVGS}</>;
|
|
}}
|
|
</Async.Fulfilled>
|
|
</Async>
|
|
);
|
|
}
|
|
}
|
|
|
|
const Label = ({
|
|
label,
|
|
dilatedValue,
|
|
coords,
|
|
inverseTransform,
|
|
opacity,
|
|
colorAccessor,
|
|
displayLabel,
|
|
onMouseEnter,
|
|
onMouseOut,
|
|
}) => {
|
|
/*
|
|
Render a label at a given coordinate.
|
|
*/
|
|
let fontSize = "15px";
|
|
let fontWeight = null;
|
|
if (label === dilatedValue) {
|
|
fontSize = "18px";
|
|
fontWeight = "800";
|
|
}
|
|
|
|
return (
|
|
<g
|
|
key={label}
|
|
className="centroid-label"
|
|
transform={`translate(${coords[0]}, ${coords[1]})`}
|
|
data-testclass="centroid-label"
|
|
data-testid={`${label}-centroid-label`}
|
|
>
|
|
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events --- the mouse actions for centroid labels do not have a screen reader alternative*/}
|
|
<text
|
|
transform={inverseTransform}
|
|
textAnchor="middle"
|
|
style={{
|
|
fontSize,
|
|
fontWeight,
|
|
fill: "black",
|
|
userSelect: "none",
|
|
opacity: { opacity },
|
|
}}
|
|
onMouseEnter={(e) => onMouseEnter(e, colorAccessor, label)}
|
|
onMouseOut={(e) => onMouseOut(e, colorAccessor, label)}
|
|
pointerEvents="visiblePainted"
|
|
>
|
|
{displayLabel}
|
|
</text>
|
|
</g>
|
|
);
|
|
};
|