jbrowse-plugin-mafviewer 1.3.0 → 1.3.2

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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/BigMafAdapter/BigMafAdapter.js +39 -28
  3. package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
  4. package/dist/LinearMafDisplay/components/Crosshairs.js +1 -1
  5. package/dist/LinearMafDisplay/components/Crosshairs.js.map +1 -1
  6. package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +2 -3
  7. package/dist/LinearMafDisplay/components/MAFTooltip.js +6 -19
  8. package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
  9. package/dist/LinearMafDisplay/stateModel.d.ts +8 -0
  10. package/dist/LinearMafDisplay/stateModel.js +10 -0
  11. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  12. package/dist/LinearMafDisplay/util.d.ts +20 -0
  13. package/dist/LinearMafDisplay/util.js +29 -0
  14. package/dist/LinearMafDisplay/util.js.map +1 -1
  15. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +3 -0
  16. package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -2
  17. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  18. package/dist/LinearMafRenderer/components/ReactComponent.d.ts +3 -0
  19. package/dist/LinearMafRenderer/components/ReactComponent.js +41 -2
  20. package/dist/LinearMafRenderer/components/ReactComponent.js.map +1 -1
  21. package/dist/LinearMafRenderer/components/util.d.ts +1 -0
  22. package/dist/LinearMafRenderer/components/util.js +13 -0
  23. package/dist/LinearMafRenderer/components/util.js.map +1 -0
  24. package/dist/LinearMafRenderer/makeImageData.d.ts +4 -5
  25. package/dist/LinearMafRenderer/makeImageData.js +28 -146
  26. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  27. package/dist/LinearMafRenderer/rendering/features.d.ts +21 -0
  28. package/dist/LinearMafRenderer/rendering/features.js +58 -0
  29. package/dist/LinearMafRenderer/rendering/features.js.map +1 -0
  30. package/dist/LinearMafRenderer/rendering/gaps.d.ts +12 -0
  31. package/dist/LinearMafRenderer/rendering/gaps.js +35 -0
  32. package/dist/LinearMafRenderer/rendering/gaps.js.map +1 -0
  33. package/dist/LinearMafRenderer/rendering/index.d.ts +8 -0
  34. package/dist/LinearMafRenderer/rendering/index.js +10 -0
  35. package/dist/LinearMafRenderer/rendering/index.js.map +1 -0
  36. package/dist/LinearMafRenderer/rendering/insertions.d.ts +14 -0
  37. package/dist/LinearMafRenderer/rendering/insertions.js +84 -0
  38. package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -0
  39. package/dist/LinearMafRenderer/rendering/matches.d.ts +13 -0
  40. package/dist/LinearMafRenderer/rendering/matches.js +41 -0
  41. package/dist/LinearMafRenderer/rendering/matches.js.map +1 -0
  42. package/dist/LinearMafRenderer/rendering/mismatches.d.ts +13 -0
  43. package/dist/LinearMafRenderer/rendering/mismatches.js +47 -0
  44. package/dist/LinearMafRenderer/rendering/mismatches.js.map +1 -0
  45. package/dist/LinearMafRenderer/rendering/spatialIndex.d.ts +60 -0
  46. package/dist/LinearMafRenderer/rendering/spatialIndex.js +99 -0
  47. package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -0
  48. package/dist/LinearMafRenderer/rendering/text.d.ts +12 -0
  49. package/dist/LinearMafRenderer/rendering/text.js +42 -0
  50. package/dist/LinearMafRenderer/rendering/text.js.map +1 -0
  51. package/dist/LinearMafRenderer/rendering/types.d.ts +67 -0
  52. package/dist/LinearMafRenderer/rendering/types.js +15 -0
  53. package/dist/LinearMafRenderer/rendering/types.js.map +1 -0
  54. package/dist/MafTabixAdapter/MafTabixAdapter.js +48 -22
  55. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  56. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -8
  57. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  58. package/dist/out.js +34520 -0
  59. package/dist/out.js.map +7 -0
  60. package/dist/util/fastaUtils.js.map +1 -1
  61. package/package.json +5 -3
  62. package/src/BigMafAdapter/BigMafAdapter.ts +50 -29
  63. package/src/LinearMafDisplay/components/Crosshairs.tsx +1 -7
  64. package/src/LinearMafDisplay/components/MAFTooltip.tsx +14 -33
  65. package/src/LinearMafDisplay/stateModel.ts +10 -0
  66. package/src/LinearMafDisplay/util.ts +57 -0
  67. package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -2
  68. package/src/LinearMafRenderer/components/ReactComponent.tsx +70 -2
  69. package/src/LinearMafRenderer/components/util.ts +13 -0
  70. package/src/LinearMafRenderer/makeImageData.ts +49 -196
  71. package/src/LinearMafRenderer/rendering/features.ts +138 -0
  72. package/src/LinearMafRenderer/rendering/gaps.ts +71 -0
  73. package/src/LinearMafRenderer/rendering/index.ts +9 -0
  74. package/src/LinearMafRenderer/rendering/insertions.ts +170 -0
  75. package/src/LinearMafRenderer/rendering/matches.ts +79 -0
  76. package/src/LinearMafRenderer/rendering/mismatches.ts +125 -0
  77. package/src/LinearMafRenderer/rendering/spatialIndex.ts +136 -0
  78. package/src/LinearMafRenderer/rendering/text.ts +72 -0
  79. package/src/LinearMafRenderer/rendering/types.ts +81 -0
  80. package/src/MafTabixAdapter/MafTabixAdapter.ts +77 -22
  81. package/src/util/fastaUtils.ts +2 -1
