diff --git a/packages/libs/coreui/src/components/inputs/SelectList.tsx b/packages/libs/coreui/src/components/inputs/SelectList.tsx index aa6b80bfdc..1de1366795 100644 --- a/packages/libs/coreui/src/components/inputs/SelectList.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectList.tsx @@ -7,6 +7,8 @@ export interface SelectListProps extends CheckboxListProps { /** A button's content if/when no values are currently selected */ defaultButtonDisplayContent: ReactNode; isDisabled?: boolean; + /** Are contents loading? */ + isLoading?: boolean; } export default function SelectList({ @@ -18,6 +20,7 @@ export default function SelectList({ children, defaultButtonDisplayContent, isDisabled = false, + isLoading = false, ...props }: SelectListProps) { const [selected, setSelected] = useState['value']>(value); @@ -67,6 +70,7 @@ export default function SelectList({ margin: '0.5em', }} > + {isLoading &&
Loading...
} ; +export const LabeledRange = intersection([ + type({ + label: string, + }), + partial({ + max: string, + min: string, + }), +]); + export type ContinousVariableMetadataResponse = TypeOf< typeof ContinousVariableMetadataResponse >; diff --git a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx index 5bc1d7e7b5..d3b8a91438 100644 --- a/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx +++ b/packages/libs/eda/src/lib/core/components/computations/plugins/differentialabundance.tsx @@ -1,4 +1,10 @@ -import { useCollectionVariables, useStudyMetadata } from '../../..'; +import { + ContinuousVariableDataShape, + LabeledRange, + useCollectionVariables, + usePromise, + useStudyMetadata, +} from '../../..'; import { VariableDescriptor } from '../../../types/variable'; import { volcanoPlotVisualization } from '../../visualizations/implementations/VolcanoPlotVisualization'; import { ComputationConfigProps, ComputationPlugin } from '../Types'; @@ -7,8 +13,11 @@ import { useConfigChangeHandler, assertComputationWithConfig } from '../Utils'; import * as t from 'io-ts'; import { Computation } from '../../../types/visualization'; import SingleSelect from '@veupathdb/coreui/lib/components/inputs/SingleSelect'; -import { useFindEntityAndVariable } from '../../../hooks/workspace'; -import { useMemo } from 'react'; +import { + useDataClient, + useFindEntityAndVariable, +} from '../../../hooks/workspace'; +import { useCallback, useMemo } from 'react'; import { ComputationStepContainer } from '../ComputationStepContainer'; import VariableTreeDropdown from '../../variableTrees/VariableTreeDropdown'; import { ValuePicker } from '../../visualizations/implementations/ValuePicker'; @@ -19,6 +28,10 @@ import { SwapHorizOutlined } from '@material-ui/icons'; import './Plugins.scss'; import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import { Tooltip } from '@material-ui/core'; +import { + GetBinRangesProps, + getBinRanges, +} from '../../../../map/analysis/utils/defaultOverlayConfig'; const cx = makeClassNameHelper('AppStepConfigurationContainer'); @@ -44,8 +57,8 @@ export type DifferentialAbundanceConfig = t.TypeOf< const Comparator = t.intersection([ t.partial({ - groupA: t.array(t.string), - groupB: t.array(t.string), + groupA: t.array(LabeledRange), + groupB: t.array(LabeledRange), }), t.type({ variable: VariableDescriptor, @@ -111,6 +124,7 @@ function DifferentialAbundanceConfigDescriptionComponent({ collectionVariable ) ); + return (

@@ -150,6 +164,7 @@ export function DifferentialAbundanceConfiguration( const configuration = computation.descriptor .configuration as DifferentialAbundanceConfig; const studyMetadata = useStudyMetadata(); + const dataClient = useDataClient(); const toggleStarredVariable = useToggleStarredVariable(props.analysisState); const filters = analysisState.analysis?.descriptor.subset.descriptor; const findEntityAndVariable = useFindEntityAndVariable(filters); @@ -215,10 +230,56 @@ export function DifferentialAbundanceConfiguration( } }, [configuration, findEntityAndVariable]); + // If the variable is continuous, ask the backend for a list of bins + const continuousVariableBins = usePromise( + useCallback(async () => { + if ( + !ContinuousVariableDataShape.is( + selectedComparatorVariable?.variable.dataShape + ) + ) + return; + + const binRangeProps: GetBinRangesProps = { + studyId: studyMetadata.id, + ...configuration.comparator?.variable, + filters: filters ?? [], + dataClient, + binningMethod: 'quantile', + }; + const bins = await getBinRanges(binRangeProps); + return bins; + }, [ + dataClient, + configuration?.comparator?.variable, + filters, + selectedComparatorVariable, + studyMetadata.id, + ]) + ); + const disableSwapGroupValuesButton = !configuration?.comparator?.groupA && !configuration?.comparator?.groupB; const disableGroupValueSelectors = !configuration?.comparator?.variable; + // Create the options for groupA and groupB. Organizing into the LabeledRange[] format + // here in order to keep the later code clean. + const groupValueOptions = continuousVariableBins.value + ? continuousVariableBins.value.map((bin): LabeledRange => { + return { + min: bin.binStart, + max: bin.binEnd, + label: bin.binLabel, + }; + }) + : selectedComparatorVariable?.variable.vocabulary?.map( + (value): LabeledRange => { + return { + label: value, + }; + } + ); + return ( Group A option.label) + : undefined } - selectedValues={configuration?.comparator?.groupA} - disabledValues={configuration?.comparator?.groupB} - onSelectedValuesChange={(newValues) => + selectedValues={configuration?.comparator?.groupA?.map( + (entry) => entry.label + )} + disabledValues={configuration?.comparator?.groupB?.map( + (entry) => entry.label + )} + onSelectedValuesChange={(newValues) => { changeConfigHandler('comparator', { variable: configuration?.comparator?.variable ?? undefined, - groupA: newValues.length ? newValues : undefined, + groupA: newValues.length + ? groupValueOptions?.filter((option) => + newValues.includes(option.label) + ) + : undefined, groupB: configuration?.comparator?.groupB ?? undefined, - }) - } + }); + }} disabledCheckboxTooltipContent="Values cannot overlap between groups" showClearSelectionButton={false} disableInput={disableGroupValueSelectors} + isLoading={continuousVariableBins.pending} /> Group B option.label) + : undefined } - selectedValues={configuration?.comparator?.groupB} - disabledValues={configuration?.comparator?.groupA} + selectedValues={configuration?.comparator?.groupB?.map( + (entry) => entry.label + )} + disabledValues={configuration?.comparator?.groupA?.map( + (entry) => entry.label + )} onSelectedValuesChange={(newValues) => changeConfigHandler('comparator', { variable: configuration?.comparator?.variable ?? undefined, groupA: configuration?.comparator?.groupA ?? undefined, - groupB: newValues.length ? newValues : undefined, + groupB: newValues.length + ? groupValueOptions?.filter((option) => + newValues.includes(option.label) + ) + : undefined, }) } disabledCheckboxTooltipContent="Values cannot overlap between groups" showClearSelectionButton={false} disableInput={disableGroupValueSelectors} + isLoading={continuousVariableBins.pending} />

diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ValuePicker.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ValuePicker.tsx index 0aefb33d93..4aa00f306d 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/ValuePicker.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/ValuePicker.tsx @@ -11,6 +11,8 @@ export type ValuePickerProps = { disabledCheckboxTooltipContent?: ReactNode; disableInput?: boolean; showClearSelectionButton?: boolean; + /** Show loading spinner */ + isLoading?: boolean; }; const EMPTY_ALLOWED_VALUES_ARRAY: string[] = []; @@ -25,6 +27,7 @@ export function ValuePicker({ disabledCheckboxTooltipContent, disableInput = false, showClearSelectionButton = true, + isLoading = false, }: ValuePickerProps) { const items = allowedValues.map((value) => ({ display: {value}, @@ -41,6 +44,7 @@ export function ValuePicker({ value={selectedValues} disabledCheckboxTooltipContent={disabledCheckboxTooltipContent} isDisabled={disableInput} + isLoading={isLoading} /> {showClearSelectionButton && ( ) { // Standard volcano plots have -log10(raw p value) as the y axis const yAxisMin = -Math.log10(dataYMax); const yAxisMax = -Math.log10(dataYMin); + // Add a little padding to prevent clipping the glyph representing the extreme points return { min: Math.floor(yAxisMin - (yAxisMax - yAxisMin) * AXIS_PADDING_FACTOR), @@ -382,8 +383,14 @@ function VolcanoPlotViz(props: VisualizationProps) { computationConfiguration.comparator?.groupA && computationConfiguration.comparator?.groupB ? [ - 'Up in ' + computationConfiguration.comparator.groupA.join(', '), - 'Up in ' + computationConfiguration.comparator.groupB.join(', '), + 'Up in ' + + computationConfiguration.comparator.groupA + .map((entry) => entry.label) + .join(','), + 'Up in ' + + computationConfiguration.comparator.groupB + .map((entry) => entry.label) + .join(','), ] : []; @@ -562,17 +569,17 @@ function VolcanoPlotViz(props: VisualizationProps) { markerColor: significanceColors['inconclusive'], }, { - label: `Up regulated in ${computationConfiguration.comparator.groupB?.join( - ', ' - )} (${countsData[significanceColors['high']]})`, + label: `Up regulated in ${computationConfiguration.comparator.groupB + ?.map((entry) => entry.label) + .join(',')} (${countsData[significanceColors['high']]})`, marker: 'circle', hasData: true, markerColor: significanceColors['high'], }, { - label: `Up regulated in ${computationConfiguration.comparator.groupA?.join( - ', ' - )} (${countsData[significanceColors['low']]})`, + label: `Up regulated in ${computationConfiguration.comparator.groupA + ?.map((entry) => entry.label) + .join(',')} (${countsData[significanceColors['low']]})`, marker: 'circle', hasData: true, markerColor: significanceColors['low'], diff --git a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts index 275750918e..e8cc10e6ad 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts +++ b/packages/libs/eda/src/lib/core/components/visualizations/options/types.ts @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { OverlayConfig } from '../../../api/DataClient'; import { Filter } from '../../../types/filter'; import { VariableDescriptor } from '../../../types/variable'; @@ -11,7 +12,7 @@ export interface OverlayOptions { getOverlayVariable?: ( computeConfig: unknown ) => VariableDescriptor | undefined; - getOverlayVariableHelp?: () => string; + getOverlayVariableHelp?: () => ReactNode; getOverlayType?: () => OverlayConfig['overlayType'] | undefined; getOverlayVocabulary?: () => string[] | undefined; getCheckedLegendItems?: (computeConfig: unknown) => string[] | undefined; diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableVisualization.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableVisualization.tsx index 701c00b4c7..9f3d47492d 100644 --- a/packages/libs/eda/src/lib/map/analysis/DraggableVisualization.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableVisualization.tsx @@ -75,7 +75,7 @@ export default function DraggableVisualization({ panelTitle={activeVizOverview?.displayName || ''} defaultPosition={{ x: 535, - y: 142, + y: 220, }} onPanelDismiss={() => setActiveVisualizationId(undefined)} > diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index e1fd0ee3bd..ad70931ae4 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -636,7 +636,14 @@ function MapAnalysisImpl(props: ImplProps) { ); const plugins = useStandaloneVizPlugins({ - selectedOverlayConfig: activeOverlayConfig.value, + selectedOverlayConfig: + activeMarkerConfigurationType === 'bubble' + ? undefined + : activeOverlayConfig.value, + overlayHelp: + activeMarkerConfigurationType === 'bubble' + ? 'Overlay variables are not available for this map type' + : undefined, }); const subsetVariableAndEntity = useMemo(() => { diff --git a/packages/libs/eda/src/lib/map/analysis/MapHeader.scss b/packages/libs/eda/src/lib/map/analysis/MapHeader.scss index 7aa2bcd877..e77298a133 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapHeader.scss +++ b/packages/libs/eda/src/lib/map/analysis/MapHeader.scss @@ -62,17 +62,16 @@ align-items: flex-start; flex-direction: column; - &__SaveableTextEditorContainer { - cursor: text; - font-size: 19px; + .wdk-SaveableTextEditor { + font-style: italic; } &__StudyName { cursor: default; + margin-right: 1em; } &__AnalysisTitle { - font-style: italic; padding: 0; font-size: 19px; } diff --git a/packages/libs/eda/src/lib/map/analysis/MapHeader.tsx b/packages/libs/eda/src/lib/map/analysis/MapHeader.tsx index 40ba17b006..d390dc0208 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapHeader.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapHeader.tsx @@ -180,30 +180,19 @@ function HeaderContent({ return (
-
- void) => { - return ( -

- e.stopPropagation()} - className={headerContent('__StudyName')} - > - {safeHtml(studyName, { style: { fontWeight: 'bold' } })}:{' '} - - {analysisName} -

- ); - }} - maxLength={ANALYSIS_NAME_MAX_LENGTH} - onSave={onAnalysisNameEdit} - value={analysisName} - /> +
+

+ MapVEu — + + {safeHtml(studyName)} + + {analysisName}} + maxLength={ANALYSIS_NAME_MAX_LENGTH} + onSave={onAnalysisNameEdit} + value={analysisName} + /> +

{filterList}
diff --git a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx index 99bacf8cad..84885d2f0a 100644 --- a/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MarkerConfiguration/BubbleMarkerConfigurationMenu.tsx @@ -13,6 +13,7 @@ import { aggregationHelp, AggregationInputs, } from '../../../core/components/visualizations/implementations/LineplotVisualization'; +import { DataElementConstraint } from '../../../core/types/visualization'; type AggregatorOption = typeof aggregatorOptions[number]; const aggregatorOptions = ['mean', 'median'] as const; @@ -189,7 +190,22 @@ export function BubbleMarkerConfigurationMenu({ onChange={handleInputVariablesOnChange} starredVariables={starredVariables} toggleStarredVariable={toggleStarredVariable} - constraints={constraints} + constraints={ + // TEMPORARILY disable date vars; TO DO for dates - remove! + constraints?.map((constraint) => { + return Object.fromEntries( + Object.keys(constraint).map((key) => [ + key, + { + ...constraint[key], + allowedTypes: constraint[key]?.allowedTypes?.filter( + (t) => t !== 'date' + ) ?? ['string', 'number', 'integer'], + } as DataElementConstraint, // assertion seems required due to spread operator + ]) + ); + }) + } flexDirection="column" />
diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts index 2abe58a43d..b239a33681 100644 --- a/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts +++ b/packages/libs/eda/src/lib/map/analysis/hooks/standaloneVizPlugins.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import * as t from 'io-ts'; import { ComputationPlugin } from '../../../core/components/computations/Types'; import { ZeroConfigWithButton } from '../../../core/components/computations/ZeroConfiguration'; @@ -33,12 +33,14 @@ import _ from 'lodash'; interface Props { selectedOverlayConfig?: OverlayConfig | BubbleOverlayConfig; + overlayHelp?: ReactNode; } type StandaloneVizOptions = LayoutOptions & OverlayOptions; export function useStandaloneVizPlugins({ selectedOverlayConfig, + overlayHelp = 'The overlay variable can be selected via the top-right panel.', }: Props): Record { return useMemo(() => { function vizWithOptions( @@ -67,8 +69,7 @@ export function useStandaloneVizPlugins({ return overlayValues; } }, - getOverlayVariableHelp: () => - 'The overlay variable can be selected via the top-right panel.', + getOverlayVariableHelp: () => overlayHelp, }); } diff --git a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts index 9c44d8a44c..b60f795c2b 100644 --- a/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts +++ b/packages/libs/eda/src/lib/map/analysis/utils/defaultOverlayConfig.ts @@ -154,7 +154,7 @@ async function getMostFrequentValues({ : [...sortedValues.slice(0, numValues), UNSELECTED_TOKEN]; } -type GetBinRangesProps = { +export type GetBinRangesProps = { studyId: string; variableId: string; entityId: string; @@ -164,7 +164,7 @@ type GetBinRangesProps = { }; // get the equal spaced bin definitions (for now at least) -async function getBinRanges({ +export async function getBinRanges({ studyId, variableId, entityId, diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx index bfcf7ef0c8..482eacf7a7 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx @@ -52,7 +52,6 @@ import { useAnnouncementsState } from '@veupathdb/web-common/lib/hooks/announcem import { useCommunitySiteRootUrl } from '@veupathdb/web-common/lib/hooks/staticData'; import { STATIC_ROUTE_PATH } from '@veupathdb/web-common/lib/routes'; import { formatReleaseDate } from '@veupathdb/web-common/lib/util/formatters'; -import { useWdkStudyRecords } from '@veupathdb/eda/lib/core/hooks/study'; import { getWdkStudyRecords } from '@veupathdb/eda/lib/core/utils/study-records'; import { PreferredOrganismsSummary } from '@veupathdb/preferred-organisms/lib/components/PreferredOrganismsSummary'; @@ -63,14 +62,16 @@ import { PageDescription } from './PageDescription'; import { makeVpdbClassNameHelper } from './Utils'; import './VEuPathDBHomePage.scss'; -import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { + makeClassNameHelper, + safeHtml, +} from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import SubsettingClient from '@veupathdb/eda/lib/core/api/SubsettingClient'; import { WdkDependenciesContext } from '@veupathdb/wdk-client/lib/Hooks/WdkDependenciesEffect'; import { useNonNullableContext } from '@veupathdb/wdk-client/lib/Hooks/NonNullableContext'; -import { useWdkService } from '@veupathdb/wdk-client/lib/Hooks/WdkServiceHook'; import { Question } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; -import { StudyRecord } from '@veupathdb/eda/lib/core/types/study'; import { Warning } from '@veupathdb/coreui'; +import { useWdkService } from '@veupathdb/wdk-client/lib/Hooks/WdkServiceHook'; const vpdbCx = makeVpdbClassNameHelper(''); @@ -368,6 +369,13 @@ const useHeaderMenuItems = ( (q) => q.urlSegment === QUESTION_FOR_MAP_DATASETS ) ); + const mapStudy = useWdkService( + (wdkService) => + wdkService + .getRecord('dataset', [{ name: 'dataset_id', value: 'DS_480c976ef9' }]) + .catch(() => {}), + [] + ); // const showInteractiveMaps = mapMenuItemsQuestion != null; // const mapMenuItems = useMapMenuItems(mapMenuItemsQuestion); const showInteractiveMaps = projectId === VectorBase && !!useEda; @@ -601,7 +609,7 @@ const useHeaderMenuItems = ( type: 'reactRoute', display: ( <> - MapVEu - Multi-study Interactive Map{' '} + MapVEu - {safeHtml(mapStudy?.displayName ?? '')}{' '} BETA ), @@ -609,7 +617,7 @@ const useHeaderMenuItems = ( url: '/workspace/maps/DS_480c976ef9/new', target: '_blank', metadata: { - test: () => showInteractiveMaps, + test: () => showInteractiveMaps && mapStudy != null, }, }, {