Skip to content
Open
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
29 changes: 27 additions & 2 deletions kubernetes/loculus/values.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't find this intuitive - if I see

Closest reference: A (variant)

I would assume that the closest reference is a variant of A, rather than that this sequence is a variant. But it's not my area

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a different display you would find more intuitive? Also tagging @rneher as he requested this. Perhaps closest reference should be changed? We could call it L segment lineage: A (variant) would that be clearer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a great instinct - but yes, I guess that would be clearer but then I'm not sure if that naming is actually accurate - would this use case be called a lineage in the cases we're interested in?

Original file line number Diff line number Diff line change
Expand Up @@ -2014,14 +2014,39 @@ defaultOrganisms:
header: "Host"
ingest: ncbiHostName
initiallyVisible: true
- name: variant
isSequenceFilter: true
perSegment: true
header: "Clade & Lineage"
oneHeader: true
displayName: "Variant"
type: boolean
noInput: true
autocomplete: true
initiallyVisible: false
includeInDownloadsByDefault: false
customDisplay:
type: variantReference
displayGroup: reference
label: Closest reference
preprocessing:
function: is_above_threshold
args:
threshold: 1000
inputs: {input: "nextclade.totalSubstitutions"} #custom nextclade dataset does not have private mutations, so using total substitutions as a proxy for distance from reference
- name: reference
oneHeader: true
header: "Clade & Lineage"
isSequenceFilter: true
displayName: Closest Reference
displayName: Closest reference
customDisplay:
type: variantReference
displayGroup: reference
label: Closest reference
noInput: true
generateIndex: true
autocomplete: true
initiallyVisible: true
hideOnSequenceDetailsPage: true
perSegment: true
preprocessing:
inputs: {input: ASSIGNED_REFERENCE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1172,12 +1172,14 @@ def is_above_threshold(
],
)
input_datum = input_data["input"]
if not input_datum:
if input_datum is None or (isinstance(input_datum, str) and not input_datum.strip()):
return ProcessingResult(datum=None, warnings=[], errors=[])
try:
threshold = float(args["threshold"]) # type: ignore
input = float(input_datum)
except (ValueError, TypeError):
msg = f"Field {output_field} has non-numeric threshold value."
logger.error(msg)
return ProcessingResult(
datum=None,
warnings=[],
Expand All @@ -1186,7 +1188,7 @@ def is_above_threshold(
input_fields,
[output_field],
AnnotationSourceType.METADATA,
message=(f"Field {output_field} has non-numeric threshold value."),
message=(msg),
)
],
)
Expand Down
9 changes: 2 additions & 7 deletions website/src/components/SearchPage/ReferenceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FieldValues, SetSomeFieldValues } from '../../types/config.ts';
import { type ReferenceGenomesInfo } from '../../types/referencesGenomes.ts';
import { getReferenceIdentifier } from '../../utils/referenceSelection.ts';
import type { MetadataFilterSchema } from '../../utils/search.ts';
import { segmentsWithMultipleReferences } from '../../utils/sequenceTypeHelpers.ts';
import { getReferenceDisplayNameMap, segmentsWithMultipleReferences } from '../../utils/sequenceTypeHelpers.ts';

type ReferenceSelectorProps = {
filterSchema: MetadataFilterSchema;
Expand Down Expand Up @@ -46,12 +46,7 @@ export const ReferenceSelector: FC<ReferenceSelectorProps> = ({
}, [filterSchema, referenceIdentifierField]);

const referenceDisplayNameMap = useMemo(
() =>
new Map(
Object.entries(referenceGenomesInfo.segmentReferenceGenomes[segmentName]).map(
([ref, refData]) => [ref, refData.displayName ?? ref] as const,
),
),
() => getReferenceDisplayNameMap(referenceGenomesInfo, segmentName),
[referenceGenomesInfo.segmentReferenceGenomes, segmentName],
);

Expand Down
3 changes: 3 additions & 0 deletions website/src/components/SequenceDetailsPage/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const DataTableComponent: React.FC<Props> = ({
key={index}
data={entry}
dataUseTermsHistory={dataUseTermsHistory}
referenceGenomesInfo={referenceGenomesInfo}
/>
))}
</div>
Expand Down Expand Up @@ -128,6 +129,7 @@ const DataTableComponent: React.FC<Props> = ({
key={index}
data={entry}
dataUseTermsHistory={dataUseTermsHistory}
referenceGenomesInfo={referenceGenomesInfo}
/>
))}
</div>
Expand Down Expand Up @@ -166,6 +168,7 @@ const DataTableComponent: React.FC<Props> = ({
key={index}
data={entry}
dataUseTermsHistory={dataUseTermsHistory}
referenceGenomesInfo={referenceGenomesInfo}
/>
))}
</div>
Expand Down
16 changes: 13 additions & 3 deletions website/src/components/SequenceDetailsPage/DataTableEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@ import React from 'react';
import DataTableEntryValue from './DataTableEntryValue';
import { type TableDataEntry } from './types';
import { type DataUseTermsHistoryEntry } from '../../types/backend';
import type { ReferenceGenomesInfo } from '../../types/referencesGenomes';

