jbrowse-plugin-mafviewer 1.3.1 → 1.4.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 (101) hide show
  1. package/README.md +1 -48
  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 +9 -0
  16. package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -2
  17. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  18. package/dist/LinearMafRenderer/components/LinearMafRendering.d.ts +13 -0
  19. package/dist/LinearMafRenderer/components/LinearMafRendering.js +46 -0
  20. package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -0
  21. package/dist/LinearMafRenderer/components/ReactComponent.d.ts +3 -0
  22. package/dist/LinearMafRenderer/components/ReactComponent.js +41 -2
  23. package/dist/LinearMafRenderer/components/ReactComponent.js.map +1 -1
  24. package/dist/LinearMafRenderer/components/util.d.ts +1 -0
  25. package/dist/LinearMafRenderer/components/util.js +13 -0
  26. package/dist/LinearMafRenderer/components/util.js.map +1 -0
  27. package/dist/LinearMafRenderer/index.js +1 -1
  28. package/dist/LinearMafRenderer/index.js.map +1 -1
  29. package/dist/LinearMafRenderer/makeImageData.d.ts +6 -5
  30. package/dist/LinearMafRenderer/makeImageData.js +35 -146
  31. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  32. package/dist/LinearMafRenderer/rendering/features.d.ts +4 -0
  33. package/dist/LinearMafRenderer/rendering/features.js +41 -0
  34. package/dist/LinearMafRenderer/rendering/features.js.map +1 -0
  35. package/dist/LinearMafRenderer/rendering/gaps.d.ts +2 -0
  36. package/dist/LinearMafRenderer/rendering/gaps.js +19 -0
  37. package/dist/LinearMafRenderer/rendering/gaps.js.map +1 -0
  38. package/dist/LinearMafRenderer/rendering/index.d.ts +8 -0
  39. package/dist/LinearMafRenderer/rendering/index.js +10 -0
  40. package/dist/LinearMafRenderer/rendering/index.js.map +1 -0
  41. package/dist/LinearMafRenderer/rendering/insertions.d.ts +2 -0
  42. package/dist/LinearMafRenderer/rendering/insertions.js +78 -0
  43. package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -0
  44. package/dist/LinearMafRenderer/rendering/matches.d.ts +2 -0
  45. package/dist/LinearMafRenderer/rendering/matches.js +34 -0
  46. package/dist/LinearMafRenderer/rendering/matches.js.map +1 -0
  47. package/dist/LinearMafRenderer/rendering/mismatches.d.ts +13 -0
  48. package/dist/LinearMafRenderer/rendering/mismatches.js +57 -0
  49. package/dist/LinearMafRenderer/rendering/mismatches.js.map +1 -0
  50. package/dist/LinearMafRenderer/rendering/spatialIndex.d.ts +9 -0
  51. package/dist/LinearMafRenderer/rendering/spatialIndex.js +19 -0
  52. package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -0
  53. package/dist/LinearMafRenderer/rendering/text.d.ts +12 -0
  54. package/dist/LinearMafRenderer/rendering/text.js +42 -0
  55. package/dist/LinearMafRenderer/rendering/text.js.map +1 -0
  56. package/dist/LinearMafRenderer/rendering/types.d.ts +55 -0
  57. package/dist/LinearMafRenderer/rendering/types.js +15 -0
  58. package/dist/LinearMafRenderer/rendering/types.js.map +1 -0
  59. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js +37 -35
  60. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
  61. package/dist/MafTabixAdapter/MafTabixAdapter.js +48 -22
  62. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  63. package/dist/index.js +0 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +5 -29
  66. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  67. package/dist/out.js +32310 -0
  68. package/dist/out.js.map +7 -0
  69. package/dist/util/fastaUtils.js.map +1 -1
  70. package/package.json +3 -2
  71. package/src/BigMafAdapter/BigMafAdapter.ts +49 -28
  72. package/src/LinearMafDisplay/components/Crosshairs.tsx +1 -7
  73. package/src/LinearMafDisplay/components/MAFTooltip.tsx +14 -33
  74. package/src/LinearMafDisplay/stateModel.ts +10 -0
  75. package/src/LinearMafDisplay/util.ts +57 -0
  76. package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -2
  77. package/src/LinearMafRenderer/components/LinearMafRendering.tsx +76 -0
  78. package/src/LinearMafRenderer/components/util.ts +13 -0
  79. package/src/LinearMafRenderer/index.ts +1 -1
  80. package/src/LinearMafRenderer/makeImageData.ts +64 -196
  81. package/src/LinearMafRenderer/rendering/features.ts +111 -0
  82. package/src/LinearMafRenderer/rendering/gaps.ts +33 -0
  83. package/src/LinearMafRenderer/rendering/index.ts +9 -0
  84. package/src/LinearMafRenderer/rendering/insertions.ts +154 -0
  85. package/src/LinearMafRenderer/rendering/matches.ts +62 -0
  86. package/src/LinearMafRenderer/rendering/mismatches.ts +113 -0
  87. package/src/LinearMafRenderer/rendering/spatialIndex.ts +40 -0
  88. package/src/LinearMafRenderer/rendering/text.ts +72 -0
  89. package/src/LinearMafRenderer/rendering/types.ts +65 -0
  90. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +5 -6
  91. package/src/MafTabixAdapter/MafTabixAdapter.ts +77 -22
  92. package/src/index.ts +0 -2
  93. package/src/util/fastaUtils.ts +2 -1
  94. package/src/BgzipTaffyAdapter/BgzipTaffyAdapter.ts +0 -307
  95. package/src/BgzipTaffyAdapter/configSchema.ts +0 -59
  96. package/src/BgzipTaffyAdapter/index.ts +0 -16
  97. package/src/BgzipTaffyAdapter/rowInstructions.ts +0 -91
  98. package/src/BgzipTaffyAdapter/types.ts +0 -16
  99. package/src/BgzipTaffyAdapter/util.ts +0 -25
  100. package/src/BgzipTaffyAdapter/virtualOffset.ts +0 -29
  101. package/src/LinearMafRenderer/components/ReactComponent.tsx +0 -13
