jbrowse-plugin-mafviewer 1.4.3 → 1.4.5

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.d.ts +14 -0
  3. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js +69 -0
  4. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js.map +1 -0
  5. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +4 -4
  6. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
  7. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js +2 -2
  8. package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -1
  9. package/dist/LinearMafDisplay/components/Sidebar/RectBg.d.ts +1 -1
  10. package/dist/LinearMafDisplay/components/Sidebar/RectBg.js +2 -3
  11. package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -1
  12. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js +81 -11
  13. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -1
  14. package/dist/LinearMafDisplay/components/Sidebar/Tree.js +30 -9
  15. package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -1
  16. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.d.ts +0 -1
  17. package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -1
  18. package/dist/LinearMafDisplay/renderSvg.js +1 -1
  19. package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
  20. package/dist/LinearMafDisplay/stateModel.d.ts +69 -3
  21. package/dist/LinearMafDisplay/stateModel.js +96 -1
  22. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  23. package/dist/LinearMafDisplay/util.d.ts +1 -0
  24. package/dist/LinearMafDisplay/util.js +3 -2
  25. package/dist/LinearMafDisplay/util.js.map +1 -1
  26. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +4 -7
  27. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  28. package/dist/LinearMafRenderer/components/LinearMafRendering.js +27 -9
  29. package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -1
  30. package/dist/LinearMafRenderer/makeImageData.js +6 -7
  31. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  32. package/dist/LinearMafRenderer/rendering/features.d.ts +0 -1
  33. package/dist/LinearMafRenderer/rendering/features.js +1 -14
  34. package/dist/LinearMafRenderer/rendering/features.js.map +1 -1
  35. package/dist/LinearMafRenderer/rendering/insertions.js +8 -6
  36. package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -1
  37. package/dist/LinearMafRenderer/rendering/matches.d.ts +1 -1
  38. package/dist/LinearMafRenderer/rendering/matches.js +3 -15
  39. package/dist/LinearMafRenderer/rendering/matches.js.map +1 -1
  40. package/dist/LinearMafRenderer/rendering/spatialIndex.js +8 -2
  41. package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -1
  42. package/dist/LinearMafRenderer/rendering/text.js +1 -3
  43. package/dist/LinearMafRenderer/rendering/text.js.map +1 -1
  44. package/dist/LinearMafRenderer/rendering/types.d.ts +5 -4
  45. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -7
  46. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  47. package/dist/util/fastaUtils.js +23 -50
  48. package/dist/util/fastaUtils.js.map +1 -1
  49. package/package.json +7 -7
  50. package/src/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.tsx +105 -0
  51. package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +4 -4
  52. package/src/LinearMafDisplay/components/Sidebar/ColorLegend.tsx +2 -6
  53. package/src/LinearMafDisplay/components/Sidebar/RectBg.tsx +8 -3
  54. package/src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx +117 -15
  55. package/src/LinearMafDisplay/components/Sidebar/Tree.tsx +53 -8
  56. package/src/LinearMafDisplay/components/Sidebar/YScaleBars.tsx +0 -1
  57. package/src/LinearMafDisplay/renderSvg.tsx +1 -1
  58. package/src/LinearMafDisplay/stateModel.ts +109 -1
  59. package/src/LinearMafDisplay/util.ts +4 -2
  60. package/src/LinearMafRenderer/LinearMafRenderer.ts +2 -4
  61. package/src/LinearMafRenderer/components/LinearMafRendering.tsx +49 -29
  62. package/src/LinearMafRenderer/makeImageData.ts +5 -14
  63. package/src/LinearMafRenderer/rendering/features.ts +2 -36
  64. package/src/LinearMafRenderer/rendering/insertions.ts +11 -6
  65. package/src/LinearMafRenderer/rendering/matches.ts +2 -27
  66. package/src/LinearMafRenderer/rendering/spatialIndex.ts +9 -2
  67. package/src/LinearMafRenderer/rendering/text.ts +1 -2
  68. package/src/LinearMafRenderer/rendering/types.ts +7 -4
  69. package/src/util/fastaUtils.ts +28 -54
  70. package/dist/LinearMafRenderer/components/util.d.ts +0 -1
  71. package/dist/LinearMafRenderer/components/util.js +0 -13
  72. package/dist/LinearMafRenderer/components/util.js.map +0 -1
  73. package/src/LinearMafRenderer/components/util.ts +0 -13