interface Props {
data: TableDataEntry;
dataUseTermsHistory: DataUseTermsHistoryEntry[];
referenceGenomesInfo: ReferenceGenomesInfo;
}

const DataTableComponent: React.FC<Props> = ({ data, dataUseTermsHistory }) => {
const DataTableComponent: React.FC<Props> = ({ data, dataUseTermsHistory, referenceGenomesInfo }) => {
const { label, type } = data;
return (
<>
{type.kind === 'metadata' && (
<div className='text-sm grid my-1' style={{ gridTemplateColumns: '200px 1fr' }}>
<div className='font-medium text-gray-900 break-inside-avoid pr-4'>{label}</div>
<DataTableEntryValue data={data} dataUseTermsHistory={dataUseTermsHistory} />
<DataTableEntryValue
data={data}
dataUseTermsHistory={dataUseTermsHistory}
referenceGenomesInfo={referenceGenomesInfo}
/>
</div>
)}

{type.kind === 'mutation' && (
<div className='text-sm my-1'>
<div className='font-medium text-gray-900 break-inside-avoid py-2'>{label}</div>
<DataTableEntryValue data={data} dataUseTermsHistory={dataUseTermsHistory} />
<DataTableEntryValue
data={data}
dataUseTermsHistory={dataUseTermsHistory}
referenceGenomesInfo={referenceGenomesInfo}
/>
</div>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import CustomDisplayComponent from './DataTableEntryValue';
import type { TableDataEntry } from './types';
import type { ReferenceGenomesInfo } from '../../types/referencesGenomes';

const makeData = (jsonValue: object, overrides: Partial<TableDataEntry> = {}): TableDataEntry => ({
label: 'Test Field',
name: 'testField',
value: JSON.stringify(jsonValue),
header: 'Test Header',
type: { kind: 'metadata', metadataType: 'string' },
customDisplay: { type: 'variantReference' },
...overrides,
});

const renderVariantReference = (jsonValue: object, referenceGenomesInfo: ReferenceGenomesInfo) =>
render(
<CustomDisplayComponent
data={makeData(jsonValue)}
dataUseTermsHistory={[]}
referenceGenomesInfo={referenceGenomesInfo}
/>,
);

const singleSegInfo: ReferenceGenomesInfo = {
segmentReferenceGenomes: {
main: {
refA: { lapisName: 'main', insdcAccessionFull: null, genes: [], displayName: 'Reference A' },
refB: { lapisName: 'main-refB', insdcAccessionFull: null, genes: [], displayName: 'Reference B' },
},
},
segmentDisplayNames: { main: 'Main Segment' },
isMultiSegmented: false,
useLapisMultiSegmentedEndpoint: false,
};

describe('VariantReferenceComponent', () => {
it('shows the reference displayName when not a variant', () => {
renderVariantReference(
[
{ name: 'reference_main', value: 'refA' },
{ name: 'variant_main', value: 'false' },
],
singleSegInfo,
);

expect(screen.getByText('Reference A')).toBeInTheDocument();
expect(screen.queryByText(/variant/)).not.toBeInTheDocument();
});

it('appends "(variant)" when variant is true', () => {
renderVariantReference(
[
{ name: 'reference_main', value: 'refA' },
{ name: 'variant_main', value: 'true' },
],
singleSegInfo,
);

expect(screen.getByText('Reference A (variant)')).toBeInTheDocument();
});

it('shows "N/A" when no matching reference entry is present', () => {
renderVariantReference([], singleSegInfo);

expect(screen.getByText('N/A')).toBeInTheDocument();
});

it('falls back to the raw reference name when no displayName is configured', () => {
const infoWithoutDisplayName: ReferenceGenomesInfo = {
segmentReferenceGenomes: {
main: {
refA: { lapisName: 'main', insdcAccessionFull: null, genes: [] },
},
},
segmentDisplayNames: {},
isMultiSegmented: false,
useLapisMultiSegmentedEndpoint: false,
};

renderVariantReference([{ name: 'reference_main', value: 'refA' }], infoWithoutDisplayName);

expect(screen.getByText('refA')).toBeInTheDocument();
});

it('treats absent variant entry as non-variant', () => {
renderVariantReference([{ name: 'reference_main', value: 'refA' }], singleSegInfo);

expect(screen.getByText('Reference A')).toBeInTheDocument();
expect(screen.queryByText(/variant/)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { PlainValueDisplay } from './PlainValueDisplay.tsx';
import { type TableDataEntry } from './types.ts';
import { type DataUseTermsHistoryEntry } from '../../types/backend.ts';
import type { MutationBadgeData } from '../../types/config.ts';
import type { ReferenceGenomesInfo } from '../../types/referencesGenomes.ts';
import { getReferenceDisplayNameMap } from '../../utils/sequenceTypeHelpers.ts';

interface Props {
data: TableDataEntry;
dataUseTermsHistory: DataUseTermsHistoryEntry[];
referenceGenomesInfo: ReferenceGenomesInfo;
}

const GroupComponent: React.FC<{ jsonString: string }> = ({ jsonString }) => {
Expand Down Expand Up @@ -86,6 +89,35 @@ const GeoLocationComponent: React.FC<{ jsonString: string }> = ({ jsonString })
return <>{displayText}</>;
};

const VariantReferenceComponent: React.FC<{ jsonString: string; referenceGenomesInfo: ReferenceGenomesInfo }> = ({
jsonString,
referenceGenomesInfo,
}) => {
const entries = JSON.parse(jsonString) as TableDataEntry[];

let variant = false;
let reference: string | undefined = undefined;
let referenceDisplayName: string | undefined = undefined;
for (const segmentName of Object.keys(referenceGenomesInfo.segmentReferenceGenomes)) {
variant =
entries
.find((e) => e.name === 'variant_' + segmentName)
?.value.toString()
.toLowerCase() === 'true';
reference = entries.find((e) => e.name === 'reference_' + segmentName)?.value.toString();
if (reference) {
referenceDisplayName =
getReferenceDisplayNameMap(referenceGenomesInfo, segmentName).get(reference) ?? segmentName;
break;
}
}

if (variant) {
return <>{referenceDisplayName ?? reference ?? 'N/A'} (variant)</>;
}
return <>{referenceDisplayName ?? reference ?? 'N/A'}</>;
};

type FileEntry = {
fileId: string;
name: string;
Expand Down Expand Up @@ -159,7 +191,7 @@ export function parseMutations(input: string): MutationBadgeData[] {
.filter((m): m is MutationBadgeData => m !== null);
}

const CustomDisplayComponent: React.FC<Props> = ({ data, dataUseTermsHistory }) => {
const CustomDisplayComponent: React.FC<Props> = ({ data, dataUseTermsHistory, referenceGenomesInfo }) => {
const { value, customDisplay } = data;

return (
Expand Down Expand Up @@ -223,6 +255,9 @@ const CustomDisplayComponent: React.FC<Props> = ({ data, dataUseTermsHistory })
{customDisplay?.type === 'geoLocation' && typeof value == 'string' && (
<GeoLocationComponent jsonString={value} />
)}
{customDisplay?.type === 'variantReference' && typeof value == 'string' && (
<VariantReferenceComponent jsonString={value} referenceGenomesInfo={referenceGenomesInfo} />
)}
{customDisplay?.type === 'fileList' && typeof value == 'string' && (
<FileListComponent jsonString={value} />
)}
Expand Down
11 changes: 11 additions & 0 deletions website/src/utils/sequenceTypeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,17 @@ export function getInsdcAccessionsFromSegmentReferences(
return references;
}

export function getReferenceDisplayNameMap(
referenceGenomesInfo: ReferenceGenomesInfo,
segmentName: string,
): Map<string, string> {
return new Map(
Object.entries(referenceGenomesInfo.segmentReferenceGenomes[segmentName]).map(
([ref, refData]) => [ref, refData.displayName ?? ref] as const,
),
);
}

/**
* @param referenceGenomesInfo - The reference genome lightweight schema
* @returns Returns a map from LAPIS names to displayNames (segment or gene names).
Expand Down
Loading