Skip to content

Commit

Permalink
move TimelinePlot to TimelinePlotWithSpecies, and wrap it with the sp…
Browse files Browse the repository at this point in the history
…ecies selector component
  • Loading branch information
jay-hodgson committed Sep 20, 2023
1 parent bd0b27e commit c9881da
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 184 deletions.
2 changes: 1 addition & 1 deletion apps/portals/src/configurations/nf/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const metadataFilesSql = `SELECT id, resourceType, dataType, assay, diagn
export const fundersSql = 'SELECT * FROM syn16858699'
export const hackathonsSql = 'SELECT * FROM syn25585549'
export const observationsSql =
'SELECT observationSubmitterName as "submitterName", synapseId as "submitterUserId", observationTime as "time", observationTimeUnits as "timeUnits", observationText as "text", observationType as "tag" FROM syn51735464'
'SELECT observationSubmitterName as "submitterName", synapseId as "submitterUserId", observationTime as "time", observationTimeUnits as "timeUnits", observationText as "text", observationType as "tag", observationPhase as "phase" FROM syn51735464'
export const investigatorSql = `SELECT investigatorName as "firstName", ' ' as "lastName", institution, investigatorSynapseId as "USERID" FROM syn51734029 WHERE (investigatorName IS NOT NULL OR investigatorSynapseId IS NOT NULL)`
export const developmentPublicationSql = `SELECT * FROM syn51735467`
export const fundingAgencySql = `SELECT funderName as "Funding Agency" FROM syn51734076`
Expand Down
10 changes: 10 additions & 0 deletions apps/portals/src/configurations/nf/synapseConfigs/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,16 @@ export const toolDetailsPageConfig: DetailsPageProps = {
title: 'Observations',
uriValue: 'Observations',
synapseConfigArray: [
{
name: 'TimelinePlot',
outsideContainerClassName: 'home-spacer',
props: {
sql: observationsSql,
sqlOperator: ColumnSingleValueFilterOperator.EQUAL,
},
tableSqlKeys: ['resourceId'],
columnName: 'resourceId',
},
{
name: 'CardContainerLogic',
props: {
Expand Down
7 changes: 7 additions & 0 deletions apps/portals/src/types/portal-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
UserCardListRotateProps,
UserCardProps,
ChallengeDataDownloadProps,
TimelinePlotProps,
} from 'synapse-react-client'
import { RouteControlWrapperProps } from '../portal-components/RouteControlWrapper'
import { HomePageCardContainerProps } from '../portal-components/csbc-home-page/HomePageCardContainer'
Expand Down Expand Up @@ -281,6 +282,11 @@ type OrientationBanner = {
props: OrientationBannerProps
}

type TimelinePlot = {
name: 'TimelinePlot'
props: TimelinePlotProps
}

type RedirectWithQuery = {
name: 'RedirectWithQuery'
props: RedirectProps
Expand Down Expand Up @@ -347,6 +353,7 @@ export type SynapseConfig = (
| ChallengeDetailPageWrapper
| ChallengeDataDownload
| ChallengeSubmissionWrapper
| TimelinePlot
) &
Metadata

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { act, render, screen } from '@testing-library/react'
import React from 'react'
import TimelinePlot, { TimelinePlotProps } from './TimelinePlot'
import TimelinePlotWithSpecies, {
TimelinePlotWithSpeciesProps,
} from './TimelinePlotWithSpecies'
import { createWrapper } from '../../testutils/TestingLibraryUtils'

import { SynapseClient } from '../../index'
import queryResultBundleJson from '../../mocks/query/syn51735464'
import { ColumnSingleValueFilterOperator } from '@sage-bionetworks/synapse-types'

const timelineProps: TimelinePlotProps = {
const timelineProps: TimelinePlotWithSpeciesProps = {
observationsSql: 'select * from syn123',
species: 'Rattus norvegicus',
resourceId: '9971e47e-976a-4631-8edd-5cae04304b01',
additionalFilters: [
{
concreteType:
'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter',
operator: ColumnSingleValueFilterOperator.EQUAL,
columnName: 'resourceId',
values: ['9971e47e-976a-4631-8edd-5cae04304b01'],
},
],
}

async function renderTimeline(props: TimelinePlotProps = timelineProps) {
async function renderTimeline(
props: TimelinePlotWithSpeciesProps = timelineProps,
) {
let component
// eslint-disable-next-line @typescript-eslint/require-await
await act(async () => {
component = render(<TimelinePlot {...props} />, {
component = render(<TimelinePlotWithSpecies {...props} />, {
wrapper: createWrapper(),
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Meta, StoryObj } from '@storybook/react'
import TimelinePlot from './TimelinePlot'
import TimelinePlotWithSpecies from './TimelinePlotWithSpecies'
import { rest } from 'msw'
import { MOCK_REPO_ORIGIN } from '../../utils/functions/getEndpoint'
import { getHandlersForTableQuery } from '../../mocks/msw/handlers/tableQueryHandlers'
import { mockTableEntity } from '../../mocks/entity/mockTableEntity'
import queryResultBundleJson from '../../mocks/query/syn51735464'
import { getUserProfileHandlers } from '../../mocks/msw/handlers/userProfileHandlers'
import { ColumnSingleValueFilterOperator } from '@sage-bionetworks/synapse-types'

const meta = {
title: 'Components/TimelinePlot',
component: TimelinePlot,
component: TimelinePlotWithSpecies,
parameters: { stack: 'mock' },
} satisfies Meta
export default meta
Expand All @@ -19,7 +20,15 @@ export const Demo: Story = {
args: {
observationsSql: `SELECT observationId as "id", observationPhase as "phase", observationSubmitterName as "submitterName", synapseId as "submitterUserId", observationTime as "time", observationTimeUnits as "timeUnits", observationText as "text", observationType as "tag" FROM syn51735464`,
species: 'Rattus norvegicus',
resourceId: '9971e47e-976a-4631-8edd-5cae04304b01',
additionalFilters: [
{
concreteType:
'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter',
operator: ColumnSingleValueFilterOperator.EQUAL,
columnName: 'resourceId',
values: ['9971e47e-976a-4631-8edd-5cae04304b01'],
},
],
},
parameters: {
msw: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,202 +1,85 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import { useGetFullTableQueryResults } from '../../synapse-queries'
import { BUNDLE_MASK_QUERY_RESULTS } from '../../utils/SynapseConstants'
import hardcodedPhasesQueryResponseData, {
phaseObservationIndex,
phaseSpeciesIndex,
} from './phasesQueryResponseData'
import TimelinePhase from './TimelinePhase'
import getColorPalette from '../ColorGradient/ColorGradient'
import { Box } from '@mui/system'
import { ColumnSingleValueFilterOperator } from '@sage-bionetworks/synapse-types'
import { ObservationCardSchema } from '../row_renderers/ObservationCard'
import { parseEntityIdFromSqlStatement } from '../../utils/functions'
import { SizeMe } from 'react-sizeme'
import TimelineLegendItem from './TimelineLegendItem'
import { Skeleton } from '@mui/material'

const OBSERVATION_PHASE_COLUMN_NAME = 'phase'
const OBSERVATION_TIME_COLUMN_NAME = 'time'
const OBSERVATION_TIME_UNITS_COLUMN_NAME = 'timeunits'
const OBSERVATION_SUBMITTER_NAME_COLUMN_NAME = 'submittername'
const OBSERVATION_TEXT_COLUMN_NAME = 'text'
const OBSERVATION_TYPE_COLUMN_NAME = 'tag'
const OBSERVATION_SUBMITTER_USER_ID_COLUMN_NAME = 'submitteruserid'
import {
SQLOperator,
getAdditionalFilters,
parseEntityIdFromSqlStatement,
} from '../../utils/functions'
import { InputLabel, Select, MenuItem } from '@mui/material'
import { StyledFormControl } from '../styled'
import TimelinePlotWithSpecies from './TimelinePlotWithSpecies'

export type TimelinePlotProps = {
species: string
resourceId: string
observationsSql: string
sql: string
searchParams?: Record<string, string>
sqlOperator?: SQLOperator
}
export const TimelinePlot = ({
observationsSql,
species,
resourceId,
sql,
searchParams,
sqlOperator,
}: TimelinePlotProps) => {
// Fetch the table data
const eventsTableId = parseEntityIdFromSqlStatement(observationsSql)
const eventsTableId = parseEntityIdFromSqlStatement(sql)
const [species, setSpecies] = useState<string | undefined | null>()
const queryFilters = getAdditionalFilters(
eventsTableId,
searchParams,
sqlOperator,
)
// Fetch the species
const eventTableQuery = useGetFullTableQueryResults({
entityId: eventsTableId,
query: {
sql: `${observationsSql} WHERE observationTime IS NOT NULL`,
sort: [
{
column: 'observationTime',
direction: 'ASC',
},
],
additionalFilters: [
{
columnName: 'resourceId',
concreteType:
'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter',
values: [resourceId],
operator: ColumnSingleValueFilterOperator.EQUAL,
},
{
columnName: 'species',
concreteType:
'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter',
values: [species],
operator: ColumnSingleValueFilterOperator.EQUAL,
},
],
sql: `SELECT species FROM ${eventsTableId} WHERE species IS NOT null GROUP BY species`,
additionalFilters: queryFilters,
},

partMask: BUNDLE_MASK_QUERY_RESULTS,
concreteType: 'org.sagebionetworks.repo.model.table.QueryBundleRequest',
})

const { data: eventsData, isLoading } = eventTableQuery

if (isLoading) {
return <LoadingTimelinePlot />
}
const observationPhaseIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_PHASE_COLUMN_NAME,
)!
const observationTimeIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_TIME_COLUMN_NAME,
)!
const observationTimeUnitIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header =>
header.name.toLowerCase() === OBSERVATION_TIME_UNITS_COLUMN_NAME,
)!
const observationSubmitterNameIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header =>
header.name.toLowerCase() === OBSERVATION_SUBMITTER_NAME_COLUMN_NAME,
)!
const observationTextIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_TEXT_COLUMN_NAME,
)!
const observationTypeIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_TYPE_COLUMN_NAME,
)!
const submitterUserIdIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header =>
header.name.toLowerCase() === OBSERVATION_SUBMITTER_USER_ID_COLUMN_NAME,
)!

const schema: ObservationCardSchema = {
submitterName: observationSubmitterNameIndex,
submitterUserId: submitterUserIdIndex,
tag: observationTypeIndex,
text: observationTextIndex,
time: observationTimeIndex,
timeUnits: observationTimeUnitIndex,
}
const { data: speciesData, isLoading } = eventTableQuery
const rows = speciesData?.queryResult?.queryResults?.rows

// filter the phases query response data to the specific species
const phasesForTargetSpecies =
hardcodedPhasesQueryResponseData.queryResult?.queryResults.rows.filter(
row => {
return row.values[phaseSpeciesIndex] == species
},
)
// then walk through the phases and create a plot for each (iff event data exists for that phase!)
useEffect(() => {
if (rows) {
setSpecies(rows[0].values[0])
}
}, [rows])

if (!phasesForTargetSpecies || phasesForTargetSpecies.length == 0) {
if (isLoading || !rows || rows.length == 0) {
return <></>
}

const phaseRowsWithData = phasesForTargetSpecies.filter(phaseRow => {
const phaseEventRows = eventsData?.queryResult?.queryResults.rows.filter(
row => {
return (
row.values[observationPhaseIndex] ==
phaseRow.values[phaseObservationIndex]
)
},
)
return phaseEventRows?.length && phaseEventRows?.length > 0
})
return (
<Box>
{/* Legend */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: '25px' }}>
{phaseRowsWithData.map((phaseRow, index) => {
const { colorPalette } = getColorPalette(index, 1)
return (
<TimelineLegendItem
key={phaseRow.rowId}
color={colorPalette[0]}
phaseName={phaseRow.values[phaseObservationIndex]}
/>
)
})}
</Box>
{/* Phase plots */}
<SizeMe refreshMode="debounce" noPlaceholder={true}>
{({ size }) => (
<Box sx={{ display: 'flex' }} className="forcePlotlyDefaultCursor">
{phaseRowsWithData.map((phaseRow, index) => {
const { colorPalette } = getColorPalette(index, 1)
const phaseEventRows =
eventsData?.queryResult?.queryResults.rows.filter(row => {
return (
row.values[observationPhaseIndex] ==
phaseRow.values[phaseObservationIndex]
)
})
return (
<TimelinePhase
key={phaseRow.rowId}
name={phaseRow.values[phaseObservationIndex]!}
color={colorPalette[0]}
rowData={phaseEventRows!}
schema={schema}
widthPx={
size.width ? size.width / phaseRowsWithData.length : 0
}
/>
)
})}
</Box>
)}
</SizeMe>
<StyledFormControl>
<InputLabel>Species</InputLabel>
<Select
value={species}
defaultValue={rows[0].values[0]}
label="Species"
onChange={event => setSpecies(event.target.value)}
>
{rows?.map(row => {
const species = row.values[0]!
return (
<MenuItem key={species} value={species}>
{species}
</MenuItem>
)
})}
</Select>
</StyledFormControl>
{species && (
<TimelinePlotWithSpecies
observationsSql={sql}
species={species}
additionalFilters={queryFilters}
/>
)}
</Box>
)
}

const LoadingTimelinePlot = () => {
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
<Skeleton height="45px" width="80px" />
<Skeleton height="45px" width="80px" />
</Box>
<Box sx={{ display: 'flex' }}>
<Skeleton height="150px" width="100%" />
</Box>
</Box>
)
}

export default TimelinePlot
Loading

0 comments on commit c9881da

Please sign in to comment.