@@ -1,18 +1,18 @@
1
1
  import { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
2
2
  import { createJBrowseTheme } from '@jbrowse/core/ui'
3
- import { Feature, featureSpanPx, measureText } from '@jbrowse/core/util'
3
+ import { Feature } from '@jbrowse/core/util'
4
+ import RBush from 'rbush'
4
5
 
5
6
  import {
6
- fillRect,
7
- getCharWidthHeight,
8
- getColorBaseMap,
9
- getContrastBaseMap,
10
- } from './util'
7
+ FONT_CONFIG,
8
+ RenderedBase,
9
+ RenderingContext,
10
+ Sample,
11
+ processFeatureAlignment,
12
+ processFeatureInsertions,
13
+ } from './rendering'
14
+ import { getColorBaseMap, getContrastBaseMap } from './util'
11
15
 
12
- interface Sample {
13
- id: string
14
- color?: string
15
- }
16
16
  interface RenderArgs extends RenderArgsDeserialized {
17
17
  samples: Sample[]
18
18
  rowHeight: number
@@ -24,10 +24,6 @@ interface RenderArgs extends RenderArgsDeserialized {
24
24
  showAsUpperCase: boolean
25
25
  }
26
26
 
27
- function getLetter(a: string, showAsUpperCase: boolean) {
28
- return showAsUpperCase ? a.toUpperCase() : a
29
- }
30
-
31
27
  export function makeImageData({
32
28
  ctx,
33
29
  renderArgs,
@@ -47,204 +43,61 @@ export function makeImageData({
47
43
  features,
48
44
  showAsUpperCase,
49
45
  } = renderArgs
46
+
50
47
  const region = regions[0]!
51
48
  const canvasWidth = (region.end - region.start) / bpPerPx
52
49
  const h = rowHeight * rowProportion
53
50
  const theme = createJBrowseTheme(configTheme)
54
51
  const colorForBase = getColorBaseMap(theme)
55
52
  const contrastForBase = getContrastBaseMap(theme)
56
-
57
- const { charHeight } = getCharWidthHeight()
58
53
  const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]))
59
54
  const scale = 1 / bpPerPx
60
- const f = 0.4
61
- const h2 = rowHeight / 2
62
55
  const hp2 = h / 2
63
56
  const offset = (rowHeight - h) / 2
64
57
 
65
- // sample as alignments
66
- ctx.font = 'bold 10px Courier New,monospace'
67
-
68
- for (const feature of features.values()) {
69
- const [leftPx] = featureSpanPx(feature, region, bpPerPx)
70
- const vals = feature.get('alignments') as Record<string, { seq: string }>
71
- const seq = feature.get('seq').toLowerCase()
72
- const r = Object.entries(vals)
73
- for (const [sample, val] of r) {
74
- const origAlignment = val.seq
75
- const alignment = origAlignment.toLowerCase()
76
-
77
- const row = sampleToRowMap.get(sample)
78
- if (row === undefined) {
79
- continue
80
- }
81
-
82
- const t = rowHeight * row
83
- const t2 = offset + t
84
-
85
- // gaps
86
- ctx.beginPath()
87
- ctx.fillStyle = 'black'
88
- for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
89
- if (seq[i] !== '-') {
90
- if (alignment[i] === '-') {
91
- const l = leftPx + scale * o
92
- ctx.moveTo(l, t + h2)
93
- ctx.lineTo(l + scale + f, t + h2)
94
- }
95
- o++
96
- }
97
- }
98
- ctx.stroke()
58
+ ctx.font = FONT_CONFIG
99
59
 
100
- if (!showAllLetters) {
101
- // matches
102
- ctx.fillStyle = 'lightgrey'
103
- for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
104
- if (seq[i] !== '-') {
105
- const c = alignment[i]
106
- const l = leftPx + scale * o
107
- if (seq[i] === c && c !== '-' && c !== ' ') {
108
- fillRect(ctx, l, t2, scale + f, h, canvasWidth)
109
- }
110
- o++
111
- }
112
- }
113
- }
114
-
115
- // mismatches
116
- for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
117
- const c = alignment[i]
118
- if (seq[i] !== '-') {
119
- if (c !== '-') {
120
- const l = leftPx + scale * o
121
- if (seq[i] !== c && c !== ' ') {
122
- fillRect(
123
- ctx,
124
- l,
125
- t2,
126
- scale + f,
127
- h,
128
- canvasWidth,
129
- mismatchRendering
130
- ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
131
- (colorForBase[c as keyof typeof colorForBase] ?? 'black')
132
- : 'orange',
133
- )
134
- } else if (showAllLetters) {
135
- fillRect(
136
- ctx,
137
- l,
138
- t2,
139
- scale + f,
140
- h,
141
- canvasWidth,
142
- mismatchRendering
143
- ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
144
- (colorForBase[c as keyof typeof colorForBase] ?? 'black')
145
- : 'lightblue',
146
- )
147
- }
148
- }
149
- o++
150
- }
151
- }
152
-
153
- // font
154
- const charSizeW = 10
155
- if (scale >= charSizeW) {
156
- for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
157
- if (seq[i] !== '-') {
158
- const l = leftPx + scale * o
159
- const offset = (scale - charSizeW) / 2 + 1
160
- const c = alignment[i]!
161
- if ((showAllLetters || seq[i] !== c) && c !== '-') {
162
- ctx.fillStyle = mismatchRendering
163
- ? (contrastForBase[c] ?? 'white')
164
- : 'black'
165
- if (rowHeight > charHeight) {
166
- ctx.fillText(
167
- getLetter(origAlignment[i] || '', showAsUpperCase),
168
- l + offset,
169
- hp2 + t + 3,
170
- )
171
- }
172
- }
173
- o++
174
- }
175
- }
176
- }
177
- }
60
+ const renderingContext: RenderingContext = {
61
+ ctx,
62
+ scale,
63
+ canvasWidth,
64
+ rowHeight,
65
+ h,
66
+ hp2,
67
+ offset,
68
+ colorForBase,
69
+ contrastForBase,
70
+ showAllLetters,
71
+ mismatchRendering,
72
+ showAsUpperCase,
73
+ spatialIndex: new RBush<RenderedBase>(),
74
+ lastInsertedX: -Infinity, // Start with -Infinity so first item is always inserted
178
75
  }
