Skip to content

Commit

Permalink
progress?
Browse files Browse the repository at this point in the history
  • Loading branch information
jay-hodgson committed Sep 14, 2023
1 parent 082cfb6 commit ab2e1b3
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import React, { useEffect, useRef, useState } from 'react'
import React, { useRef, useState } from 'react'
import Plotly, { Layout } from 'plotly.js-basic-dist'
import createPlotlyComponent from 'react-plotly.js/factory'
import { ObservationEvent } from './TimelinePlot'
import dayjs, { ManipulateType } from 'dayjs'
import { Paper, Typography } from '@mui/material'
import { Dialog } from '@mui/material'
import {
ObservationCard,
ObservationCardSchema,
} from '../row_renderers/ObservationCard'
import { Row } from '@sage-bionetworks/synapse-types'
import pluralize from 'pluralize'
const Plot = createPlotlyComponent(Plotly)

const getTimelineData = (
start: dayjs.Dayjs,
observationEvents: ObservationEvent[],
rowData: Row[],
schema: ObservationCardSchema,
hoverEventRowId?: number, //if supplied, will highlight the hovered event
) => {
const data = observationEvents.map(event => {
const timepoint = start.add(event.time!, event.timeUnit as ManipulateType)
const data = rowData.map(row => {
const time = parseInt(row.values[schema.time]!)
const timeUnit = row.values[schema.timeUnits]
const timepoint = start.add(time, timeUnit as ManipulateType)
const utcFormattedTimepoint = timepoint.format()
const isHoveredOver = row.rowId == hoverEventRowId
return {
x: [utcFormattedTimepoint, utcFormattedTimepoint, utcFormattedTimepoint],
y: [0, 0.5, 1],
mode: 'lines',
line: {
color: 'gray',
color: isHoveredOver ? 'black' : 'gray',
width: 2,
},
// Add event into in the customdata
customdata: [event.id, event.id, event.id],
customdata: [row.rowId, row.rowId, row.rowId],
// but tell Plotly that we do not want it to show a hover tooltip (we're going to handle this)
hoverinfo: 'none',
}
Expand All @@ -35,9 +45,33 @@ const getLayout = (
timeMax: number,
timeUnits: string,
color: string,
observationEvents: ObservationEvent[],
annotateTime?: number,
annotateTimeUnits?: ManipulateType,
): Partial<Layout> => {
const end = start.add(timeMax, timeUnits as ManipulateType)
const annotations: Partial<Plotly.Annotations>[] = [
{
x: start.format(),
y: -0.1,
text: ' ',
showarrow: false,
},
{
x: end.format(),
y: -0.1,
text: ' ',
showarrow: false,
},
]
if (annotateTime && annotateTimeUnits) {
const x = start.add(annotateTime, annotateTimeUnits)
annotations.push({
x: x.format(),
y: -0.1,
text: `${annotateTime} ${pluralize(annotateTimeUnits, annotateTime)}`,
showarrow: false,
})
}
return {
hovermode: 'closest',
showlegend: false,
Expand All @@ -54,20 +88,7 @@ const getLayout = (
showline: false,
showticklabels: false,
},

// event annotations
// annotations: observationEvents.map(event => {
// const x = start.add(event.time!, event.timeUnit as ManipulateType)
// const annotation: Partial<Plotly.Annotations> = {
// x: x.format(),
// y: -0.4,
// text: `${event.time} ${pluralize(event.timeUnit!, event.time!)}`,
// showarrow: false,
// textangle: '270',
// }
// return annotation
// }),

annotations: annotations,
// Each phase has a shape
shapes: [
{
Expand All @@ -91,72 +112,71 @@ type TimelinePhaseProps = {
color: string
timeMax: number // how long is this phase?
timeUnits: string // in what time units is the timeMax measured? (days? weeks?)
observationEvents: ObservationEvent[]
rowData: Row[]
schema: ObservationCardSchema
}
const TimelinePhase = ({
color,
timeMax,
timeUnits,
observationEvents,
rowData,
schema,
}: TimelinePhaseProps) => {
const [clickEvent, setClickEvent] = useState<Plotly.PlotMouseEvent>()
const [hoverEvent, setHoverEvent] = useState<Plotly.PlotHoverEvent>()
const start = dayjs()

// hide the hover UI if we detect that the user moves the mouse outside of this component boundary
const componentRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (componentRef.current) {
const componentRect = componentRef.current.getBoundingClientRect()
const mouseX = e.clientX
const mouseY = e.clientY

// Check if the mouse is outside the component boundaries
if (
mouseX < componentRect.left ||
mouseX > componentRect.right ||
mouseY < componentRect.top ||
mouseY > componentRect.bottom
) {
setHoverEvent(undefined)
}
}
}
const rowId = clickEvent?.points[0].customdata as number
const selectedRow = rowData.filter(row => {
return row.rowId === rowId
})[0]
const hoverEventRowId = hoverEvent?.points[0].customdata as number
const hoverRow = rowData.filter(row => {
return row.rowId === hoverEventRowId
})[0]
const annotateTime = hoverRow
? parseInt(hoverRow.values[schema.time]!)
: undefined
const annotateTimeUnits = hoverRow
? (hoverRow.values[schema.timeUnits] as ManipulateType)
: undefined

// Add the event listener to the window
window.addEventListener('mousemove', handleMouseMove)

// Clean up the event listener on component unmount
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])

const id = hoverEvent?.points[0].customdata as string
return (
<div ref={componentRef}>
<Plot
data={getTimelineData(start, observationEvents)}
layout={getLayout(start, timeMax, timeUnits, color, observationEvents)}
data={getTimelineData(start, rowData, schema, hoverEventRowId)}
layout={getLayout(
start,
timeMax,
timeUnits,
color,
annotateTime,
annotateTimeUnits,
)}
config={{ displayModeBar: false }}
style={{ maxHeight: '300px' }}
useResizeHandler={true}
onClick={eventData => {
setClickEvent(eventData)
}}
onHover={eventData => {
setHoverEvent(eventData)
}}
/>
<Paper
sx={{
position: 'fixed',
top: `${hoverEvent?.event.y}px`,
left: `${hoverEvent?.event.x}px`,
display: hoverEvent ? 'block' : 'none',
onUnhover={() => {
setHoverEvent(undefined)
}}
>
<Typography sx={{ p: 2 }} variant="body1">
{id}
</Typography>
</Paper>
/>
{selectedRow && (
<Dialog onClose={() => setClickEvent(undefined)} open={!!selectedRow}>
<ObservationCard
data={selectedRow.values}
schema={schema}
includePortalCardClass={false}
/>
</Dialog>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ type Story = StoryObj<typeof meta>

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: 'Mus musculus',
resourceId: '9971e47e-976a-4631-8edd-5cae04304b01',
eventsTableId: 'syn51735464',
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,52 @@ import hardcodedPhasesQueryResponseData, {
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'

const OBSERVATION_PHASE_COLUMN_NAME = 'observationphase'
const OBSERVATION_ID_COLUMN_NAME = 'observationid'
const OBSERVATION_TIME_COLUMN_NAME = 'observationtime'
const OBSERVATION_TIME_UNITS_COLUMN_NAME = 'observationtimeunits'
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'

type TimelinePlotProps = {
species: string
resourceId: string
eventsTableId: string
}
export type ObservationEvent = {
id: string | null
time: number | null
timeUnit: string | null
observationsSql: string
}
const TimelinePlot = ({
observationsSql,
species,
resourceId,
eventsTableId,
}: TimelinePlotProps) => {
// Fetch the table data
const eventsTableId = parseEntityIdFromSqlStatement(observationsSql)
const eventTableQuery = useGetFullTableQueryResults({
entityId: eventsTableId,
query: {
sql: `SELECT * FROM ${eventsTableId} WHERE resourceId = '${resourceId}' and species = '${species}'`,
sql: `${observationsSql} WHERE observationTime IS NOT NULL`,
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,
},
],
},

partMask: BUNDLE_MASK_QUERY_RESULTS,
concreteType: 'org.sagebionetworks.repo.model.table.QueryBundleRequest',
})
Expand All @@ -46,10 +65,6 @@ const TimelinePlot = ({
if (isLoading) {
return <></>
}
const observationIdIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_ID_COLUMN_NAME,
)!
const observationPhaseIndex =
eventsData?.queryResult?.queryResults.headers.findIndex(
header => header.name.toLowerCase() === OBSERVATION_PHASE_COLUMN_NAME,
Expand All @@ -63,6 +78,24 @@ const TimelinePlot = ({
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,
)!

// filter the phases query response data to the specific species
const phasesForTargetSpecies =
Expand All @@ -87,28 +120,29 @@ const TimelinePlot = ({
phaseRow.values[phaseObservationIndex]
)
})
// transform into ObservationEvents and create a new TimelinePhase
const observationEvents = phaseEventRows?.map(row => {
const observationEvent: ObservationEvent = {
id: row.values[observationIdIndex],
time: parseInt(row.values[observationTimeIndex]!),
timeUnit: row.values[observationTimeUnitIndex],
}
return observationEvent
})
return (
observationEvents?.length &&
observationEvents?.length > 0 && (
const schema: ObservationCardSchema = {
submitterName: observationSubmitterNameIndex,
submitterUserId: submitterUserIdIndex,
tag: observationTypeIndex,
text: observationTextIndex,
time: observationTimeIndex,
timeUnits: observationTimeUnitIndex,
}
const isPhaseEvents =
phaseEventRows?.length && phaseEventRows?.length > 0
if (isPhaseEvents) {
return (
<TimelinePhase
key={phaseRow.rowId}
name={phaseRow.values[phaseObservationIndex]!}
color={colorPalette[0]}
timeMax={parseInt(phaseRow.values[phaseObservationTimeMaxIndex]!)}
timeUnits={phaseRow.values[phaseObservationTimeMaxUnitsIndex]!}
observationEvents={observationEvents}
rowData={phaseEventRows}
schema={schema}
/>
)
)
} else return <></>
})}
</Box>
)
Expand Down
Loading

0 comments on commit ab2e1b3

Please sign in to comment.