Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(website): simple mutation badges #1666

Merged
merged 5 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineConfig({
screenshot: 'only-on-failure',
},
globalSetup: './tests/playwrightSetup.ts',
timeout: 2 * 60 * 1000, // Extend further if we get timeouts in CI

projects: [
{
Expand Down
12 changes: 10 additions & 2 deletions website/src/components/SequenceDetailsPage/DataTable.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { DataUseTermsHistoryModal } from './DataUseTermsHistoryModal';
import { SubstitutionsContainer } from './MutationBadge';
import { toHeaderMap, type TableDataEntry } from './getTableData';
import { type DataUseTermsHistoryEntry } from '../../types/backend';

Expand All @@ -23,9 +24,16 @@ const headerMap = toHeaderMap(tableData);
{names.map(({ label, value, customDisplay }) => (
<tr>
anna-parker marked this conversation as resolved.
Show resolved Hide resolved
<td class='py-1 w-44 text-sm font-medium text-gray-900 text-right'>{label}</td>
<td class='px-4 py-1 whitespace-normal text-sm text-gray-600'>
<div class='flex items-center gap-3'>
<td class='px-4 py-1 text-sm text-gray-600'>
<div class='items-center gap-3 whitespace-wrap'>
{customDisplay === undefined && value}
{customDisplay !== undefined &&
customDisplay.type === 'badge' &&
(customDisplay.value === undefined ? (
''
) : (
<SubstitutionsContainer values={customDisplay.value} />
))}
{customDisplay !== undefined &&
customDisplay.type === 'link' &&
customDisplay.url !== undefined && (
Expand Down
81 changes: 81 additions & 0 deletions website/src/components/SequenceDetailsPage/MutationBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type FC } from 'react';

import type { MutationProportionCount } from '../../types/lapis';

export type SubProps = {
position: number;
mutationTo: string;
mutationFrom: string;
sequenceName: string | null;
};

export const SubBadge: FC<SubProps> = ({ position, mutationTo, mutationFrom, sequenceName }) => {
return (
<li key={position} className='inline-block'>
<span className='rounded-[3px] font-mono text-xs overflow-auto'>
{sequenceName === null ? (
<span className='px-[4px] py-[2px] rounded-s-[3px]' style={{ background: getColor(mutationFrom) }}>
{mutationFrom}
</span>
) : (
<>
<span className='px-[4px] py-[2px] rounded-s-[3px] bg-gray-200'>{sequenceName}:</span>
<span className='px-[4px] py-[2px]' style={{ background: getColor(mutationFrom) }}>
{mutationFrom}
</span>
</>
)}
<span className='px-[4px] py-[2px] bg-gray-200'>{position + 1}</span>
<span className='px-[4px] py-[2px] rounded-e-[3px]' style={{ background: getColor(mutationTo) }}>
{mutationTo}
</span>
</span>
</li>
);
};

// Based from http://ugene.net/forum/YaBB.pl?num=1337064665
export const COLORS: Record<string, string> = {
anna-parker marked this conversation as resolved.
Show resolved Hide resolved
'A': '#db8070',
'C': '#859dfc',
'G': '#c2b553',
'T': '#7fbb81',
'V': '#e5e57c',
'L': '#e5e550',
'I': '#e5e514',
'B': '#e54c4c',
'D': '#e5774e',
'E': '#e59c6c',
'F': '#e2e54d',
'H': '#9ddde5',
'K': '#b4a2e5',
'M': '#b7e525',
'N': '#e57875',
'P': '#b6b5e5',
'Q': '#e5aacd',
'R': '#878fe5',
'S': '#e583d8',
'W': '#4aa7e5',
'X': '#aaaaaa',
'Y': '#57cfe5',
'Z': '#777777',
'*': '#777777',
'-': '#444444',
};

export function getColor(code: string): string {
return COLORS[code] ?? COLORS.X;
}

export const SubstitutionsContainer = ({ values }: { values: MutationProportionCount[] }) => {
return values.map(({ mutationFrom, mutationTo, position, sequenceName }) => (
<span>
<SubBadge
sequenceName={sequenceName}
mutationFrom={mutationFrom}
position={position}
mutationTo={mutationTo}
/>{' '}
</span>
));
};
58 changes: 56 additions & 2 deletions website/src/components/SequenceDetailsPage/getTableData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,31 @@ describe('getTableData', () => {
expect(data).toContainEqual({
label: 'Nucleotide substitutions',
name: 'nucleotideSubstitutions',
value: 'T10A, C30G',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: {
type: 'badge',
value: [
{
count: 0,
mutation: 'T10A',
mutationFrom: 'T',
mutationTo: 'A',
position: 10,
proportion: 0,
sequenceName: null,
},
{
count: 0,
mutation: 'C30G',
mutationFrom: 'C',
mutationTo: 'G',
position: 30,
proportion: 0,
sequenceName: null,
},
],
},
});
expect(data).toContainEqual({
label: 'Nucleotide deletions',
Expand All @@ -163,8 +186,31 @@ describe('getTableData', () => {
expect(data).toContainEqual({
label: 'Amino acid substitutions',
name: 'aminoAcidSubstitutions',
value: 'gene1:N10Y, gene1:T30N',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: {
type: 'badge',
value: [
{
count: 0,
mutation: 'gene1:N10Y',
mutationFrom: 'N',
mutationTo: 'Y',
position: 10,
proportion: 0,
sequenceName: 'gene1',
},
{
count: 0,
mutation: 'gene1:T30N',
mutationFrom: 'T',
mutationTo: 'N',
position: 30,
proportion: 0,
sequenceName: 'gene1',
},
],
},
});
expect(data).toContainEqual({
label: 'Amino acid deletions',
Expand Down Expand Up @@ -428,6 +474,10 @@ const defaultMutationsInsertionsDeletionsList: TableDataEntry[] = [
name: 'nucleotideSubstitutions',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: {
type: 'badge',
value: [],
},
},
{
label: 'Nucleotide deletions',
Expand All @@ -446,6 +496,10 @@ const defaultMutationsInsertionsDeletionsList: TableDataEntry[] = [
name: 'aminoAcidSubstitutions',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: {
type: 'badge',
value: [],
},
},
{
label: 'Amino acid deletions',
Expand Down
99 changes: 57 additions & 42 deletions website/src/components/SequenceDetailsPage/getTableData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,55 @@ export function toHeaderMap(listTableDataEntries: TableDataEntry[]): { [key: str
return groupedData;
}

function mutationDetails(
nucleotideMutations: MutationProportionCount[],
aminoAcidMutations: MutationProportionCount[],
nucleotideInsertions: InsertionCount[],
aminoAcidInsertions: InsertionCount[],
): TableDataEntry[] {
const data: TableDataEntry[] = [
{
label: 'Nucleotide substitutions',
name: 'nucleotideSubstitutions',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: { type: 'badge', value: substitutionsList(nucleotideMutations) },
},
{
label: 'Nucleotide deletions',
name: 'nucleotideDeletions',
value: deletionsToCommaSeparatedString(nucleotideMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Nucleotide insertions',
name: 'nucleotideInsertions',
value: insertionsToCommaSeparatedString(nucleotideInsertions),
header: 'Mutations, insertions, deletions',
},
{
label: 'Amino acid substitutions',
name: 'aminoAcidSubstitutions',
value: '',
header: 'Mutations, insertions, deletions',
customDisplay: { type: 'badge', value: substitutionsList(aminoAcidMutations) },
},
{
label: 'Amino acid deletions',
name: 'aminoAcidDeletions',
value: deletionsToCommaSeparatedString(aminoAcidMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Amino acid insertions',
name: 'aminoAcidInsertions',
value: insertionsToCommaSeparatedString(aminoAcidInsertions),
header: 'Mutations, insertions, deletions',
},
];
return data;
}

function toTableData(config: Schema) {
return ({
details,
Expand All @@ -122,44 +171,13 @@ function toTableData(config: Schema) {
value: mapValueToDisplayedValue(details[metadata.name], metadata),
header: metadata.header ?? '',
}));
data.push(
{
label: 'Nucleotide substitutions',
name: 'nucleotideSubstitutions',
value: substitutionsToCommaSeparatedString(nucleotideMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Nucleotide deletions',
name: 'nucleotideDeletions',
value: deletionsToCommaSeparatedString(nucleotideMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Nucleotide insertions',
name: 'nucleotideInsertions',
value: insertionsToCommaSeparatedString(nucleotideInsertions),
header: 'Mutations, insertions, deletions',
},
{
label: 'Amino acid substitutions',
name: 'aminoAcidSubstitutions',
value: substitutionsToCommaSeparatedString(aminoAcidMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Amino acid deletions',
name: 'aminoAcidDeletions',
value: deletionsToCommaSeparatedString(aminoAcidMutations),
header: 'Mutations, insertions, deletions',
},
{
label: 'Amino acid insertions',
name: 'aminoAcidInsertions',
value: insertionsToCommaSeparatedString(aminoAcidInsertions),
header: 'Mutations, insertions, deletions',
},
const mutations = mutationDetails(
nucleotideMutations,
aminoAcidMutations,
nucleotideInsertions,
aminoAcidInsertions,
);
data.push(...mutations);

return data;
};
Expand All @@ -177,11 +195,8 @@ function mapValueToDisplayedValue(value: undefined | null | string | number, met
return value;
}

function substitutionsToCommaSeparatedString(mutationData: MutationProportionCount[]) {
return mutationData
.filter((m) => m.mutationTo !== '-')
.map((m) => m.mutation)
.join(', ');
function substitutionsList(mutationData: MutationProportionCount[]) {
return mutationData.filter((m) => m.mutationTo !== '-');
}

function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[]) {
Expand Down
3 changes: 2 additions & 1 deletion website/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import z from 'zod';

import { orderByType } from './lapis.ts';
import { orderByType, mutationProportionCount } from './lapis.ts';
import { referenceGenomes } from './referencesGenomes.ts';

// These metadata types need to be kept in sync with the backend config class `MetadataType` in Config.kt
Expand All @@ -9,6 +9,7 @@ export const metadataPossibleTypes = ['string', 'date', 'int', 'float', 'pango_l
export const customDisplay = z.object({
type: z.string(),
url: z.string().optional(),
value: z.array(mutationProportionCount).optional(),
});

export const metadata = z.object({
Expand Down
2 changes: 1 addition & 1 deletion website/src/types/lapis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type LapisBaseRequest = z.infer<typeof lapisBaseRequest>;

export const mutationsRequest = lapisBaseRequest.extend({ minProportion: z.number().optional() });

const mutationProportionCount = z.object({
export const mutationProportionCount = z.object({
mutation: z.string(),
proportion: z.number(),
count: z.number(),
Expand Down
2 changes: 1 addition & 1 deletion website/tests/pages/sequences/sequences.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class SequencePage {
}

public async loadSequences() {
await expect(this.loadButton).toBeVisible();
await expect(this.loadButton).toBeVisible({ timeout: 60000 });
await this.loadButton.click();
}

Expand Down
Loading