179
76
 
180
- // second pass for insertions, has slightly improved look since the
181
- // insertions are always 'on top' of the other features
77
+ // First pass: render alignments (gaps, matches, mismatches, text)
182
78
  for (const feature of features.values()) {
183
- const [leftPx] = featureSpanPx(feature, region, bpPerPx)
184
- const vals = feature.get('alignments') as Record<string, { seq: string }>
185
- const seq = feature.get('seq').toLowerCase()
186
-
187
- for (const [sample, val] of Object.entries(vals)) {
188
- const origAlignment = val.seq
189
- const alignment = origAlignment.toLowerCase()
190
- const row = sampleToRowMap.get(sample)
191
- if (row === undefined) {
192
- continue
193
- }
79
+ processFeatureAlignment(
80
+ feature,
81
+ region,
82
+ bpPerPx,
83
+ sampleToRowMap,
84
+ renderingContext,
85
+ )
86
+ }
194
87
 
195
- const t = rowHeight * row
196
- const t2 = offset + t
197
- for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
198
- let ins = ''
199
- while (seq[i] === '-') {
200
- if (alignment[i] !== '-' && alignment[i] !== ' ') {
201
- ins += alignment[i]
202
- }
203
- i++
204
- }
205
- if (ins.length > 0) {
206
- const l = leftPx + scale * o - 1
88
+ // Second pass: render insertions on top
89
+ for (const feature of features.values()) {
90
+ processFeatureInsertions(
91
+ feature,
92
+ region,
93
+ bpPerPx,
94
+ sampleToRowMap,
95
+ renderingContext,
96
+ )
97
+ }
207
98
 
208
- if (ins.length > 10) {
209
- const txt = `${ins.length}`
210
- if (bpPerPx > 10) {
211
- fillRect(ctx, l - 1, t2, 2, h, canvasWidth, 'purple')
212
- } else if (h > charHeight) {
213
- const rwidth = measureText(txt, 10)
214
- const padding = 2
215
- fillRect(
216
- ctx,
217
- l - rwidth / 2 - padding,
218
- t2,
219
- rwidth + 2 * padding,
220
- h,
221
- canvasWidth,
222
- 'purple',
223
- )
224
- ctx.fillStyle = 'white'
225
- ctx.fillText(txt, l - rwidth / 2, t + h)
226
- } else {
227
- const padding = 2
228
- fillRect(
229
- ctx,
230
- l - padding,
231
- t2,
232
- 2 * padding,
233
- h,
234
- canvasWidth,
235
- 'purple',
236
- )
237
- }
238
- } else {
239
- fillRect(ctx, l, t2, 1, h, canvasWidth, 'purple')
240
- if (bpPerPx < 0.2 && rowHeight > 5) {
241
- fillRect(ctx, l - 2, t2, 5, 1, canvasWidth)
242
- fillRect(ctx, l - 2, t2 + h - 1, 5, 1, canvasWidth)
243
- }
244
- }
245
- }
246
- o++
247
- }
248
- }
99
+ // Return serialized RBush spatial index
100
+ return {
101
+ rbush: renderingContext.spatialIndex.toJSON(),
249
102
  }