@@ -26,6 +26,10 @@ const SetRowHeightDialog = lazy(
26
26
  () => import('./components/SetRowHeightDialog/SetRowHeightDialog'),
27
27
  )
28
28
 
29
+ const InsertionSequenceDialog = lazy(
30
+ () => import('./components/InsertionSequenceDialog/InsertionSequenceDialog'),
31
+ )
32
+
29
33
  /**
30
34
  * #stateModel LinearMafDisplay
31
35
  * extends LinearBasicDisplay
@@ -82,6 +86,10 @@ export default function stateModelFactory(
82
86
  * #property
83
87
  */
84
88
  showAsUpperCase: true,
89
+ /**
90
+ * #property
91
+ */
92
+ showSidebar: true,
85
93
  }),
86
94
  )
87
95
  .volatile(() => ({
@@ -101,6 +109,14 @@ export default function stateModelFactory(
101
109
  * #volatile
102
110
  */
103
111
  volatileTree: undefined as any,
112
+ /**
113
+ * #volatile
114
+ */
115
+ highlightedRowNames: undefined as string[] | undefined,
116
+ /**
117
+ * #volatile
118
+ */
119
+ hoveredTreeNode: undefined as { x: number; y: number } | undefined,
104
120
  }))
105
121
  .actions(self => ({
106
122
  /**
@@ -150,6 +166,46 @@ export default function stateModelFactory(
150
166
  setShowAsUpperCase(arg: boolean) {
151
167
  self.showAsUpperCase = arg
152
168
  },
169
+ /**
170
+ * #action
171
+ */
172
+ setTreeAreaWidth(width: number) {
173
+ self.treeAreaWidth = width
174
+ },
175
+ /**
176
+ * #action
177
+ */
178
+ setShowSidebar(arg: boolean) {
179
+ self.showSidebar = arg
180
+ },
181
+ /**
182
+ * #action
183
+ */
184
+ setHighlightedRowNames(
185
+ names?: string[],
186
+ nodePosition?: { x: number; y: number },
187
+ ) {
188
+ self.highlightedRowNames = names
189
+ self.hoveredTreeNode = nodePosition
190
+ },
191
+ /**
192
+ * #action
193
+ */
194
+ showInsertionSequenceDialog(insertionData: {
195
+ sequence: string
196
+ sampleLabel: string
197
+ chr: string
198
+ pos: number
199
+ }) {
200
+ getSession(self).queueDialog(handleClose => [
201
+ InsertionSequenceDialog,
202
+ {
203
+ model: self,
204
+ onClose: handleClose,
205
+ insertionData,
206
+ },
207
+ ])
208
+ },
153
209
  }))
154
210
  .views(self => ({
155
211
  /**
@@ -198,10 +254,16 @@ export default function stateModelFactory(
198
254
  const r = self.root
199
255
  if (r) {
200
256
  const width = self.treeAreaWidth
257
+ // Use totalHeight - rowHeight so leaves are centered in rows
258
+ // (first leaf at rowHeight/2, last at totalHeight - rowHeight/2)
201
259
  const clust = cluster<NodeWithIds>()
202
- .size([this.totalHeight, width])
260
+ .size([this.totalHeight - self.rowHeight, width])
203
261
  .separation(() => 1)
204
262
  clust(r)
263
+ // Offset all nodes by rowHeight/2 to center in rows
264
+ for (const node of r.descendants()) {
265
+ node.x = node.x! + self.rowHeight / 2
266
+ }
205
267
  setBrLength(r, (r.data.length = 0), width / maxLength(r))
206
268
  return r as HierarchyNode<NodeWithIdsAndLength>
207
269
  } else {
@@ -238,6 +300,38 @@ export default function stateModelFactory(
238
300
  get leaves() {
239
301
  return self.root?.leaves()
240
302
  },
303
+ /**
304
+ * #getter
305
+ */
306
+ get leafMap() {
307
+ return new Map(this.leaves?.map(leaf => [leaf.data.name, leaf]))
308
+ },
309
+ /**
310
+ * #getter
311
+ * Precomputed map from hierarchy node to its descendant leaf names
312
+ */
313
+ get nodeDescendantNames() {
314
+ const map = new Map<unknown, string[]>()
315
+ function computeDescendants(
316
+ node: HierarchyNode<NodeWithIdsAndLength>,
317
+ ): string[] {
318
+ if (!node.children || node.children.length === 0) {
319
+ const names = [node.data.name]
320
+ map.set(node, names)
321
+ return names
322
+ }
323
+ const names: string[] = []
324
+ for (const child of node.children) {
325
+ names.push(...computeDescendants(child))
326
+ }
327
+ map.set(node, names)
328
+ return names
329
+ }
330
+ if (this.hierarchy) {
331
+ computeDescendants(this.hierarchy)
332
+ }
333
+ return map
334
+ },
241
335
  /**
242
336
  * #getter
243
337
  */
@@ -348,6 +442,14 @@ export default function stateModelFactory(
348
442
  self.setMismatchRendering(!self.mismatchRendering)
349
443
  },
350
444
  },
445
+ {
446
+ label: 'Show sidebar',
447
+ type: 'checkbox',
448
+ checked: self.showSidebar,
449
+ onClick: () => {
450
+ self.setShowSidebar(!self.showSidebar)
451
+ },
452
+ },
351
453
  ]
352
454
  },
353
455
  }
