@platforma-sdk/ui-vue 1.33.17 → 1.34.4

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 (26) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/lib.css +1 -1
  3. package/dist/lib.js +8355 -8293
  4. package/dist/lib.js.map +1 -1
  5. package/dist/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.d.ts +3 -2
  6. package/dist/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue.d.ts.map +1 -1
  7. package/dist/src/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue.d.ts.map +1 -1
  8. package/dist/src/components/PlMultiSequenceAlignment/Toolbar.vue.d.ts +7 -11
  9. package/dist/src/components/PlMultiSequenceAlignment/Toolbar.vue.d.ts.map +1 -1
  10. package/dist/src/components/PlMultiSequenceAlignment/data.d.ts +21 -12
  11. package/dist/src/components/PlMultiSequenceAlignment/data.d.ts.map +1 -1
  12. package/dist/src/components/PlMultiSequenceAlignment/migrations.d.ts +4 -0
  13. package/dist/src/components/PlMultiSequenceAlignment/migrations.d.ts.map +1 -0
  14. package/dist/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.d.ts +0 -5
  15. package/dist/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.d.ts.map +1 -1
  16. package/dist/src/components/PlMultiSequenceAlignment/types.d.ts +0 -4
  17. package/dist/src/components/PlMultiSequenceAlignment/types.d.ts.map +1 -1
  18. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  19. package/package.json +7 -7
  20. package/src/components/PlMultiSequenceAlignment/MultiSequenceAlignmentView.vue +30 -37
  21. package/src/components/PlMultiSequenceAlignment/PlMultiSequenceAlignment.vue +68 -20
  22. package/src/components/PlMultiSequenceAlignment/Toolbar.vue +5 -5
  23. package/src/components/PlMultiSequenceAlignment/data.ts +140 -146
  24. package/src/components/PlMultiSequenceAlignment/migrations.ts +41 -0
  25. package/src/components/PlMultiSequenceAlignment/multi-sequence-alignment.ts +3 -14
  26. package/src/components/PlMultiSequenceAlignment/types.ts +0 -5