250
103
  }
@@ -0,0 +1,138 @@
1
+ import { Feature, featureSpanPx } from '@jbrowse/core/util'
2
+
3
+ import { renderGaps } from './gaps'
4
+ import { renderInsertions } from './insertions'
5
+ import { renderMatches } from './matches'
6
+ import { renderMismatches } from './mismatches'
7
+ import { renderText } from './text'
8
+
9
+ import type { AlignmentRecord, GenomicRegion, RenderingContext } from './types'
10
+
11
+ /**
12
+ * Processes alignment data for a single feature, rendering gaps, matches, mismatches, and text
13
+ * @param feature - JBrowse feature containing alignment data
14
+ * @param region - Genomic region being rendered
15
+ * @param bpPerPx - Base pairs per pixel (zoom level)
16
+ * @param sampleToRowMap - Maps sample IDs to row indices
17
+ * @param renderingContext - Shared rendering parameters
18
+ */
19
+ export function processFeatureAlignment(
20
+ feature: Feature,
21
+ region: GenomicRegion,
22
+ bpPerPx: number,
23
+ sampleToRowMap: Map<string, number>,
24
+ renderingContext: RenderingContext,
25
+ ) {
26
+ const [leftPx] = featureSpanPx(feature, region, bpPerPx)
27
+ const alignments = feature.get('alignments') as Record<
28
+ string,
29
+ AlignmentRecord
30
+ >
31
+ const referenceSeq = feature.get('seq').toLowerCase()
32
+ const featureId =
33
+ feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
34
+
35
+ for (const [sampleId, alignmentData] of Object.entries(alignments)) {
36
+ const row = sampleToRowMap.get(sampleId)
37
+ if (row === undefined) {
38
+ continue
39
+ }
40
+
41
+ const originalAlignment = alignmentData.seq
42
+ const alignment = originalAlignment.toLowerCase()
43
+ const rowTop = renderingContext.offset + renderingContext.rowHeight * row
44
+
45
+ renderGaps(
46
+ renderingContext,
47
+ alignment,
48
+ referenceSeq,
49
+ leftPx,
50
+ rowTop,
51
+ sampleId,
52
+ featureId,
53
+ alignmentData.start,
54
+ alignmentData.chr,
55
+ )
56
+ renderMatches(
57
+ renderingContext,
58
+ alignment,
59
+ referenceSeq,
60
+ leftPx,
61
+ rowTop,
62
+ sampleId,
63
+ featureId,
64
+ alignmentData.start,
65
+ alignmentData.chr,
66
+ )
67
+ renderMismatches(
68
+ renderingContext,
69
+ alignment,
70
+ referenceSeq,
71
+ leftPx,
72
+ rowTop,
73
+ sampleId,
74
+ featureId,
75
+ alignmentData.start,
76
+ alignmentData.chr,
77
+ )
78
+ renderText(
79
+ renderingContext,
80
+ alignment,
81
+ originalAlignment,
82
+ referenceSeq,
83
+ leftPx,
84
+ rowTop,
85
+ sampleId,
86
+ featureId,
87
+ )
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Processes insertion data for a single feature in a separate pass
93
+ * Insertions are rendered on top to ensure visibility
94
+ * @param feature - JBrowse feature containing alignment data
95
+ * @param region - Genomic region being rendered
96
+ * @param bpPerPx - Base pairs per pixel (zoom level)
97
+ * @param sampleToRowMap - Maps sample IDs to row indices
98
+ * @param renderingContext - Shared rendering parameters
99
+ */
100
+ export function processFeatureInsertions(
101
+ feature: Feature,
102
+ region: GenomicRegion,
103
+ bpPerPx: number,
104
+ sampleToRowMap: Map<string, number>,
105
+ renderingContext: RenderingContext,
106
+ ) {
107
+ const [leftPx] = featureSpanPx(feature, region, bpPerPx)
108
+ const alignments = feature.get('alignments') as Record<
109
+ string,
110
+ AlignmentRecord
111
+ >
112
+ const referenceSeq = feature.get('seq').toLowerCase()
113
+ const featureId =
114
+ feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
115
+
116
+ for (const [sampleId, alignmentData] of Object.entries(alignments)) {
117
+ const row = sampleToRowMap.get(sampleId)
118
+ if (row === undefined) {
119
+ continue
120
+ }
121
+
122
+ const alignment = alignmentData.seq.toLowerCase()
123
+ const rowTop = renderingContext.offset + renderingContext.rowHeight * row
124
+
125
+ renderInsertions(
126
+ renderingContext,
127
+ alignment,
128
+ referenceSeq,
129
+ leftPx,
130
+ rowTop,
131
+ bpPerPx,
132
+ sampleId,
133
+ featureId,
134
+ alignmentData.start,
135
+ alignmentData.chr,
136
+ )
137
+ }
138
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ addToSpatialIndex,
3
+ createRenderedBase,
4
+ shouldAddToSpatialIndex,
5
+ } from './spatialIndex'
6
+ import { GAP_STROKE_OFFSET } from './types'
7
+
8
+ import type { RenderingContext } from './types'
9
+
10
+ /**
11
+ * Renders gap indicators (horizontal lines) where the alignment has deletions relative to reference
12
+ * @param context - Rendering context with canvas and styling info
13
+ * @param alignment - The aligned sequence for this sample
14
+ * @param seq - The reference sequence
15
+ * @param leftPx - Left pixel position of the feature
16
+ * @param rowTop - Top pixel position of the row
17
+ * @param alignmentStart - Start position of the alignment
18
+ * @param chr - Chromosome/sequence name
19
+ */
20
+ export function renderGaps(
21
+ context: RenderingContext,
22
+ alignment: string,
23
+ seq: string,
24
+ leftPx: number,
25
+ rowTop: number,
26
+ sampleId: string,
27
+ featureId: string,
28
+ alignmentStart: number,
29
+ chr: string,
30
+ ) {
31
+ const { ctx, scale } = context
32
+ const h2 = context.rowHeight / 2
33
+
34
+ ctx.beginPath()
35
+ ctx.fillStyle = 'black'
36
+
37
+ for (
38
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
39
+ i < seqLength;
40
+ i++
41
+ ) {
42
+ if (seq[i] !== '-') {
43
+ if (alignment[i] === '-') {
44
+ const xPos = leftPx + scale * genomicOffset
45
+ ctx.moveTo(xPos, rowTop + h2)
46
+ ctx.lineTo(xPos + scale + GAP_STROKE_OFFSET, rowTop + h2)
47
+
48
+ // Add to spatial index if distance filter allows
49
+ if (shouldAddToSpatialIndex(xPos, context)) {
50
+ const renderedBase = createRenderedBase(
51
+ xPos,
52
+ rowTop,
53
+ context,
54
+ genomicOffset + alignmentStart,
55
+ chr,
56
+ sampleId,
57
+ '-',
58
+ false,
59
+ false,
60
+ true,
61
+ false,
62
+ featureId,
63
+ )
64
+ addToSpatialIndex(context, renderedBase)
65
+ }
66
+ }
67
+ genomicOffset++
68
+ }
69
+ }
70
+ ctx.stroke()
71
+ }
@@ -0,0 +1,9 @@
1
+ // Re-export all rendering modules
2
+ export * from './types'
3
+ export * from './spatialIndex'
4
+ export * from './gaps'
5
+ export * from './matches'
6
+ export * from './mismatches'
7
+ export * from './text'
8
+ export * from './insertions'
9
+ export * from './features'
@@ -0,0 +1,170 @@
1
+ import { measureText } from '@jbrowse/core/util'
2
+
3
+ import { fillRect, getCharWidthHeight } from '../util'
4
+ import {
5
+ addToSpatialIndex,
6
+ createRenderedInsertion,
7
+ shouldAddToSpatialIndex,
8
+ } from './spatialIndex'
9
+ import {
10
+ CHAR_SIZE_WIDTH,
11
+ HIGH_BP_PER_PX_THRESHOLD,
12
+ HIGH_ZOOM_THRESHOLD,
13
+ INSERTION_BORDER_HEIGHT,
14
+ INSERTION_BORDER_WIDTH,
15
+ INSERTION_LINE_WIDTH,
16
+ INSERTION_PADDING,
17
+ LARGE_INSERTION_THRESHOLD,
18
+ MIN_ROW_HEIGHT_FOR_BORDERS,
19
+ } from './types'
20
+
21
+ import type { RenderingContext } from './types'
22
+
23
+ /**
24
+ * Renders insertion markers where the alignment has bases not present in reference
25
+ * Large insertions show count, small ones show as colored bars with optional borders
26
+ * @param context - Rendering context with canvas and styling info
27
+ * @param alignment - The aligned sequence for this sample
28
+ * @param seq - The reference sequence
29
+ * @param leftPx - Left pixel position of the feature
30
+ * @param rowTop - Top pixel position of the row
31
+ * @param bpPerPx - Base pairs per pixel (zoom level)
32
+ * @param alignmentStart - Start position of the alignment
33
+ * @param chr - Chromosome/sequence name
34
+ */
35
+ export function renderInsertions(
36
+ context: RenderingContext,
37
+ alignment: string,
38
+ seq: string,
39
+ leftPx: number,
40
+ rowTop: number,
41
+ bpPerPx: number,
42
+ sampleId: string,
43
+ featureId: string,
44
+ alignmentStart: number,
45
+ chr: string,
46
+ ) {
47
+ const { ctx, scale, h, canvasWidth, rowHeight } = context
48
+ const { charHeight } = getCharWidthHeight()
49
+
50
+ for (
51
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
52
+ i < seqLength;
53
+ i++
54
+ ) {
55
+ let insertionSequence = ''
56
+ while (seq[i] === '-') {
57
+ if (alignment[i] !== '-' && alignment[i] !== ' ') {
58
+ insertionSequence += alignment[i]
59
+ }
60
+ i++
61
+ }
62
+ if (insertionSequence.length > 0) {
63
+ // Found an insertion
64
+ const xPos = leftPx + scale * genomicOffset - INSERTION_LINE_WIDTH
65
+
66
+ // Determine actual rendered width and position for spatial index
67
+ let actualXPos: number
68
+ let actualWidth: number
69
+
70
+ // Large insertions: show count instead of individual bases
71
+ if (insertionSequence.length > LARGE_INSERTION_THRESHOLD) {
72
+ const lengthText = `${insertionSequence.length}`
73
+ if (bpPerPx > HIGH_BP_PER_PX_THRESHOLD) {
74
+ // Very zoomed out: simple line
75
+ actualXPos = xPos - INSERTION_LINE_WIDTH
76
+ actualWidth = INSERTION_BORDER_WIDTH
77
+ fillRect(
78
+ ctx,
79
+ actualXPos,
80
+ rowTop,
81
+ actualWidth,
82
+ h,
83
+ canvasWidth,
84
+ 'purple',
85
+ )
86
+ } else if (h > charHeight) {
87
+ // Medium zoom: show count in colored box
88
+ const textWidth = measureText(lengthText, CHAR_SIZE_WIDTH)
89
+ const padding = INSERTION_PADDING
90
+ actualXPos = xPos - textWidth / 2 - padding
91
+ actualWidth = textWidth + 2 * padding
92
+ fillRect(
93
+ ctx,
94
+ actualXPos,
95
+ rowTop,
96
+ actualWidth,
97
+ h,
98
+ canvasWidth,
99
+ 'purple',
100
+ )
101
+ ctx.fillStyle = 'white'
102
+ ctx.fillText(lengthText, xPos - textWidth / 2, rowTop + (h * 7) / 8)
103
+ } else {
104
+ const padding = INSERTION_PADDING
105
+ actualXPos = xPos - padding
106
+ actualWidth = 2 * padding
107
+ fillRect(
108
+ ctx,
109
+ actualXPos,
110
+ rowTop,
111
+ actualWidth,
112
+ h,
113
+ canvasWidth,
114
+ 'purple',
115
+ )
116
+ }
117
+ } else {
118
+ // Small insertions: vertical line with optional border at high zoom
119
+ actualXPos = xPos
120
+ actualWidth = INSERTION_LINE_WIDTH
121
+ fillRect(ctx, actualXPos, rowTop, actualWidth, h, canvasWidth, 'purple')
122
+ if (
123
+ bpPerPx < HIGH_ZOOM_THRESHOLD &&
124
+ rowHeight > MIN_ROW_HEIGHT_FOR_BORDERS
125
+ ) {
126
+ // Add horizontal borders for visibility at high zoom
127
+ // Note: borders extend the effective clickable area
128
+ actualXPos = xPos - INSERTION_BORDER_WIDTH
129
+ actualWidth = INSERTION_BORDER_HEIGHT
130
+ fillRect(
131
+ ctx,
132
+ xPos - INSERTION_BORDER_WIDTH,
133
+ rowTop,
134
+ INSERTION_BORDER_HEIGHT,
135
+ INSERTION_LINE_WIDTH,
136
+ canvasWidth,
137
+ )
138
+ fillRect(
139
+ ctx,
140
+ xPos - INSERTION_BORDER_WIDTH,
141
+ rowTop + h - INSERTION_LINE_WIDTH,
142
+ INSERTION_BORDER_HEIGHT,
143
+ INSERTION_LINE_WIDTH,
144
+ canvasWidth,
145
+ )
146
+ }
147
+ }
148
+
149
+ // Add insertion to spatial index with actual rendered dimensions
150
+ // Insertions always bypass distance filter
151
+ if (shouldAddToSpatialIndex(actualXPos, context, true)) {
152
+ addToSpatialIndex(
153
+ context,
154
+ createRenderedInsertion(
155
+ actualXPos,
156
+ rowTop,
157
+ actualWidth,
158
+ context,
159
+ genomicOffset + alignmentStart,
160
+ chr,
161
+ sampleId,
162
+ insertionSequence,
163
+ featureId,
164
+ ),
165
+ )
166
+ }
167
+ }
168
+ genomicOffset++
169
+ }
170
+ }