Skip to content

Commit

Permalink
move set of DE2 hardcoded "metadata slices" to the backend (#31)
Browse files Browse the repository at this point in the history
A "metadata slice" is a slice ID that can be used by Data Explorer 2 as
a variable in a context or to color points.

Previously, there was a hardcoded JSON object on the frontend that
described these slices and how they can be used. Now that hardcoded data
has been moved to the backend and requested asynchronously.

That new async behavior is the first step in making context properties
sourced from Breadbox. Eventually the hardcoded data will be removed and
a request will be made to Breadbox to discover what types of metadata
are available.
  • Loading branch information
rcreasi authored Aug 14, 2024
1 parent e479906 commit 1f76c0c
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 193 deletions.
7 changes: 6 additions & 1 deletion frontend/packages/@depmap/data-explorer-2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ export {
fetchGeneTeaTermContext,
fetchLinearRegression,
fetchMetadataColumn,
fetchMetadataSlices,
fetchPlotDimensions,
fetchUniqueValuesOrRange,
fetchWaterfall,
persistContext,
} from "./src/api";

export type { GeneTeaEnrichedTerms, GeneTeaTermContext } from "./src/api";
export type {
GeneTeaEnrichedTerms,
GeneTeaTermContext,
MetadataSlices,
} from "./src/api";

export {
DataExplorerSettingsProvider,
Expand Down
18 changes: 18 additions & 0 deletions frontend/packages/@depmap/data-explorer-2/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,24 @@ export async function fetchLinearRegression(
return postJson("/linear_regression", json);
}

type SliceId = string;
export type MetadataSlices = Record<
SliceId,
{
name: string;
valueType: "categorical" | "list_strings";
isHighCardinality?: boolean;
isPartialSliceId?: boolean;
entityTypeLabel?: string;
}
>;

export async function fetchMetadataSlices(dimension_type: string) {
const query = `dimension_type=${encodeURIComponent(dimension_type)}`;

return fetchJson<MetadataSlices>(`/metadata_slices?${query}`);
}

// *****************************************************************************
// * Context cache *
// *****************************************************************************
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React from "react";
import { PlotConfigSelect, SliceLabelSelector } from "@depmap/data-explorer-2";
import React, { useEffect, useState } from "react";
import {
fetchMetadataSlices,
MetadataSlices,
PlotConfigSelect,
SliceLabelSelector,
} from "@depmap/data-explorer-2";
import {
containsPartialSlice,
getDatasetIdFromSlice,
Expand All @@ -25,16 +30,31 @@ function DatasetMetadataSelector({
value,
onChange,
}: Props) {
const options = getOptions(entity_type);
const hasDynamicLabel = containsPartialSlice(value, entity_type);
const [isLoading, setIsLoading] = useState(false);
const [metadataSlices, setMetadataSlices] = useState<MetadataSlices>({});

useEffect(() => {
(async () => {
setIsLoading(true);

const slices = await fetchMetadataSlices(entity_type);
setMetadataSlices(slices);

setIsLoading(false);
})();
}, [entity_type]);

const hasDynamicLabel = containsPartialSlice(metadataSlices, value);

const value1 = hasDynamicLabel
? slicePrefix(value as string, entity_type)
? slicePrefix(metadataSlices, value as string)
: value;
const value2 = hasDynamicLabel ? sliceLabel(value as string) : null;

const options = getOptions(metadataSlices);

if (typeof value1 === "string" && !(value1 in options)) {
options[value1] = "(unknown property)";
options[value1] = isLoading ? "Loading…" : "(unknown property)";
}

return (
Expand All @@ -44,21 +64,22 @@ function DatasetMetadataSelector({
isClearable
placeholder="Choose property…"
show={show}
enable={enable}
enable={enable && !isLoading}
value={value1}
options={options}
onChange={onChange}
isLoading={isLoading}
/>
{hasDynamicLabel && (
<SliceLabelSelector
value={value2}
onChange={onChange}
isClearable={false}
menuPortalTarget={null}
dataset_id={getDatasetIdFromSlice(value as string, entity_type)}
dataset_id={getDatasetIdFromSlice(metadataSlices, value as string)}
entityTypeLabel={getMetadataEntityTypeLabelFromSlice(
value as string,
entity_type
metadataSlices,
value as string
)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import metadataSlices from "src/data-explorer-2/json/metadata-slices.json";
import { MetadataSlices } from "@depmap/data-explorer-2";

export const slicePrefix = (value: string, entity_type: string) => {
const md = (metadataSlices as Record<string, object>)[entity_type];
export const slicePrefix = (slices: MetadataSlices, value: string) => {
let out = "";

Object.keys(md).forEach((sliceId) => {
Object.keys(slices).forEach((sliceId) => {
if (value.includes(sliceId)) {
out = sliceId;
}
Expand All @@ -22,57 +21,49 @@ export const sliceLabel = (value: string) => {
return label ? decodeURIComponent(label) : null;
};

export const getDatasetIdFromSlice = (value: string, entity_type: string) => {
return slicePrefix(value, entity_type).replace("slice/", "").slice(0, -1);
export const getDatasetIdFromSlice = (
slices: MetadataSlices,
value: string
) => {
return slicePrefix(slices, value).replace("slice/", "").slice(0, -1);
};

export const getMetadataEntityTypeLabelFromSlice = (
value: string,
entity_type: string
slices: MetadataSlices,
value: string
) => {
const md = (metadataSlices as Record<string, object>)[entity_type];
let out = "";

Object.entries(md).forEach(([sliceId, descriptor]) => {
if (sliceId === slicePrefix(value, entity_type)) {
out = descriptor.entityTypeLabel;
Object.entries(slices).forEach(([sliceId, descriptor]) => {
if (sliceId === slicePrefix(slices, value)) {
out = descriptor.entityTypeLabel as string;
}
});

return out;
};

export const containsPartialSlice = (
value: string | null,
entity_type: string
slices: MetadataSlices,
value: string | null
) => {
if (!value) {
return false;
}

const md = (metadataSlices as Record<string, object>)[entity_type];

return Object.entries(md).some(
([sliceId, descriptor]) =>
sliceId === slicePrefix(value, entity_type) && descriptor.isPartialSliceId
return Object.entries(slices).some(
([sliceId, sliceInfo]) =>
sliceId === slicePrefix(slices, value) && sliceInfo.isPartialSliceId
);
};

export const getOptions = (entity_type: string) => {
const md = metadataSlices;
const dimensionMetadata: Record<
string,
{ name: string; isHighCardinality?: boolean }
> = md[entity_type as "depmap_model" | "gene" | "compound_experiment"];

export const getOptions = (slices: MetadataSlices) => {
const options: Record<string, string> = {};

Object.keys(dimensionMetadata || {})
.filter(
(slice_id: string) => !dimensionMetadata[slice_id].isHighCardinality
)
Object.keys(slices)
.filter((slice_id: string) => !slices[slice_id].isHighCardinality)
.forEach((slice_id) => {
options[slice_id] = dimensionMetadata[slice_id].name;
options[slice_id] = slices[slice_id].name;
});

return options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Checkbox } from "react-bootstrap";
import {
capitalize,
fetchDatasetsByIndexType,
fetchMetadataSlices,
getDimensionTypeLabel,
PlotConfigSelect,
pluralize,
Expand All @@ -13,7 +14,6 @@ import {
DataExplorerDatasetDescriptor,
DataExplorerPlotConfig,
} from "@depmap/types";
import metadataSlices from "src/data-explorer-2/json/metadata-slices.json";
import HelpTip from "src/data-explorer-2/components/HelpTip";
import styles from "src/data-explorer-2/styles/ConfigurationPanel.scss";

Expand Down Expand Up @@ -142,6 +142,15 @@ export function ColorByTypeSelector({
onChange: (nextValue: DataExplorerPlotConfig["color_by"]) => void;
}) {
const entityTypeLabel = capitalize(getDimensionTypeLabel(entity_type));
const [hasSomeColorProperty, setHasSomeColorProperty] = useState(false);

useEffect(() => {
(async () => {
const keyedSlices = await fetchMetadataSlices(entity_type);
const slices = Object.values(keyedSlices);
setHasSomeColorProperty(slices.some((slice) => !slice.isHighCardinality));
})();
}, [entity_type]);

const options: Record<string, string> = {
entity: entityTypeLabel,
Expand All @@ -163,25 +172,15 @@ export function ColorByTypeSelector({
);
}

const md = metadataSlices as Record<string, object>;

if (Object.keys(md).includes(entity_type)) {
const hasSomeColorProperty = Object.values(md[entity_type]).some(
(entry) => {
return !entry.isHighCardinality;
}
if (hasSomeColorProperty || value === "property") {
options.property = `${entityTypeLabel} Property`;
helpContent.push(
<p key={2}>
Choose <b>{entityTypeLabel} property</b> to color by major properties of
the {entityTypeLabel}, such as selectivity for genes or lineage for
models.
</p>
);

if (hasSomeColorProperty) {
options.property = `${entityTypeLabel} Property`;
helpContent.push(
<p key={2}>
Choose <b>{entityTypeLabel} property</b> to color by major properties
of the {entityTypeLabel}, such as selectivity for genes or lineage for
models.
</p>
);
}
}

if (entity_type !== "other") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React, {
useCallback,
useEffect,
Expand All @@ -12,7 +11,7 @@ import {
fetchUniqueValuesOrRange,
isPartialSliceId,
} from "@depmap/data-explorer-2";
import metadata from "src/data-explorer-2/json/metadata-slices.json";
import { useContextBuilderContext } from "src/data-explorer-2/components/ContextBuilder/ContextBuilderContext";
import {
floor,
getOperator,
Expand Down Expand Up @@ -62,6 +61,7 @@ function Comparison({
const [summary, setSummary] = useState<Summary | null>(null);
const [, forceRender] = useState<null>();
const ref = useRef<HTMLDivElement | null>(null);
const { metadataSlices, isLoading } = useContextBuilderContext();

useLayoutEffect(() => forceRender(null), []);

Expand Down Expand Up @@ -100,10 +100,7 @@ function Comparison({
const handleChangeDataSelect = useCallback(
(option: { value: string } | null) => {
const selectedSlice = option!.value;
const valueType = getValueType(
(metadata as any)[entity_type],
selectedSlice
);
const valueType = getValueType(metadataSlices, selectedSlice);
const operator = valueType === "list_strings" ? "has_any" : "==";
const operands = [{ var: selectedSlice }, null];
const nextValue = { [operator]: operands };
Expand All @@ -116,7 +113,7 @@ function Comparison({
},
});
},
[dispatch, path, entity_type]
[dispatch, path, metadataSlices]
);

useEffect(() => {
Expand Down Expand Up @@ -184,8 +181,8 @@ function Comparison({
path={path}
op={op}
dispatch={dispatch}
value_type={getValueType((metadata as any)[entity_type], slice_id)}
isLoading={slice_id && !summary}
value_type={getValueType(metadataSlices, slice_id)}
isLoading={isLoading || (slice_id && !summary)}
/>
<RhsComponent
key={slice_id}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import { fetchMetadataSlices, MetadataSlices } from "@depmap/data-explorer-2";

const ContextBuilderContext = createContext({
metadataSlices: {} as MetadataSlices,
isLoading: false,
});

export const useContextBuilderContext = () => {
return useContext(ContextBuilderContext);
};

export const ContextBuilderContextProvider = ({
dimension_type,
children,
}: {
dimension_type: string | undefined;
children: React.ReactNode;
}) => {
const [isLoading, setIsLoading] = useState(true);
const [metadataSlices, setMetadataSlices] = useState<MetadataSlices>({});

useEffect(() => {
(async () => {
if (dimension_type) {
setIsLoading(true);

const slices = await fetchMetadataSlices(dimension_type);
setMetadataSlices(slices);

setIsLoading(false);
}
})();
}, [dimension_type]);

return (
<ContextBuilderContext.Provider
value={{
isLoading,
metadataSlices,
}}
>
{children}
</ContextBuilderContext.Provider>
);
};
Loading

0 comments on commit 1f76c0c

Please sign in to comment.