-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): add mutation over time plot for wastewater / WISE
- Loading branch information
1 parent
c22984a
commit 3920b6c
Showing
10 changed files
with
446 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { type Dataset } from './Dataset'; | ||
import { type Operator } from './Operator'; | ||
import { fetchAggregated, fetchDetails } from '../lapisApi/lapisApi'; | ||
import { type AggregatedItem } from '../lapisApi/lapisTypes'; | ||
import { type LapisFilter } from '../types'; | ||
|
||
export class FetchDetailsOperator<Fields extends Record<string, unknown>> implements Operator<Fields> { | ||
constructor( | ||
private filter: LapisFilter, | ||
private fields: string[] = [], | ||
) {} | ||
|
||
async evaluate(lapisUrl: string, signal?: AbortSignal): Promise<Dataset<Fields>> { | ||
const detailsResponse = ( | ||
await fetchDetails( | ||
lapisUrl, | ||
{ | ||
...this.filter, | ||
fields: this.fields, | ||
}, | ||
signal, | ||
) | ||
).data; | ||
|
||
return { content: detailsResponse as Fields[] }; | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
...onents/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { type Meta, type StoryObj } from '@storybook/preact'; | ||
|
||
import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time'; | ||
import { WISE_LAPIS_URL } from '../../../constants'; | ||
import referenceGenome from '../../../lapisApi/__mockData__/referenceGenome.json'; | ||
import { LapisUrlContext } from '../../LapisUrlContext'; | ||
import { ReferenceGenomeContext } from '../../ReferenceGenomeContext'; | ||
|
||
const meta: Meta<WastewaterMutationsOverTimeProps> = { | ||
title: 'Visualization/Wastewater Mutation over time', | ||
component: WastewaterMutationsOverTime, | ||
argTypes: { | ||
width: { control: 'text' }, | ||
height: { control: 'text' }, | ||
lapisFilter: { control: 'object' }, | ||
sequenceType: { | ||
options: ['nucleotide', 'amino acid'], | ||
control: { type: 'radio' }, | ||
}, | ||
}, | ||
parameters: { | ||
fetchMock: {}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
const Template = { | ||
render: (args: WastewaterMutationsOverTimeProps) => ( | ||
<LapisUrlContext.Provider value={WISE_LAPIS_URL}> | ||
<ReferenceGenomeContext.Provider value={referenceGenome}> | ||
<WastewaterMutationsOverTime | ||
width={args.width} | ||
height={args.height} | ||
lapisFilter={args.lapisFilter} | ||
sequenceType={args.sequenceType} | ||
/> | ||
</ReferenceGenomeContext.Provider> | ||
</LapisUrlContext.Provider> | ||
), | ||
}; | ||
|
||
// This test uses mock data: defaultMockData.ts (through mutationOverTimeWorker.mock.ts) | ||
export const Default: StoryObj<WastewaterMutationsOverTimeProps> = { | ||
...Template, | ||
args: { | ||
width: '100%', | ||
height: '700px', | ||
lapisFilter: {}, | ||
sequenceType: 'nucleotide', | ||
}, | ||
}; |
170 changes: 170 additions & 0 deletions
170
components/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { type FunctionComponent } from 'preact'; | ||
import { type Dispatch, type StateUpdater, useContext, useState } from 'preact/hooks'; | ||
|
||
import { queryWastewaterData } from '../../../query/queryWastewaterData'; | ||
import { type LapisFilter, type SequenceType } from '../../../types'; | ||
import { LapisUrlContext } from '../../LapisUrlContext'; | ||
import { type ColorScale } from '../../components/color-scale-selector'; | ||
import { ColorScaleSelectorDropdown } from '../../components/color-scale-selector-dropdown'; | ||
import { CsvDownloadButton } from '../../components/csv-download-button'; | ||
import { ErrorBoundary } from '../../components/error-boundary'; | ||
import { Fullscreen } from '../../components/fullscreen'; | ||
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../../components/info'; | ||
import { LoadingDisplay } from '../../components/loading-display'; | ||
import { NoDataDisplay } from '../../components/no-data-display'; | ||
import { ResizeContainer } from '../../components/resize-container'; | ||
import Tabs from '../../components/tabs'; | ||
import { | ||
BaseMutationOverTimeDataMap, | ||
type MutationOverTimeDataMap, | ||
} from '../../mutationsOverTime/MutationOverTimeData'; | ||
import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid'; | ||
import { useQuery } from '../../useQuery'; | ||
|
||
export interface WastewaterMutationsOverTimeProps { | ||
width: string; | ||
height: string; | ||
lapisFilter: LapisFilter; | ||
sequenceType: SequenceType; | ||
} | ||
|
||
export const WastewaterMutationsOverTime: FunctionComponent<WastewaterMutationsOverTimeProps> = (componentProps) => { | ||
const { width, height } = componentProps; | ||
const size = { height, width }; | ||
|
||
return ( | ||
<ErrorBoundary size={size}> | ||
<ResizeContainer size={size}> | ||
<WastewaterMutationsOverTimeInner {...componentProps} /> | ||
</ResizeContainer> | ||
</ErrorBoundary> | ||
); | ||
}; | ||
|
||
export const WastewaterMutationsOverTimeInner: FunctionComponent<WastewaterMutationsOverTimeProps> = ( | ||
componentProps, | ||
) => { | ||
const lapis = useContext(LapisUrlContext); | ||
|
||
const { data, error, isLoading } = useQuery(() => queryWastewaterData(lapis, componentProps.lapisFilter), []); | ||
|
||
if (isLoading) { | ||
return <LoadingDisplay />; | ||
} | ||
|
||
if (error !== null) { | ||
throw error; | ||
} | ||
|
||
if (data === null || data === undefined) { | ||
return <NoDataDisplay />; | ||
} | ||
|
||
const locationMap = new Map<string, MutationOverTimeDataMap>(); | ||
for (const row of data) { | ||
if (!locationMap.has(row.location)) { | ||
locationMap.set(row.location, new BaseMutationOverTimeDataMap()); | ||
} | ||
const map = locationMap.get(row.location)!; | ||
for (const mutation of componentProps.sequenceType === 'nucleotide' | ||
? row.nucleotideMutationFrequency | ||
: row.aminoAcidMutationFrequency) { | ||
map.set(mutation.mutation, row.date, { proportion: mutation.proportion, count: NaN, totalCount: NaN }); | ||
} | ||
} | ||
const mutationOverTimeDataPerLocation = [...locationMap.entries()].map(([location, data]) => ({ location, data })); | ||
|
||
return ( | ||
<MutationsOverTimeTabs | ||
mutationOverTimeDataPerLocation={mutationOverTimeDataPerLocation} | ||
originalComponentProps={componentProps} | ||
/> | ||
); | ||
}; | ||
|
||
type MutationOverTimeDataPerLocation = { | ||
location: string; | ||
data: MutationOverTimeDataMap; | ||
}[]; | ||
|
||
type MutationOverTimeTabsProps = { | ||
mutationOverTimeDataPerLocation: MutationOverTimeDataPerLocation; | ||
originalComponentProps: WastewaterMutationsOverTimeProps; | ||
}; | ||
|
||
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({ | ||
mutationOverTimeDataPerLocation, | ||
originalComponentProps, | ||
}) => { | ||
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' }); | ||
|
||
const tabs = mutationOverTimeDataPerLocation.map(({ location, data }) => ({ | ||
title: location, | ||
content: <MutationsOverTimeGrid data={data} colorScale={colorScale} />, | ||
})); | ||
|
||
const toolbar = (activeTab: string) => ( | ||
<Toolbar | ||
activeTab={activeTab} | ||
colorScale={colorScale} | ||
setColorScale={setColorScale} | ||
originalComponentProps={originalComponentProps} | ||
data={mutationOverTimeDataPerLocation} | ||
/> | ||
); | ||
|
||
return <Tabs tabs={tabs} toolbar={toolbar} />; | ||
}; | ||
|
||
type ToolbarProps = { | ||
activeTab: string; | ||
colorScale: ColorScale; | ||
setColorScale: Dispatch<StateUpdater<ColorScale>>; | ||
originalComponentProps: WastewaterMutationsOverTimeProps; | ||
data: MutationOverTimeDataPerLocation; | ||
}; | ||
|
||
const Toolbar: FunctionComponent<ToolbarProps> = ({ | ||
activeTab, | ||
colorScale, | ||
setColorScale, | ||
originalComponentProps, | ||
data, | ||
}) => { | ||
return ( | ||
<> | ||
{activeTab === 'Grid' && ( | ||
<ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} /> | ||
)} | ||
<CsvDownloadButton | ||
className='mx-1 btn btn-xs' | ||
getData={() => getDownloadData(data)} | ||
filename='wastewater_mutations_over_time.csv' | ||
/> | ||
<WastewaterMutationsOverTimeInfo originalComponentProps={originalComponentProps} /> | ||
<Fullscreen /> | ||
</> | ||
); | ||
}; | ||
|
||
type WastewaterMutationsOverTimeInfoProps = { | ||
originalComponentProps: WastewaterMutationsOverTimeProps; | ||
}; | ||
|
||
const WastewaterMutationsOverTimeInfo: FunctionComponent<WastewaterMutationsOverTimeInfoProps> = ({ | ||
originalComponentProps, | ||
}) => { | ||
const lapis = useContext(LapisUrlContext); | ||
return ( | ||
<Info> | ||
<InfoHeadline1>Info for mutations over time</InfoHeadline1> | ||
<InfoParagraph>TODO: Ask for text</InfoParagraph> | ||
<InfoComponentCode componentName='mutations-over-time' params={originalComponentProps} lapisUrl={lapis} /> | ||
</Info> | ||
); | ||
}; | ||
|
||
function getDownloadData(data: MutationOverTimeDataPerLocation) { | ||
// TODO | ||
return []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { parseDateStringToTemporal, type TemporalClass, toTemporalClass } from '../utils/temporalClass'; | ||
import { FetchDetailsOperator } from '../operator/FetchDetailsOperator'; | ||
import { type Substitution, SubstitutionClass } from '../utils/mutations'; | ||
import { LapisFilter } from '../types'; | ||
|
||
export type WastewaterData = { | ||
location: string; | ||
date: TemporalClass; | ||
nucleotideMutationFrequency: { mutation: Substitution; proportion: number }[]; | ||
aminoAcidMutationFrequency: { mutation: Substitution; proportion: number }[]; | ||
}[]; | ||
|
||
export async function queryWastewaterData( | ||
lapis: string, | ||
lapisFilter: LapisFilter, | ||
signal?: AbortSignal, | ||
): Promise<WastewaterData> { | ||
const fetchData = new FetchDetailsOperator<Record<string, string | null | number>>(lapisFilter, [ | ||
'date', | ||
'location', | ||
'reference', | ||
'nucleotideMutationFrequency', | ||
'aminoAcidMutationFrequency', | ||
]); | ||
const data = (await fetchData.evaluate(lapis, signal)).content; | ||
|
||
return data.map((row) => ({ | ||
location: row.location as string, | ||
date: toTemporalClass(parseDateStringToTemporal(row.date as string, 'day')), | ||
nucleotideMutationFrequency: | ||
row.nucleotideMutationFrequency !== null | ||
? transformMutations(JSON.parse(row.nucleotideMutationFrequency as string)) | ||
: [], | ||
aminoAcidMutationFrequency: | ||
row.aminoAcidMutationFrequency !== null | ||
? transformMutations(JSON.parse(row.aminoAcidMutationFrequency as string)) | ||
: [], | ||
})); | ||
} | ||
|
||
function transformMutations(input: Record<string, number>): { mutation: Substitution; proportion: number }[] { | ||
return Object.entries(input).map(([key, value]) => ({ | ||
mutation: SubstitutionClass.parse(key)!, | ||
proportion: value, | ||
})); | ||
} |
Oops, something went wrong.