@@ -1,18 +1,17 @@
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 Flatbush from 'flatbush'
4
5
 
5
6
  import {
6
- fillRect,
7
- getCharWidthHeight,
8
- getColorBaseMap,
9
- getContrastBaseMap,
10
- } from './util'
7
+ FONT_CONFIG,
8
+ RenderingContext,
9
+ Sample,
10
+ processFeatureAlignment,
11
+ processFeatureInsertions,
12
+ } from './rendering'
13
+ import { getColorBaseMap, getContrastBaseMap } from './util'
11
14
 
12
- interface Sample {
13
- id: string
14
- color?: string
15
- }
16
15
  interface RenderArgs extends RenderArgsDeserialized {
17
16
  samples: Sample[]
18
17
  rowHeight: number
@@ -24,10 +23,6 @@ interface RenderArgs extends RenderArgsDeserialized {
24
23
  showAsUpperCase: boolean
25
24
  }
26
25
 
27
- function getLetter(a: string, showAsUpperCase: boolean) {
28
- return showAsUpperCase ? a.toUpperCase() : a
29
- }
30
-
31
26
  export function makeImageData({
32
27
  ctx,
33
28
  renderArgs,
@@ -47,204 +42,77 @@ export function makeImageData({
47
42
  features,
48
43
  showAsUpperCase,
49
44
  } = renderArgs
45
+
50
46
  const region = regions[0]!
51
47
  const canvasWidth = (region.end - region.start) / bpPerPx
52
48
  const h = rowHeight * rowProportion
53
49
  const theme = createJBrowseTheme(configTheme)
54
50
  const colorForBase = getColorBaseMap(theme)
55
51
  const contrastForBase = getContrastBaseMap(theme)
56
-
57
- const { charHeight } = getCharWidthHeight()
58
52
  const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]))
59
53
  const scale = 1 / bpPerPx
60
- const f = 0.4
61
- const h2 = rowHeight / 2
62
54
  const hp2 = h / 2
63
55
  const offset = (rowHeight - h) / 2
64
56
 
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()
99
-
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
- }
57
+ ctx.font = FONT_CONFIG
114
58
 
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
- }
59
+ const renderingContext: RenderingContext = {
60
+ ctx,
61
+ scale,
62
+ canvasWidth,
63
+ rowHeight,
64
+ h,
65
+ hp2,
66
+ offset,
67
+ colorForBase,
68
+ contrastForBase,
69
+ showAllLetters,
70
+ mismatchRendering,
71
+ showAsUpperCase,
72
+ spatialIndex: [],
73
+ spatialIndexCoords: [],
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
+ const flatbush = new Flatbush(renderingContext.spatialIndex.length)
100
+ for (
101
+ let i = 0, l = renderingContext.spatialIndexCoords.length;
102
+ i < l;
103
+ i += 4
104
+ ) {
105
+ flatbush.add(
106
+ renderingContext.spatialIndexCoords[i]!,
107
+ renderingContext.spatialIndexCoords[i + 1]!,
108
+ renderingContext.spatialIndexCoords[i + 2]!,
109
+ renderingContext.spatialIndexCoords[i + 3]!,
110
+ )
111
+ }
112
+ flatbush.finish()
113
+ return {
114
+ flatbush: flatbush.data,
115
+ items: renderingContext.spatialIndex,
116
+ samples,
249
117
  }
250
118
  }
