Skip to content

Commit

Permalink
feat(components): allow filter by mutation type for mutations tab
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKellerer committed Apr 9, 2024
1 parent 0b077e2 commit aea2a4f
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 280 deletions.
30 changes: 30 additions & 0 deletions components/src/preact/components/mutation-type-selector.tsx
Original file line number Diff line number Diff line change
@@ -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<MutationTypeSelectorProps> = ({
displayedMutationTypes,
setDisplayedMutationTypes,
}) => {
const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label);
const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`;

return (
<CheckboxSelector
className='mx-1'
items={displayedMutationTypes}
label={mutationTypesSelectorLabel}
setItems={(items) => setDisplayedMutationTypes(items)}
/>
);
};
19 changes: 5 additions & 14 deletions components/src/preact/mutationComparison/mutation-comparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,10 +33,6 @@ export interface MutationComparisonProps {
views: View[];
}

export type DisplayedMutationType = CheckboxItem & {
type: SubstitutionOrDeletion;
};

export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({ variants, sequenceType, views }) => {
const lapis = useContext(LapisUrlContext);

Expand Down Expand Up @@ -166,9 +162,6 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
proportionInterval,
setProportionInterval,
}) => {
const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label);
const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`;

return (
<div class='flex flex-row'>
<ProportionSelectorDropdown
Expand All @@ -177,11 +170,9 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
/>
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
<CheckboxSelector
className='mx-1'
items={displayedMutationTypes}
label={mutationTypesSelectorLabel}
setItems={(items) => setDisplayedMutationTypes(items)}
<MutationTypeSelector
displayedMutationTypes={displayedMutationTypes}
setDisplayedMutationTypes={setDisplayedMutationTypes}
/>
<CsvDownloadButton
className='mx-1 btn btn-xs'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type DisplayedMutationType, type MutationComparisonVariant } from './mutation-comparison';
import { type MutationComparisonVariant } from './mutation-comparison';
import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions';
import { type SubstitutionOrDeletionEntry } from '../../types';
import { type DisplayedSegment } from '../components/SegmentSelector';
import { type DisplayedMutationType } from '../components/mutation-type-selector';

export type MutationData = {
displayName: string;
Expand Down
166 changes: 95 additions & 71 deletions components/src/preact/mutations/mutations.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { type FunctionComponent } from 'preact';
import { useContext, useEffect, useState } from 'preact/hooks';
import { type Dispatch, type StateUpdater, useContext, useState } from 'preact/hooks';

import { getInsertionsTableData } from './getInsertionsTableData';
import { getMutationsTableData } from './getMutationsTableData';
import { MutationsGrid } from './mutations-grid';
import { InsertionsTable } from './mutations-insertions-table';
import MutationsTable from './mutations-table';
import { queryInsertions } from '../../query/queryInsertions';
import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions';
import { filterMutationsData, queryMutationsData } from './queryMutations';
import {
type InsertionEntry,
type LapisFilter,
type MutationEntry,
type SequenceType,
type SubstitutionOrDeletionEntry,
} from '../../types';
Expand All @@ -22,7 +20,9 @@ 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';
import Tabs from '../components/tabs';
import { useQuery } from '../useQuery';
Expand All @@ -38,37 +38,10 @@ export interface MutationsProps {
export const Mutations: FunctionComponent<MutationsProps> = ({ 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<DisplayedSegment[]>([]);
useEffect(() => {
if (data !== null) {
setDisplayedSegments(
data.segments.map((segment) => ({
segment,
label: segment,
checked: true,
})),
);
}
}, [data]);

const headline = 'Mutations';
if (isLoading) {
return (
Expand All @@ -94,59 +67,116 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
);
}

const bySelectedSegments = (mutationEntry: MutationEntry) => {
if (mutationEntry.mutation.segment === undefined) {
return true;
}
return displayedSegments.some(
(displayedSegment) =>
displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked,
);
};
return (
<Headline heading={headline}>
<MutationsTabs
mutationsData={data.mutationsData}
sequenceType={sequenceType}
segments={data.segments}
views={views}
/>
</Headline>
);
};

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<MutationTabsProps> = ({ mutationsData, segments, sequenceType, views }) => {
const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 1 });

const [displayedSegments, setDisplayedSegments] = useState<DisplayedSegment[]>(
segments.map((segment) => ({
segment,
label: segment,
checked: true,
})),
);
const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
{ 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: (
<MutationsTable
data={data.substitutionsOrDeletions.filter(byProportion).filter(bySelectedSegments)}
/>
),
content: <MutationsTable data={filteredData.tableData} />,
};
case 'grid':
return {
title: 'Grid',
content: (
<MutationsGrid
data={data.substitutionsOrDeletions.filter(byProportion).filter(bySelectedSegments)}
sequenceType={sequenceType}
/>
),
content: <MutationsGrid data={filteredData.gridData} sequenceType={sequenceType} />,
};
case 'insertions':
return {
title: 'Insertions',
content: <InsertionsTable data={data.insertions.filter(bySelectedSegments)} />,
content: <InsertionsTable data={filteredData.insertions} />,
};
}
};

const tabs = views.map((view) => {
return getTab(view, data.data);
});
const tabs = views.map((view) => getTab(view));

const toolbar = (activeTab: string) => (
<Toolbar
activeTab={activeTab}
displayedSegments={displayedSegments}
setDisplayedSegments={setDisplayedSegments}
displayedMutationTypes={displayedMutationTypes}
setDisplayedMutationTypes={setDisplayedMutationTypes}
filteredData={filteredData}
proportionInterval={proportionInterval}
setProportionInterval={setProportionInterval}
/>
);

return <Tabs tabs={tabs} toolbar={toolbar} />;
};

type ToolbarProps = {
activeTab: string;
displayedSegments: DisplayedSegment[];
setDisplayedSegments: (segments: DisplayedSegment[]) => void;
displayedMutationTypes: DisplayedMutationType[];
setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
filteredData: { tableData: SubstitutionOrDeletionEntry[]; insertions: InsertionEntry[] };
proportionInterval: ProportionInterval;
setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
};

const Toolbar: FunctionComponent<ToolbarProps> = ({
activeTab,
displayedSegments,
setDisplayedSegments,
displayedMutationTypes,
setDisplayedMutationTypes,
filteredData,
proportionInterval,
setProportionInterval,
}) => {
return (
<div class='flex flex-row'>
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
{activeTab === 'Table' && (
<MutationTypeSelector
setDisplayedMutationTypes={setDisplayedMutationTypes}
displayedMutationTypes={displayedMutationTypes}
/>
)}
{(activeTab === 'Table' || activeTab === 'Grid') && (
<>
<ProportionSelectorDropdown
Expand All @@ -157,25 +187,19 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
/>
<CsvDownloadButton
className='mx-1 btn btn-xs'
getData={() => getMutationsTableData(data.data.substitutionsOrDeletions)}
getData={() => getMutationsTableData(filteredData.tableData)}
filename='substitutionsAndDeletions.csv'
/>
</>
)}
{activeTab === 'Insertions' && (
<CsvDownloadButton
className='mx-1 btn btn-xs'
getData={() => getInsertionsTableData(data.data.insertions)}
getData={() => getInsertionsTableData(filteredData.insertions)}
filename='insertions.csv'
/>
)}
<Info className='mx-1' content='Info for mutations' />
</div>
);

return (
<Headline heading={headline}>
<Tabs tabs={tabs} toolbar={toolbar} />
</Headline>
);
};

0 comments on commit aea2a4f

Please sign in to comment.