diff --git a/components/src/preact/components/mutation-type-selector.tsx b/components/src/preact/components/mutation-type-selector.tsx new file mode 100644 index 00000000..b75a7679 --- /dev/null +++ b/components/src/preact/components/mutation-type-selector.tsx @@ -0,0 +1,30 @@ +import { type FunctionComponent } from 'preact/compat'; + +import { type CheckboxItem, CheckboxSelector } from './checkbox-selector'; +import type { SubstitutionOrDeletion } from '../../types'; + +export type DisplayedMutationType = CheckboxItem & { + type: SubstitutionOrDeletion; +}; + +export type MutationTypeSelectorProps = { + displayedMutationTypes: DisplayedMutationType[]; + setDisplayedMutationTypes: (mutationTypes: DisplayedMutationType[]) => void; +}; + +export const MutationTypeSelector: FunctionComponent = ({ + displayedMutationTypes, + setDisplayedMutationTypes, +}) => { + const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label); + const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`; + + return ( + setDisplayedMutationTypes(items)} + /> + ); +}; diff --git a/components/src/preact/mutationComparison/mutation-comparison.tsx b/components/src/preact/mutationComparison/mutation-comparison.tsx index 8c8e3a07..91d5bf1d 100644 --- a/components/src/preact/mutationComparison/mutation-comparison.tsx +++ b/components/src/preact/mutationComparison/mutation-comparison.tsx @@ -5,15 +5,15 @@ import { getMutationComparisonTableData } from './getMutationComparisonTableData import { MutationComparisonTable } from './mutation-comparison-table'; import { MutationComparisonVenn } from './mutation-comparison-venn'; import { filterMutationData, type MutationData, queryMutationData } from './queryMutationData'; -import { type LapisFilter, type SequenceType, type SubstitutionOrDeletion } from '../../types'; +import { type LapisFilter, type SequenceType } from '../../types'; import { LapisUrlContext } from '../LapisUrlContext'; import { type DisplayedSegment, SegmentSelector } from '../components/SegmentSelector'; -import { type CheckboxItem, CheckboxSelector } from '../components/checkbox-selector'; import { CsvDownloadButton } from '../components/csv-download-button'; import { ErrorDisplay } from '../components/error-display'; import Headline from '../components/headline'; import Info from '../components/info'; import { LoadingDisplay } from '../components/loading-display'; +import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector'; import { NoDataDisplay } from '../components/no-data-display'; import { type ProportionInterval } from '../components/proportion-selector'; import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown'; @@ -33,10 +33,6 @@ export interface MutationComparisonProps { views: View[]; } -export type DisplayedMutationType = CheckboxItem & { - type: SubstitutionOrDeletion; -}; - export const MutationComparison: FunctionComponent = ({ variants, sequenceType, views }) => { const lapis = useContext(LapisUrlContext); @@ -166,9 +162,6 @@ const Toolbar: FunctionComponent = ({ proportionInterval, setProportionInterval, }) => { - const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label); - const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`; - return (
= ({ setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))} /> - setDisplayedMutationTypes(items)} + = ({ variant, sequenceType, views }) => { const lapis = useContext(LapisUrlContext); - const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 1 }); - const { data, error, isLoading } = useQuery(async () => { - const substitutionsOrDeletions = await querySubstitutionsOrDeletions(variant, sequenceType, lapis); - const insertions = await queryInsertions(variant, sequenceType, lapis); - - const mutationSegments = substitutionsOrDeletions.content - .map((mutationEntry) => mutationEntry.mutation.segment) - .filter((segment): segment is string => segment !== undefined); - - const segments = [...new Set(mutationSegments)]; - - return { - data: { substitutionsOrDeletions: substitutionsOrDeletions.content, insertions: insertions.content }, - segments, - }; + return queryMutationsData(variant, sequenceType, lapis); }, [variant, sequenceType, lapis]); - const [displayedSegments, setDisplayedSegments] = useState([]); - useEffect(() => { - if (data !== null) { - setDisplayedSegments( - data.segments.map((segment) => ({ - segment, - label: segment, - checked: true, - })), - ); - } - }, [data]); - const headline = 'Mutations'; if (isLoading) { return ( @@ -94,59 +66,79 @@ export const Mutations: FunctionComponent = ({ variant, sequence ); } - function bySelectedSegments(mutationEntry: Mutation) { - if (mutationEntry.mutation.segment === undefined) { - return true; - } - return displayedSegments.some( - (displayedSegment) => - displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked, - ); - } + return ( + + + + ); +}; - const byProportion = (mutationEntry: SubstitutionOrDeletionEntry) => { - return mutationEntry.proportion >= proportionInterval.min && mutationEntry.proportion <= proportionInterval.max; - }; +type MutationTabsProps = { + mutationsData: { insertions: InsertionEntry[]; substitutionsOrDeletions: SubstitutionOrDeletionEntry[] }; + segments: string[]; + sequenceType: SequenceType; + views: View[]; +}; - const getTab = ( - view: View, - data: { substitutionsOrDeletions: SubstitutionOrDeletionEntry[]; insertions: InsertionEntry[] }, - ) => { +const MutationsTabs: FunctionComponent = ({ mutationsData, segments, sequenceType, views }) => { + const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 1 }); + + const [displayedSegments, setDisplayedSegments] = useState( + segments.map((segment) => ({ + segment, + label: segment, + checked: true, + })), + ); + const [displayedMutationTypes, setDisplayedMutationTypes] = useState([ + { label: 'Substitutions', checked: true, type: 'substitution' }, + { label: 'Deletions', checked: true, type: 'deletion' }, + ]); + + const filteredData = filterMutationsData( + mutationsData, + displayedSegments, + proportionInterval.min, + proportionInterval.max, + displayedMutationTypes, + ); + + const getTab = (view: View) => { switch (view) { case 'table': return { title: 'Table', - content: ( - - ), + content: , }; case 'grid': return { title: 'Grid', - content: ( - - ), + content: , }; case 'insertions': return { title: 'Insertions', - content: , + content: , }; } }; - const tabs = views.map((view) => { - return getTab(view, data.data); - }); + const tabs = views.map((view) => getTab(view)); const toolbar = (activeTab: string) => (
+ {activeTab === 'Table' && ( + + )} {(activeTab === 'Table' || activeTab === 'Grid') && ( <> = ({ variant, sequence /> getMutationsTableData(data.data.substitutionsOrDeletions)} + getData={() => getMutationsTableData(filteredData.tableData)} filename='substitutionsAndDeletions.csv' /> @@ -165,7 +157,7 @@ export const Mutations: FunctionComponent = ({ variant, sequence {activeTab === 'Insertions' && ( getInsertionsTableData(data.data.insertions)} + getData={() => getInsertionsTableData(filteredData.insertions)} filename='insertions.csv' /> )} @@ -173,9 +165,5 @@ export const Mutations: FunctionComponent = ({ variant, sequence
); - return ( - - - - ); + return ; }; diff --git a/components/src/preact/mutations/queryMutations.ts b/components/src/preact/mutations/queryMutations.ts new file mode 100644 index 00000000..cffa6a83 --- /dev/null +++ b/components/src/preact/mutations/queryMutations.ts @@ -0,0 +1,69 @@ +import { queryInsertions } from '../../query/queryInsertions'; +import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions'; +import { + type InsertionEntry, + type LapisFilter, + type MutationEntry, + type SubstitutionOrDeletionEntry, +} from '../../types'; +import { type DisplayedSegment } from '../components/SegmentSelector'; +import type { DisplayedMutationType } from '../mutationComparison/mutation-comparison'; + +export async function queryMutationsData( + variant: LapisFilter, + sequenceType: 'nucleotide' | 'amino acid', + lapis: string, +) { + const substitutionsOrDeletions = (await querySubstitutionsOrDeletions(variant, sequenceType, lapis)).content; + const insertions = (await queryInsertions(variant, sequenceType, lapis)).content; + + const mutationSegments = substitutionsOrDeletions + .map((mutationEntry) => mutationEntry.mutation.segment) + .filter((segment): segment is string => segment !== undefined); + + const segments = [...new Set(mutationSegments)]; + + return { + mutationsData: { substitutionsOrDeletions, insertions }, + segments, + }; +} + +export function filterMutationsData( + data: { insertions: InsertionEntry[]; substitutionsOrDeletions: SubstitutionOrDeletionEntry[] }, + displayedSegments: DisplayedSegment[], + minProportion: number, + maxProportion: number, + displayedMutationTypes: DisplayedMutationType[], +) { + function bySelectedSegments(mutationEntry: Mutation) { + if (mutationEntry.mutation.segment === undefined) { + return true; + } + return displayedSegments.some( + (displayedSegment) => + displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked, + ); + } + + const byProportion = (mutationEntry: SubstitutionOrDeletionEntry) => { + return mutationEntry.proportion >= minProportion && mutationEntry.proportion <= maxProportion; + }; + + const byDisplayedMutationTypes = (mutationEntry: SubstitutionOrDeletionEntry) => { + return displayedMutationTypes.some( + (displayedMutationType) => + displayedMutationType.checked && displayedMutationType.type === mutationEntry.type, + ); + }; + + const filteredSubstitutionsOrDeletions = data.substitutionsOrDeletions + .filter(byProportion) + .filter(bySelectedSegments); + + return { + insertions: data.insertions.filter(bySelectedSegments), + tableData: filteredSubstitutionsOrDeletions.filter(byDisplayedMutationTypes), + gridData: filteredSubstitutionsOrDeletions, + }; +} diff --git a/components/src/preact/textInput/test-input.stories.tsx b/components/src/preact/textInput/text-input.stories.tsx similarity index 90% rename from components/src/preact/textInput/test-input.stories.tsx rename to components/src/preact/textInput/text-input.stories.tsx index 0199b6b0..602c9929 100644 --- a/components/src/preact/textInput/test-input.stories.tsx +++ b/components/src/preact/textInput/text-input.stories.tsx @@ -1,8 +1,9 @@ -import { Meta, StoryObj } from '@storybook/preact'; -import { TextInput, TextInputProps } from './text-input'; +import { type Meta, type StoryObj } from '@storybook/preact'; + +import data from './__mockData__/aggregated_hosts.json'; +import { TextInput, type TextInputProps } from './text-input'; import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants'; import { LapisUrlContext } from '../LapisUrlContext'; -import data from './__mockData__/aggregated_hosts.json'; const meta: Meta = { title: 'Input/TextInput', diff --git a/components/src/preact/textInput/text-input.tsx b/components/src/preact/textInput/text-input.tsx index 82917184..44e5bc6c 100644 --- a/components/src/preact/textInput/text-input.tsx +++ b/components/src/preact/textInput/text-input.tsx @@ -1,11 +1,12 @@ -import { FunctionComponent } from 'preact'; +import { type FunctionComponent } from 'preact'; import { useContext, useRef } from 'preact/hooks'; -import { LapisUrlContext } from '../LapisUrlContext'; -import { useQuery } from '../useQuery'; + import { fetchAutocompleteList } from './fetchAutocompleteList'; -import { LoadingDisplay } from '../components/loading-display'; +import { LapisUrlContext } from '../LapisUrlContext'; import { ErrorDisplay } from '../components/error-display'; +import { LoadingDisplay } from '../components/loading-display'; import { NoDataDisplay } from '../components/no-data-display'; +import { useQuery } from '../useQuery'; export interface TextInputProps { lapisField: string; diff --git a/components/src/web-components/input/text-input-component.stories.ts b/components/src/web-components/input/text-input-component.stories.ts index fc9de364..d946383a 100644 --- a/components/src/web-components/input/text-input-component.stories.ts +++ b/components/src/web-components/input/text-input-component.stories.ts @@ -1,12 +1,12 @@ +import { expect, fn, userEvent, waitFor } from '@storybook/test'; import type { Meta, StoryObj } from '@storybook/web-components'; -import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants'; - import { html } from 'lit'; + +import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants'; import '../app'; import './text-input-component'; -import { withinShadowRoot } from '../withinShadowRoot.story'; -import { expect, fn, userEvent, waitFor } from '@storybook/test'; import data from '../../preact/textInput/__mockData__/aggregated_hosts.json'; +import { withinShadowRoot } from '../withinShadowRoot.story'; const meta: Meta = { title: 'Input/Text input', diff --git a/components/src/web-components/input/text-input-component.tsx b/components/src/web-components/input/text-input-component.tsx index b423d024..4f84057e 100644 --- a/components/src/web-components/input/text-input-component.tsx +++ b/components/src/web-components/input/text-input-component.tsx @@ -1,6 +1,7 @@ -import { PreactLitAdapter } from '../PreactLitAdapter'; import { customElement, property } from 'lit/decorators.js'; + import { TextInput } from '../../preact/textInput/text-input'; +import { PreactLitAdapter } from '../PreactLitAdapter'; @customElement('gs-text-input') export class TextInputComponent extends PreactLitAdapter {