@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/package-test.js +1 -1
  3. package/dist/package-test.js.map +1 -1
  4. package/dist/package.js +1 -1
  5. package/dist/package.js.map +1 -1
  6. package/package.json +4 -3
  7. package/src/apps/pattern/model/event-bus.ts +23 -6
  8. package/src/apps/pattern/model/translator.ts +27 -1
  9. package/src/apps/pattern/view/components/bulk-convert/column-input.ts +13 -3
  10. package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +4 -3
  11. package/src/apps/pattern/view/components/load-block-controls.ts +4 -2
  12. package/src/apps/pattern/view/svg-utils/const.ts +15 -1
  13. package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
  14. package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
  15. package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
  16. package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +4 -5
  17. package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +16 -4
  18. package/src/apps/pattern/view/svg-utils/svg-renderer.ts +32 -377
  19. package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
  20. package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
  21. package/src/apps/translator/view/ui.ts +1 -1
  22. package/src/package.ts +22 -6
  23. package/src/polytool/const.ts +3 -15
  24. package/src/polytool/pt-conversion.ts +307 -0
  25. package/src/polytool/pt-dialog.ts +115 -0
  26. package/src/polytool/pt-enumeration.ts +127 -0
  27. package/src/polytool/pt-rules.ts +73 -0
  28. package/src/polytool/utils.ts +7 -0
  29. package/src/tests/helm-to-nucleotides.ts +0 -5
  30. package/tsconfig.json +1 -1
  31. package/src/apps/pattern/view/svg-utils/dimensions-calculator.ts +0 -498
  32. package/src/polytool/transformation.ts +0 -326
  33. 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.2.9",
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.40.3",
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.2",
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 _selectedColumn = new rxjs.BehaviorSubject<{strand: string, colName: string} | null>(null);
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
- selectColumn(strand: StrandType, colName: string) {
463
- this._selectedColumn.next({strand, colName});
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
- getSelectedColumn(strand: StrandType): string | null {
467
- return this._selectedColumn.getValue()?.colName ?? null;
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 {TERMINI, TERMINUS} from './const';
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.selectColumn(strand, colName)
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('ID column', columns[0], columns, () => { });
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 {STRAND} from '../../../model/const';
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 convertSequenceButton = ui.bigButton('Convert', () => this.processConvertButtonClick());
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
- convertSequenceButton,
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 (!choiceInput.value?.includes(patternName))
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((patternName) => patternName.includes(this.eventBus.getPatternName())) ?? patternList[0];
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 = 14,
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
+ }