@platforma-sdk/ui-vue 1.42.53 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +213 -207
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/AgGridVue/useAgGridOptions.js +2 -3
- package/dist/AgGridVue/useAgGridOptions.js.map +1 -1
- package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js +6 -0
- package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js.map +1 -0
- package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js +5 -0
- package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js.map +1 -0
- package/dist/components/PlAgDataTable/PlAgRowCount.vue.js +2 -3
- package/dist/components/PlAgDataTable/PlAgRowCount.vue.js.map +1 -1
- package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js +11 -12
- package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js.map +1 -1
- package/dist/components/PlAgRowNumHeader.vue.js +8 -9
- package/dist/components/PlAgRowNumHeader.vue.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/Consensus.vue.d.ts +1 -0
- package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js +48 -46
- package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js +5 -7
- package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js +14 -13
- package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/Legend.vue3.js +9 -8
- package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.d.ts +16 -9
- package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js +117 -85
- package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js +25 -18
- package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.d.ts +8 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js +10 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js.map +1 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js +77 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js.map +1 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js +9 -0
- package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js.map +1 -0
- package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.d.ts +26 -18
- package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js +119 -120
- package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js +7 -124
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js +124 -2
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js +9 -0
- package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js.map +1 -0
- package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js +90 -90
- package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/Toolbar.vue3.js +9 -7
- package/dist/components/PlMultiSequenceAlignment/cell-size.d.ts +4 -0
- package/dist/components/PlMultiSequenceAlignment/cell-size.js +8 -0
- package/dist/components/PlMultiSequenceAlignment/cell-size.js.map +1 -0
- package/dist/components/PlMultiSequenceAlignment/data.d.ts +15 -10
- package/dist/components/PlMultiSequenceAlignment/data.js +309 -202
- package/dist/components/PlMultiSequenceAlignment/data.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/markup.js +9 -7
- package/dist/components/PlMultiSequenceAlignment/markup.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/migrations.js +15 -13
- package/dist/components/PlMultiSequenceAlignment/migrations.js.map +1 -1
- package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.d.ts +6 -0
- package/dist/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.d.ts +7 -0
- package/dist/components/PlMultiSequenceAlignment/settings.js +3 -4
- package/dist/components/PlMultiSequenceAlignment/settings.js.map +1 -1
- package/dist/index.js +25 -27
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +1 -2
- package/package.json +6 -6
- package/src/components/PlMultiSequenceAlignment/Consensus.vue +38 -39
- package/src/components/PlMultiSequenceAlignment/Legend.vue +9 -9
- package/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue +222 -126
- package/src/components/PlMultiSequenceAlignment/PhylogeneticTree.vue +110 -0
- package/src/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue +28 -22
- package/src/components/PlMultiSequenceAlignment/SeqLogo.vue +77 -69
- package/src/components/PlMultiSequenceAlignment/Toolbar.vue +47 -39
- package/src/components/PlMultiSequenceAlignment/cell-size.ts +4 -0
- package/src/components/PlMultiSequenceAlignment/data.ts +361 -149
- package/src/components/PlMultiSequenceAlignment/markup.ts +10 -8
- package/src/components/PlMultiSequenceAlignment/migrations.ts +6 -1
- package/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.ts +54 -0
- package/src/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.ts +89 -0
- package/src/components/PlMultiSequenceAlignment/settings.ts +1 -2
- package/src/lib.ts +1 -3
- package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.d.ts +0 -7
- package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.js +0 -51
- package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.js.map +0 -1
- package/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.ts +0 -101
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isJsonEqual } from '@milaboratories/helpers';
|
|
2
2
|
import type { ListOptionNormalized } from '@milaboratories/uikit';
|
|
3
3
|
import {
|
|
4
|
+
Annotation,
|
|
4
5
|
type CalculateTableDataRequest,
|
|
6
|
+
type CalculateTableDataResponse,
|
|
5
7
|
type CanonicalizedJson,
|
|
6
8
|
canonicalizeJson,
|
|
7
9
|
createRowSelectionColumn,
|
|
@@ -23,23 +25,25 @@ import {
|
|
|
23
25
|
type PTableSorting,
|
|
24
26
|
pTableValue,
|
|
25
27
|
readAnnotation,
|
|
26
|
-
Annotation,
|
|
27
28
|
readAnnotationJson,
|
|
28
29
|
} from '@platforma-sdk/model';
|
|
29
|
-
import { ref, watch } from 'vue';
|
|
30
|
+
import { onWatcherCleanup, ref, watch } from 'vue';
|
|
31
|
+
import { objectHash } from '../../objectHash';
|
|
30
32
|
import { highlightByChemicalProperties } from './chemical-properties';
|
|
33
|
+
import type { Markup } from './markup';
|
|
31
34
|
import {
|
|
32
35
|
highlightByMarkup,
|
|
33
36
|
markupAlignedSequence,
|
|
34
37
|
parseMarkup,
|
|
35
38
|
} from './markup';
|
|
36
|
-
import
|
|
39
|
+
import type * as MultiSequenceAlignmentWorker from './multi-sequence-alignment.worker';
|
|
40
|
+
import type * as PhylogeneticTreeWorker from './phylogenetic-tree.worker';
|
|
37
41
|
import { getResidueCounts } from './residue-counts';
|
|
38
42
|
import type { HighlightLegend, ResidueCounts } from './types';
|
|
39
43
|
|
|
40
44
|
const getPFrameDriver = () => getRawPlatformaInstance().pFrameDriver;
|
|
41
45
|
|
|
42
|
-
export const
|
|
46
|
+
export const SEQUENCE_LIMIT = 1000;
|
|
43
47
|
|
|
44
48
|
export const useSequenceColumnsOptions = refreshOnDeepChange(
|
|
45
49
|
getSequenceColumnsOptions,
|
|
@@ -57,10 +61,7 @@ export const useMultipleAlignmentData = refreshOnDeepChange(
|
|
|
57
61
|
getMultipleAlignmentData,
|
|
58
62
|
);
|
|
59
63
|
|
|
60
|
-
async function getSequenceColumnsOptions({
|
|
61
|
-
pFrame,
|
|
62
|
-
sequenceColumnPredicate,
|
|
63
|
-
}: {
|
|
64
|
+
async function getSequenceColumnsOptions({ pFrame, sequenceColumnPredicate }: {
|
|
64
65
|
pFrame: PFrameHandle | undefined;
|
|
65
66
|
sequenceColumnPredicate: (column: PColumnIdAndSpec) => boolean;
|
|
66
67
|
}): Promise<OptionsWithDefaults<PObjectId> | undefined> {
|
|
@@ -68,6 +69,7 @@ async function getSequenceColumnsOptions({
|
|
|
68
69
|
|
|
69
70
|
const pFrameDriver = getPFrameDriver();
|
|
70
71
|
const columns = await pFrameDriver.listColumns(pFrame);
|
|
72
|
+
|
|
71
73
|
const options = columns.values()
|
|
72
74
|
.filter((column) => sequenceColumnPredicate(column))
|
|
73
75
|
.map(({ spec, columnId }) => ({
|
|
@@ -75,14 +77,13 @@ async function getSequenceColumnsOptions({
|
|
|
75
77
|
value: columnId,
|
|
76
78
|
}))
|
|
77
79
|
.toArray();
|
|
80
|
+
|
|
78
81
|
const defaults = options.map(({ value }) => value);
|
|
82
|
+
|
|
79
83
|
return { options, defaults };
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
async function getLabelColumnsOptions({
|
|
83
|
-
pFrame,
|
|
84
|
-
sequenceColumnIds,
|
|
85
|
-
}: {
|
|
86
|
+
async function getLabelColumnsOptions({ pFrame, sequenceColumnIds }: {
|
|
86
87
|
pFrame: PFrameHandle | undefined;
|
|
87
88
|
sequenceColumnIds: PObjectId[] | undefined;
|
|
88
89
|
}): Promise<OptionsWithDefaults<PTableColumnId> | undefined> {
|
|
@@ -90,7 +91,6 @@ async function getLabelColumnsOptions({
|
|
|
90
91
|
|
|
91
92
|
const pFrameDriver = getPFrameDriver();
|
|
92
93
|
const columns = await pFrameDriver.listColumns(pFrame);
|
|
93
|
-
const optionMap = new Map<CanonicalizedJson<PTableColumnId>, string>();
|
|
94
94
|
|
|
95
95
|
const sequenceColumnsAxes = new Map(
|
|
96
96
|
sequenceColumnIds.values().flatMap((id) => {
|
|
@@ -103,6 +103,7 @@ async function getLabelColumnsOptions({
|
|
|
103
103
|
}),
|
|
104
104
|
);
|
|
105
105
|
|
|
106
|
+
const optionMap = new Map<CanonicalizedJson<PTableColumnId>, string>();
|
|
106
107
|
for (const [axisIdJson, axisSpec] of sequenceColumnsAxes.entries()) {
|
|
107
108
|
const axisId = parseJson(axisIdJson);
|
|
108
109
|
const labelColumn = columns.find(({ spec }) =>
|
|
@@ -127,7 +128,10 @@ async function getLabelColumnsOptions({
|
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
for (const { columnId, spec } of compatibleColumns) {
|
|
130
|
-
const columnIdJson = canonicalizeJson({
|
|
131
|
+
const columnIdJson = canonicalizeJson<PTableColumnId>({
|
|
132
|
+
type: 'column',
|
|
133
|
+
id: columnId,
|
|
134
|
+
});
|
|
131
135
|
if (optionMap.has(columnIdJson)) continue;
|
|
132
136
|
optionMap.set(
|
|
133
137
|
columnIdJson,
|
|
@@ -147,59 +151,180 @@ async function getLabelColumnsOptions({
|
|
|
147
151
|
})
|
|
148
152
|
.map(({ value }) => value)
|
|
149
153
|
.toArray();
|
|
154
|
+
|
|
150
155
|
return { options, defaults };
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
async function getMarkupColumnsOptions({
|
|
154
|
-
pFrame,
|
|
155
|
-
sequenceColumnIds,
|
|
156
|
-
}: {
|
|
158
|
+
async function getMarkupColumnsOptions({ pFrame, sequenceColumnIds }: {
|
|
157
159
|
pFrame: PFrameHandle | undefined;
|
|
158
160
|
sequenceColumnIds: PObjectId[] | undefined;
|
|
159
|
-
}): Promise<ListOptionNormalized<PObjectId>[] | undefined> {
|
|
160
|
-
if (!pFrame || sequenceColumnIds
|
|
161
|
+
}): Promise<ListOptionNormalized<PObjectId[]>[] | undefined> {
|
|
162
|
+
if (!pFrame || !sequenceColumnIds) return;
|
|
161
163
|
|
|
162
164
|
const pFrameDriver = getPFrameDriver();
|
|
163
165
|
const columns = await pFrameDriver.listColumns(pFrame);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
|
|
167
|
+
const sequenceColumns = sequenceColumnIds.map((columnId) => {
|
|
168
|
+
const column = columns.find((column) => column.columnId === columnId);
|
|
169
|
+
if (!column) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Couldn't find sequence column (ID: \`${sequenceColumnIds[0]}\`).`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return column;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const columnPairs = sequenceColumns
|
|
178
|
+
.flatMap((sequenceColumn) =>
|
|
179
|
+
columns
|
|
180
|
+
.filter((column) =>
|
|
181
|
+
readAnnotationJson(column.spec, Annotation.Sequence.IsAnnotation)
|
|
182
|
+
&& isJsonEqual(sequenceColumn.spec.axesSpec, column.spec.axesSpec)
|
|
183
|
+
&& Object.entries(sequenceColumn.spec.domain ?? {})
|
|
184
|
+
.every(([key, value]) => column.spec.domain?.[key] === value),
|
|
185
|
+
)
|
|
186
|
+
.map((markupColumn) => ({ markupColumn, sequenceColumn })),
|
|
170
187
|
);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
|
|
189
|
+
const groupedByDomainDiff = Map.groupBy(
|
|
190
|
+
columnPairs,
|
|
191
|
+
({ markupColumn, sequenceColumn }) => {
|
|
192
|
+
const domainDiff = Object.fromEntries(
|
|
193
|
+
Object.entries(markupColumn.spec.domain ?? {})
|
|
194
|
+
.filter(([key]) => sequenceColumn.spec.domain?.[key] == undefined),
|
|
195
|
+
);
|
|
196
|
+
return canonicalizeJson(domainDiff);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return groupedByDomainDiff.entries()
|
|
201
|
+
.map(([domainDiffJson, columnPairs]) => ({
|
|
202
|
+
label: Object.values(parseJson(domainDiffJson)).join(', '),
|
|
203
|
+
value: columnPairs.map(({ markupColumn }) => markupColumn.columnId),
|
|
182
204
|
}))
|
|
183
205
|
.toArray();
|
|
184
206
|
}
|
|
185
207
|
|
|
186
|
-
async function getMultipleAlignmentData(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
async function getMultipleAlignmentData(
|
|
209
|
+
{
|
|
210
|
+
pFrame,
|
|
211
|
+
sequenceColumnIds,
|
|
212
|
+
labelColumnIds,
|
|
213
|
+
selection,
|
|
214
|
+
colorScheme,
|
|
215
|
+
alignmentParams,
|
|
216
|
+
shouldBuildPhylogeneticTree,
|
|
217
|
+
}: {
|
|
218
|
+
pFrame: PFrameHandle | undefined;
|
|
219
|
+
sequenceColumnIds: PObjectId[] | undefined;
|
|
220
|
+
labelColumnIds: PTableColumnId[] | undefined;
|
|
221
|
+
selection: PlSelectionModel | undefined;
|
|
222
|
+
colorScheme: PlMultiSequenceAlignmentColorSchemeOption;
|
|
223
|
+
alignmentParams: PlMultiSequenceAlignmentSettings['alignmentParams'];
|
|
224
|
+
shouldBuildPhylogeneticTree: boolean;
|
|
225
|
+
},
|
|
226
|
+
abortSignal: AbortSignal,
|
|
227
|
+
): Promise<MultipleAlignmentData | undefined> {
|
|
201
228
|
if (!pFrame || !sequenceColumnIds?.length || !labelColumnIds) return;
|
|
202
229
|
|
|
230
|
+
const table = await getTableData({
|
|
231
|
+
pFrame,
|
|
232
|
+
sequenceColumnIds,
|
|
233
|
+
labelColumnIds,
|
|
234
|
+
selection,
|
|
235
|
+
colorScheme,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const rowCount = table.at(0)?.data.data.length ?? 0;
|
|
239
|
+
|
|
240
|
+
if (rowCount < 2) return;
|
|
241
|
+
|
|
242
|
+
const exceedsLimit = rowCount > SEQUENCE_LIMIT;
|
|
243
|
+
const rawSequences = extractSequences(sequenceColumnIds, table);
|
|
244
|
+
const labels = extractLabels(labelColumnIds, table);
|
|
245
|
+
const markups = colorScheme.type === 'markup'
|
|
246
|
+
? extractMarkups(colorScheme.columnIds, table)
|
|
247
|
+
: undefined;
|
|
248
|
+
|
|
249
|
+
const highlightLegend: HighlightLegend = {};
|
|
250
|
+
|
|
251
|
+
const alignedSequences = await Promise.all(
|
|
252
|
+
rawSequences.map(async ({ name, rows }) => ({
|
|
253
|
+
name,
|
|
254
|
+
rows: await alignSequences(
|
|
255
|
+
rows,
|
|
256
|
+
JSON.parse(JSON.stringify(alignmentParams)),
|
|
257
|
+
abortSignal,
|
|
258
|
+
),
|
|
259
|
+
})),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
let phylogeneticTree: PhylogeneticTreeWorker.TreeNodeData[] | undefined;
|
|
263
|
+
if (shouldBuildPhylogeneticTree) {
|
|
264
|
+
phylogeneticTree = await buildPhylogeneticTree(
|
|
265
|
+
alignedSequences,
|
|
266
|
+
abortSignal,
|
|
267
|
+
);
|
|
268
|
+
const rowOrder = phylogeneticTree.values()
|
|
269
|
+
.filter(({ id }) => id >= 0)
|
|
270
|
+
.map(({ id }) => id)
|
|
271
|
+
.toArray();
|
|
272
|
+
for (const sequencesColumn of alignedSequences) {
|
|
273
|
+
sequencesColumn.rows = rowOrder.map((i) => sequencesColumn.rows[i]);
|
|
274
|
+
}
|
|
275
|
+
for (const labelsColumn of labels) {
|
|
276
|
+
labelsColumn.rows = rowOrder.map((i) => labelsColumn.rows[i]);
|
|
277
|
+
}
|
|
278
|
+
for (const markupsColumn of markups ?? []) {
|
|
279
|
+
markupsColumn.rows = rowOrder.map((i) => markupsColumn.rows[i]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sequences = await Promise.all(
|
|
284
|
+
alignedSequences.map(async ({ name, rows }, index) => {
|
|
285
|
+
const residueCounts = getResidueCounts(rows);
|
|
286
|
+
const image = generateHighlightImage({
|
|
287
|
+
colorScheme,
|
|
288
|
+
sequences: rows,
|
|
289
|
+
residueCounts,
|
|
290
|
+
markup: markups?.at(index),
|
|
291
|
+
});
|
|
292
|
+
if (image) {
|
|
293
|
+
Object.assign(highlightLegend, image.legend);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
name,
|
|
297
|
+
rows,
|
|
298
|
+
residueCounts,
|
|
299
|
+
...image && {
|
|
300
|
+
highlightImageUrl: await blobToBase64(image.blob),
|
|
301
|
+
},
|
|
302
|
+
} satisfies MultipleAlignmentData['sequences'][number];
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
sequences,
|
|
308
|
+
labels,
|
|
309
|
+
...Object.keys(highlightLegend).length && {
|
|
310
|
+
highlightLegend,
|
|
311
|
+
},
|
|
312
|
+
...phylogeneticTree && {
|
|
313
|
+
phylogeneticTree,
|
|
314
|
+
},
|
|
315
|
+
exceedsLimit,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function getTableData(
|
|
320
|
+
{ pFrame, sequenceColumnIds, labelColumnIds, selection, colorScheme }: {
|
|
321
|
+
pFrame: PFrameHandle;
|
|
322
|
+
sequenceColumnIds: PObjectId[];
|
|
323
|
+
labelColumnIds: PTableColumnId[];
|
|
324
|
+
selection: PlSelectionModel | undefined;
|
|
325
|
+
colorScheme: PlMultiSequenceAlignmentColorSchemeOption;
|
|
326
|
+
},
|
|
327
|
+
): Promise<CalculateTableDataResponse> {
|
|
203
328
|
const pFrameDriver = getPFrameDriver();
|
|
204
329
|
const columns = await pFrameDriver.listColumns(pFrame);
|
|
205
330
|
const linkerColumns = columns.filter((column) => isLinkerColumn(column.spec));
|
|
@@ -250,7 +375,9 @@ async function getMultipleAlignmentData({
|
|
|
250
375
|
|
|
251
376
|
// and markup
|
|
252
377
|
if (colorScheme.type === 'markup') {
|
|
253
|
-
|
|
378
|
+
for (const column of colorScheme.columnIds) {
|
|
379
|
+
secondaryEntry.push({ type: 'column', column });
|
|
380
|
+
}
|
|
254
381
|
}
|
|
255
382
|
|
|
256
383
|
const sorting: PTableSorting[] = Array.from(
|
|
@@ -282,125 +409,203 @@ async function getMultipleAlignmentData({
|
|
|
282
409
|
sorting,
|
|
283
410
|
};
|
|
284
411
|
|
|
285
|
-
|
|
412
|
+
return pFrameDriver.calculateTableData(
|
|
286
413
|
pFrame,
|
|
287
414
|
JSON.parse(JSON.stringify(request)),
|
|
288
415
|
{
|
|
289
416
|
offset: 0,
|
|
290
417
|
// +1 is a hack to check whether the selection is over the limit
|
|
291
|
-
length:
|
|
418
|
+
length: SEQUENCE_LIMIT + 1,
|
|
292
419
|
},
|
|
293
420
|
);
|
|
421
|
+
}
|
|
294
422
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const sequenceColumns = sequenceColumnIds.map((columnId) => {
|
|
423
|
+
const extractSequences = (
|
|
424
|
+
columnIds: PObjectId[],
|
|
425
|
+
table: CalculateTableDataResponse,
|
|
426
|
+
): { name: string; rows: string[] }[] =>
|
|
427
|
+
columnIds.map((columnId) => {
|
|
303
428
|
const column = table.find(({ spec }) => spec.id === columnId);
|
|
304
429
|
if (!column) {
|
|
305
430
|
throw new Error(`Couldn't find sequence column (ID: \`${columnId}\`).`);
|
|
306
431
|
}
|
|
307
|
-
|
|
432
|
+
const name = readAnnotation(column.spec.spec, Annotation.Label)
|
|
433
|
+
?? 'Unlabeled column';
|
|
434
|
+
const rows = column.data.data
|
|
435
|
+
.keys()
|
|
436
|
+
.take(SEQUENCE_LIMIT)
|
|
437
|
+
.map((row) =>
|
|
438
|
+
pTableValue(column.data, row, { absent: '', na: '' })?.toString()
|
|
439
|
+
?? '',
|
|
440
|
+
)
|
|
441
|
+
.toArray();
|
|
442
|
+
return { name, rows };
|
|
308
443
|
});
|
|
309
444
|
|
|
310
|
-
|
|
445
|
+
const extractLabels = (
|
|
446
|
+
columnIds: PTableColumnId[],
|
|
447
|
+
table: CalculateTableDataResponse,
|
|
448
|
+
): { rows: string[] }[] =>
|
|
449
|
+
columnIds.map((columnId) => {
|
|
311
450
|
const column = table.find(({ spec }) => {
|
|
312
|
-
if (
|
|
313
|
-
return isJsonEqual(
|
|
451
|
+
if (columnId.type === 'axis' && spec.type === 'axis') {
|
|
452
|
+
return isJsonEqual(columnId.id, spec.id);
|
|
314
453
|
}
|
|
315
|
-
if (
|
|
316
|
-
return
|
|
454
|
+
if (columnId.type === 'column' && spec.type === 'column') {
|
|
455
|
+
return columnId.id === spec.id;
|
|
317
456
|
}
|
|
318
457
|
});
|
|
319
458
|
if (!column) {
|
|
320
|
-
throw new Error(`Couldn't find label column (ID: \`${
|
|
459
|
+
throw new Error(`Couldn't find label column (ID: \`${columnId}\`).`);
|
|
321
460
|
}
|
|
322
|
-
|
|
461
|
+
const rows = column.data.data
|
|
462
|
+
.keys()
|
|
463
|
+
.take(SEQUENCE_LIMIT)
|
|
464
|
+
.map((row) =>
|
|
465
|
+
pTableValue(column.data, row, { absent: '', na: '' })?.toString()
|
|
466
|
+
?? '',
|
|
467
|
+
)
|
|
468
|
+
.toArray();
|
|
469
|
+
return { rows };
|
|
323
470
|
});
|
|
324
471
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
472
|
+
const extractMarkups = (
|
|
473
|
+
columnIds: PObjectId[],
|
|
474
|
+
table: CalculateTableDataResponse,
|
|
475
|
+
): { labels: Record<string, string>; rows: Markup[] }[] =>
|
|
476
|
+
columnIds.map((columnId) => {
|
|
477
|
+
const column = table.find(({ spec }) => spec.id === columnId);
|
|
478
|
+
if (!column) {
|
|
479
|
+
throw new Error(`Couldn't find markup column (ID: \`${columnId}\`).`);
|
|
480
|
+
}
|
|
481
|
+
const labels = readAnnotationJson(
|
|
482
|
+
column.spec.spec,
|
|
483
|
+
Annotation.Sequence.Annotation.Mapping,
|
|
484
|
+
) ?? {};
|
|
485
|
+
const rows = column.data.data
|
|
486
|
+
.keys()
|
|
487
|
+
.take(SEQUENCE_LIMIT)
|
|
488
|
+
.map((row) =>
|
|
489
|
+
parseMarkup(
|
|
490
|
+
pTableValue(column.data, row, { absent: '', na: '' })?.toString()
|
|
491
|
+
?? '',
|
|
333
492
|
),
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
const sequences = Array.from(
|
|
340
|
-
{ length: rowCount },
|
|
341
|
-
(_, row) => alignedSequences.map((column) => column[row]),
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
const sequenceNames = sequenceColumns.map((column) =>
|
|
345
|
-
readAnnotation(column.spec.spec, Annotation.Label) ?? 'Unlabeled column',
|
|
346
|
-
);
|
|
493
|
+
)
|
|
494
|
+
.toArray();
|
|
495
|
+
return { labels, rows };
|
|
496
|
+
});
|
|
347
497
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
498
|
+
const alignSequences = (() => {
|
|
499
|
+
const cache = new Map<string, string[]>();
|
|
500
|
+
return async (
|
|
501
|
+
sequences: string[],
|
|
502
|
+
alignmentParams: PlMultiSequenceAlignmentSettings['alignmentParams'],
|
|
503
|
+
abortSignal: AbortSignal,
|
|
504
|
+
): Promise<string[]> => {
|
|
505
|
+
const hash = await objectHash([sequences, alignmentParams]);
|
|
506
|
+
let result = cache.get(hash);
|
|
507
|
+
if (result) return result;
|
|
508
|
+
result = await runInWorker<
|
|
509
|
+
MultiSequenceAlignmentWorker.RequestMessage,
|
|
510
|
+
MultiSequenceAlignmentWorker.ResponseMessage
|
|
511
|
+
>(
|
|
512
|
+
new Worker(
|
|
513
|
+
new URL('./multi-sequence-alignment.worker.ts', import.meta.url),
|
|
514
|
+
{ type: 'module' },
|
|
353
515
|
),
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const result: MultipleAlignmentData = {
|
|
360
|
-
sequences: concatenatedSequences,
|
|
361
|
-
sequenceNames,
|
|
362
|
-
labelRows: labels,
|
|
363
|
-
exceedsLimit,
|
|
364
|
-
residueCounts,
|
|
516
|
+
{ sequences, params: alignmentParams },
|
|
517
|
+
abortSignal,
|
|
518
|
+
);
|
|
519
|
+
cache.set(hash, result);
|
|
520
|
+
return result;
|
|
365
521
|
};
|
|
366
|
-
|
|
522
|
+
})();
|
|
523
|
+
|
|
524
|
+
function generateHighlightImage(
|
|
525
|
+
{ colorScheme, sequences, residueCounts, markup }: {
|
|
526
|
+
colorScheme: PlMultiSequenceAlignmentColorSchemeOption;
|
|
527
|
+
sequences: string[];
|
|
528
|
+
residueCounts: ResidueCounts;
|
|
529
|
+
markup: { labels: Record<string, string>; rows: Markup[] } | undefined;
|
|
530
|
+
},
|
|
531
|
+
): { blob: Blob; legend: HighlightLegend } | undefined {
|
|
367
532
|
if (colorScheme.type === 'chemical-properties') {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const markupColumn = table.find(({ spec }) =>
|
|
374
|
-
spec.id === colorScheme.columnId,
|
|
375
|
-
);
|
|
376
|
-
if (!markupColumn) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
`Couldn't find markup column (ID: \`${colorScheme.columnId}\`).`,
|
|
379
|
-
);
|
|
533
|
+
return highlightByChemicalProperties({ sequences, residueCounts });
|
|
534
|
+
}
|
|
535
|
+
if (colorScheme.type === 'markup') {
|
|
536
|
+
if (!markup) {
|
|
537
|
+
throw new Error('Missing markup data.');
|
|
380
538
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return markupAlignedSequence(sequences[row][0], markup);
|
|
390
|
-
},
|
|
391
|
-
);
|
|
392
|
-
const labels = readAnnotationJson(markupColumn.spec.spec, Annotation.Sequence.Annotation.Mapping) ?? {};
|
|
393
|
-
result.highlightImage = highlightByMarkup({
|
|
394
|
-
markupRows,
|
|
395
|
-
columnCount: concatenatedSequences.at(0)?.length ?? 0,
|
|
396
|
-
labels,
|
|
539
|
+
return highlightByMarkup({
|
|
540
|
+
markupRows: sequences.map((sequence, row) => {
|
|
541
|
+
const markupRow = markup.rows.at(row);
|
|
542
|
+
if (!markupRow) throw new Error(`Missing markup for row ${row}.`);
|
|
543
|
+
return markupAlignedSequence(sequence, markupRow);
|
|
544
|
+
}),
|
|
545
|
+
columnCount: sequences.at(0)?.length ?? 0,
|
|
546
|
+
labels: markup.labels,
|
|
397
547
|
});
|
|
398
548
|
}
|
|
399
|
-
|
|
400
|
-
return result;
|
|
401
549
|
}
|
|
402
550
|
|
|
403
|
-
|
|
551
|
+
const blobToBase64 = (blob: Blob): Promise<string> =>
|
|
552
|
+
new Promise<string>((resolve, reject) => {
|
|
553
|
+
const reader = new FileReader();
|
|
554
|
+
reader.addEventListener('load', () => resolve(reader.result as string));
|
|
555
|
+
reader.addEventListener('error', () => reject(reader.error));
|
|
556
|
+
reader.readAsDataURL(blob);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const buildPhylogeneticTree = (() => {
|
|
560
|
+
const cache = new Map<string, PhylogeneticTreeWorker.TreeNodeData[]>();
|
|
561
|
+
return async (data: { rows: string[] }[], abortSignal: AbortSignal) => {
|
|
562
|
+
const concatenatedSequences = data.at(0)?.rows
|
|
563
|
+
.keys()
|
|
564
|
+
.map((row) => data.map((column) => column.rows.at(row) ?? '').join(''))
|
|
565
|
+
.toArray() ?? [];
|
|
566
|
+
const hash = await objectHash(concatenatedSequences);
|
|
567
|
+
let result = cache.get(hash);
|
|
568
|
+
if (result) return result;
|
|
569
|
+
result = await runInWorker<
|
|
570
|
+
PhylogeneticTreeWorker.RequestMessage,
|
|
571
|
+
PhylogeneticTreeWorker.ResponseMessage
|
|
572
|
+
>(
|
|
573
|
+
new Worker(
|
|
574
|
+
new URL('./phylogenetic-tree.worker.ts', import.meta.url),
|
|
575
|
+
{ type: 'module' },
|
|
576
|
+
),
|
|
577
|
+
concatenatedSequences,
|
|
578
|
+
abortSignal,
|
|
579
|
+
);
|
|
580
|
+
cache.set(hash, result);
|
|
581
|
+
return result;
|
|
582
|
+
};
|
|
583
|
+
})();
|
|
584
|
+
|
|
585
|
+
const runInWorker = <RequestMessage, ResponseMessage>(
|
|
586
|
+
worker: Worker,
|
|
587
|
+
message: RequestMessage,
|
|
588
|
+
abortSignal: AbortSignal,
|
|
589
|
+
) =>
|
|
590
|
+
new Promise<ResponseMessage>((resolve, reject) => {
|
|
591
|
+
worker.addEventListener('message', ({ data }) => {
|
|
592
|
+
resolve(data);
|
|
593
|
+
worker.terminate();
|
|
594
|
+
});
|
|
595
|
+
worker.addEventListener('error', ({ error, message }) => {
|
|
596
|
+
reject(error ?? message);
|
|
597
|
+
worker.terminate();
|
|
598
|
+
});
|
|
599
|
+
abortSignal.addEventListener('abort', () => {
|
|
600
|
+
reject(abortSignal.reason);
|
|
601
|
+
worker.terminate();
|
|
602
|
+
});
|
|
603
|
+
worker.postMessage(message);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
function refreshOnDeepChange<T, P>(
|
|
607
|
+
cb: (params: P, abortSignal: AbortSignal) => Promise<T>,
|
|
608
|
+
) {
|
|
404
609
|
const data = ref<T>();
|
|
405
610
|
const isLoading = ref(true);
|
|
406
611
|
const error = ref<Error>();
|
|
@@ -408,11 +613,15 @@ function refreshOnDeepChange<T, P>(cb: (params: P) => Promise<T>) {
|
|
|
408
613
|
return (paramsGetter: () => P) => {
|
|
409
614
|
watch(paramsGetter, async (params, prevParams) => {
|
|
410
615
|
if (isJsonEqual(params, prevParams)) return;
|
|
616
|
+
const abortController = new AbortController();
|
|
411
617
|
const currentRequestId = requestId = Symbol();
|
|
618
|
+
onWatcherCleanup(() => {
|
|
619
|
+
abortController.abort();
|
|
620
|
+
});
|
|
412
621
|
try {
|
|
413
622
|
error.value = undefined;
|
|
414
623
|
isLoading.value = true;
|
|
415
|
-
const result = await cb(params);
|
|
624
|
+
const result = await cb(params, abortController.signal);
|
|
416
625
|
if (currentRequestId === requestId) {
|
|
417
626
|
data.value = result;
|
|
418
627
|
}
|
|
@@ -432,14 +641,17 @@ function refreshOnDeepChange<T, P>(cb: (params: P) => Promise<T>) {
|
|
|
432
641
|
}
|
|
433
642
|
|
|
434
643
|
type MultipleAlignmentData = {
|
|
435
|
-
sequences:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
644
|
+
sequences: {
|
|
645
|
+
name: string;
|
|
646
|
+
rows: string[];
|
|
647
|
+
residueCounts: ResidueCounts;
|
|
648
|
+
highlightImageUrl?: string;
|
|
649
|
+
}[];
|
|
650
|
+
labels: {
|
|
651
|
+
rows: string[];
|
|
652
|
+
}[];
|
|
653
|
+
highlightLegend?: HighlightLegend;
|
|
654
|
+
phylogeneticTree?: PhylogeneticTreeWorker.TreeNodeData[];
|
|
443
655
|
exceedsLimit: boolean;
|
|
444
656
|
};
|
|
445
657
|
|
|
@@ -67,14 +67,16 @@ export function highlightByMarkup(
|
|
|
67
67
|
}
|
|
68
68
|
const legend: HighlightLegend = Object.fromEntries(
|
|
69
69
|
Object.entries(labels)
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
.map(([id, label], index) =>
|
|
71
|
+
[
|
|
72
|
+
id,
|
|
73
|
+
{
|
|
74
|
+
label,
|
|
75
|
+
color: markupColors[index % markupColors.length],
|
|
76
|
+
},
|
|
77
|
+
] as const,
|
|
78
|
+
)
|
|
79
|
+
.filter(([id]) => linesById.has(id)),
|
|
78
80
|
);
|
|
79
81
|
const blob = new Blob(
|
|
80
82
|
(function*() {
|