@@ -1,9 +1,6 @@
1
- import type { ListOption } from '@milaboratories/uikit';
2
- import type {
3
- JoinEntry,
4
- } from '@platforma-sdk/model';
1
+ import { isJsonEqual } from '@milaboratories/helpers';
2
+ import type { ListOptionNormalized } from '@milaboratories/uikit';
5
3
  import {
6
- type AxisId,
7
4
  type CalculateTableDataRequest,
8
5
  type CanonicalizedJson,
9
6
  canonicalizeJson,
@@ -11,6 +8,7 @@ import {
11
8
  getAxisId,
12
9
  getRawPlatformaInstance,
13
10
  isLabelColumn,
11
+ type JoinEntry,
14
12
  matchAxisId,
15
13
  parseJson,
16
14
  type PColumnIdAndSpec,
@@ -18,20 +16,19 @@ import {
18
16
  type PFrameHandle,
19
17
  type PlSelectionModel,
20
18
  type PObjectId,
21
- type PTableColumnIdAxis,
22
- type PTableColumnIdColumn,
23
- type PTableColumnIdJson,
19
+ type PTableColumnId,
24
20
  pTableValue,
25
- stringifyPTableColumnId,
26
21
  } from '@platforma-sdk/model';
27
22
  import { computedAsync } from '@vueuse/core';
28
- import { type MaybeRefOrGetter, toValue } from 'vue';
29
- import type { SequenceRow } from './types';
23
+ import { type MaybeRefOrGetter, ref, toValue } from 'vue';
24
+ import { multiSequenceAlignment } from './multi-sequence-alignment';
30
25
 
31
26
  const getPFrameDriver = () => getRawPlatformaInstance().pFrameDriver;
32
27
 
33
28
  const getEmptyOptions = () => ({ defaults: [], options: [] });
34
29
 
30
+ export const sequenceLimit = 1000;
31
+
35
32
  export function useSequenceColumnsOptions(
36
33
  params: MaybeRefOrGetter<{
37
34
  pFrame: PFrameHandle | undefined;
@@ -41,7 +38,12 @@ export function useSequenceColumnsOptions(
41
38
  const result = computedAsync(
42
39
  () => getSequenceColumnsOptions(toValue(params)),
43
40
  getEmptyOptions(),
44
- { onError: () => (result.value = getEmptyOptions()) },
41
+ {
42
+ onError: (error) => {
43
+ console.error(error);
44
+ result.value = getEmptyOptions();
45
+ },
46
+ },
45
47
  );
46
48
  return result;
47
49
  }
@@ -58,26 +60,38 @@ export function useLabelColumnsOptions(
58
60
  const result = computedAsync(
59
61
  () => getLabelColumnsOptions(toValue(params)),
60
62
  getEmptyOptions(),
61
- { onError: () => (result.value = getEmptyOptions()) },
63
+ {
64
+ onError: (error) => {
65
+ console.error(error);
66
+ result.value = getEmptyOptions();
67
+ },
68
+ },
62
69
  );
63
70
  return result;
64
71
  }
65
72
 
66
- export function useSequenceRows(
73
+ export function useMultipleAlignmentData(
67
74
  params: MaybeRefOrGetter<{
68
75
  pframe: PFrameHandle | undefined;
69
76
  sequenceColumnIds: PObjectId[];
70
- labelColumnIds: PTableColumnIdJson[];
77
+ labelColumnIds: PTableColumnId[];
71
78
  linkerColumnPredicate: PColumnPredicate | undefined;
72
79
  selection: PlSelectionModel | undefined;
73
80
  }>,
74
81
  ) {
82
+ const loading = ref(true);
75
83
  const result = computedAsync(
76
- () => getSequenceRows(toValue(params)),
77
- [],
78
- { onError: () => (result.value = []) },
84
+ () => getMultipleAlignmentData(toValue(params)),
85
+ { sequences: [], labels: [] },
86
+ {
87
+ onError: (error) => {
88
+ console.error(error);
89
+ result.value = { sequences: [], labels: [] };
90
+ },
91
+ evaluating: loading,
92
+ },
79
93
  );
80
- return result;
94
+ return { data: result, loading };
81
95
  }
82
96
 
83
97
  async function getSequenceColumnsOptions({
@@ -87,7 +101,7 @@ async function getSequenceColumnsOptions({
87
101
  pFrame: PFrameHandle | undefined;
88
102
  sequenceColumnPredicate: (column: PColumnIdAndSpec) => boolean;
89
103
  }): Promise<{
90
- options: ListOption<PObjectId>[];
104
+ options: ListOptionNormalized<PObjectId>[];
91
105
  defaults: PObjectId[];
92
106
  }> {
93
107
  if (!pFrame) return getEmptyOptions();
@@ -103,85 +117,84 @@ async function getSequenceColumnsOptions({
103
117
  return { options, defaults };
104
118
  }
105
119
 
106
- async function getLabelColumnsOptions(
107
- {
108
- pFrame,
109
- sequenceColumnIds,
110
- labelColumnOptionPredicate,
111
- }: {
112
- pFrame: PFrameHandle | undefined;
113
- sequenceColumnIds: PObjectId[];
114
- labelColumnOptionPredicate:
115
- | ((column: PColumnIdAndSpec) => boolean)
116
- | undefined;
117
- },
118
- ): Promise<{
119
- options: ListOption<PTableColumnIdJson>[];
120
- defaults: PTableColumnIdJson[];
120
+ async function getLabelColumnsOptions({
121
+ pFrame,
122
+ sequenceColumnIds,
123
+ labelColumnOptionPredicate,
124
+ }: {
125
+ pFrame: PFrameHandle | undefined;
126
+ sequenceColumnIds: PObjectId[];
127
+ labelColumnOptionPredicate:
128
+ | ((column: PColumnIdAndSpec) => boolean)
129
+ | undefined;
130
+ }): Promise<{
131
+ options: ListOptionNormalized<PTableColumnId>[];
132
+ defaults: PTableColumnId[];
121
133
  }> {
122
134
  if (!pFrame) return getEmptyOptions();
123
- const processedAxes = new Set<CanonicalizedJson<AxisId>>();
124
- const optionLabels = new Map<PTableColumnIdJson, string>();
125
135
  const pFrameDriver = getPFrameDriver();
126
136
  const columns = await pFrameDriver.listColumns(pFrame);
137
+ const optionMap = new Map<CanonicalizedJson<PTableColumnId>, string>();
127
138
  for (const column of columns) {
128
139
  if (sequenceColumnIds.includes(column.columnId)) {
129
140
  for (const axisSpec of column.spec.axesSpec) {
130
141
  const axisId = getAxisId(axisSpec);
131
- const canonicalizedAxisId = canonicalizeJson(axisId);
132
- if (processedAxes.has(canonicalizedAxisId)) continue;
133
- processedAxes.add(canonicalizedAxisId);
142
+ const axisIdJson = canonicalizeJson({ type: 'axis', id: axisId });
143
+ if (optionMap.has(axisIdJson)) continue;
134
144
  const labelColumn = columns.find(
135
145
  ({ spec }) =>
136
146
  isLabelColumn(spec)
137
147
  && matchAxisId(axisId, getAxisId(spec.axesSpec[0])),
138
148
  );
139
- optionLabels.set(
149
+ optionMap.set(
140
150
  labelColumn
141
151
  ? canonicalizeJson({ type: 'column', id: labelColumn.columnId })
142
- : canonicalizeJson({ type: 'axis', id: axisId }),
152
+ : axisIdJson,
143
153
  axisSpec.annotations?.['pl7.app/label'] ?? 'Unlabelled axis',
144
154
  );
145
155
  }
146
156
  }
147
157
  if (labelColumnOptionPredicate?.(column)) {
148
- optionLabels.set(
158
+ optionMap.set(
149
159
  canonicalizeJson({ type: 'column', id: column.columnId }),
150
160
  column.spec.annotations?.['pl7.app/label'] ?? 'Unlabelled column',
151
161
  );
152
162
  }
153
163
  }
154
- const options = Array.from(optionLabels).map(
155
- ([value, label]) => ({ label, value }),
164
+ const options = Array.from(optionMap).map(
165
+ ([value, label]) => ({ label, value: parseJson(value) }),
156
166
  );
157
167
  const defaults = options
158
- .filter((option) => {
159
- const value = parseJson(option.value);
168
+ .filter(({ value }) => {
160
169
  if (value.type === 'axis') return true;
161
-
162
- const column = columns.find((c) => c.columnId === value.id);
170
+ const column = columns.find(({ columnId }) => columnId === value.id);
163
171
  return column && isLabelColumn(column.spec);
164
172
  })
165
- .map((o) => o.value);
173
+ .map(({ value }) => value);
166
174
  return { options, defaults };
167
175
  }
168
176
 
169
- async function getSequenceRows(
177
+ async function getMultipleAlignmentData(
170
178
  {
171
179
  pframe,
172
180
  sequenceColumnIds,
173
181
  labelColumnIds,
174
182
  linkerColumnPredicate,
175
- selection: rowSelectionModel,
183
+ selection,
176
184
  }: {
177
185
  pframe: PFrameHandle | undefined;
178
186
  sequenceColumnIds: PObjectId[];
179
- labelColumnIds: PTableColumnIdJson[];
187
+ labelColumnIds: PTableColumnId[];
180
188
  linkerColumnPredicate: PColumnPredicate | undefined;
181
189
  selection: PlSelectionModel | undefined;
182
190
  },
183
- ): Promise<SequenceRow[]> {
184
- if (!pframe || sequenceColumnIds.length === 0) return [];
191
+ ): Promise<{
192
+ sequences: string[][];
193
+ labels: string[][];
194
+ }> {
195
+ if (!pframe || sequenceColumnIds.length === 0) {
196
+ return { sequences: [], labels: [] };
197
+ }
185
198
 
186
199
  const pFrameDriver = getPFrameDriver();
187
200
  const columns = await pFrameDriver.listColumns(pframe);
@@ -189,18 +202,14 @@ async function getSequenceRows(
189
202
  ? columns.filter((c) => linkerColumnPredicate(c))
190
203
  : [];
191
204
 
192
- const FilterColumnId = '__FILTER_COLUMN__' as PObjectId;
193
- const filterColumn = createRowSelectionColumn(
194
- FilterColumnId,
195
- rowSelectionModel,
196
- );
205
+ const filterColumn = createRowSelectionColumn({ selection });
197
206
 
198
207
  // inner join of sequence columns
199
208
  let primaryEntry: JoinEntry<PObjectId> = {
200
209
  type: 'inner',
201
- entries: sequenceColumnIds.map((c) => ({
202
- type: 'column' as const,
203
- column: c,
210
+ entries: sequenceColumnIds.map((column) => ({
211
+ type: 'column',
212
+ column,
204
213
  })),
205
214
  };
206
215
 
@@ -209,127 +218,112 @@ async function getSequenceRows(
209
218
  primaryEntry = {
210
219
  type: 'outer',
211
220
  primary: primaryEntry,
212
- secondary: linkerColumns.map((c) => ({
213
- type: 'column' as const,
214
- column: c.columnId,
221
+ secondary: linkerColumns.map(({ columnId }) => ({
222
+ type: 'column',
223
+ column: columnId,
215
224
  })),
216
225
  };
217
226
  }
218
227
 
219
228
  // inner join with filters
220
- if (filterColumn && filterColumn.data.length > 0) {
229
+ if (filterColumn) {
221
230
  primaryEntry = {
222
231
  type: 'inner',
223
232
  entries: [
224
233
  primaryEntry,
225
234
  {
226
- type: 'inlineColumn' as const,
235
+ type: 'inlineColumn',
227
236
  column: filterColumn,
228
237
  },
229
238
  ],
230
239
  };
231
240
  }
232
241
 
242
+ const secondaryEntry: JoinEntry<PObjectId>[] = labelColumnIds
243
+ .flatMap((column) => {
244
+ if (column.type !== 'column') return [];
245
+ return { type: 'column', column: column.id };
246
+ });
247
+
233
248
  // left join with labels
234
- const predef: CalculateTableDataRequest<PObjectId> = {
249
+ const request: CalculateTableDataRequest<PObjectId> = {
235
250
  src: {
236
251
  type: 'outer',
237
252
  primary: primaryEntry,
238
- secondary: [
239
- ...labelColumnIds.map((c) => parseJson(c))
240
- .filter((c) => c.type === 'column')
241
- .map((c) => c.id),
242
- ].map((c) => ({
243
- type: 'column',
244
- column: c,
245
- })),
253
+ secondary: secondaryEntry,
246
254
  },
247
255
  filters: [],
248
- sorting: sequenceColumnIds.map((c) => ({
256
+ sorting: sequenceColumnIds.map((id) => ({
249
257
  column: {
250
258
  type: 'column',
251
- id: c,
259
+ id,
252
260
  },
253
261
  ascending: true,
254
262
  naAndAbsentAreLeastValues: true,
255
263
  })),
256
264
  };
257
265
 
258
- const def = JSON.parse(JSON.stringify(predef));
259
- const table = await pFrameDriver.calculateTableData(pframe, def);
260
-
261
- const result: SequenceRow[] = [];
262
- // map index in spec (join result) to index in input (dropdown selection)
263
- const labelColumnsMap = new Map<number, number>();
264
- const sequenceColumnsMap = new Map<number, number>();
265
- for (let i = 0; i < table.length; i++) {
266
- const spec = table[i].spec;
266
+ const table = await pFrameDriver.calculateTableData(
267
+ pframe,
268
+ JSON.parse(JSON.stringify(request)),
269
+ {
270
+ offset: 0,
271
+ length: sequenceLimit,
272
+ },
273
+ );
267
274
 
268
- if (spec.type === 'column') {
269
- if (spec.id === FilterColumnId) continue;
275
+ const rowCount = table?.[0].data.data.length ?? 0;
270
276
 
271
- const sequenceIndex = sequenceColumnIds.findIndex((id) => id === spec.id);
272
- if (sequenceIndex !== -1) {
273
- sequenceColumnsMap.set(i, sequenceIndex);
274
- continue;
275
- }
277
+ const sequenceColumns = sequenceColumnIds.map((columnId) => {
278
+ const column = table.find(({ spec }) => spec.id === columnId);
279
+ if (!column) {
280
+ throw new Error(
281
+ `Can't find a sequence column (ID: \`${columnId}\`) in the calculateTableData output.`,
282
+ );
276
283
  }
284
+ return column;
285
+ });
277
286
 
278
- const stringifiedId = stringifyPTableColumnId(
279
- spec.type === 'axis'
280
- ? {
281
- type: 'axis',
282
- id: getAxisId(spec.spec),
283
- } satisfies PTableColumnIdAxis
284
- : {
285
- type: 'column',
286
- id: spec.id,
287
- } satisfies PTableColumnIdColumn,
288
- );
289
- const labelIndex = labelColumnIds.findIndex((id) => id === stringifiedId);
290
- if (labelIndex !== -1) {
291
- labelColumnsMap.set(i, labelIndex);
287
+ const labelColumns = labelColumnIds.map((labelColumn) => {
288
+ const column = table.find(({ spec }) => {
289
+ if (labelColumn.type === 'axis' && spec.type === 'axis') {
290
+ return isJsonEqual(labelColumn.id, spec.id);
291
+ }
292
+ if (labelColumn.type === 'column' && spec.type === 'column') {
293
+ return labelColumn.id === spec.id;
294
+ }
295
+ });
296
+ if (!column) {
297
+ throw new Error(
298
+ `Can't find a label column (ID: \`${labelColumn}\`) in the calculateTableData output.`,
299
+ );
292
300
  }
293
- }
294
- if (
295
- labelColumnsMap.size !== labelColumnIds.length
296
- || sequenceColumnsMap.size !== sequenceColumnIds.length
297
- ) {
298
- throw new Error('Some label or sequence columns are missing');
299
- }
301
+ return column;
302
+ });
300
303
 
301
- /// sort by index in input dropdowns
302
- const labelColumnsIndices = [...labelColumnsMap.keys()];
303
- labelColumnsIndices.sort((a, b) =>
304
- labelColumnsMap.get(a)! - labelColumnsMap.get(b)!,
304
+ const alignedSequences = await Promise.all(
305
+ sequenceColumns.map((column) =>
306
+ multiSequenceAlignment(Array.from(
307
+ { length: rowCount },
308
+ (_, row) =>
309
+ pTableValue(column.data, row, { na: '', absent: '' })?.toString()
310
+ ?? '',
311
+ )),
312
+ ),
305
313
  );
306
- const sequenceColumnsIndices = [...sequenceColumnsMap.keys()];
307
- sequenceColumnsIndices.sort((a, b) =>
308
- sequenceColumnsMap.get(a)! - sequenceColumnsMap.get(b)!,
314
+
315
+ const sequences = Array.from(
316
+ { length: rowCount },
317
+ (_, row) => alignedSequences.map((column) => column[row]),
309
318
  );
310
319
 
311
- const rowCount = table[0].data.data.length;
312
- for (let iRow = 0; iRow < rowCount; iRow++) {
313
- const labels = labelColumnsIndices.map((iCol) =>
314
- pTableValue(table[iCol].data, iRow, { na: '', absent: '' })?.toString(),
315
- );
316
- const sequences = sequenceColumnsIndices.map((iCol) =>
317
- pTableValue(table[iCol].data, iRow, { na: '', absent: '' })?.toString(),
318
- );
320
+ const labels = Array.from(
321
+ { length: rowCount },
322
+ (_, row) =>
323
+ labelColumns.map((column) =>
324
+ pTableValue(column.data, row, { na: '', absent: '' })?.toString() ?? '',
325
+ ),
326
+ );
319
327
 
320
- const isValid = (s: unknown): s is string => typeof s === 'string';
321
- if (labels.every(isValid) && sequences.every(isValid)) {
322
- result.push({
323
- labels,
324
- sequence: sequences.join(''),
325
- });
326
- } else {
327
- console.warn(
328
- `skipping record at row ${iRow} because of invalid labels or sequences`,
329
- labels,
330
- sequences,
331
- );
332
- }
333
- }
334
- return result;
328
+ return { sequences, labels };
335
329
  }
@@ -0,0 +1,41 @@
1
+ import {
2
+ type CanonicalizedJson,
3
+ parseJson,
4
+ type PlMultiSequenceAlignmentModel,
5
+ type PTableColumnId,
6
+ } from '@platforma-sdk/model';
7
+ import { type Ref } from 'vue';
8
+
9
+ const latestVersion = 1;
10
+
11
+ export function runMigrations(model: Ref<PlMultiSequenceAlignmentModel>) {
12
+ const currentVersion = getCurrentVersion(model.value);
13
+ try {
14
+ if (currentVersion < 1) {
15
+ const oldLabelColumnIds = model.value.labelColumnIds as unknown as
16
+ | CanonicalizedJson<PTableColumnId>[]
17
+ | undefined;
18
+ if (oldLabelColumnIds) {
19
+ model.value.labelColumnIds = oldLabelColumnIds
20
+ .map((id) => parseJson(id));
21
+ }
22
+ }
23
+ } catch (error) {
24
+ console.error(error);
25
+ model.value = {};
26
+ } finally {
27
+ model.value.version = latestVersion;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * If a model has a version, return it.
33
+ * If it doesn't, but contains anything at all, that's version 0,
34
+ * which is a pre-versioning version.
35
+ * Otherwise, emtpy model is treated as the latest model.
36
+ */
37
+ function getCurrentVersion(model: PlMultiSequenceAlignmentModel) {
38
+ if (model.version !== undefined) return model.version;
39
+ if (Object.keys(model).length) return 0;
40
+ return latestVersion;
41
+ }
@@ -1,20 +1,7 @@
1
- /* eslint-disable @stylistic/indent */
2
1
  import Aioli from '@milaboratories/biowasm-tools';
3
- import { computedAsync, type MaybeRefOrGetter } from '@vueuse/core';
4
- import { ref, toValue } from 'vue';
5
2
 
6
3
  const cache = new Map<string, string[]>();
7
4
 
8
- export function useAlignedSequences(sequences: MaybeRefOrGetter<string[]>) {
9
- const loading = ref(false);
10
- const data = computedAsync(
11
- () => multiSequenceAlignment(toValue(sequences)),
12
- [],
13
- { onError: () => (data.value = []), evaluating: loading },
14
- );
15
- return { data, loading };
16
- }
17
-
18
5
  export async function multiSequenceAlignment(
19
6
  sequences: string[],
20
7
  ): Promise<string[]> {
@@ -29,7 +16,9 @@ export async function multiSequenceAlignment(
29
16
  'input',
30
17
  );
31
18
  await CLI.mount(file);
32
- await CLI.exec('kalign -f fasta -i /shared/data/input -o /shared/data/output');
19
+ await CLI.exec(
20
+ 'kalign -f fasta -i /shared/data/input -o /shared/data/output',
21
+ );
33
22
  const output = await CLI.cat('/shared/data/output');
34
23
  result = parseKalignOutput(output);
35
24
  cache.set(inputHash, result);
@@ -1,8 +1,3 @@
1
- export type SequenceRow = {
2
- labels: string[];
3
- sequence: string;
4
- };
5
-
6
1
  export type ColorScheme = 'chemical-properties' | 'no-color';
7
2
 
8
3
  export type ResidueCounts = Record<string, number>[];