@datagrok/sequence-translator 1.2.9 → 1.3.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/CHANGELOG.md +7 -0
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +4 -3
- package/src/apps/pattern/model/event-bus.ts +23 -6
- package/src/apps/pattern/model/translator.ts +27 -1
- package/src/apps/pattern/view/components/bulk-convert/column-input.ts +13 -3
- package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +4 -3
- package/src/apps/pattern/view/components/load-block-controls.ts +4 -2
- package/src/apps/pattern/view/svg-utils/const.ts +15 -1
- package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
- package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
- package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
- package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +4 -5
- package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +16 -4
- package/src/apps/pattern/view/svg-utils/svg-renderer.ts +32 -377
- package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
- package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
- package/src/apps/translator/view/ui.ts +1 -1
- package/src/package.ts +22 -6
- package/src/polytool/const.ts +3 -15
- package/src/polytool/pt-conversion.ts +307 -0
- package/src/polytool/pt-dialog.ts +115 -0
- package/src/polytool/pt-enumeration.ts +127 -0
- package/src/polytool/pt-rules.ts +73 -0
- package/src/polytool/utils.ts +7 -0
- package/src/tests/helm-to-nucleotides.ts +0 -5
- package/tsconfig.json +1 -1
- package/src/apps/pattern/view/svg-utils/dimensions-calculator.ts +0 -498
- package/src/polytool/transformation.ts +0 -326
- package/src/polytool/ui.ts +0 -59
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datagrok/sequence-translator",
|
|
3
3
|
"friendlyName": "Sequence Translator",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexey Choposky",
|
|
7
7
|
"email": "achopovsky@datagrok.ai"
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"directory": "packages/SequenceTranslator"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@datagrok-libraries/bio": "^5.
|
|
16
|
+
"@datagrok-libraries/bio": "^5.41.9",
|
|
17
17
|
"@datagrok-libraries/chem-meta": "^1.2.3",
|
|
18
18
|
"@datagrok-libraries/tutorials": "^1.3.12",
|
|
19
19
|
"@datagrok-libraries/utils": "^4.1.45",
|
|
20
20
|
"@types/react": "^18.0.15",
|
|
21
21
|
"cash-dom": "^8.1.0",
|
|
22
|
-
"datagrok-api": "^1.18.
|
|
22
|
+
"datagrok-api": "^1.18.6",
|
|
23
23
|
"lodash": "^4.17.21",
|
|
24
24
|
"object-hash": "^3.0.0",
|
|
25
25
|
"openchemlib": "6.0.1",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"link-api": "npm link datagrok-api",
|
|
54
|
+
"link-bio": "npm link @datagrok-libraries/bio",
|
|
54
55
|
"link-all": "npm link @datagrok-libraries/chem-meta datagrok-api @datagrok-libraries/utils @datagrok-libraries/bio @datagrok-libraries/tutorials",
|
|
55
56
|
"debug-sequencetranslator": "grok publish",
|
|
56
57
|
"release-sequencetranslator": "grok publish localhost --release",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
2
|
import * as DG from 'datagrok-api/dg';
|
|
3
3
|
import * as rxjs from 'rxjs';
|
|
4
|
-
import {debounceTime, map, skip, switchMap} from 'rxjs/operators';
|
|
4
|
+
import {debounceTime, throttleTime, map, skip, switchMap} from 'rxjs/operators';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
GRAPH_SETTINGS_KEYS as G, LEGEND_SETTINGS_KEYS as L, PATTERN_RECORD_KEYS as R, STRAND, STRANDS, TERMINI, TERMINUS
|
|
@@ -49,7 +49,9 @@ export class EventBus {
|
|
|
49
49
|
private _patternHasUnsavedChanges$ = new rxjs.BehaviorSubject<boolean>(false);
|
|
50
50
|
private _lastLoadedPatternConfig: rxjs.BehaviorSubject<PatternConfiguration >;
|
|
51
51
|
|
|
52
|
-
private
|
|
52
|
+
private _selectedStrandColumn = new rxjs.BehaviorSubject<{[strand: string]: string | null} | null>(null);
|
|
53
|
+
private _selectedIdColumn = new rxjs.BehaviorSubject<string | null>(null);
|
|
54
|
+
|
|
53
55
|
|
|
54
56
|
constructor(
|
|
55
57
|
private dataManager: DataManager,
|
|
@@ -459,12 +461,27 @@ export class EventBus {
|
|
|
459
461
|
return this._patternHasUnsavedChanges$.asObservable();
|
|
460
462
|
}
|
|
461
463
|
|
|
462
|
-
|
|
463
|
-
this.
|
|
464
|
+
selectStrandColumn(strand: StrandType, colName: string | null) {
|
|
465
|
+
this._selectedStrandColumn.next({...this._selectedStrandColumn.getValue(), [strand]: colName});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
getSelectedStrandColumn(strand: StrandType): string | null {
|
|
469
|
+
const value = this._selectedStrandColumn.getValue();
|
|
470
|
+
return value ? value[strand] : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
selectIdColumn(colName: string) {
|
|
474
|
+
this._selectedIdColumn.next(colName);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getSelectedIdColumn(): string | null {
|
|
478
|
+
return this._selectedIdColumn.getValue();
|
|
464
479
|
}
|
|
465
480
|
|
|
466
|
-
|
|
467
|
-
return this.
|
|
481
|
+
get updateSvgContainer$(): rxjs.Observable<void> {
|
|
482
|
+
return this.patternStateChanged$.pipe(
|
|
483
|
+
debounceTime(100)
|
|
484
|
+
);
|
|
468
485
|
}
|
|
469
486
|
}
|
|
470
487
|
|
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import {STRAND, STRANDS, TERMINI, TERMINUS} from './const';
|
|
2
3
|
import {PATTERN_APP_DATA} from '../../common/model/data-loader/json-loader';
|
|
4
|
+
import {EventBus} from './event-bus';
|
|
5
|
+
|
|
6
|
+
export function bulkTranslate(eventBus: EventBus): void {
|
|
7
|
+
const df = eventBus.getTableSelection();
|
|
8
|
+
if (!df) {
|
|
9
|
+
grok.shell.warning('Please select a table');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const strandColNames = STRANDS.filter(
|
|
13
|
+
(strand) => !(strand === STRAND.ANTISENSE && !eventBus.isAntisenseStrandActive())
|
|
14
|
+
).map((strand) => eventBus.getSelectedStrandColumn(strand))
|
|
15
|
+
.filter((colName) => colName) as string[];
|
|
16
|
+
|
|
17
|
+
if (strandColNames.length === 0) {
|
|
18
|
+
grok.shell.warning('Please column for sense strand');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const idColumnName = eventBus.getSelectedIdColumn();
|
|
23
|
+
if (!idColumnName) throw new Error('No ID column selected');
|
|
24
|
+
|
|
25
|
+
const idColumn = df.getCol(idColumnName);
|
|
26
|
+
|
|
27
|
+
const strandCols = strandColNames.map((colName) => df.getCol(colName));
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
export function applyPatternToRawSequence(
|
|
5
31
|
rawNucleotideSequence: string,
|
|
@@ -27,7 +27,6 @@ export class ColumnInputManager {
|
|
|
27
27
|
|
|
28
28
|
private handleTableChoice(): void {
|
|
29
29
|
this.refreshColumnControls();
|
|
30
|
-
// grok.shell.info(`Table ${this.selectedTable?.name} selection from column input manager`);
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
private refreshColumnControls(): void {
|
|
@@ -40,6 +39,10 @@ export class ColumnInputManager {
|
|
|
40
39
|
const senseStrandColumnInput = strandColumnInput[STRAND.SENSE];
|
|
41
40
|
const antisenseStrandColumnInput = strandColumnInput[STRAND.ANTISENSE];
|
|
42
41
|
|
|
42
|
+
this.eventBus.antisenseStrandToggled$.subscribe((isAntisenseActive) => {
|
|
43
|
+
$(antisenseStrandColumnInput).toggle(isAntisenseActive);
|
|
44
|
+
});
|
|
45
|
+
|
|
43
46
|
const idColumnInput = this.createIdColumnInput();
|
|
44
47
|
|
|
45
48
|
return [senseStrandColumnInput, antisenseStrandColumnInput, idColumnInput];
|
|
@@ -54,8 +57,9 @@ export class ColumnInputManager {
|
|
|
54
57
|
`${STRAND_LABEL[strand]} column`,
|
|
55
58
|
columns[0],
|
|
56
59
|
columns,
|
|
57
|
-
(colName: string) => this.eventBus.
|
|
60
|
+
(colName: string) => this.eventBus.selectStrandColumn(strand, colName)
|
|
58
61
|
);
|
|
62
|
+
this.eventBus.selectStrandColumn(strand, columns[0]);
|
|
59
63
|
return [strand, input.root];
|
|
60
64
|
})) as Record<StrandType, HTMLElement>;
|
|
61
65
|
return strandColumnInput;
|
|
@@ -63,7 +67,13 @@ export class ColumnInputManager {
|
|
|
63
67
|
|
|
64
68
|
private createIdColumnInput(): HTMLElement {
|
|
65
69
|
const columns = this.selectedTable ? this.selectedTable.columns.names() : [];
|
|
66
|
-
const idColumnInput = ui.choiceInput(
|
|
70
|
+
const idColumnInput = ui.choiceInput(
|
|
71
|
+
'ID column',
|
|
72
|
+
columns[0],
|
|
73
|
+
columns,
|
|
74
|
+
(colName: string) => this.eventBus.selectIdColumn(colName)
|
|
75
|
+
);
|
|
76
|
+
this.eventBus.selectIdColumn(columns[0]);
|
|
67
77
|
return idColumnInput.root;
|
|
68
78
|
}
|
|
69
79
|
}
|
|
@@ -4,7 +4,7 @@ import '../../style.css';
|
|
|
4
4
|
import {EventBus} from '../../../model/event-bus';
|
|
5
5
|
import {ColumnInputManager} from './column-input';
|
|
6
6
|
import {TableInputManager} from './table-input';
|
|
7
|
-
import {
|
|
7
|
+
import {bulkTranslate} from '../../../model/translator';
|
|
8
8
|
|
|
9
9
|
export class TableControlsManager {
|
|
10
10
|
private tableInputManager: TableInputManager;
|
|
@@ -20,18 +20,19 @@ export class TableControlsManager {
|
|
|
20
20
|
const tableInput = this.tableInputManager.getTableInputContainer();
|
|
21
21
|
const columnControls = this.columnInputManager.getColumnControlsContainer();
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const convertButton = ui.bigButton('Convert', () => this.processConvertButtonClick());
|
|
24
24
|
|
|
25
25
|
return [
|
|
26
26
|
title,
|
|
27
27
|
tableInput,
|
|
28
28
|
columnControls,
|
|
29
29
|
ui.buttonsInput([
|
|
30
|
-
|
|
30
|
+
convertButton,
|
|
31
31
|
]),
|
|
32
32
|
];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
private processConvertButtonClick(): void {
|
|
36
|
+
bulkTranslate(this.eventBus);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -153,7 +153,7 @@ export class PatternLoadControlsManager {
|
|
|
153
153
|
this.subscriptions.add(
|
|
154
154
|
this.eventBus.patternLoaded$.subscribe(() => {
|
|
155
155
|
const patternName = this.eventBus.getPatternName();
|
|
156
|
-
if (
|
|
156
|
+
if (choiceInput.value !== patternName)
|
|
157
157
|
choiceInput.value = this.getPatternName(patternList);
|
|
158
158
|
})
|
|
159
159
|
);
|
|
@@ -162,7 +162,9 @@ export class PatternLoadControlsManager {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
private getPatternName(patternList: string[]): string {
|
|
165
|
-
return patternList.find(
|
|
165
|
+
return patternList.find(
|
|
166
|
+
(patternName) => patternName === this.eventBus.getPatternName()
|
|
167
|
+
) ?? patternList[0];
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
private createDeletePatternButton(): HTMLButtonElement {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {STRAND, TERMINUS, STRAND_END} from '../../model/const';
|
|
2
2
|
|
|
3
|
+
export const LEGEND_PADDING = 10;
|
|
4
|
+
|
|
3
5
|
export const enum LUMINANCE_COEFFICIENTS {
|
|
4
6
|
RED = 0.299,
|
|
5
7
|
GREEN = 0.587,
|
|
@@ -21,7 +23,7 @@ export const enum SVG_CIRCLE_SIZES {
|
|
|
21
23
|
|
|
22
24
|
export const enum SVG_TEXT_FONT_SIZES {
|
|
23
25
|
NUCLEOBASE = 17,
|
|
24
|
-
COMMENT =
|
|
26
|
+
COMMENT = 15,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export const enum SVG_ELEMENT_COLORS {
|
|
@@ -61,3 +63,15 @@ export const Y_POSITIONS_FOR_STRAND_ELEMENTS = {
|
|
|
61
63
|
NUCLEOBASE_LABEL: 7 * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS,
|
|
62
64
|
}
|
|
63
65
|
};
|
|
66
|
+
|
|
67
|
+
export const MIN_WIDTH = 200;
|
|
68
|
+
|
|
69
|
+
export const TITLE_SHIFT = 10;
|
|
70
|
+
|
|
71
|
+
export const enum FONT_SIZE {
|
|
72
|
+
TITLE = 17,
|
|
73
|
+
LEGEND = 14,
|
|
74
|
+
NUCLEOBASE = 17,
|
|
75
|
+
COMMENT = 14,
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {PatternConfiguration} from '../../model/types';
|
|
2
|
+
import {LEGEND_PADDING, SVG_CIRCLE_SIZES, SVG_ELEMENT_COLORS, SVG_TEXT_FONT_SIZES} from './const';
|
|
3
|
+
import {SVGBlockBase} from './svg-block-base';
|
|
4
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
5
|
+
import {TextDimensionsCalculator} from './text-dimensions-calculator';
|
|
6
|
+
import {getNucleobaseColorFromStyleMap} from './utils';
|
|
7
|
+
|
|
8
|
+
export class LegendBlock extends SVGBlockBase {
|
|
9
|
+
private _svgElements: SVGElement[] = [];
|
|
10
|
+
private width: number;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
svgElementFactory: SVGElementFactory,
|
|
14
|
+
config: PatternConfiguration,
|
|
15
|
+
heightShift: number
|
|
16
|
+
) {
|
|
17
|
+
super(svgElementFactory, config, heightShift);
|
|
18
|
+
const {elements, width} = this.createLegendItems();
|
|
19
|
+
this._svgElements = elements;
|
|
20
|
+
this.width = width;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private isPhosphorothioatePresent(): boolean {
|
|
24
|
+
return Object.values(this.config.phosphorothioateLinkageFlags)
|
|
25
|
+
.flat().some((element) => element);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private getModificationTypesList(): string[] {
|
|
29
|
+
return [...new Set(
|
|
30
|
+
Object.values(this.config.nucleotideSequences).flat().sort(
|
|
31
|
+
(a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
|
32
|
+
)
|
|
33
|
+
)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get svgElements(): SVGElement[] {
|
|
37
|
+
return this._svgElements;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private createLegendItem(
|
|
41
|
+
name: string,
|
|
42
|
+
xShift: number
|
|
43
|
+
): {elements: SVGElement[], width: number} {
|
|
44
|
+
const circlePosition = {
|
|
45
|
+
x: xShift,
|
|
46
|
+
y: this.yShift - SVG_CIRCLE_SIZES.LEGEND_RADIUS
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const circle = name.includes('linkage') ?
|
|
50
|
+
this.svgElementFactory.createStarElement(circlePosition, SVG_ELEMENT_COLORS.LINKAGE_STAR) :
|
|
51
|
+
this.svgElementFactory
|
|
52
|
+
.createCircleElement(circlePosition, SVG_CIRCLE_SIZES.LEGEND_RADIUS, getNucleobaseColorFromStyleMap(name));
|
|
53
|
+
const paddedCircleWidth = 2 * SVG_CIRCLE_SIZES.LEGEND_RADIUS;
|
|
54
|
+
xShift += paddedCircleWidth;
|
|
55
|
+
|
|
56
|
+
const textPosition = {y: this.yShift, x: xShift};
|
|
57
|
+
|
|
58
|
+
const textElement = this.svgElementFactory
|
|
59
|
+
.createTextElement(name, textPosition, SVG_TEXT_FONT_SIZES.COMMENT, SVG_ELEMENT_COLORS.TEXT);
|
|
60
|
+
const textWidth = TextDimensionsCalculator.getTextDimensions(name, SVG_TEXT_FONT_SIZES.COMMENT).width;
|
|
61
|
+
|
|
62
|
+
return {elements: [circle, textElement], width: paddedCircleWidth + textWidth};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private createLegendItems(): { elements: SVGElement[], width: number } {
|
|
66
|
+
let xShift = LEGEND_PADDING;
|
|
67
|
+
const items = [] as SVGElement[][];
|
|
68
|
+
|
|
69
|
+
const shift = (width: number) => xShift += width + 15;
|
|
70
|
+
|
|
71
|
+
if (this.isPhosphorothioatePresent()) {
|
|
72
|
+
const {elements, width} = this.createLegendItem('PTO linkage', xShift);
|
|
73
|
+
shift(width);
|
|
74
|
+
items.push(elements);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const modificationTypes = this.getModificationTypesList();
|
|
78
|
+
modificationTypes.forEach((name) => {
|
|
79
|
+
const {elements, width} = this.createLegendItem(name, xShift);
|
|
80
|
+
shift(width);
|
|
81
|
+
items.push(elements);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {elements: items.flat(), width: xShift};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getContentHeight(): number { return 20; };
|
|
88
|
+
|
|
89
|
+
getContentWidth(): number {
|
|
90
|
+
return this.width;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {NUCLEOTIDES} from '../../../common/model/const';
|
|
2
|
+
import {STRAND, STRANDS, TERMINUS} from '../../model/const';
|
|
3
|
+
import {PatternConfiguration} from '../../model/types';
|
|
4
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
5
|
+
import {SVG_CIRCLE_SIZES, SVG_ELEMENT_COLORS, SVG_TEXT_FONT_SIZES} from './const';
|
|
6
|
+
import {SVGBlockBase} from './svg-block-base';
|
|
7
|
+
import {SVGElementFactory} from './svg-element-factory';
|
|
8
|
+
import {TextDimensionsCalculator} from './text-dimensions-calculator';
|
|
9
|
+
import {computeTextColorForNucleobaseLabel, getNucleobaseColorFromStyleMap, getNucleobaseLabelForCircle} from './utils';
|
|
10
|
+
|
|
11
|
+
const NUMERIC_LABEL_PADDING = 5;
|
|
12
|
+
const SENSE_STRAND_HEIGHT = SVG_TEXT_FONT_SIZES.NUCLEOBASE +
|
|
13
|
+
NUMERIC_LABEL_PADDING + SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
14
|
+
const SENSE_STRAND_PADDING = 10;
|
|
15
|
+
const LEFT_LABEL_WIDTH = 55;
|
|
16
|
+
const SENSE_STRAND_HORIZONTAL_SHIFT = SENSE_STRAND_PADDING + LEFT_LABEL_WIDTH;
|
|
17
|
+
const RIGHT_LABEL_WIDTH = 20;
|
|
18
|
+
|
|
19
|
+
export class StrandsBlock extends SVGBlockBase {
|
|
20
|
+
private strands: SVGBlockBase[];
|
|
21
|
+
private labels: SVGBlockBase[];
|
|
22
|
+
constructor(
|
|
23
|
+
svgElementFactory: SVGElementFactory,
|
|
24
|
+
config: PatternConfiguration,
|
|
25
|
+
yShift: number
|
|
26
|
+
) {
|
|
27
|
+
super(svgElementFactory, config, yShift);
|
|
28
|
+
const strandTypes = STRANDS.filter((strandType) => config.nucleotideSequences[strandType].length > 0);
|
|
29
|
+
|
|
30
|
+
this.strands = strandTypes
|
|
31
|
+
.map((strand) => new SingleStrandBlock(this.svgElementFactory, config, yShift, strand));
|
|
32
|
+
|
|
33
|
+
this.labels = strandTypes.map(
|
|
34
|
+
(strandType, idx) =>
|
|
35
|
+
new StrandLabel(this.svgElementFactory, config, yShift, strandType, this.strands[idx] as SingleStrandBlock)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get svgElements(): SVGElement[] {
|
|
40
|
+
const elements = [
|
|
41
|
+
...this.strands,
|
|
42
|
+
...this.labels
|
|
43
|
+
].map((block) => block.svgElements).flat();
|
|
44
|
+
return elements;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getContentWidth(): number {
|
|
48
|
+
return Math.max(...this.labels.map((labelBlock) => labelBlock.getContentWidth()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getContentHeight(): number {
|
|
52
|
+
const result = this.strands
|
|
53
|
+
.reduce((acc, strand) => acc + strand.getContentHeight(), 0);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class SingleStrandBlock extends SVGBlockBase {
|
|
59
|
+
private _svgElements: SVGElement[];
|
|
60
|
+
private nucleotideNumericLabels: (number | null)[];
|
|
61
|
+
constructor(
|
|
62
|
+
protected svgElementFactory: SVGElementFactory,
|
|
63
|
+
protected config: PatternConfiguration,
|
|
64
|
+
protected yShift: number,
|
|
65
|
+
private strand: STRAND
|
|
66
|
+
) {
|
|
67
|
+
super(svgElementFactory, config, yShift);
|
|
68
|
+
|
|
69
|
+
// WARNING: should be computed before creating circles
|
|
70
|
+
this.nucleotideNumericLabels = this.computeNucleotideNumericLabels();
|
|
71
|
+
|
|
72
|
+
if (this.strand === STRAND.ANTISENSE) {
|
|
73
|
+
this.config.phosphorothioateLinkageFlags[this.strand].reverse();
|
|
74
|
+
this.config.nucleotideSequences[this.strand].reverse();
|
|
75
|
+
this.nucleotideNumericLabels.reverse();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._svgElements = [
|
|
79
|
+
this.createStrandCircles(),
|
|
80
|
+
this.createPTOLinkageStars(),
|
|
81
|
+
].flat();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private computeNucleotideNumericLabels(): (number | null)[] {
|
|
85
|
+
let index = 0;
|
|
86
|
+
const nucleotides = this.config.nucleotideSequences[this.strand];
|
|
87
|
+
const indices = nucleotides.map((nucleotide) => {
|
|
88
|
+
if (isOverhangNucleotide(nucleotide)) return null;
|
|
89
|
+
index++;
|
|
90
|
+
return index;
|
|
91
|
+
});
|
|
92
|
+
return indices;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get svgElements(): SVGElement[] {
|
|
96
|
+
return this._svgElements;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getStrandCircleYShift(): number {
|
|
100
|
+
return getStrandCircleYShift(this.strand, this.yShift);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private createStrandCircles(): SVGElement[] {
|
|
104
|
+
const defaultShift = {
|
|
105
|
+
x: SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + SENSE_STRAND_HORIZONTAL_SHIFT,
|
|
106
|
+
y: this.getStrandCircleYShift()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const nucleotides = this.config.nucleotideSequences[this.strand];
|
|
110
|
+
|
|
111
|
+
const elements = nucleotides
|
|
112
|
+
.map((nucleotide, index) => this.createNucleotideElementGroup(nucleotide, index, defaultShift)).flat();
|
|
113
|
+
|
|
114
|
+
return elements;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private createNucleotideElementGroup(
|
|
118
|
+
nucleotide: string,
|
|
119
|
+
index: number,
|
|
120
|
+
defaultShift: {x: number, y: number}
|
|
121
|
+
): SVGElement[] {
|
|
122
|
+
const circleElements = this.createNucleotideCircleElements(nucleotide, index, defaultShift);
|
|
123
|
+
const numericLabel = this.config.nucleotidesWithNumericLabels.includes(nucleotide) ?
|
|
124
|
+
this.createNucleotideNumericLabel(index, defaultShift) : null;
|
|
125
|
+
|
|
126
|
+
return [...circleElements, numericLabel].filter((element) => element !== null) as SVGElement[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private createNucleotideCircleElements(
|
|
130
|
+
nucleotide: string,
|
|
131
|
+
index: number,
|
|
132
|
+
defaultShift: {x: number, y: number}
|
|
133
|
+
): (SVGElement | null)[] {
|
|
134
|
+
const color = getNucleobaseColorFromStyleMap(nucleotide);
|
|
135
|
+
const centerPosition = {...defaultShift, x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER};
|
|
136
|
+
|
|
137
|
+
const circle = this.svgElementFactory
|
|
138
|
+
.createCircleElement(centerPosition, SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS, color);
|
|
139
|
+
|
|
140
|
+
const nonModifiedNucleotideLetterLabel = this.createNucleotideLetterLabel(index, defaultShift, nucleotide);
|
|
141
|
+
|
|
142
|
+
return [circle, nonModifiedNucleotideLetterLabel];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private getNucleotideCircleCenterPosition(
|
|
146
|
+
index: number,
|
|
147
|
+
defaultShift: {x: number, y: number}
|
|
148
|
+
): {x: number, y: number} {
|
|
149
|
+
return {
|
|
150
|
+
x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER,
|
|
151
|
+
y: defaultShift.y
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private createNucleotideLetterLabel(
|
|
156
|
+
index: number,
|
|
157
|
+
defaultShift: {x: number, y: number},
|
|
158
|
+
nucleobase: string
|
|
159
|
+
): SVGElement | null {
|
|
160
|
+
if (!NUCLEOTIDES.includes(nucleobase))
|
|
161
|
+
return null;
|
|
162
|
+
|
|
163
|
+
const text = getNucleobaseLabelForCircle(nucleobase);
|
|
164
|
+
const color = computeTextColorForNucleobaseLabel(nucleobase);
|
|
165
|
+
// position at the very center of the circle
|
|
166
|
+
const position = this.getPositionForNucleotideLabel(index, defaultShift);
|
|
167
|
+
return this.svgElementFactory.createTextElement(
|
|
168
|
+
text,
|
|
169
|
+
position,
|
|
170
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
171
|
+
color
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Returns the position for the letter with its center being at the center of the circle */
|
|
176
|
+
private getPositionForNucleotideLabel(
|
|
177
|
+
index: number,
|
|
178
|
+
defaultShift: {x: number, y: number}
|
|
179
|
+
): {x: number, y: number} {
|
|
180
|
+
const circleCenter = this.getNucleotideCircleCenterPosition(index, defaultShift);
|
|
181
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions('A', SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
182
|
+
return {
|
|
183
|
+
x: circleCenter.x - textDimensions.width / 2,
|
|
184
|
+
// the coefficient 1/3 is fine-tuned to make the text look centered
|
|
185
|
+
y: circleCenter.y + textDimensions.height / 3
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getNumericLabelYShift(
|
|
190
|
+
defaultShift: {x: number, y: number}
|
|
191
|
+
): number {
|
|
192
|
+
return this.strand === STRAND.SENSE ?
|
|
193
|
+
defaultShift.y - (SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + NUMERIC_LABEL_PADDING) :
|
|
194
|
+
defaultShift.y + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS + NUMERIC_LABEL_PADDING + SVG_TEXT_FONT_SIZES.COMMENT;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private createNucleotideNumericLabel(
|
|
198
|
+
index: number,
|
|
199
|
+
defaultShift: {x: number, y: number}
|
|
200
|
+
): SVGElement | null {
|
|
201
|
+
const label = this.nucleotideNumericLabels[index];
|
|
202
|
+
if (label === null) return null;
|
|
203
|
+
|
|
204
|
+
const width = TextDimensionsCalculator.getTextDimensions(label.toString(), SVG_TEXT_FONT_SIZES.COMMENT).width;
|
|
205
|
+
|
|
206
|
+
const position = {
|
|
207
|
+
x: defaultShift.x + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER - width / 2,
|
|
208
|
+
y: this.getNumericLabelYShift(defaultShift)
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return this.svgElementFactory.createTextElement(
|
|
212
|
+
label.toString(),
|
|
213
|
+
position,
|
|
214
|
+
SVG_TEXT_FONT_SIZES.COMMENT,
|
|
215
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private createPTOLinkageStars(): SVGElement[] {
|
|
220
|
+
const ptoFlags = this.config.phosphorothioateLinkageFlags[this.strand];
|
|
221
|
+
|
|
222
|
+
const yShift = this.getStrandCircleYShift() + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS * 0.8;
|
|
223
|
+
|
|
224
|
+
const elements = ptoFlags
|
|
225
|
+
.map((ptoFlag, index) => {
|
|
226
|
+
if (!ptoFlag) return null;
|
|
227
|
+
|
|
228
|
+
const centerPosition = {
|
|
229
|
+
x: SENSE_STRAND_HORIZONTAL_SHIFT + index * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER,
|
|
230
|
+
y: yShift
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const color = SVG_ELEMENT_COLORS.LINKAGE_STAR;
|
|
234
|
+
return this.svgElementFactory.createStarElement(centerPosition, color);
|
|
235
|
+
})
|
|
236
|
+
.filter((element) => element !== null) as SVGElement[];
|
|
237
|
+
|
|
238
|
+
return elements;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getContentWidth(): number {
|
|
242
|
+
const numberOfMonomers = this.config.nucleotideSequences[this.strand].length;
|
|
243
|
+
return numberOfMonomers * SVG_CIRCLE_SIZES.NUCLEOBASE_DIAMETER;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getContentHeight(): number {
|
|
247
|
+
return SENSE_STRAND_HEIGHT + SENSE_STRAND_PADDING;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class StrandLabel extends SVGBlockBase {
|
|
252
|
+
private _svgElements: SVGElement[];
|
|
253
|
+
constructor(
|
|
254
|
+
protected svgElementFactory: SVGElementFactory,
|
|
255
|
+
protected config: PatternConfiguration,
|
|
256
|
+
protected yShift: number,
|
|
257
|
+
private strand: STRAND,
|
|
258
|
+
private strandSvgWrapper: SingleStrandBlock
|
|
259
|
+
) {
|
|
260
|
+
super(svgElementFactory, config, yShift);
|
|
261
|
+
this._svgElements = this.createSVGElements();
|
|
262
|
+
// this.strandSvgWrapper.shiftElements({x: this.getLeftLabelWidth(), y: 0});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private createSVGElements(): SVGElement[] {
|
|
266
|
+
const elements = [
|
|
267
|
+
this.createLeftLabel(),
|
|
268
|
+
this.createRightLabel()
|
|
269
|
+
];
|
|
270
|
+
return elements;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getLeftLabelWidth(): number {
|
|
274
|
+
return LEFT_LABEL_WIDTH;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getRightLabelWidth(): number {
|
|
278
|
+
return RIGHT_LABEL_WIDTH;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private createLeftLabel(): SVGTextElement {
|
|
282
|
+
const terminus = this.strand === STRAND.SENSE ? TERMINUS.FIVE_PRIME : TERMINUS.THREE_PRIME;
|
|
283
|
+
const text = `${this.strand}: ${terminus} `;
|
|
284
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions(text, SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
285
|
+
const position = {
|
|
286
|
+
x: SENSE_STRAND_PADDING,
|
|
287
|
+
y: getStrandCircleYShift(this.strand, this.yShift) + textDimensions.height / 3
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return this.svgElementFactory.createTextElement(
|
|
291
|
+
text,
|
|
292
|
+
position,
|
|
293
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
294
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private createRightLabel(): SVGTextElement {
|
|
299
|
+
const terminus = this.strand === STRAND.SENSE ? TERMINUS.THREE_PRIME : TERMINUS.FIVE_PRIME;
|
|
300
|
+
const text = ` ${terminus}`;
|
|
301
|
+
const textDimensions = TextDimensionsCalculator.getTextDimensions(text, SVG_TEXT_FONT_SIZES.NUCLEOBASE);
|
|
302
|
+
const position = {
|
|
303
|
+
x: SENSE_STRAND_HORIZONTAL_SHIFT + this.strandSvgWrapper.getContentWidth() + 5,
|
|
304
|
+
y: getStrandCircleYShift(this.strand, this.yShift) + textDimensions.height / 3
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return this.svgElementFactory.createTextElement(
|
|
308
|
+
text,
|
|
309
|
+
position,
|
|
310
|
+
SVG_TEXT_FONT_SIZES.NUCLEOBASE,
|
|
311
|
+
SVG_ELEMENT_COLORS.TEXT
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get svgElements(): SVGElement[] {
|
|
316
|
+
return this._svgElements;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getContentWidth(): number {
|
|
320
|
+
return this.strandSvgWrapper.getContentWidth() + this.getLeftLabelWidth() + this.getRightLabelWidth() + SENSE_STRAND_PADDING;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getContentHeight(): number {
|
|
324
|
+
return this.strandSvgWrapper.getContentHeight();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getStrandCircleYShift(
|
|
329
|
+
strand: STRAND,
|
|
330
|
+
defaultYShift: number
|
|
331
|
+
): number {
|
|
332
|
+
return strand === STRAND.SENSE ?
|
|
333
|
+
defaultYShift + NUMERIC_LABEL_PADDING + SVG_TEXT_FONT_SIZES.COMMENT + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS :
|
|
334
|
+
defaultYShift + SENSE_STRAND_HEIGHT + SENSE_STRAND_PADDING + SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
|
|
335
|
+
}
|