@@ -0,0 +1,111 @@
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
+ export function processFeatureAlignment(
12
+ feature: Feature,
13
+ region: GenomicRegion,
14
+ bpPerPx: number,
15
+ sampleToRowMap: Map<string, number>,
16
+ renderingContext: RenderingContext,
17
+ ) {
18
+ const [leftPx] = featureSpanPx(feature, region, bpPerPx)
19
+ const alignments = feature.get('alignments') as Record<
20
+ string,
21
+ AlignmentRecord
22
+ >
23
+ const referenceSeq = feature.get('seq').toLowerCase()
24
+ const featureId =
25
+ feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
26
+
27
+ for (const [sampleId, alignmentData] of Object.entries(alignments)) {
28
+ const row = sampleToRowMap.get(sampleId)
29
+ if (row === undefined) {
30
+ continue
31
+ }
32
+
33
+ const originalAlignment = alignmentData.seq
34
+ const alignment = originalAlignment.toLowerCase()
35
+ const rowTop = renderingContext.offset + renderingContext.rowHeight * row
36
+
37
+ renderGaps(renderingContext, alignment, referenceSeq, leftPx, rowTop)
38
+ renderMatches(
39
+ renderingContext,
40
+ alignment,
41
+ referenceSeq,
42
+ leftPx,
43
+ rowTop,
44
+ row,
45
+ featureId,
46
+ alignmentData.start,
47
+ alignmentData.chr,
48
+ )
49
+ renderMismatches(
50
+ renderingContext,
51
+ alignment,
52
+ referenceSeq,
53
+ leftPx,
54
+ rowTop,
55
+ row,
56
+ featureId,
57
+ alignmentData.start,
58
+ alignmentData.chr,
59
+ )
60
+ renderText(
61
+ renderingContext,
62
+ alignment,
63
+ originalAlignment,
64
+ referenceSeq,
65
+ leftPx,
66
+ rowTop,
67
+ sampleId,
68
+ featureId,
69
+ )
70
+ }
71
+ }
72
+
73
+ export function processFeatureInsertions(
74
+ feature: Feature,
75
+ region: GenomicRegion,
76
+ bpPerPx: number,
77
+ sampleToRowMap: Map<string, number>,
78
+ renderingContext: RenderingContext,
79
+ ) {
80
+ const [leftPx] = featureSpanPx(feature, region, bpPerPx)
81
+ const alignments = feature.get('alignments') as Record<
82
+ string,
83
+ AlignmentRecord
84
+ >
85
+ const referenceSeq = feature.get('seq').toLowerCase()
86
+ const featureId =
87
+ feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
88
+
89
+ for (const [sampleId, alignmentData] of Object.entries(alignments)) {
90
+ const row = sampleToRowMap.get(sampleId)
91
+ if (row === undefined) {
92
+ continue
93
+ }
94
+
95
+ const alignment = alignmentData.seq.toLowerCase()
96
+ const rowTop = renderingContext.offset + renderingContext.rowHeight * row
97
+
98
+ renderInsertions(
99
+ renderingContext,
100
+ alignment,
101
+ referenceSeq,
102
+ leftPx,
103
+ rowTop,
104
+ bpPerPx,
105
+ row,
106
+ featureId,
107
+ alignmentData.start,
108
+ alignmentData.chr,
109
+ )
110
+ }
111
+ }
@@ -0,0 +1,33 @@
1
+ import { GAP_STROKE_OFFSET } from './types'
2
+
3
+ import type { RenderingContext } from './types'
4
+
5
+ export function renderGaps(
6
+ context: RenderingContext,
7
+ alignment: string,
8
+ seq: string,
9
+ leftPx: number,
10
+ rowTop: number,
11
+ ) {
12
+ const { ctx, scale } = context
13
+ const h2 = context.rowHeight / 2
14
+
15
+ ctx.beginPath()
16
+ ctx.fillStyle = 'black'
17
+
18
+ for (
19
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
20
+ i < seqLength;
21
+ i++
22
+ ) {
23
+ if (seq[i] !== '-') {
24
+ if (alignment[i] === '-') {
25
+ const xPos = leftPx + scale * genomicOffset
26
+ ctx.moveTo(xPos, rowTop + h2)
27
+ ctx.lineTo(xPos + scale + GAP_STROKE_OFFSET, rowTop + h2)
28
+ }
29
+ genomicOffset++
30
+ }
31
+ }
32
+ ctx.stroke()
33
+ }
@@ -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,154 @@
1
+ import { measureText } from '@jbrowse/core/util'
2
+
3
+ import { fillRect, getCharWidthHeight } from '../util'
4
+ import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
5
+ import {
6
+ CHAR_SIZE_WIDTH,
7
+ HIGH_BP_PER_PX_THRESHOLD,
8
+ HIGH_ZOOM_THRESHOLD,
9
+ INSERTION_BORDER_HEIGHT,
10
+ INSERTION_BORDER_WIDTH,
11
+ INSERTION_LINE_WIDTH,
12
+ INSERTION_PADDING,
13
+ LARGE_INSERTION_THRESHOLD,
14
+ MIN_ROW_HEIGHT_FOR_BORDERS,
15
+ } from './types'
16
+
17
+ import type { RenderingContext } from './types'
18
+
19
+ export function renderInsertions(
20
+ context: RenderingContext,
21
+ alignment: string,
22
+ seq: string,
23
+ leftPx: number,
24
+ rowTop: number,
25
+ bpPerPx: number,
26
+ sampleId: number,
27
+ _featureId: string,
28
+ alignmentStart: number,
29
+ chr: string,
30
+ ) {
31
+ const { ctx, scale, h, canvasWidth, rowHeight } = context
32
+ const { charHeight } = getCharWidthHeight()
33
+
34
+ for (
35
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
36
+ i < seqLength;
37
+ i++
38
+ ) {
39
+ let insertionSequence = ''
40
+ while (seq[i] === '-') {
41
+ if (alignment[i] !== '-' && alignment[i] !== ' ') {
42
+ insertionSequence += alignment[i]
43
+ }
44
+ i++
45
+ }
46
+ if (insertionSequence.length > 0) {
47
+ // Found an insertion
48
+ const xPos = leftPx + scale * genomicOffset - INSERTION_LINE_WIDTH
49
+
50
+ // Determine actual rendered width and position for spatial index
51
+ let actualXPos: number
52
+ let actualWidth: number
53
+
54
+ // Large insertions: show count instead of individual bases
55
+ if (insertionSequence.length > LARGE_INSERTION_THRESHOLD) {
56
+ const lengthText = `${insertionSequence.length}`
57
+ if (bpPerPx > HIGH_BP_PER_PX_THRESHOLD) {
58
+ // Very zoomed out: simple line
59
+ actualXPos = xPos - INSERTION_LINE_WIDTH
60
+ actualWidth = INSERTION_BORDER_WIDTH
61
+ fillRect(
62
+ ctx,
63
+ actualXPos,
64
+ rowTop,
65
+ actualWidth,
66
+ h,
67
+ canvasWidth,
68
+ 'purple',
69
+ )
70
+ } else if (h > charHeight) {
71
+ // Medium zoom: show count in colored box
72
+ const textWidth = measureText(lengthText, CHAR_SIZE_WIDTH)
73
+ const padding = INSERTION_PADDING
74
+ actualXPos = xPos - textWidth / 2 - padding
75
+ actualWidth = textWidth + 2 * padding
76
+ fillRect(
77
+ ctx,
78
+ actualXPos,
79
+ rowTop,
80
+ actualWidth,
81
+ h,
82
+ canvasWidth,
83
+ 'purple',
84
+ )
85
+ ctx.fillStyle = 'white'
86
+ ctx.fillText(lengthText, xPos - textWidth / 2, rowTop + (h * 7) / 8)
87
+ } else {
88
+ const padding = INSERTION_PADDING
89
+ actualXPos = xPos - padding
90
+ actualWidth = 2 * padding
91
+ fillRect(
92
+ ctx,
93
+ actualXPos,
94
+ rowTop,
95
+ actualWidth,
96
+ h,
97
+ canvasWidth,
98
+ 'purple',
99
+ )
100
+ }
101
+ } else {
102
+ // Small insertions: vertical line with optional border at high zoom
103
+ actualXPos = xPos
104
+ actualWidth = INSERTION_LINE_WIDTH
105
+ fillRect(ctx, actualXPos, rowTop, actualWidth, h, canvasWidth, 'purple')
106
+ if (
107
+ bpPerPx < HIGH_ZOOM_THRESHOLD &&
108
+ rowHeight > MIN_ROW_HEIGHT_FOR_BORDERS
109
+ ) {
110
+ // Add horizontal borders for visibility at high zoom
111
+ // Note: borders extend the effective clickable area
112
+ actualXPos = xPos - INSERTION_BORDER_WIDTH
113
+ actualWidth = INSERTION_BORDER_HEIGHT
114
+ fillRect(
115
+ ctx,
116
+ xPos - INSERTION_BORDER_WIDTH,
117
+ rowTop,
118
+ INSERTION_BORDER_HEIGHT,
119
+ INSERTION_LINE_WIDTH,
120
+ canvasWidth,
121
+ )
122
+ fillRect(
123
+ ctx,
124
+ xPos - INSERTION_BORDER_WIDTH,
125
+ rowTop + h - INSERTION_LINE_WIDTH,
126
+ INSERTION_BORDER_HEIGHT,
127
+ INSERTION_LINE_WIDTH,
128
+ canvasWidth,
129
+ )
130
+ }
131
+ }
132
+
133
+ // Add insertion to spatial index with actual rendered dimensions
134
+ // Insertions always bypass distance filter
135
+ if (shouldAddToSpatialIndex(actualXPos, context, true)) {
136
+ addToSpatialIndex(
137
+ context,
138
+ actualXPos,
139
+ rowTop,
140
+ actualXPos + actualWidth,
141
+ rowTop + context.h,
142
+ {
143
+ pos: genomicOffset + alignmentStart,
144
+ chr,
145
+ base: insertionSequence,
146
+ sampleId,
147
+ isInsertion: true,
148
+ },
149
+ )
150
+ }
151
+ }
152
+ genomicOffset++
153
+ }
154
+ }
@@ -0,0 +1,62 @@
1
+ import { fillRect } from '../util'
2
+ import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
3
+ import { GAP_STROKE_OFFSET } from './types'
4
+
5
+ import type { RenderingContext } from './types'
6
+
7
+ export function renderMatches(
8
+ context: RenderingContext,
9
+ alignment: string,
10
+ seq: string,
11
+ leftPx: number,
12
+ rowTop: number,
13
+ sampleId: number,
14
+ _featureId: string,
15
+ alignmentStart: number,
16
+ chr: string,
17
+ ) {
18
+ if (context.showAllLetters) {
19
+ return
20
+ }
21
+
22
+ const { ctx, scale, h, canvasWidth } = context
23
+ ctx.fillStyle = 'lightgrey'
24
+
25
+ // Highlight matching bases with light grey background
26
+ for (
27
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
28
+ i < seqLength;
29
+ i++
30
+ ) {
31
+ if (seq[i] !== '-') {
32
+ // Only process non-gap positions in reference
33
+ const currentChar = alignment[i]
34
+ const xPos = leftPx + scale * genomicOffset
35
+ if (
36
+ seq[i] === currentChar &&
37
+ currentChar !== '-' &&
38
+ currentChar !== ' '
39
+ ) {
40
+ fillRect(ctx, xPos, rowTop, scale + GAP_STROKE_OFFSET, h, canvasWidth)
41
+
42
+ // Add to spatial index if distance filter allows
43
+ if (shouldAddToSpatialIndex(xPos, context)) {
44
+ addToSpatialIndex(
45
+ context,
46
+ xPos,
47
+ rowTop,
48
+ xPos + context.scale + GAP_STROKE_OFFSET,
49
+ rowTop + context.h,
50
+ {
51
+ pos: genomicOffset + alignmentStart,
52
+ chr,
53
+ base: currentChar || '',
54
+ sampleId,
55
+ },
56
+ )
57
+ }
58
+ }
59
+ genomicOffset++
60
+ }
61
+ }
62
+ }