From 055b24d8e68f8f017418414dd7084cf8a9bc24c6 Mon Sep 17 00:00:00 2001 From: Severiano Badajoz Date: Fri, 10 May 2024 12:04:23 -0700 Subject: [PATCH] feat: move continuous fields to the dataset drawer if there is a single value for all cells (#918) Co-authored-by: atarashansky --- client/src/annoMatrix/annoMatrix.ts | 2 +- .../components/brushableHistogram/index.tsx | 44 ++++++++++++++++++- .../src/components/infoDrawer/infoDrawer.tsx | 22 +++++++--- .../src/components/infoDrawer/infoFormat.tsx | 34 +++++++------- client/src/reducers/index.ts | 2 + client/src/reducers/singleContinuousValue.ts | 30 +++++++++++++ 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 client/src/reducers/singleContinuousValue.ts diff --git a/client/src/annoMatrix/annoMatrix.ts b/client/src/annoMatrix/annoMatrix.ts index fc009df5d..3315b08fa 100644 --- a/client/src/annoMatrix/annoMatrix.ts +++ b/client/src/annoMatrix/annoMatrix.ts @@ -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) diff --git a/client/src/components/brushableHistogram/index.tsx b/client/src/components/brushableHistogram/index.tsx index b5f251677..5a88bbf78 100644 --- a/client/src/components/brushableHistogram/index.tsx +++ b/client/src/components/brushableHistogram/index.tsx @@ -57,6 +57,7 @@ type BrushableHistogramProps = Partial & BrushableHistogramOwnProps; isColorAccessor: state.colors.colorAccessor === field && state.colors.colorMode !== "color by categorical metadata", + singleContinuousValues: state.singleContinuousValue.singleContinuousValues, }; }) class HistogramBrush extends React.PureComponent { @@ -235,8 +236,25 @@ class HistogramBrush extends React.PureComponent { }; 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) { @@ -258,6 +276,29 @@ class HistogramBrush extends React.PureComponent { 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( @@ -290,7 +331,6 @@ class HistogramBrush extends React.PureComponent { HEIGHT_MINI ); - const isSingleValue = summary.min === summary.max; const nonFiniteExtent = summary.min === undefined || summary.max === undefined || diff --git a/client/src/components/infoDrawer/infoDrawer.tsx b/client/src/components/infoDrawer/infoDrawer.tsx index a922e8814..5b5dbf425 100644 --- a/client/src/components/infoDrawer/infoDrawer.tsx +++ b/client/src/components/infoDrawer/infoDrawer.tsx @@ -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. @@ -31,6 +32,7 @@ interface StateProps { datasetMetadata: DatasetMetadata; isOpen: boolean; schema: Schema; + singleContinuousValues: SingleContinuousValueState["singleContinuousValues"]; } type Props = DispatchProps & OwnProps & StateProps; @@ -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, }); /** @@ -58,24 +61,33 @@ class InfoDrawer extends PureComponent { }; 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 ( diff --git a/client/src/components/infoDrawer/infoFormat.tsx b/client/src/components/infoDrawer/infoFormat.tsx index 77007a3f4..429174c4a 100644 --- a/client/src/components/infoDrawer/infoFormat.tsx +++ b/client/src/components/infoDrawer/infoFormat.tsx @@ -35,10 +35,10 @@ interface MetadataView { interface Props { datasetMetadata: DatasetMetadata; - singleValueCategories: SingleValueCategories; + allSingleValues: SingleValues; } -export type SingleValueCategories = Map; +export type SingleValues = Map; /** * Sort collection links by custom sort order, create view-friendly model of link types. @@ -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 ( <> @@ -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]) => { @@ -341,17 +341,15 @@ const buildDatasetMetadataViews = ( }) .map(([key, value]) => ({ key, value: String(value) })); -const InfoFormat = React.memo( - ({ datasetMetadata, singleValueCategories }) => ( -
-
-

{datasetMetadata.collection_name}

-

{datasetMetadata.collection_description}

- {renderCollectionLinks(datasetMetadata)} - {renderDatasetMetadata(singleValueCategories)} -
+const InfoFormat = React.memo(({ datasetMetadata, allSingleValues }) => ( +
+
+

{datasetMetadata.collection_name}

+

{datasetMetadata.collection_description}

+ {renderCollectionLinks(datasetMetadata)} + {renderDatasetMetadata(allSingleValues)}
- ) -); +
+)); export default InfoFormat; diff --git a/client/src/reducers/index.ts b/client/src/reducers/index.ts index e7d6b3fc8..379004e5b 100644 --- a/client/src/reducers/index.ts +++ b/client/src/reducers/index.ts @@ -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"; @@ -40,6 +41,7 @@ const AppReducer = undoable( ["genesets", genesets], ["genesetsUI", genesetsUI], ["layoutChoice", layoutChoice], + ["singleContinuousValue", singleContinuousValue], ["categoricalSelection", categoricalSelection], ["continuousSelection", continuousSelection], ["graphSelection", graphSelection], diff --git a/client/src/reducers/singleContinuousValue.ts b/client/src/reducers/singleContinuousValue.ts new file mode 100644 index 000000000..66300b3de --- /dev/null +++ b/client/src/reducers/singleContinuousValue.ts @@ -0,0 +1,30 @@ +import { Action, AnyAction } from "redux"; + +export interface SingleContinuousValueState { + singleContinuousValues: Map; +} + +const initialState = { + singleContinuousValues: new Map(), +}; + +export interface SingleContinuousValueAction extends Action { + 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;