@@ -377,6 +479,12 @@ export default function stateModelFactory(
377
479
  0,
378
480
  )
379
481
  },
482
+ /**
483
+ * #getter
484
+ */
485
+ get sidebarWidth() {
486
+ return self.showSidebar ? this.labelWidth + 5 + self.treeWidth : 0
487
+ },
380
488
  }))
381
489
  .actions(self => ({
382
490
  afterCreate() {
@@ -6,6 +6,7 @@ import type { HierarchyNode } from 'd3-hierarchy'
6
6
 
7
7
  export interface HoveredInfo {
8
8
  sampleId: string
9
+ sampleLabel: string
9
10
  pos: number
10
11
  base: string
11
12
  chr: string
@@ -44,15 +45,16 @@ export function generateTooltipContent(
44
45
  contentLines.push(`Ref: ${p2.refName}:${toLocale(p2.coord)}`)
45
46
 
46
47
  if (hoveredInfo) {
47
- const { base, sampleId, pos, chr } = hoveredInfo
48
+ const { base, sampleLabel, pos, chr, isInsertion } = hoveredInfo
48
49
  const thresh = 20
49
50
  const len = base.length
50
51
  const lengthSuffix = len > 1 ? ` ${len}bp` : ''
51
52
  const baseDisplay =
52
53
  base.length > thresh ? base.slice(0, thresh) + '...' : base
54
+ const insertionLabel = isInsertion ? ' Insertion' : ''
53
55
 
54
56
  contentLines.push(
55
- `Alt ${sampleId}: ${chr}:${pos.toLocaleString('en-US')} (${baseDisplay}${lengthSuffix})`,
57
+ `Alt ${sampleLabel}: ${chr}:${pos.toLocaleString('en-US')} (${baseDisplay}${lengthSuffix}${insertionLabel})`,
56
58
  )
57
59
  }
58
60
  }
@@ -8,10 +8,8 @@ import {
8
8
 
9
9
  import { makeImageData } from './makeImageData'
10
10
 
11
- interface Sample {
12
- id: string
13
- color?: string
14
- }
11
+ import type { Sample } from '../LinearMafDisplay/types'
12
+
15
13
  interface RenderArgs extends RenderArgsDeserialized {
16
14
  samples: Sample[]
17
15
  rowHeight: number
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useRef } from 'react'
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react'
2
2
 
3
3
  import { PrerenderedCanvas } from '@jbrowse/core/ui'
4
4
  import Flatbush from 'flatbush'
@@ -19,48 +19,68 @@ const LinearMafRendering = observer(function (props: {
19
19
  }) {
20
20
  const { items, displayModel, height, samples, flatbush } = props
21
21
  const ref = useRef<HTMLDivElement>(null)
22
- const rbush2 = useMemo(() => Flatbush.from(flatbush), [flatbush])
22
+ const flatbush2 = useMemo(() => Flatbush.from(flatbush), [flatbush])
23
+ const [isOverLargeInsertion, setIsOverLargeInsertion] = useState(false)
23
24
 
24
- function getFeatureUnderMouse(eventClientX: number, eventClientY: number) {
25
- let offsetX = 0
26
- let offsetY = 0
27
- if (ref.current) {
28
- const r = ref.current.getBoundingClientRect()
29
- offsetX = eventClientX - r.left
30
- offsetY = eventClientY - r.top
31
- }
25
+ const getFeatureUnderMouse = useCallback(
26
+ (eventClientX: number, eventClientY: number) => {
27
+ let offsetX = 0
28
+ let offsetY = 0
29
+ if (ref.current) {
30
+ const r = ref.current.getBoundingClientRect()
31
+ offsetX = eventClientX - r.left
32
+ offsetY = eventClientY - r.top
33
+ }
32
34
 
33
- const x = rbush2.search(offsetX, offsetY, offsetX + 1, offsetY + 1)
34
- if (x.length) {
35
- const elt = x.find(idx => items[idx]?.isInsertion)
36
- const r = elt !== undefined ? items[elt]! : items[x[0]!]!
37
- const s = samples[r.sampleId]
38
- return {
39
- ...r,
40
- sampleId: s?.label || s?.id || 'unknown',
35
+ const x = flatbush2.search(offsetX, offsetY, offsetX + 1, offsetY + 1)
36
+ if (x.length) {
37
+ const elt = x.find(idx => items[idx]?.isInsertion)
38
+ const r = elt !== undefined ? items[elt]! : items[x[0]!]!
39
+ const s = samples[r.sampleId]
40
+ return {
41
+ ...r,
42
+ sampleId: s?.id ?? 'unknown',
43
+ sampleLabel: s?.label || s?.id || 'unknown',
44
+ }
45
+ } else {
46
+ return undefined
41
47
  }
42
- } else {
43
- return undefined
44
- }
45
- }
48
+ },
49
+ [flatbush2, items, samples],
50
+ )
51
+
46
52
  return (
47
53
  <div
48
54
  ref={ref}
49
- onMouseMove={e =>
50
- displayModel.setHoveredInfo?.(
51
- getFeatureUnderMouse(e.clientX, e.clientY),
55
+ onClick={e => {
56
+ const feature = getFeatureUnderMouse(e.clientX, e.clientY)
57
+ if (feature?.isLargeInsertion) {
58
+ displayModel.showInsertionSequenceDialog?.({
59
+ sequence: feature.base,
60
+ sampleLabel: feature.sampleLabel,
61
+ chr: feature.chr,
62
+ pos: feature.pos,
63
+ })
64
+ }
65
+ }}
66
+ onMouseMove={e => {
67
+ const feature = getFeatureUnderMouse(e.clientX, e.clientY)
68
+ displayModel.setHoveredInfo?.(feature)
69
+ displayModel.setHighlightedRowNames?.(
70
+ feature?.sampleId ? [feature.sampleId] : undefined,
52
71
  )
53
- }
54
- onMouseLeave={() => {
55
- displayModel.setHoveredInfo?.(undefined)
72
+ setIsOverLargeInsertion(!!feature?.isLargeInsertion)
56
73
  }}
57
- onMouseOut={() => {
74
+ onMouseLeave={() => {
58
75
  displayModel.setHoveredInfo?.(undefined)
76
+ displayModel.setHighlightedRowNames?.(undefined)
77
+ setIsOverLargeInsertion(false)
59
78
  }}
60
79
  style={{
61
80
  overflow: 'visible',
62
81
  position: 'relative',
63
82
  height,
83
+ cursor: isOverLargeInsertion ? 'pointer' : 'default',
64
84
  }}
65
85
  >
66
86
  <PrerenderedCanvas
@@ -8,9 +8,8 @@ import {
8
8
  RenderingContext,
9
9
  Sample,
10
10
  processFeatureAlignment,
11
- processFeatureInsertions,
12
11
  } from './rendering'
13
- import { getColorBaseMap, getContrastBaseMap } from './util'
12
+ import { getCharWidthHeight, getColorBaseMap, getContrastBaseMap } from './util'
14
13
 
15
14
  interface RenderArgs extends RenderArgsDeserialized {
16
15
  samples: Sample[]
@@ -53,12 +52,14 @@ export function makeImageData({
53
52
  const scale = 1 / bpPerPx
54
53
  const hp2 = h / 2
55
54
  const offset = (rowHeight - h) / 2
55
+ const { charWidth, charHeight } = getCharWidthHeight()
56
56
 
57
57
  ctx.font = FONT_CONFIG
58
58
 
59
59
  const renderingContext: RenderingContext = {
60
60
  ctx,
61
61
  scale,
62
+ bpPerPx,
62
63
  canvasWidth,
63
64
  rowHeight,
64
65
  h,
@@ -69,12 +70,13 @@ export function makeImageData({
69
70
  showAllLetters,
70
71
  mismatchRendering,
71
72
  showAsUpperCase,
73
+ charWidth,
74
+ charHeight,
72
75
  spatialIndex: [],
73
76
  spatialIndexCoords: [],
74
77
  lastInsertedX: -Infinity, // Start with -Infinity so first item is always inserted
75
78
  }
76
79
 
77
- // First pass: render alignments (gaps, matches, mismatches, text)
78
80
  for (const feature of features.values()) {
79
81
  processFeatureAlignment(
80
82
  feature,
@@ -84,17 +86,6 @@ export function makeImageData({
84
86
  renderingContext,
85
87
  )
86
88
  }
87
-
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
- }
98
89
  const flatbush = new Flatbush(renderingContext.spatialIndex.length || 1)
99
90
  if (renderingContext.spatialIndex.length === 0) {
100
91
  flatbush.add(0, 0, 1, 1)
@@ -20,7 +20,7 @@ export function processFeatureAlignment(
20
20
  string,
21
21
  AlignmentRecord
22
22
  >
23
- const referenceSeq = feature.get('seq').toLowerCase()
23
+ const referenceSeq = (feature.get('seq') as string).toLowerCase()
24
24
 
25
25
  for (const [sampleId, alignmentData] of Object.entries(alignments)) {
26
26
  const row = sampleToRowMap.get(sampleId)
@@ -33,16 +33,7 @@ export function processFeatureAlignment(
33
33
  const rowTop = renderingContext.offset + renderingContext.rowHeight * row
34
34
 
35
35
  renderGaps(renderingContext, alignment, referenceSeq, leftPx, rowTop)
36
- renderMatches(
37
- renderingContext,
38
- alignment,
39
- referenceSeq,
40
- leftPx,
41
- rowTop,
42
- row,
43
- alignmentData.start,
44
- alignmentData.chr,
45
- )
36
+ renderMatches(renderingContext, alignment, referenceSeq, leftPx, rowTop)
46
37
  renderMismatches(
47
38
  renderingContext,
48
39
  alignment,
@@ -61,31 +52,6 @@ export function processFeatureAlignment(
61
52
  leftPx,
62
53
  rowTop,
63
54
  )
64
- }
65
- }
66
-
67
- export function processFeatureInsertions(
68
- feature: Feature,
69
- region: GenomicRegion,
70
- bpPerPx: number,
71
- sampleToRowMap: Map<string, number>,
72
- renderingContext: RenderingContext,
73
- ) {
74
- const [leftPx] = featureSpanPx(feature, region, bpPerPx)
75
- const alignments = feature.get('alignments') as Record<
76
- string,
77
- AlignmentRecord
78
- >
79
- const referenceSeq = feature.get('seq').toLowerCase()
80
- for (const [sampleId, alignmentData] of Object.entries(alignments)) {
81
- const row = sampleToRowMap.get(sampleId)
82
- if (row === undefined) {
83
- continue
84
- }
85
-
86
- const alignment = alignmentData.seq.toLowerCase()
87
- const rowTop = renderingContext.offset + renderingContext.rowHeight * row
88
-
89
55
  renderInsertions(
90
56
  renderingContext,
91
57
  alignment,
@@ -1,6 +1,6 @@
1
1
  import { measureText } from '@jbrowse/core/util'
2
2
 
3
- import { fillRect, getCharWidthHeight } from '../util'
3
+ import { fillRect } from '../util'
4
4
  import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
5
5
  import {
6
6
  CHAR_SIZE_WIDTH,
@@ -27,8 +27,7 @@ export function renderInsertions(
27
27
  alignmentStart: number,
28
28
  chr: string,
29
29
  ) {
30
- const { ctx, scale, h, canvasWidth, rowHeight } = context
31
- const { charHeight } = getCharWidthHeight()
30
+ const { ctx, scale, h, canvasWidth, rowHeight, charHeight } = context
32
31
 
33
32
  for (
34
33
  let i = 0, genomicOffset = 0, seqLength = alignment.length;
@@ -102,14 +101,17 @@ export function renderInsertions(
102
101
  actualXPos = xPos
103
102
  actualWidth = INSERTION_LINE_WIDTH
104
103
  fillRect(ctx, actualXPos, rowTop, actualWidth, h, canvasWidth, 'purple')
104
+
105
+ // Always use a wider hit box for spatial index, even if visual is 1px
106
+ const hitBoxPadding = 2
107
+ actualXPos = xPos - hitBoxPadding
108
+ actualWidth = INSERTION_LINE_WIDTH + 2 * hitBoxPadding
109
+
105
110
  if (
106
111
  bpPerPx < HIGH_ZOOM_THRESHOLD &&
107
112
  rowHeight > MIN_ROW_HEIGHT_FOR_BORDERS
108
113
  ) {
109
114
  // Add horizontal borders for visibility at high zoom
110
- // Note: borders extend the effective clickable area
111
- actualXPos = xPos - INSERTION_BORDER_WIDTH
112
- actualWidth = INSERTION_BORDER_HEIGHT
113
115
  fillRect(
114
116
  ctx,
115
117
  xPos - INSERTION_BORDER_WIDTH,
@@ -131,6 +133,8 @@ export function renderInsertions(
131
133
 
132
134
  // Add insertion to spatial index with actual rendered dimensions
133
135
  // Insertions always bypass distance filter
136
+ const isLargeInsertion =
137
+ insertionSequence.length > LARGE_INSERTION_THRESHOLD
134
138
  if (shouldAddToSpatialIndex(actualXPos, context, true)) {
135
139
  addToSpatialIndex(
136
140
  context,
@@ -144,6 +148,7 @@ export function renderInsertions(
144
148
  base: insertionSequence,
145
149
  sampleId,
146
150
  isInsertion: true,
151
+ isLargeInsertion,
147
152
  },
148
153
  )
149
154
  }
@@ -1,5 +1,4 @@
1
1
  import { fillRect } from '../util'
2
- import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
3
2
  import { GAP_STROKE_OFFSET } from './types'
4
3
 
5
4
  import type { RenderingContext } from './types'
@@ -10,9 +9,6 @@ export function renderMatches(
10
9
  seq: string,
11
10
  leftPx: number,
12
11
  rowTop: number,
13
- sampleId: number,
14
- alignmentStart: number,
15
- chr: string,
16
12
  ) {
17
13
  if (context.showAllLetters) {
18
14
  return
@@ -30,30 +26,9 @@ export function renderMatches(
30
26
  if (seq[i] !== '-') {
31
27
  // Only process non-gap positions in reference
32
28
  const currentChar = alignment[i]
33
- const xPos = leftPx + scale * genomicOffset
34
- if (
35
- seq[i] === currentChar &&
36
- currentChar !== '-' &&
37
- currentChar !== ' '
38
- ) {
29
+ if (seq[i] === currentChar && currentChar !== ' ') {
30
+ const xPos = leftPx + scale * genomicOffset
39
31
  fillRect(ctx, xPos, rowTop, scale + GAP_STROKE_OFFSET, h, canvasWidth)
40
-
41
- // Add to spatial index if distance filter allows
42
- if (shouldAddToSpatialIndex(xPos, context)) {
43
- addToSpatialIndex(
44
- context,
45
- xPos,
46
- rowTop,
47
- xPos + context.scale + GAP_STROKE_OFFSET,
48
- rowTop + context.h,
49
- {
50
- pos: genomicOffset + alignmentStart,
51
- chr,
52
- base: currentChar || '',
53
- sampleId,
54
- },
55
- )
56
- }
57
32
  }
58
33
  genomicOffset++
59
34
  }
@@ -20,9 +20,16 @@ export function shouldAddToSpatialIndex(
20
20
  context: RenderingContext,
21
21
  bypassDistanceFilter = false,
22
22
  ): boolean {
23
+ if (bypassDistanceFilter) {
24
+ return true
25
+ }
26
+
27
+ // Zoom-aware distance threshold: scale threshold based on zoom level
28
+ // At high zoom (small bpPerPx), use smaller threshold for more precision
29
+ // At low zoom (large bpPerPx), use larger threshold to reduce index size
23
30
  return (
24
- bypassDistanceFilter ||
25
- Math.abs(xPos - context.lastInsertedX) > MIN_X_DISTANCE
31
+ Math.abs(xPos - context.lastInsertedX) >
32
+ MIN_X_DISTANCE * Math.max(1, context.bpPerPx)
26
33
  )
27
34
  }
28
35
 
@@ -1,4 +1,3 @@
1
- import { getCharWidthHeight } from '../util'
2
1
  import { CHAR_SIZE_WIDTH, VERTICAL_TEXT_OFFSET } from './types'
3
2
 
4
3
  import type { RenderingContext } from './types'
@@ -34,8 +33,8 @@ export function renderText(
34
33
  mismatchRendering,
35
34
  contrastForBase,
36
35
  showAsUpperCase,
36
+ charHeight,
37
37
  } = context
38
- const { charHeight } = getCharWidthHeight()
39
38
 
40
39
  // Render text labels when zoomed in enough and row is tall enough
41
40
  if (scale >= CHAR_SIZE_WIDTH) {
@@ -13,10 +13,7 @@ export const HIGH_BP_PER_PX_THRESHOLD = 10
13
13
  export const INSERTION_BORDER_HEIGHT = 5
14
14
  export const MIN_X_DISTANCE = 0.5
15
15
 
16
- export interface Sample {
17
- id: string
18
- color?: string
19
- }
16
+ export type { Sample } from '../../LinearMafDisplay/types'
20
17
 
21
18
  export interface GenomicRegion {
22
19
  start: number
@@ -30,6 +27,7 @@ export interface RenderedBase {
30
27
  base: string
31
28
  sampleId: number
32
29
  isInsertion?: boolean
30
+ isLargeInsertion?: boolean
33
31
  }
34
32
 
35
33
  /**
@@ -38,6 +36,7 @@ export interface RenderedBase {
38
36
  export interface RenderingContext {
39
37
  ctx: CanvasRenderingContext2D
40
38
  scale: number
39
+ bpPerPx: number
41
40
  canvasWidth: number
42
41
  rowHeight: number
43
42
  h: number
@@ -49,6 +48,10 @@ export interface RenderingContext {
49
48
  mismatchRendering: boolean
50
49
  showAsUpperCase: boolean
51
50
 
51
+ // Cached char dimensions
52
+ charWidth: number
53
+ charHeight: number
54
+
52
55
  // RBush spatial index for efficient spatial queries
53
56
  spatialIndex: RenderedBase[]
54
57
  spatialIndexCoords: number[]