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

feat: move continuous fields to the dataset drawer if there is a single value for all cells #918

Merged
merged 9 commits into from
May 10, 2024
2 changes: 1 addition & 1 deletion client/src/annoMatrix/annoMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ export default abstract class AnnoMatrix {
_resolveCachedQueries(field: Field, queries: Query[]): LabelType[] {
return queries
.map((query: Query) =>
// @ts-expect-error ts-migrate --- suppressing TS defect (https://github.com/microsoft/TypeScript/issues/44373).
// Compiler is complaining that expression is not callable on array union types. Remove suppression once fixed.
// @ts-expect-error ts-migrate --- suppressing TS defect (https://github.com/microsoft/TypeScript/issues/44373).
_whereCacheGet(this._whereCache, this.schema, field, query).filter(
(cacheKey: LabelType | undefined): cacheKey is LabelType =>
cacheKey !== undefined && this._cache[field].hasCol(cacheKey)
Expand Down
44 changes: 42 additions & 2 deletions client/src/components/brushableHistogram/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type BrushableHistogramProps = Partial<RootState> & BrushableHistogramOwnProps;
isColorAccessor:
state.colors.colorAccessor === field &&
state.colors.colorMode !== "color by categorical metadata",
singleContinuousValues: state.singleContinuousValue.singleContinuousValues,
};
})
class HistogramBrush extends React.PureComponent<BrushableHistogramProps> {
Expand Down Expand Up @@ -235,8 +236,25 @@ class HistogramBrush extends React.PureComponent<BrushableHistogramProps> {
};

fetchAsyncProps = async () => {
const { annoMatrix, width, onGeneExpressionComplete } = this.props;
const {
annoMatrix,
width,
onGeneExpressionComplete,
field,
dispatch,
singleContinuousValues,
} = this.props;
const { isClipped } = annoMatrix;
if (singleContinuousValues.has(field)) {
return {
histogram: undefined,
range: undefined,
unclippedRange: undefined,
unclippedRangeColor: globals.blue,
isSingleValue: true,
OK2Render: false,
};
}

const query = this.createQuery();
if (!query) {
Expand All @@ -258,6 +276,29 @@ class HistogramBrush extends React.PureComponent<BrushableHistogramProps> {
const summary = column.summarizeContinuous();
const range = [summary.min, summary.max];

// seve: if the anno matrix is not a view and it is a single value, remove it from histograms and send it to the dataset drawer
// NOTE: this also includes embedding views, so if the default embedding subsets to a view and there is a single continuous value for a field, it will not be added to the dataset drawer
if (summary.min === summary.max && !annoMatrix.isView) {
dispatch({
type: "add single continuous value",
field,
value: summary.min,
});
return {
histogram: undefined,
range,
unclippedRange: range,
unclippedRangeColor: globals.blue,
isSingleValue: true,
OK2Render: false,
};
}

const isSingleValue = summary.min === summary.max;

// if we are clipped, fetch both our value and our unclipped value,
// as we need the absolute min/max range, not just the clipped min/max.

let unclippedRange = [...range];
if (isClipped) {
const parent: Dataframe = await annoMatrix.viewOf.fetch(
Expand Down Expand Up @@ -290,7 +331,6 @@ class HistogramBrush extends React.PureComponent<BrushableHistogramProps> {
HEIGHT_MINI
);

const isSingleValue = summary.min === summary.max;
const nonFiniteExtent =
summary.min === undefined ||
summary.max === undefined ||
Expand Down
22 changes: 17 additions & 5 deletions client/src/components/infoDrawer/infoDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { connect } from "react-redux";
import { Drawer, Position } from "@blueprintjs/core";

/* App dependencies */
import InfoFormat, { SingleValueCategories } from "./infoFormat";
import InfoFormat, { SingleValues } from "./infoFormat";
import { AppDispatch, RootState } from "../../reducers";
import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers";
import { DatasetMetadata } from "../../common/types/entities";
import { Schema } from "../../common/types/schema";
import { SingleContinuousValueState } from "../../reducers/singleContinuousValue";

/**
* Actions dispatched by info drawer.
Expand All @@ -31,6 +32,7 @@ interface StateProps {
datasetMetadata: DatasetMetadata;
isOpen: boolean;
schema: Schema;
singleContinuousValues: SingleContinuousValueState["singleContinuousValues"];
}

type Props = DispatchProps & OwnProps & StateProps;
Expand All @@ -42,6 +44,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
datasetMetadata: state.datasetMetadata?.datasetMetadata,
isOpen: state.controls.datasetDrawer,
schema: state.annoMatrix.schema,
singleContinuousValues: state.singleContinuousValue.singleContinuousValues,
});

/**
Expand All @@ -58,24 +61,33 @@ class InfoDrawer extends PureComponent<Props> {
};

render(): JSX.Element {
const { datasetMetadata, position, schema, isOpen } = this.props;
const {
datasetMetadata,
position,
schema,
isOpen,
singleContinuousValues,
} = this.props;

const allCategoryNames = selectableCategoryNames(schema).sort();
const singleValueCategories: SingleValueCategories = new Map();
const allSingleValues: SingleValues = new Map();

allCategoryNames.forEach((catName) => {
const isUserAnno = schema?.annotations?.obsByName[catName]?.writable;
const colSchema = schema.annotations.obsByName[catName];
if (!isUserAnno && colSchema.categories?.length === 1) {
singleValueCategories.set(catName, colSchema.categories[0]);
allSingleValues.set(catName, colSchema.categories[0]);
}
});
singleContinuousValues.forEach((value, catName) => {
allSingleValues.set(catName, value);
});
return (
<Drawer size={480} onClose={this.handleClose} {...{ isOpen, position }}>
<InfoFormat
{...{
datasetMetadata,
singleValueCategories,
allSingleValues,
}}
/>
</Drawer>
Expand Down
34 changes: 16 additions & 18 deletions client/src/components/infoDrawer/infoFormat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ interface MetadataView {

interface Props {
datasetMetadata: DatasetMetadata;
singleValueCategories: SingleValueCategories;
allSingleValues: SingleValues;
}

export type SingleValueCategories = Map<string, Category>;
export type SingleValues = Map<string, Category>;

/**
* Sort collection links by custom sort order, create view-friendly model of link types.
Expand Down Expand Up @@ -244,16 +244,16 @@ const renderCollectionLinks = (

/**
* Render dataset metadata. That is, attributes found in categorical fields.
* @param singleValueCategories - Attributes from categorical fields
* @param renderSingleValues - Attributes from categorical fields
* @returns Markup for displaying meta in table format.
*/
const renderDatasetMetadata = (
singleValueCategories: SingleValueCategories
renderSingleValues: SingleValues
): JSX.Element | null => {
if (singleValueCategories.size === 0) {
if (renderSingleValues.size === 0) {
return null;
}
const metadataViews = buildDatasetMetadataViews(singleValueCategories);
const metadataViews = buildDatasetMetadataViews(renderSingleValues);
metadataViews.sort(sortDatasetMetadata);
return (
<>
Expand Down Expand Up @@ -328,7 +328,7 @@ const transformLinkTypeToDisplay = (type: string): string => {
* @returns Array of metadata key/value pairs.
*/
const buildDatasetMetadataViews = (
singleValueCategories: SingleValueCategories
singleValueCategories: SingleValues
): MetadataView[] =>
Array.from(singleValueCategories.entries())
.filter(([key, value]) => {
Expand All @@ -341,17 +341,15 @@ const buildDatasetMetadataViews = (
})
.map(([key, value]) => ({ key, value: String(value) }));

const InfoFormat = React.memo<Props>(
({ datasetMetadata, singleValueCategories }) => (
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<H3>{datasetMetadata.collection_name}</H3>
<p>{datasetMetadata.collection_description}</p>
{renderCollectionLinks(datasetMetadata)}
{renderDatasetMetadata(singleValueCategories)}
</div>
const InfoFormat = React.memo<Props>(({ datasetMetadata, allSingleValues }) => (
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<H3>{datasetMetadata.collection_name}</H3>
<p>{datasetMetadata.collection_description}</p>
{renderCollectionLinks(datasetMetadata)}
{renderDatasetMetadata(allSingleValues)}
</div>
)
);
</div>
));

export default InfoFormat;
2 changes: 2 additions & 0 deletions client/src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import genesetsUI from "./genesetsUI";
import centroidLabels from "./centroidLabels";
import pointDialation from "./pointDilation";
import quickGenes from "./quickGenes";
import singleContinuousValue from "./singleContinuousValue";

import { gcMiddleware as annoMatrixGC } from "../annoMatrix";

Expand All @@ -40,6 +41,7 @@ const AppReducer = undoable(
["genesets", genesets],
["genesetsUI", genesetsUI],
["layoutChoice", layoutChoice],
["singleContinuousValue", singleContinuousValue],
["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
["graphSelection", graphSelection],
Expand Down
30 changes: 30 additions & 0 deletions client/src/reducers/singleContinuousValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Action, AnyAction } from "redux";

export interface SingleContinuousValueState {
singleContinuousValues: Map<string, string>;
}

const initialState = {
singleContinuousValues: new Map(),
};

export interface SingleContinuousValueAction extends Action<string> {
field: string;
value: string;
}

const singleContinuousValue = (
state = initialState,
action: AnyAction
): SingleContinuousValueState => {
switch (action.type) {
case "add single continuous value":

state.singleContinuousValues.set(action.field, action.value);
return state;
default:
return state;
}
};

export default singleContinuousValue;
Loading