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
3 changes: 3 additions & 0 deletions components/src/lapisApi/lapisApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ export async function fetchSubstitutionsOrDeletions(
return mutationsResponse.parse(await response.json());
}

/**
* The 2D 'data' array is: 1st axis mutation, 2nd axis date ranges.
*/
export async function fetchMutationsOverTime(
lapisUrl: string,
body: MutationsOverTimeRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,26 +200,12 @@ export const UsesHideGaps: StoryObj<MutationsOverTimeProps> = {
...Default,
args: {
...Default.args,
initialMeanProportionInterval: { min: 0, max: 1 },
displayMutations: ['A13121T', 'G24872T', 'T21653-'],
},
parameters: {
fetchMock: {
mocks: [
{
matcher: {
url: `${LAPIS_URL}/sample/nucleotideMutations`,
body: {
pangoLineage: 'JN.1*',
dateFrom: '2024-01-01',
dateTo: '2024-07-31',
minProportion: 0.001,
},
response: {
status: 200,
body: mockDefaultNucleotideMutations,
},
},
},
{
matcher: {
url: `${LAPIS_URL}/component/nucleotideMutationsOverTime`,
Expand Down Expand Up @@ -325,6 +311,7 @@ export const WithCustomColumns: StoryObj<MutationsOverTimeProps> = {
...Default,
args: {
...Default.args,
initialMeanProportionInterval: { min: 0, max: 1 },
displayMutations: ['A13121T', 'G24872T', 'T21653-'],
customColumns: [
{
Expand All @@ -339,21 +326,6 @@ export const WithCustomColumns: StoryObj<MutationsOverTimeProps> = {
parameters: {
fetchMock: {
mocks: [
{
matcher: {
url: `${LAPIS_URL}/sample/nucleotideMutations`,
body: {
pangoLineage: 'JN.1*',
dateFrom: '2024-01-01',
dateTo: '2024-07-31',
minProportion: 0.001,
},
response: {
status: 200,
body: mockDefaultNucleotideMutations,
},
},
},
{
matcher: {
url: `${LAPIS_URL}/component/nucleotideMutationsOverTime`,
Expand Down
44 changes: 5 additions & 39 deletions components/src/query/queryMutationsOverTime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
data: {
data: [
[
{ count: 4, coverage: 10 },
{ count: 9, coverage: 10 },
{ count: 0, coverage: 10 },
{ count: 0, coverage: 10 },
],
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('queryMutationsOverTimeNewEndpoint', () => {

const expectedData = [
[
{ type: 'valueWithCoverage', count: 4, coverage: 10, totalCount: 11 },
{ type: 'valueWithCoverage', count: 9, coverage: 10, totalCount: 11 },
{ type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 12 },
{ type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 13 },
],
Expand Down Expand Up @@ -122,8 +122,8 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
code: 'otherSequenceName:G234C',
type: 'substitution',
},
count: 4,
proportion: 0.22,
count: 9,
proportion: 0.3,
},
{
type: 'substitution',
Expand All @@ -136,7 +136,7 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
type: 'substitution',
},
count: 6,
proportion: 0.21,
proportion: 0.2,
},
]);
});
Expand Down Expand Up @@ -762,23 +762,6 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
},
);

lapisRequestMocks.multipleMutations(
[
{
body: {
...lapisFilter,
dateFieldFrom: '2023-01-01',
dateFieldTo: '2023-02-28',
minProportion: 0.001,
},
response: {
data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
},
},
],
'nucleotide',
);

const dateRanges = [
{
dateFrom: '2023-01-01',
Expand Down Expand Up @@ -864,23 +847,6 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
},
);

lapisRequestMocks.multipleMutations(
[
{
body: {
...lapisFilter,
dateFieldFrom: '2023-01-01',
dateFieldTo: '2023-02-28',
minProportion: 0.001,
},
response: {
data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
},
},
],
'nucleotide',
);

const dateRanges = [
{
dateFrom: '2023-01-01',
Expand Down
165 changes: 53 additions & 112 deletions components/src/query/queryMutationsOverTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../types';
import { type Map2DContents } from '../utils/map2d';
import { type Deletion, type Substitution, DeletionClass, SubstitutionClass } from '../utils/mutations';
import { type Temporal } from '../utils/temporalClass';
import { type TemporalClass, type Temporal } from '../utils/temporalClass';

export type ProportionValue =
| {
Expand Down Expand Up @@ -53,95 +53,6 @@ export function getProportion(value: ProportionValue) {
const MAX_NUMBER_OF_GRID_COLUMNS = 200;
export const MUTATIONS_OVER_TIME_MIN_PROPORTION = 0.001;

/**
* Create SubstitutionOrDeletionEntry for given code with count and proportion 0.
* @param code a mutation code like G44T or A23-
*/
function codeToEmptyEntry(code: string): SubstitutionOrDeletionEntry | null {
const maybeDeletion = DeletionClass.parse(code);
if (maybeDeletion) {
return {
type: 'deletion',
mutation: maybeDeletion,
count: 0,
proportion: 0,
};
}
const maybeSubstitution = SubstitutionClass.parse(code);
if (maybeSubstitution) {
return {
type: 'substitution',
mutation: maybeSubstitution,
count: 0,
proportion: 0,
};
}
return null;
}

/**
* Return counts and proportions for all mutations that match the lapisFilter.
* If `includeMutations` are given, the result will also be filtered for those.
* Any mutation that isn't in the result, but is in the `includeMutations` will
* be in the result with count and proportion as 0.
*/
async function queryOverallMutationData({
lapisFilter,
sequenceType,
lapis,
granularity,
lapisDateField,
includeMutations,
signal,
}: {
lapisFilter: LapisFilter;
sequenceType: SequenceType;
lapis: string;
granularity: TemporalGranularity;
lapisDateField: string;
includeMutations?: string[];
signal?: AbortSignal;
}) {
const requestedDateRanges = await queryDatesInDataset(lapisFilter, lapis, granularity, lapisDateField, signal);

if (requestedDateRanges.length === 0) {
if (includeMutations) {
return {
content: includeMutations
.map(codeToEmptyEntry)
.filter((e): e is SubstitutionOrDeletionEntry => e !== null),
};
} else {
return {
content: [],
};
}
}

const filter = {
...lapisFilter,
[`${lapisDateField}From`]: requestedDateRanges[0].firstDay.toString(),
[`${lapisDateField}To`]: requestedDateRanges[requestedDateRanges.length - 1].lastDay.toString(),
};

let dataPromise = fetchAndPrepareSubstitutionsOrDeletions(filter, sequenceType).evaluate(lapis, signal);

if (includeMutations) {
dataPromise = dataPromise.then((data) => {
return {
content: includeMutations
.map((code) => {
const found = data.content.find((m) => m.mutation.code === code);
return found ?? codeToEmptyEntry(code);
})
.filter((e): e is SubstitutionOrDeletionEntry => e !== null),
};
});
}

return dataPromise;
}

export async function queryMutationsOverTimeData(
lapisFilter: LapisFilter,
sequenceType: SequenceType,
Expand All @@ -162,18 +73,21 @@ export async function queryMutationsOverTimeData(
);
}

const overallMutationData = await queryOverallMutationData({
lapisFilter,
sequenceType,
lapis,
lapisDateField,
includeMutations: displayMutations,
granularity,
}).then((r) => r.content);

overallMutationData.sort((a, b) => sortSubstitutionsAndDeletions(a.mutation, b.mutation));
const mutationsToQuery =
displayMutations ??
(await queryAllMutationCodes({
lapisFilter,
requestedDateRanges,
sequenceType,
lapis,
lapisDateField,
signal,
}));
const sortedMutationCodes = mutationsToQuery
.map(parseMutationCode)
.sort((a, b) => sortSubstitutionsAndDeletions(a, b))
.map((m) => m.code);
Comment on lines +76 to +89
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

displayMutations values are now parsed via parseMutationCode for sorting. If a user provides an invalid mutation string, this will throw a generic Error and likely surface as an unhelpful runtime failure. Consider validating displayMutations up front and throwing a UserFacingError (or filtering out invalid codes) with a message that tells the user which entry is invalid and what formats are accepted.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm, it's not really a 'user' input per se, it's like if you misspelled the date field, it'll just error. IMO that's how we're doing things, so no change required?

Maybe we could add the parsing earlier, so we get the 'invalid component attributes' error, but I don't want to change more of the structure in the PR at the moment, because like Fabian said, we might already get merge conflicts the way it is.

But we could do it.


const includeMutations = overallMutationData.map((value) => value.mutation.code);
const apiResult = await fetchMutationsOverTime(
lapis,
{
Expand All @@ -182,7 +96,7 @@ export async function queryMutationsOverTimeData(
dateFrom: date.firstDay.toString(),
dateTo: date.lastDay.toString(),
})),
includeMutations,
includeMutations: sortedMutationCodes,
dateField: lapisDateField,
},
sequenceType,
Expand All @@ -192,21 +106,22 @@ export async function queryMutationsOverTimeData(
const totalCounts = apiResult.data.totalCountsByDateRange;
const responseMutations = apiResult.data.mutations.map(parseMutationCode);
const mutationEntries: SubstitutionOrDeletionEntry[] = responseMutations.map((mutation, i) => {
const numbers = {
count: overallMutationData[i].count,
proportion: overallMutationData[i].proportion,
};
const count = apiResult.data.data[i].map(({ count }) => count).reduce((acc, c) => acc + c, 0);
const coverage = apiResult.data.data[i].map(({ coverage }) => coverage).reduce((acc, c) => acc + c, 0);
const proportion = coverage === 0 ? 0 : count / coverage;
if (mutation.type === 'deletion') {
return {
type: 'deletion',
mutation,
...numbers,
count,
proportion,
};
} else {
return {
type: 'substitution',
mutation,
...numbers,
count,
proportion,
};
}
});
Expand Down Expand Up @@ -257,6 +172,36 @@ export async function queryMutationsOverTimeData(
};
}

async function queryAllMutationCodes({
lapisFilter,
requestedDateRanges,
sequenceType,
lapis,
lapisDateField,
signal,
}: {
lapisFilter: LapisFilter;
requestedDateRanges: TemporalClass[];
sequenceType: SequenceType;
lapis: string;
lapisDateField: string;
signal?: AbortSignal;
}) {
if (requestedDateRanges.length === 0) {
return [];
}

const filter = {
...lapisFilter,
[`${lapisDateField}From`]: requestedDateRanges[0].firstDay.toString(),
[`${lapisDateField}To`]: requestedDateRanges[requestedDateRanges.length - 1].lastDay.toString(),
};

return new FetchSubstitutionsOrDeletionsOperator(filter, sequenceType, MUTATIONS_OVER_TIME_MIN_PROPORTION)
.evaluate(lapis, signal)
.then((r) => r.content.map((v) => v.mutation.code));
}

function parseMutationCode(code: string): SubstitutionClass | DeletionClass {
const maybeDeletion = DeletionClass.parse(code);
if (maybeDeletion) {
Expand All @@ -269,10 +214,6 @@ function parseMutationCode(code: string): SubstitutionClass | DeletionClass {
throw Error(`Given code is not valid: ${code}`);
}

function fetchAndPrepareSubstitutionsOrDeletions(filter: LapisFilter, sequenceType: SequenceType) {
return new FetchSubstitutionsOrDeletionsOperator(filter, sequenceType, MUTATIONS_OVER_TIME_MIN_PROPORTION);
}

export function serializeSubstitutionOrDeletion(mutation: Substitution | Deletion) {
return mutation.code;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ mutation,2024-01,2024-02,2024-03,2024-04,2024-05,2024-06,2024-07
C44T,0.5921375921375921,0.5685053380782918,0.5679558011049723,0.7314410480349345,0.8050993949870354,0.863654699475595,0.7934734018775146
C774T,0.2632696390658174,0.2690614136732329,0.2372408776821433,0.13129770992366413,0.04133493292616425,0.01090598915493816,0.005494505494505495
C7113T,0.0029329708848987767,0.011585225969259004,0.06797510770703687,0.26403641881638845,0.5209133823690855,0.6900846729660081,0.7367996775493753
C12616T,0.0017875017875017875,0.0014855445091989487,0.00035816618911174784,0.00045139933794763765,0.006197673154289442,0.058114638771640054,0.1533717656522322
A13121T,0.00007127583749109053,0.00005699629524080935,0.00011879306248515087,0,0.00465569510610654,0.05599416980444553,0.1498393144081414
G15372T,0.0005756223917110375,0.0006303002521201008,0.005145387100634199,0.030404378230465188,0.08474015226746111,0.18635617437830607,0.25857449088960344
G17334T,0.004642525533890436,0.011425307055127107,0.0672939494997618,0.25772259236826167,0.5089117679619174,0.6875799476152769,0.730991015153547
T18453C,0.2116254036598493,0.20737986270022885,0.18033178183554124,0.08815551537070525,0.025955690703735882,0.004694835680751174,0.0025397674107739605
A19722G,0.00007136739937196689,0.0003997487293701102,0.004284184219921457,0.029509183980728697,0.08565217391304347,0.18521225409586456,0.2576974564926372
G21641T,0.08183956525265634,0.027512670308694795,0.033673017279574655,0.10217065868263472,0.09022353891731753,0.05198019801980198,0.023539668700959023
T21653-,0.0009321669295855443,0.0008644038494784763,0.004929069487857658,0.036100533130236104,0.15123729562527619,0.31595969647966166,0.44197766276219014
C21654-,0.000860832137733142,0.0008068699210420149,0.004930254930254931,0.03563813585135547,0.1510330350237543,0.3159400460227626,0.4437910284463895
T21655-,0.0008608938948274625,0.0008067304367869079,0.004924333413403795,0.035622252539032895,0.15042117930204574,0.3109536318651109,0.43546877089741876
Expand All @@ -18,3 +17,5 @@ T23011-,0.9004643064422518,0.9066964548180375,0.8651630301544496,0.8372413793103
C23039G,0.0001443209698369173,0.0004090457546894174,0.00486677211339579,0.05498020103563814,0.18751371516348475,0.3774672060806669,0.45300420453004203
C23277T,0.07554827684420393,0.15478835526690593,0.26534253092293053,0.2802825368199579,0.14819224940463305,0.0635382563356372,0.04864720778355325
G24872T,0.003051216854340719,0.011751765659372467,0.06819282945736434,0.2551692449073365,0.5131870567375887,0.6895231035334816,0.7373190856215339
G25012T,0.028579697561681498,0.06180391251659069,0.1189469870788552,0.14080635308491143,0.07751596564633341,0.019103194103194103,0.007280571659700688
C25566T,0.03338315894369706,0.0688515246508977,0.12407716122886402,0.14221286831028263,0.07809110629067245,0.019222606270086713,0.006937033084311633
Loading