@datagrok/sequence-translator 1.2.6 → 1.2.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/sequence-translator",
3
3
  "friendlyName": "Sequence Translator",
4
- "version": "1.2.6",
4
+ "version": "1.2.7",
5
5
  "author": {
6
6
  "name": "Alexey Choposky",
7
7
  "email": "achopovsky@datagrok.ai"
@@ -19,7 +19,7 @@
19
19
  "@datagrok-libraries/utils": "^1.17.2",
20
20
  "@types/react": "^18.0.15",
21
21
  "cash-dom": "^8.1.0",
22
- "datagrok-api": "^1.15.2",
22
+ "datagrok-api": "^1.17.4",
23
23
  "openchemlib": "6.0.1",
24
24
  "save-svg-as-png": "^1.4.17",
25
25
  "ts-loader": "^9.3.1",
@@ -3,6 +3,9 @@ import * as grok from 'datagrok-api/grok';
3
3
  import * as ui from 'datagrok-api/ui';
4
4
  import * as DG from 'datagrok-api/dg';
5
5
 
6
+ import {UnitsHandler} from '@datagrok-libraries/bio/src/utils/units-handler';
7
+ import {ALPHABET, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
8
+
6
9
  import * as rxjs from 'rxjs';
7
10
  import '../style/translator-app.css';
8
11
 
@@ -12,7 +15,7 @@ import {SequenceToMolfileConverter} from '../../model/structure-app/sequence-to-
12
15
  import {getTranslatedSequences} from '../../model/translator-app/conversion-utils';
13
16
  import {MoleculeImage} from '../utils/molecule-img';
14
17
  import {download} from '../../model/helpers';
15
- import {SEQUENCE_COPIED_MSG, SEQ_TOOLTIP_MSG} from '../const/oligo-translator';
18
+ import {SEQUENCE_COPIED_MSG, SEQ_TOOLTIP_MSG, NUCLEOTIDES} from '../const/oligo-translator';
16
19
  import {DEFAULT_AXOLABS_INPUT} from '../const/ui';
17
20
  import {FormatDetector} from '../../model/parsing-validation/format-detector';
18
21
  import {SequenceValidator} from '../../model/parsing-validation/sequence-validator';
@@ -20,9 +23,15 @@ import {FormatConverter} from '../../model/translator-app/format-converter';
20
23
  import {codesToHelmDictionary} from '../../model/data-loading-utils/json-loader';
21
24
  import {DEFAULT_FORMATS} from '../../model/const';
22
25
 
26
+ const enum REQUIRED_COLUMN_LABEL {
27
+ SEQUENCE = 'Sequence',
28
+ }
29
+ const REQUIRED_COLUMN_LABELS = [ REQUIRED_COLUMN_LABEL.SEQUENCE ];
30
+
23
31
  export class TranslatorLayoutHandler {
32
+ private eventBus: EventBus;
33
+ private inputFormats = Object.keys(codesToHelmDictionary).concat(DEFAULT_FORMATS.HELM);
24
34
  constructor() {
25
- const INPUT_FORMATS = Object.keys(codesToHelmDictionary).concat(DEFAULT_FORMATS.HELM);
26
35
  this.moleculeImgDiv = ui.div([]);
27
36
  this.moleculeImgDiv.className = 'mol-host';
28
37
  this.moleculeImgDiv.style.border = '1px solid var(--grey-2)';
@@ -30,7 +39,7 @@ export class TranslatorLayoutHandler {
30
39
  this.moleculeImgDiv.style.marginTop = '12px';
31
40
 
32
41
  this.outputTableDiv = ui.div([]);
33
- this.formatChoiceInput = ui.choiceInput('', DEFAULT_FORMATS.HELM, INPUT_FORMATS, async () => {
42
+ this.formatChoiceInput = ui.choiceInput('', DEFAULT_FORMATS.HELM, this.inputFormats, async () => {
34
43
  this.format = this.formatChoiceInput.value;
35
44
  this.updateTable();
36
45
  await this.updateMolImg();
@@ -46,6 +55,8 @@ export class TranslatorLayoutHandler {
46
55
  this.updateTable();
47
56
  await this.updateMolImg();
48
57
  });
58
+
59
+ this.eventBus = EventBus.getInstance();
49
60
  }
50
61
 
51
62
  // todo: reduce # of state vars by further refactoring legacy code
@@ -59,6 +70,97 @@ export class TranslatorLayoutHandler {
59
70
  private format: string | null;
60
71
 
61
72
  async getHtmlElement(): Promise<HTMLDivElement> {
73
+ const singleSequenceControls = this.constructSingleSequenceControls();
74
+ const bulkControls = this.constructBulkTranslationControls();
75
+ const layout = ui.box(
76
+ ui.panel([
77
+ singleSequenceControls,
78
+ bulkControls,
79
+ ui.block([ui.box(this.moleculeImgDiv)])
80
+ ]),
81
+ );
82
+
83
+ this.formatChoiceInput.value = this.format;
84
+ this.updateTable();
85
+ await this.updateMolImg();
86
+ return layout;
87
+ }
88
+
89
+ private constructBulkTranslationControls(): HTMLDivElement {
90
+ const title = ui.h1('Bulk');
91
+ ui.tooltip.bind(title, 'Bulk translation from table input');
92
+
93
+ const tableControlsManager = new TableControlsManager(this.eventBus);
94
+ const tableControls = tableControlsManager.createUIComponents();
95
+ const inputFormats = ui.choiceInput('Input format', DEFAULT_FORMATS.AXOLABS, this.inputFormats, (value: string) => this.eventBus.selectInputFormat(value));
96
+ const outputFormats = ui.choiceInput('Output format', NUCLEOTIDES, [NUCLEOTIDES], () => {});
97
+ const convertBulkButton = this.createConvertBulkButton();
98
+
99
+ const tableControlsContainer = ui.div([
100
+ ...tableControls,
101
+ inputFormats,
102
+ outputFormats,
103
+ convertBulkButton
104
+ ], 'ui-form');
105
+
106
+ const bulkTranslationControls = ui.block25([
107
+ title,
108
+ tableControlsContainer,
109
+ ]);
110
+ return bulkTranslationControls;
111
+ }
112
+
113
+ private createConvertBulkButton(): HTMLButtonElement {
114
+ const convertBulkButton = ui.bigButton('Convert', () => this.processConvertBulkButtonClick());
115
+
116
+ ui.tooltip.bind(convertBulkButton, 'Convert sequences from table input');
117
+ $(convertBulkButton).css({
118
+ 'float': 'right',
119
+ 'margin-top': '20px',
120
+ });
121
+
122
+ return convertBulkButton;
123
+ }
124
+
125
+ private processConvertBulkButtonClick(): void {
126
+ const selectedTable = this.eventBus.getSelectedTable();
127
+ if (!selectedTable) {
128
+ grok.shell.warning('No table selected');
129
+ return;
130
+ }
131
+
132
+ const inputFormat = this.eventBus.getSelectedInputFormat();
133
+ const outputFormat = NUCLEOTIDES;
134
+ const sequenceColumn = this.eventBus.getSelectedColumn(REQUIRED_COLUMN_LABEL.SEQUENCE);
135
+ if (!sequenceColumn) {
136
+ grok.shell.warning('No sequence column selected');
137
+ return;
138
+ }
139
+
140
+ const newColumnName = `${sequenceColumn.name} (${outputFormat})`;
141
+ const translatedColumn = DG.Column.fromList(
142
+ DG.TYPE.STRING,
143
+ newColumnName,
144
+ sequenceColumn.toList().map((sequence: string) => {
145
+ const translatedSequences = getTranslatedSequences(sequence, -1, inputFormat);
146
+ return translatedSequences[outputFormat];
147
+ })
148
+ );
149
+
150
+ translatedColumn.semType = DG.SEMTYPE.MACROMOLECULE;
151
+ translatedColumn.setTag(DG.TAGS.UNITS, NOTATION.FASTA);
152
+ const unitsHandler = UnitsHandler.getOrCreate(translatedColumn);
153
+ UnitsHandler.setUnitsToFastaColumn(unitsHandler);
154
+
155
+ // add newColumn to the table
156
+ selectedTable.columns.add(translatedColumn);
157
+ // update the view
158
+
159
+ grok.data.detectSemanticTypes(selectedTable);
160
+ grok.shell.v = grok.shell.getTableView(selectedTable.name);
161
+ }
162
+
163
+ private constructSingleSequenceControls(): HTMLDivElement {
62
164
  const sequenceColoredInput = new ColoredTextInput(this.sequenceInputBase, highlightInvalidSubsequence);
63
165
 
64
166
  const downloadMolfileButton = ui.button(
@@ -84,29 +186,24 @@ export class TranslatorLayoutHandler {
84
186
  textInput: sequenceColoredInput.root,
85
187
  clearBtn: clearButton
86
188
  };
87
- const upperBlock = ui.table(
189
+ const singleSequenceInputControls = ui.table(
88
190
  [inputTableRow], (item) => [item.format, item.textInput, item.clearBtn]
89
191
  );
90
- upperBlock.classList.add('st-translator-input-table');
192
+ singleSequenceInputControls.classList.add('st-translator-input-table');
91
193
 
92
- const outputTable = ui.block([
194
+ const singleSequenceOutputTable = ui.block([
93
195
  this.outputTableDiv,
94
196
  downloadMolfileButton,
95
197
  copySmilesButton,
96
198
  ]);
97
199
 
98
- const mainTabBody = ui.box(
99
- ui.panel([
100
- upperBlock,
101
- outputTable,
102
- ui.block([ui.box(this.moleculeImgDiv)])
103
- ]),
104
- );
200
+ const singleSequenceControls = ui.block75([
201
+ ui.h1('Single sequence'),
202
+ singleSequenceInputControls,
203
+ singleSequenceOutputTable,
204
+ ])
105
205
 
106
- this.formatChoiceInput.value = this.format;
107
- this.updateTable();
108
- await this.updateMolImg();
109
- return mainTabBody;
206
+ return singleSequenceControls;
110
207
  }
111
208
 
112
209
  private saveMolfile(): void {
@@ -182,3 +279,223 @@ export class TranslatorLayoutHandler {
182
279
  return molfile;
183
280
  }
184
281
  }
282
+
283
+ // todo: port to dedicated file, together with event bus
284
+ class TableControlsManager {
285
+ private tableInputManager: TableInputManager;
286
+ private columnInputManager: ColumnInputsManager;
287
+
288
+ constructor(eventBus: EventBus) {
289
+ this.tableInputManager = new TableInputManager(eventBus);
290
+ this.columnInputManager = new ColumnInputsManager(eventBus);
291
+ }
292
+
293
+ createUIComponents(): HTMLElement[] {
294
+ const tableInput = this.tableInputManager.getTableInputContainer();
295
+ const columnControls = this.columnInputManager.getColumnControlsContainer();
296
+
297
+ return [
298
+ tableInput,
299
+ columnControls,
300
+ ];
301
+ }
302
+ }
303
+
304
+ class TableInputManager {
305
+ private availableTables: DG.DataFrame[] = [];
306
+ private tableInputContainer: HTMLDivElement = ui.div([]);
307
+
308
+ constructor(private eventBus: EventBus) {
309
+ this.subscribeToTableEvents();
310
+ this.refreshTableInput();
311
+ }
312
+
313
+ getTableInputContainer(): HTMLDivElement {
314
+ return this.tableInputContainer;
315
+ }
316
+
317
+ private subscribeToTableEvents(): void {
318
+ grok.events.onTableAdded.subscribe((eventData) => this.handleTableAdded(eventData));
319
+ grok.events.onTableRemoved.subscribe((eventData) => this.handleTableRemoved(eventData));
320
+ this.eventBus.tableSelected$.subscribe(() => this.handleTableChoice());
321
+ }
322
+
323
+ private getTableFromEventData(eventData: any): DG.DataFrame {
324
+ if (! eventData && eventData.args && eventData.args.dataFrame instanceof DG.DataFrame)
325
+ throw new Error(`EventData does not contain a dataframe`, eventData);
326
+
327
+ return eventData.args.dataFrame as DG.DataFrame;
328
+ }
329
+
330
+ private handleTableAdded(eventData: any): void {
331
+ const table = this.getTableFromEventData(eventData);
332
+
333
+ if (this.availableTables.some((t: DG.DataFrame) => t.name === table.name))
334
+ return;
335
+
336
+ this.availableTables.push(table);
337
+ this.eventBus.selectTable(table);
338
+ this.refreshTableInput();
339
+ }
340
+
341
+ private handleTableRemoved(eventData: any): void {
342
+ const removedTable = this.getTableFromEventData(eventData);
343
+ this.availableTables = this.availableTables.filter((table: DG.DataFrame) => table.name !== removedTable.name);
344
+
345
+ const table = this.availableTables[0];
346
+ this.eventBus.selectTable(table ? table : null);
347
+ this.refreshTableInput();
348
+ }
349
+
350
+ private refreshTableInput(): void {
351
+ const tableInput = this.createTableInput();
352
+ $(this.tableInputContainer).empty();
353
+ this.tableInputContainer.append(tableInput.root);
354
+ }
355
+
356
+ private createTableInput(): DG.InputBase<DG.DataFrame | null> {
357
+ const currentlySelectedTable = this.eventBus.getSelectedTable();
358
+
359
+ const tableInput = ui.tableInput(
360
+ 'Table',
361
+ currentlySelectedTable,
362
+ this.availableTables,
363
+ (table: DG.DataFrame) => {
364
+ // WARNING: non-null check necessary to prevent resetting columns to
365
+ // null upon handling onTableAdded
366
+ if (table !== null && table instanceof DG.DataFrame)
367
+ this.eventBus.selectTable(table);
368
+ });
369
+ return tableInput;
370
+ }
371
+
372
+ private handleTableChoice(): void {
373
+ const selectedTable = this.eventBus.getSelectedTable();
374
+ if (!selectedTable) return;
375
+ if (!this.isTableDisplayed(selectedTable))
376
+ this.displayTable(selectedTable);
377
+ }
378
+
379
+ private isTableDisplayed(table: DG.DataFrame): boolean {
380
+ return grok.shell.tableNames.includes(table.name);
381
+ }
382
+
383
+ private displayTable(table: DG.DataFrame): void {
384
+ const previousView = grok.shell.v;
385
+ grok.shell.addTableView(table);
386
+ grok.shell.v = previousView;
387
+ }
388
+ }
389
+
390
+ class ColumnInputsManager {
391
+ private columnControlsContainer: HTMLDivElement = ui.div([]);
392
+
393
+ constructor(private eventBus: EventBus) {
394
+ this.eventBus.tableSelected$.subscribe(() => this.handleTableChoice());
395
+ this.refreshColumnControls();
396
+ }
397
+
398
+ getColumnControlsContainer(): HTMLDivElement {
399
+ return this.columnControlsContainer;
400
+ }
401
+
402
+ private handleTableChoice(): void {
403
+ this.refreshColumnControls();
404
+ }
405
+
406
+ private refreshColumnControls(): void {
407
+ const columnInputs = this.createColumnInputs();
408
+ $(this.columnControlsContainer).empty();
409
+ const inputRoots = columnInputs.map((input) => input.root);
410
+ this.columnControlsContainer.append(
411
+ ...inputRoots
412
+ );
413
+ }
414
+
415
+ private createColumnInputs(): DG.ChoiceInput<string | null>[] {
416
+ const selectedTable = this.eventBus.getSelectedTable();
417
+ const columnNames = selectedTable !== null ?
418
+ selectedTable.columns.names().sort((a, b) => a.localeCompare(b)) : [];
419
+
420
+ const columnInputs = REQUIRED_COLUMN_LABELS.map((columnLabel: REQUIRED_COLUMN_LABEL) => {
421
+ const input = this.createColumnInput(columnLabel, columnNames, selectedTable);
422
+ return input;
423
+ });
424
+ return columnInputs;
425
+ }
426
+
427
+ private createColumnInput(
428
+ columnLabel: REQUIRED_COLUMN_LABEL,
429
+ columnNames: string[],
430
+ selectedTable: DG.DataFrame | null
431
+ ): DG.ChoiceInput<string | null> {
432
+ const namePattern = columnLabel.toLowerCase();
433
+ const matchingColumnName = columnNames.find((name: string) => name.toLowerCase().includes(namePattern));
434
+ const selectedColumnName = matchingColumnName ? matchingColumnName : columnNames[0];
435
+ this.selectColumnIfTableNotNull(selectedTable, selectedColumnName, columnLabel);
436
+
437
+ const input = ui.choiceInput(
438
+ `${columnLabel}`,
439
+ selectedColumnName, columnNames,
440
+ (colName: string) => this.selectColumnIfTableNotNull(selectedTable, colName, columnLabel)
441
+ );
442
+
443
+ return input;
444
+ }
445
+
446
+ private selectColumnIfTableNotNull(
447
+ table: DG.DataFrame | null, columnName: string, columnLabel: REQUIRED_COLUMN_LABEL
448
+ ): void {
449
+ if (table !== null) {
450
+ const selectedColumn = table.getCol(columnName);
451
+ this.eventBus.selectColumn(columnLabel, selectedColumn);
452
+ }
453
+ }
454
+ }
455
+
456
+ export class EventBus {
457
+ private static _instance: EventBus;
458
+
459
+ private _tableSelection$ = new rxjs.BehaviorSubject<DG.DataFrame | null>(null);
460
+ private _columnSelection = Object.fromEntries(REQUIRED_COLUMN_LABELS.map((columnLabel: string) => {
461
+ const columnSelection$ = new rxjs.BehaviorSubject<DG.Column<string> | null>(null);
462
+ return [columnLabel, columnSelection$];
463
+ }));
464
+ private _inputFormatSelection$ = new rxjs.BehaviorSubject<string>(DEFAULT_FORMATS.AXOLABS);
465
+
466
+ private constructor() {}
467
+
468
+ public static getInstance(): EventBus {
469
+ if (EventBus._instance === undefined)
470
+ EventBus._instance = new EventBus();
471
+ return EventBus._instance;
472
+ }
473
+
474
+ get tableSelected$(): rxjs.Observable<DG.DataFrame | null> {
475
+ return this._tableSelection$.asObservable();
476
+ }
477
+
478
+ getSelectedTable(): DG.DataFrame | null {
479
+ return this._tableSelection$.getValue();
480
+ }
481
+
482
+ selectTable(table: DG.DataFrame | null): void {
483
+ this._tableSelection$.next(table);
484
+ }
485
+
486
+ selectColumn(columnLabel: REQUIRED_COLUMN_LABEL, column: DG.Column<string>): void {
487
+ this._columnSelection[columnLabel].next(column);
488
+ }
489
+
490
+ getSelectedColumn(columnLabel: REQUIRED_COLUMN_LABEL): DG.Column<string> | null {
491
+ return this._columnSelection[columnLabel].getValue();
492
+ }
493
+
494
+ getSelectedInputFormat(): string {
495
+ return this._inputFormatSelection$.getValue();
496
+ }
497
+
498
+ selectInputFormat(format: string): void {
499
+ this._inputFormatSelection$.next(format);
500
+ }
501
+ }
@@ -1,3 +1,5 @@
1
1
  export const DEFAULT_INPUT = 'fAmCmGmAmCpsmU';
2
2
  export const SEQUENCE_COPIED_MSG = 'Copied';
3
3
  export const SEQ_TOOLTIP_MSG = 'Copy sequence';
4
+
5
+ export const NUCLEOTIDES = 'Nucleotides';