@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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +213 -207
  2. package/.turbo/turbo-type-check.log +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/dist/AgGridVue/useAgGridOptions.js +2 -3
  5. package/dist/AgGridVue/useAgGridOptions.js.map +1 -1
  6. package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js +6 -0
  7. package/dist/assets/multi-sequence-alignment.worker-Cm0gZp19.js.map +1 -0
  8. package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js +5 -0
  9. package/dist/assets/phylogenetic-tree.worker-4CrExYEo.js.map +1 -0
  10. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js +2 -3
  11. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js.map +1 -1
  12. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js +11 -12
  13. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js.map +1 -1
  14. package/dist/components/PlAgRowNumHeader.vue.js +8 -9
  15. package/dist/components/PlAgRowNumHeader.vue.js.map +1 -1
  16. package/dist/components/PlMultiSequenceAlignment/Consensus.vue.d.ts +1 -0
  17. package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js +48 -46
  18. package/dist/components/PlMultiSequenceAlignment/Consensus.vue2.js.map +1 -1
  19. package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js +5 -7
  20. package/dist/components/PlMultiSequenceAlignment/Consensus.vue3.js.map +1 -1
  21. package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js +14 -13
  22. package/dist/components/PlMultiSequenceAlignment/Legend.vue2.js.map +1 -1
  23. package/dist/components/PlMultiSequenceAlignment/Legend.vue3.js +9 -8
  24. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.d.ts +16 -9
  25. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js +117 -85
  26. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue2.js.map +1 -1
  27. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js +25 -18
  28. package/dist/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue3.js.map +1 -1
  29. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.d.ts +8 -0
  30. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js +10 -0
  31. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue.js.map +1 -0
  32. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js +77 -0
  33. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue2.js.map +1 -0
  34. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js +9 -0
  35. package/dist/components/PlMultiSequenceAlignment/PhylogeneticTree.vue3.js.map +1 -0
  36. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.d.ts +26 -18
  37. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js +119 -120
  38. package/dist/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue2.js.map +1 -1
  39. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js +7 -124
  40. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue.js.map +1 -1
  41. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js +124 -2
  42. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue2.js.map +1 -1
  43. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js +9 -0
  44. package/dist/components/PlMultiSequenceAlignment/SeqLogo.vue3.js.map +1 -0
  45. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js +90 -90
  46. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue2.js.map +1 -1
  47. package/dist/components/PlMultiSequenceAlignment/Toolbar.vue3.js +9 -7
  48. package/dist/components/PlMultiSequenceAlignment/cell-size.d.ts +4 -0
  49. package/dist/components/PlMultiSequenceAlignment/cell-size.js +8 -0
  50. package/dist/components/PlMultiSequenceAlignment/cell-size.js.map +1 -0
  51. package/dist/components/PlMultiSequenceAlignment/data.d.ts +15 -10
  52. package/dist/components/PlMultiSequenceAlignment/data.js +309 -202
  53. package/dist/components/PlMultiSequenceAlignment/data.js.map +1 -1
  54. package/dist/components/PlMultiSequenceAlignment/markup.js +9 -7
  55. package/dist/components/PlMultiSequenceAlignment/markup.js.map +1 -1
  56. package/dist/components/PlMultiSequenceAlignment/migrations.js +15 -13
  57. package/dist/components/PlMultiSequenceAlignment/migrations.js.map +1 -1
  58. package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.d.ts +6 -0
  59. package/dist/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.d.ts +7 -0
  60. package/dist/components/PlMultiSequenceAlignment/settings.js +3 -4
  61. package/dist/components/PlMultiSequenceAlignment/settings.js.map +1 -1
  62. package/dist/index.js +25 -27
  63. package/dist/index.js.map +1 -1
  64. package/dist/lib.d.ts +1 -2
  65. package/package.json +6 -6
  66. package/src/components/PlMultiSequenceAlignment/Consensus.vue +38 -39
  67. package/src/components/PlMultiSequenceAlignment/Legend.vue +9 -9
  68. package/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue +222 -126
  69. package/src/components/PlMultiSequenceAlignment/PhylogeneticTree.vue +110 -0
  70. package/src/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue +28 -22
  71. package/src/components/PlMultiSequenceAlignment/SeqLogo.vue +77 -69
  72. package/src/components/PlMultiSequenceAlignment/Toolbar.vue +47 -39
  73. package/src/components/PlMultiSequenceAlignment/cell-size.ts +4 -0
  74. package/src/components/PlMultiSequenceAlignment/data.ts +361 -149
  75. package/src/components/PlMultiSequenceAlignment/markup.ts +10 -8
  76. package/src/components/PlMultiSequenceAlignment/migrations.ts +6 -1
  77. package/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.worker.ts +54 -0
  78. package/src/components/PlMultiSequenceAlignment/phylogenetic-tree.worker.ts +89 -0
  79. package/src/components/PlMultiSequenceAlignment/settings.ts +1 -2
  80. package/src/lib.ts +1 -3
  81. package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.d.ts +0 -7
  82. package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.js +0 -51
  83. package/dist/components/PlMultiSequenceAlignment/multi-sequence-alignment.js.map +0 -1
  84. 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 { multiSequenceAlignment } from './multi-sequence-alignment';
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 sequenceLimit = 1000;
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({ type: 'column', id: columnId } satisfies PTableColumnId);
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?.length !== 1) return;
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
- const sequenceColumn = columns.find((column) =>
165
- column.columnId === sequenceColumnIds[0],
166
- );
167
- if (!sequenceColumn) {
168
- throw new Error(
169
- `Couldn't find sequence column (ID: \`${sequenceColumnIds[0]}\`).`,
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
- return columns.values()
173
- .filter((column) =>
174
- !!readAnnotationJson(column.spec, Annotation.Sequence.IsAnnotation)
175
- && isJsonEqual(sequenceColumn.spec.axesSpec, column.spec.axesSpec)
176
- && Object.entries(sequenceColumn.spec.domain ?? {}).every((
177
- [key, value],
178
- ) => column.spec.domain?.[key] === value),
179
- ).map(({ columnId, spec }) => ({
180
- value: columnId,
181
- label: readAnnotation(spec, Annotation.Label) ?? 'Unlabeled column',
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
- pFrame,
188
- sequenceColumnIds,
189
- labelColumnIds,
190
- selection,
191
- colorScheme,
192
- alignmentParams,
193
- }: {
194
- pFrame: PFrameHandle | undefined;
195
- sequenceColumnIds: PObjectId[] | undefined;
196
- labelColumnIds: PTableColumnId[] | undefined;
197
- selection: PlSelectionModel | undefined;
198
- colorScheme: PlMultiSequenceAlignmentColorSchemeOption;
199
- alignmentParams: PlMultiSequenceAlignmentSettings['alignmentParams'];
200
- }): Promise<MultipleAlignmentData | undefined> {
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
- secondaryEntry.push({ type: 'column', column: colorScheme.columnId });
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
- const table = await pFrameDriver.calculateTableData(
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: sequenceLimit + 1,
418
+ length: SEQUENCE_LIMIT + 1,
292
419
  },
293
420
  );
421
+ }
294
422
 
295
- let rowCount = table?.[0].data.data.length ?? 0;
296
- let exceedsLimit = false;
297
- if (rowCount > sequenceLimit) {
298
- rowCount = sequenceLimit;
299
- exceedsLimit = true;
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
- return column;
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
- const labelColumns = labelColumnIds.map((labelColumn) => {
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 (labelColumn.type === 'axis' && spec.type === 'axis') {
313
- return isJsonEqual(labelColumn.id, spec.id);
451
+ if (columnId.type === 'axis' && spec.type === 'axis') {
452
+ return isJsonEqual(columnId.id, spec.id);
314
453
  }
315
- if (labelColumn.type === 'column' && spec.type === 'column') {
316
- return labelColumn.id === spec.id;
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: \`${labelColumn}\`).`);
459
+ throw new Error(`Couldn't find label column (ID: \`${columnId}\`).`);
321
460
  }
322
- return column;
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
- const alignedSequences = await Promise.all(
326
- sequenceColumns.map((column) =>
327
- multiSequenceAlignment(
328
- Array.from(
329
- { length: rowCount },
330
- (_, row) =>
331
- pTableValue(column.data, row, { na: '', absent: '' })?.toString()
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
- alignmentParams,
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
- const labels = Array.from(
349
- { length: rowCount },
350
- (_, row) =>
351
- labelColumns.map((column) =>
352
- pTableValue(column.data, row, { na: '', absent: '' })?.toString() ?? '',
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
- const concatenatedSequences = sequences.map((row) => row.join(' '));
357
- const residueCounts = getResidueCounts(concatenatedSequences);
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
- result.highlightImage = highlightByChemicalProperties({
369
- sequences: concatenatedSequences,
370
- residueCounts,
371
- });
372
- } else if (colorScheme.type === 'markup') {
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
- const markupRows = Array.from(
382
- { length: rowCount },
383
- (_, row) => {
384
- const markup = parseMarkup(
385
- pTableValue(markupColumn.data, row, { na: '', absent: '' })
386
- ?.toString()
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
- function refreshOnDeepChange<T, P>(cb: (params: P) => Promise<T>) {
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: string[];
436
- sequenceNames: string[];
437
- labelRows: string[][];
438
- residueCounts: ResidueCounts;
439
- highlightImage?: {
440
- blob: Blob;
441
- legend: HighlightLegend;
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
- .filter(([id]) => linesById.has(id))
71
- .map(([id, label], index) => [
72
- id,
73
- {
74
- label,
75
- color: markupColors[index % markupColors.length],
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*() {