@datagrok/sequence-translator 1.2.7 → 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 (107) hide show
  1. package/.eslintrc.json +5 -5
  2. package/CHANGELOG.md +14 -0
  3. package/dist/package-test.js +2 -1
  4. package/dist/package-test.js.LICENSE.txt +8 -0
  5. package/dist/package-test.js.map +1 -1
  6. package/dist/package.js +2 -1
  7. package/dist/package.js.LICENSE.txt +8 -0
  8. package/dist/package.js.map +1 -1
  9. package/files/pattern-app-data.json +80 -0
  10. package/package.json +22 -14
  11. package/src/{model → apps/common/model}/const.ts +1 -1
  12. package/src/{model/data-loading-utils → apps/common/model/data-loader}/const.ts +7 -2
  13. package/src/apps/common/model/data-loader/json-loader.ts +48 -0
  14. package/src/{model/data-loading-utils → apps/common/model/data-loader}/types.ts +13 -6
  15. package/src/{model → apps/common/model}/monomer-lib/lib-wrapper.ts +9 -12
  16. package/src/apps/common/model/oligo-toolkit-package.ts +30 -0
  17. package/src/{model → apps/common/model}/parsing-validation/format-detector.ts +5 -5
  18. package/src/{model → apps/common/model}/parsing-validation/format-handler.ts +18 -19
  19. package/src/{model → apps/common/model}/parsing-validation/sequence-validator.ts +1 -1
  20. package/src/apps/common/view/app-ui-base.ts +28 -0
  21. package/src/apps/common/view/combined-app-ui.ts +66 -0
  22. package/src/{view/utils → apps/common/view/components}/colored-input/colored-text-input.ts +1 -1
  23. package/src/{view/utils → apps/common/view/components}/draw-molecule.ts +1 -1
  24. package/src/{view/utils → apps/common/view/components}/molecule-img.ts +3 -3
  25. package/src/{view/const/ui.ts → apps/common/view/const.ts} +4 -4
  26. package/src/apps/common/view/isolated-app-ui.ts +43 -0
  27. package/src/{view/monomer-lib-viewer/viewer.ts → apps/common/view/monomer-lib-viewer.ts} +2 -2
  28. package/src/apps/common/view/utils.ts +29 -0
  29. package/src/apps/pattern/model/const.ts +121 -0
  30. package/src/apps/pattern/model/data-manager.ts +297 -0
  31. package/src/apps/pattern/model/event-bus.ts +487 -0
  32. package/src/apps/pattern/model/router.ts +46 -0
  33. package/src/apps/pattern/model/subscription-manager.ts +21 -0
  34. package/src/apps/pattern/model/translator.ts +94 -0
  35. package/src/apps/pattern/model/types.ts +52 -0
  36. package/src/apps/pattern/model/utils.ts +110 -0
  37. package/src/apps/pattern/view/components/bulk-convert/column-input.ts +79 -0
  38. package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +38 -0
  39. package/src/apps/pattern/view/components/bulk-convert/table-input.ts +95 -0
  40. package/src/apps/pattern/view/components/edit-block-controls.ts +196 -0
  41. package/src/apps/pattern/view/components/left-section.ts +44 -0
  42. package/src/apps/pattern/view/components/load-block-controls.ts +200 -0
  43. package/src/apps/pattern/view/components/numeric-label-visibility-controls.ts +69 -0
  44. package/src/apps/pattern/view/components/right-section.ts +148 -0
  45. package/src/apps/pattern/view/components/strand-editor/dialog.ts +79 -0
  46. package/src/apps/pattern/view/components/strand-editor/header-controls.ts +105 -0
  47. package/src/apps/pattern/view/components/strand-editor/strand-controls.ts +159 -0
  48. package/src/apps/pattern/view/components/terminal-modification-editor.ts +127 -0
  49. package/src/apps/pattern/view/components/translation-examples-block.ts +139 -0
  50. package/src/{view/style/pattern-app.css → apps/pattern/view/style.css} +4 -0
  51. package/src/apps/pattern/view/svg-utils/const.ts +77 -0
  52. package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
  53. package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
  54. package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
  55. package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +44 -0
  56. package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +94 -0
  57. package/src/apps/pattern/view/svg-utils/svg-renderer.ts +51 -0
  58. package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
  59. package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
  60. package/src/apps/pattern/view/svg-utils/utils.ts +37 -0
  61. package/src/apps/pattern/view/types.ts +14 -0
  62. package/src/apps/pattern/view/ui.ts +61 -0
  63. package/src/{model/structure-app → apps/structure/model}/mol-transformations.ts +3 -3
  64. package/src/{model/structure-app → apps/structure/model}/monomer-code-parser.ts +9 -10
  65. package/src/{model/structure-app → apps/structure/model}/oligo-structure.ts +4 -4
  66. package/src/{model/structure-app → apps/structure/model}/sequence-to-molfile.ts +2 -2
  67. package/src/{view/apps/oligo-structure.ts → apps/structure/view/ui.ts} +31 -17
  68. package/src/{model/translator-app → apps/translator/model}/conversion-utils.ts +25 -7
  69. package/src/{model/translator-app → apps/translator/model}/format-converter.ts +7 -12
  70. package/src/{view/const/oligo-translator.ts → apps/translator/view/const.ts} +1 -1
  71. package/src/{view/apps/oligo-translator.ts → apps/translator/view/ui.ts} +88 -42
  72. package/src/demo/demo-st-ui.ts +12 -32
  73. package/src/package.ts +91 -55
  74. package/src/plugins/mermade.ts +9 -9
  75. package/src/polytool/const.ts +28 -0
  76. package/src/polytool/csv-to-json-monomer-lib-converter.ts +40 -0
  77. package/src/polytool/cyclized.ts +56 -0
  78. package/src/polytool/monomer-lib-handler.ts +115 -0
  79. package/src/polytool/pt-conversion.ts +307 -0
  80. package/src/polytool/pt-dialog.ts +115 -0
  81. package/src/polytool/pt-enumeration.ts +127 -0
  82. package/src/polytool/pt-rules.ts +73 -0
  83. package/src/polytool/utils.ts +27 -0
  84. package/src/tests/const.ts +5 -5
  85. package/src/tests/formats-support.ts +6 -6
  86. package/src/tests/formats-to-helm.ts +5 -5
  87. package/src/tests/helm-to-nucleotides.ts +5 -10
  88. package/tsconfig.json +3 -9
  89. package/webpack.config.js +3 -0
  90. package/files/axolabs-style.json +0 -97
  91. package/src/model/data-loading-utils/json-loader.ts +0 -38
  92. package/src/model/pattern-app/const.ts +0 -33
  93. package/src/model/pattern-app/draw-svg.ts +0 -193
  94. package/src/model/pattern-app/helpers.ts +0 -96
  95. package/src/model/pattern-app/oligo-pattern.ts +0 -111
  96. package/src/view/app-ui.ts +0 -193
  97. package/src/view/apps/oligo-pattern.ts +0 -759
  98. /package/src/{model → apps/common/model}/helpers.ts +0 -0
  99. /package/src/{model → apps/common/model}/monomer-lib/const.ts +0 -0
  100. /package/src/{view/utils → apps/common/view/components}/app-info-dialog.ts +0 -0
  101. /package/src/{view/utils → apps/common/view/components}/colored-input/input-painters.ts +0 -0
  102. /package/src/{view/style/colored-text-input.css → apps/common/view/components/colored-input/style.css} +0 -0
  103. /package/src/{view/utils → apps/common/view/components}/router.ts +0 -0
  104. /package/src/{model/structure-app → apps/structure/model}/const.ts +0 -0
  105. /package/src/{view/style/structure-app.css → apps/structure/view/style.css} +0 -0
  106. /package/src/{model/translator-app → apps/translator/model}/const.ts +0 -0
  107. /package/src/{view/style/translator-app.css → apps/translator/view/style.css} +0 -0
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ import {PatternConfiguration} from '../../model/types';
2
+ import {SVGElementFactory} from './svg-element-factory';
3
+
4
+ /** Horizontal block within SVG: title, strands, legend */
5
+ export abstract class SVGBlockBase {
6
+ // protected svgElements: SVGElement[] = [];
7
+ constructor(
8
+ protected svgElementFactory: SVGElementFactory,
9
+ protected config: PatternConfiguration,
10
+ protected yShift: number
11
+ ) {}
12
+
13
+ abstract get svgElements(): SVGElement[];
14
+
15
+ abstract getContentHeight(): number;
16
+
17
+ shiftElements(shift: {x: number, y: number}): void {
18
+ this.svgElements.forEach((element) => {
19
+ const transform = element.getAttribute('transform') || '';
20
+ const match = transform.match(/translate\(([^,]+),([^,]+)\)/);
21
+ const x = match ? parseFloat(match[1]) : 0;
22
+ const y = match ? parseFloat(match[2]) : 0;
23
+ const newTransform = `translate(${x + shift.x},${y + shift.y})`;
24
+ element.setAttribute('transform', `${transform} ${newTransform}`);
25
+ });
26
+ }
27
+
28
+ adjustContentWithinGlobalContainer(globalWidth: number): void {
29
+ const contentWidth = this.getContentWidth();
30
+ if (contentWidth < globalWidth) {
31
+ const shift = (globalWidth - contentWidth) / 2;
32
+ this.shiftElements({x: shift, y: 0});
33
+ }
34
+ }
35
+
36
+ abstract getContentWidth(): number;
37
+ }
@@ -0,0 +1,44 @@
1
+ /* Do not change these import lines to match external modules in webpack configuration */
2
+ import * as ui from 'datagrok-api/ui';
3
+
4
+ import {EventBus} from '../../model/event-bus';
5
+ import {PatternConfiguration} from '../../model/types';
6
+ //@ts-ignore
7
+ import * as svgExport from 'save-svg-as-png';
8
+ import {NucleotidePatternSVGRenderer} from './svg-renderer';
9
+
10
+ export class SvgDisplayManager {
11
+ private svgDisplayDiv = ui.div([]);
12
+ private svgElement: SVGElement;
13
+
14
+ private constructor(
15
+ private eventBus: EventBus
16
+ ) {
17
+ eventBus.updateSvgContainer$.subscribe(() => this.updateSvgContainer());
18
+ eventBus.svgSaveRequested$.subscribe(() => this.saveSvgAsPng());
19
+ }
20
+
21
+ static createSvgDiv(eventBus: EventBus): HTMLDivElement {
22
+ const displayManager = new SvgDisplayManager(eventBus);
23
+ return displayManager.svgDisplayDiv;
24
+ }
25
+
26
+ private updateSvgContainer(): void {
27
+ $(this.svgDisplayDiv).empty();
28
+ const patternConfig = this.eventBus.getPatternConfig();
29
+ this.svgElement = this.createSvg(patternConfig);
30
+ this.svgDisplayDiv.append(this.svgElement);
31
+ }
32
+
33
+ private createSvg(patternConfig: PatternConfiguration) {
34
+ // const renderer = new NucleotidePatternSVGRenderer(patternConfig);
35
+ const renderer = new NucleotidePatternSVGRenderer(patternConfig);
36
+ const svg = renderer.renderPattern();
37
+ return svg;
38
+ }
39
+
40
+ private saveSvgAsPng(): void {
41
+ const patternName = this.eventBus.getPatternName();
42
+ svgExport.saveSvgAsPng(this.svgElement, patternName, {backgroundColor: 'white'});
43
+ }
44
+ }
@@ -0,0 +1,94 @@
1
+ import {Position} from '../types';
2
+
3
+ export class SVGElementFactory {
4
+ private readonly xmlNamespace = 'http://www.w3.org/2000/svg';
5
+
6
+ private createElement(elementType: string): Element {
7
+ return document.createElementNS(this.xmlNamespace, elementType);
8
+ }
9
+
10
+ private setAttributes(targetElement: Element, attributes: { [key: string]: any }): void {
11
+ Object.entries(attributes).forEach(([key, value]) => {
12
+ targetElement.setAttribute(key, String(value));
13
+ });
14
+ }
15
+
16
+ createCanvas(width: number, height: number): SVGElement {
17
+ const svgElement = this.createElement('svg') as SVGElement;
18
+ this.setAttributes(svgElement, {
19
+ id: 'mySvg',
20
+ width,
21
+ height,
22
+ });
23
+ return svgElement;
24
+ }
25
+
26
+ createCircleElement(centerPosition: Position, radius: number, color: string): SVGCircleElement {
27
+ const circle = this.createElement('circle') as SVGCircleElement;
28
+ this.setAttributes(circle, {
29
+ cx: centerPosition.x,
30
+ cy: centerPosition.y,
31
+ r: radius,
32
+ fill: color,
33
+ });
34
+ return circle;
35
+ }
36
+
37
+ createTextElement(textContent: string, position: Position, fontSize: number, color: string): SVGTextElement {
38
+ const textElement = this.createElement('text') as SVGTextElement;
39
+ this.setAttributes(textElement, {
40
+ 'x': position.x,
41
+ 'y': position.y,
42
+ 'font-size': fontSize,
43
+ 'font-weight': 'normal',
44
+ 'font-family': 'Arial',
45
+ 'fill': color,
46
+ });
47
+ textElement.textContent = textContent;
48
+ return textElement;
49
+ }
50
+
51
+ createStarElement(centerPosition: Position, color: string): SVGPolygonElement {
52
+ const star = this.createElement('polygon') as SVGPolygonElement;
53
+ const points = this.computeStarVertexCoordinates(centerPosition);
54
+ const pointsAttribute = points.map((point) => point.join(',')).join(' ');
55
+
56
+ this.setAttributes(star, {
57
+ points: pointsAttribute,
58
+ fill: color,
59
+ });
60
+ return star;
61
+ }
62
+
63
+ private computeStarVertexCoordinates(centerPosition: Position): [number, number][] {
64
+ const outerVerticesPerStar = 5;
65
+ const innerVertexRadius = 3;
66
+ const outerVertexRadius = innerVertexRadius * 2;
67
+ const radiansPerVertex = Math.PI / outerVerticesPerStar;
68
+ const radiansOffset = - radiansPerVertex / 2;
69
+ const totalNumberOfVertices = outerVerticesPerStar * 2;
70
+
71
+ const points: [number, number][] = Array.from({length: totalNumberOfVertices}, (_, i) => {
72
+ const isOuterVertex = i % 2 === 0;
73
+ const radius = isOuterVertex ? outerVertexRadius : innerVertexRadius;
74
+ const angle = i * radiansPerVertex + radiansOffset;
75
+ const x = centerPosition.x + Math.cos(angle) * radius;
76
+ const y = centerPosition.y + Math.sin(angle) * radius;
77
+ return [x, y];
78
+ });
79
+
80
+ return points;
81
+ }
82
+
83
+ createRectangleElement(topLeftCorner: Position, width: number, height: number, color: string): SVGRectElement {
84
+ const rectangle = this.createElement('rect') as SVGRectElement;
85
+ this.setAttributes(rectangle, {
86
+ x: topLeftCorner.x,
87
+ y: topLeftCorner.y,
88
+ width,
89
+ height,
90
+ fill: color,
91
+ });
92
+ return rectangle;
93
+ }
94
+ }
@@ -0,0 +1,51 @@
1
+ import _ from 'lodash';
2
+ import {PatternConfiguration} from '../../model/types';
3
+ import {StrandsBlock} from './strands-block';
4
+ import {SVGBlockBase} from './svg-block-base';
5
+ import {SVGElementFactory} from './svg-element-factory';
6
+ import {TitleBlock} from './title-block';
7
+ import {LegendBlock} from './legend-block';
8
+ import {LEGEND_PADDING, TITLE_SHIFT} from './const';
9
+
10
+ export class NucleotidePatternSVGRenderer {
11
+ private title: TitleBlock;
12
+ private strands: StrandsBlock;
13
+ private legend: LegendBlock;
14
+ private svgElementFactory = new SVGElementFactory();
15
+
16
+ constructor(patternConfig: PatternConfiguration) {
17
+ const config = _.cloneDeep(patternConfig);
18
+
19
+ let heightShift = TITLE_SHIFT;
20
+
21
+ this.title = new TitleBlock(this.svgElementFactory, config, heightShift);
22
+ heightShift += this.title.getContentHeight();
23
+
24
+ this.strands = new StrandsBlock(this.svgElementFactory, config, heightShift);
25
+ heightShift += this.strands.getContentHeight() + LEGEND_PADDING;
26
+
27
+ this.legend = new LegendBlock(this.svgElementFactory, config, heightShift);
28
+ }
29
+
30
+ renderPattern(): SVGElement {
31
+ const width = this.getGlobalWidth();
32
+ const height = this.getGlobalHeight();
33
+ const canvas = this.svgElementFactory.createCanvas(width, height);
34
+
35
+ const elements = [this.title, this.strands, this.legend].map((block) => block.svgElements).flat();
36
+ canvas.append(...elements);
37
+
38
+ return canvas;
39
+ }
40
+
41
+ private getGlobalWidth(): number {
42
+ const blocks = [this.title, this.strands, this.legend] as SVGBlockBase[];
43
+ return Math.max(...blocks.map((block) => block.getContentWidth()));
44
+ }
45
+
46
+ private getGlobalHeight(): number {
47
+ const blocks = [this.title, this.strands, this.legend];
48
+ const height = blocks.reduce((acc, block) => acc + block.getContentHeight(), TITLE_SHIFT);
49
+ return height;
50
+ }
51
+ }
@@ -0,0 +1,29 @@
1
+ export class TextDimensionsCalculator {
2
+ private static instance: TextDimensionsCalculator;
3
+ private canvas: HTMLCanvasElement;
4
+ private constructor() { }
5
+
6
+ private static getInstance(): TextDimensionsCalculator {
7
+ if (!TextDimensionsCalculator.instance) {
8
+ TextDimensionsCalculator.instance = new TextDimensionsCalculator();
9
+ TextDimensionsCalculator.instance.canvas = document.createElement('canvas');
10
+ }
11
+ return TextDimensionsCalculator.instance;
12
+ }
13
+
14
+ static getTextDimensions(text: string, fontSize: number): {width: number, height: number} {
15
+ const canvas = TextDimensionsCalculator.getInstance().canvas;
16
+ const context = canvas.getContext('2d');
17
+ if (!context)
18
+ throw new Error('Canvas 2D context is not available');
19
+
20
+ context.font = `${fontSize}px Arial`;
21
+ const scaleFactor = 1.1;
22
+
23
+ return {
24
+ width: context.measureText(text).width * scaleFactor,
25
+ height: fontSize * scaleFactor
26
+ };
27
+ }
28
+ }
29
+
@@ -0,0 +1,53 @@
1
+ import {STRAND} from '../../model/const';
2
+ import {PatternConfiguration} from '../../model/types';
3
+ import {FONT_SIZE} from './const';
4
+ import {SVGBlockBase} from './svg-block-base';
5
+ import {SVGElementFactory} from './svg-element-factory';
6
+ import {TextDimensionsCalculator} from './text-dimensions-calculator';
7
+
8
+ const TITLE_LEFT_PADDING = 15;
9
+
10
+ export class TitleBlock extends SVGBlockBase {
11
+ private titleText: string;
12
+ private _svgElements: SVGElement[] = [];
13
+ constructor(
14
+ protected svgElementFactory: SVGElementFactory,
15
+ protected config: PatternConfiguration,
16
+ protected yShift: number
17
+ ) {
18
+ super(svgElementFactory, config, yShift);
19
+ this.titleText = this.getTitleText();
20
+ this._svgElements = [this.getTitle()];
21
+ }
22
+
23
+ get svgElements(): SVGElement[] {
24
+ return this._svgElements;
25
+ }
26
+
27
+ private getTitle(): SVGElement {
28
+ return this.svgElementFactory.createTextElement(
29
+ this.titleText,
30
+ {x: TITLE_LEFT_PADDING, y: this.yShift + FONT_SIZE.TITLE},
31
+ FONT_SIZE.TITLE,
32
+ 'black'
33
+ );
34
+ }
35
+
36
+ getContentWidth(): number {
37
+ return TextDimensionsCalculator.getTextDimensions(this.titleText, FONT_SIZE.TITLE).width;
38
+ }
39
+
40
+ getContentHeight(): number {
41
+ return TextDimensionsCalculator.getTextDimensions(this.titleText, FONT_SIZE.TITLE).height;
42
+ }
43
+
44
+ private getTitleText(): string {
45
+ const senseStrandLength = `${this.config.nucleotideSequences[STRAND.SENSE].length}`;
46
+ const antisenseStrandLength = this.config.isAntisenseStrandIncluded ?
47
+ `/${this.config.nucleotideSequences[STRAND.ANTISENSE].length}` : '';
48
+ const titleText = `${this.config.patternName} for ${senseStrandLength}${antisenseStrandLength}-mer`;
49
+
50
+ return titleText;
51
+ }
52
+ }
53
+
@@ -0,0 +1,37 @@
1
+ import {NUCLEOTIDES} from '../../../common/model/const';
2
+ import {PATTERN_APP_DATA} from '../../../common/model/data-loader/json-loader';
3
+ import {LUMINANCE_COEFFICIENTS, TEXT_COLOR, SVG_CIRCLE_SIZES} from './const';
4
+ import {isOverhangNucleotide} from '../../model/utils';
5
+
6
+ export function computeLegendCircleYPosition(isAntisenseStrandActive: boolean): number {
7
+ return (isAntisenseStrandActive ? 9.5 : 6) * SVG_CIRCLE_SIZES.NUCLEOBASE_RADIUS;
8
+ }
9
+
10
+ export function getNucleobaseLabelForCircle(nucleobase: string): string {
11
+ const criterion = !isOverhangNucleotide(nucleobase) && NUCLEOTIDES.includes(nucleobase);
12
+
13
+ return criterion ? nucleobase : '';
14
+ }
15
+
16
+ export function computeTextColorForNucleobaseLabel(nucleobase: string): string {
17
+ const nucleobaseColor = getNucleobaseColorFromStyleMap(nucleobase);
18
+
19
+ const rgbValues = nucleobaseColor.match(/\d+/g)?.map(Number);
20
+ if (!rgbValues || rgbValues.length < 3)
21
+ return TEXT_COLOR.LIGHT;
22
+
23
+
24
+ const [r, g, b] = rgbValues;
25
+ const luminance = r * LUMINANCE_COEFFICIENTS.RED + g * LUMINANCE_COEFFICIENTS.GREEN + b * LUMINANCE_COEFFICIENTS.BLUE;
26
+ return luminance > LUMINANCE_COEFFICIENTS.THRESHOLD ? TEXT_COLOR.DARK : TEXT_COLOR.LIGHT;
27
+ }
28
+
29
+ export function getNucleobaseColorFromStyleMap(nucleobase: string): string {
30
+ // todo: optimize
31
+ const format = Object.keys(PATTERN_APP_DATA)[0];
32
+ if (!format)
33
+ throw new Error('No format found in PATTERN_APP_DATA');
34
+
35
+ const styleMap = PATTERN_APP_DATA[format];
36
+ return styleMap[nucleobase].color || '';
37
+ }