jbrowse-plugin-mafviewer 1.3.1 → 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 +49 -28
  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
@@ -0,0 +1,79 @@
1
+ import { fillRect } from '../util'
2
+ import {
3
+ addToSpatialIndex,
4
+ createRenderedBase,
5
+ shouldAddToSpatialIndex,
6
+ } from './spatialIndex'
7
+ import { GAP_STROKE_OFFSET } from './types'
8
+
9
+ import type { RenderingContext } from './types'
10
+
11
+ /**
12
+ * Renders background rectangles for positions where alignment matches reference
13
+ * Only renders when showAllLetters is false
14
+ * @param context - Rendering context with canvas and styling info
15
+ * @param alignment - The aligned sequence for this sample
16
+ * @param seq - The reference sequence
17
+ * @param leftPx - Left pixel position of the feature
18
+ * @param rowTop - Top pixel position of the row
19
+ * @param alignmentStart - Start position of the alignment
20
+ * @param chr - Chromosome/sequence name
21
+ */
22
+ export function renderMatches(
23
+ context: RenderingContext,
24
+ alignment: string,
25
+ seq: string,
26
+ leftPx: number,
27
+ rowTop: number,
28
+ sampleId: string,
29
+ featureId: string,
30
+ alignmentStart: number,
31
+ chr: string,
32
+ ) {
33
+ if (context.showAllLetters) {
34
+ return
35
+ }
36
+
37
+ const { ctx, scale, h, canvasWidth } = context
38
+ ctx.fillStyle = 'lightgrey'
39
+
40
+ // Highlight matching bases with light grey background
41
+ for (
42
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
43
+ i < seqLength;
44
+ i++
45
+ ) {
46
+ if (seq[i] !== '-') {
47
+ // Only process non-gap positions in reference
48
+ const currentChar = alignment[i]
49
+ const xPos = leftPx + scale * genomicOffset
50
+ if (
51
+ seq[i] === currentChar &&
52
+ currentChar !== '-' &&
53
+ currentChar !== ' '
54
+ ) {
55
+ fillRect(ctx, xPos, rowTop, scale + GAP_STROKE_OFFSET, h, canvasWidth)
56
+
57
+ // Add to spatial index if distance filter allows
58
+ if (shouldAddToSpatialIndex(xPos, context)) {
59
+ const renderedBase = createRenderedBase(
60
+ xPos,
61
+ rowTop,
62
+ context,
63
+ genomicOffset + alignmentStart,
64
+ chr,
65
+ sampleId,
66
+ currentChar || '',
67
+ true,
68
+ false,
69
+ false,
70
+ false,
71
+ featureId,
72
+ )
73
+ addToSpatialIndex(context, renderedBase)
74
+ }
75
+ }
76
+ genomicOffset++
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,125 @@
1
+ import { fillRect } from '../util'
2
+ import {
3
+ addToSpatialIndex,
4
+ createRenderedBase,
5
+ shouldAddToSpatialIndex,
6
+ } from './spatialIndex'
7
+ import { GAP_STROKE_OFFSET } from './types'
8
+
9
+ import type { RenderingContext } from './types'
10
+
11
+ /**
12
+ * Renders colored rectangles for mismatches and matches (when showAllLetters is true)
13
+ * Colors are determined by base type when mismatchRendering is enabled
14
+ * @param context - Rendering context with canvas and styling info
15
+ * @param alignment - The aligned sequence for this sample
16
+ * @param seq - The reference sequence
17
+ * @param leftPx - Left pixel position of the feature
18
+ * @param rowTop - Top pixel position of the row
19
+ * @param alignmentStart - Start position of the alignment
20
+ * @param chr - Chromosome/sequence name
21
+ */
22
+ export function renderMismatches(
23
+ context: RenderingContext,
24
+ alignment: string,
25
+ seq: string,
26
+ leftPx: number,
27
+ rowTop: number,
28
+ sampleId: string,
29
+ featureId: string,
30
+ alignmentStart: number,
31
+ chr: string,
32
+ ) {
33
+ const {
34
+ ctx,
35
+ scale,
36
+ h,
37
+ canvasWidth,
38
+ showAllLetters,
39
+ mismatchRendering,
40
+ colorForBase,
41
+ } = context
42
+
43
+ for (
44
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
45
+ i < seqLength;
46
+ i++
47
+ ) {
48
+ const currentChar = alignment[i]
49
+ if (seq[i] !== '-') {
50
+ if (currentChar !== '-') {
51
+ const xPos = leftPx + scale * genomicOffset
52
+ if (seq[i] !== currentChar && currentChar !== ' ') {
53
+ // Mismatch: use base-specific color or orange
54
+ fillRect(
55
+ ctx,
56
+ xPos,
57
+ rowTop,
58
+ scale + GAP_STROKE_OFFSET,
59
+ h,
60
+ canvasWidth,
61
+ mismatchRendering
62
+ ? (colorForBase[currentChar!] ?? 'black')
63
+ : 'orange',
64
+ )
65
+
66
+ // Add to spatial index if distance filter allows
67
+ if (shouldAddToSpatialIndex(xPos, context)) {
68
+ addToSpatialIndex(
69
+ context,
70
+ createRenderedBase(
71
+ xPos,
72
+ rowTop,
73
+ context,
74
+ genomicOffset + alignmentStart,
75
+ chr,
76
+ sampleId,
77
+ currentChar!,
78
+ false,
79
+ true,
80
+ false,
81
+ false,
82
+ featureId,
83
+ ),
84
+ )
85
+ }
86
+ } else if (showAllLetters) {
87
+ // Match (when showing all letters): use base-specific color or light blue
88
+ fillRect(
89
+ ctx,
90
+ xPos,
91
+ rowTop,
92
+ scale + GAP_STROKE_OFFSET,
93
+ h,
94
+ canvasWidth,
95
+ mismatchRendering
96
+ ? (colorForBase[currentChar!] ?? 'black')
97
+ : 'lightblue',
98
+ )
99
+
100
+ // Add to spatial index if distance filter allows
101
+ if (shouldAddToSpatialIndex(xPos, context)) {
102
+ addToSpatialIndex(
103
+ context,
104
+ createRenderedBase(
105
+ xPos,
106
+ rowTop,
107
+ context,
108
+ genomicOffset + alignmentStart,
109
+ chr,
110
+ sampleId,
111
+ currentChar!,
112
+ true,
113
+ false,
114
+ false,
115
+ false,
116
+ featureId,
117
+ ),
118
+ )
119
+ }
120
+ }
121
+ }
122
+ genomicOffset++
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,136 @@
1
+ import { GAP_STROKE_OFFSET, MIN_X_DISTANCE } from './types'
2
+
3
+ import type { RenderedBase, RenderingContext } from './types'
4
+
5
+ /**
6
+ * Creates a RenderedBase object for spatial indexing
7
+ * @param xPos - X coordinate of the base
8
+ * @param rowTop - Y coordinate of the row top
9
+ * @param context - Rendering context with dimensions
10
+ * @param pos - Genomic coordinate
11
+ * @param chr - Chromosome/sequence name
12
+ * @param sampleId - Sample identifier
13
+ * @param base - The base character
14
+ * @param isMatch - Whether this base matches the reference
15
+ * @param isMismatch - Whether this base is a mismatch
16
+ * @param isGap - Whether this is a gap
17
+ * @param isInsertion - Whether this is an insertion
18
+ * @param featureId - Feature identifier
19
+ */
20
+ export function createRenderedBase(
21
+ xPos: number,
22
+ rowTop: number,
23
+ context: RenderingContext,
24
+ pos: number,
25
+ chr: string,
26
+ sampleId: string,
27
+ base: string,
28
+ isMatch: boolean,
29
+ isMismatch: boolean,
30
+ isGap: boolean,
31
+ isInsertion: boolean,
32
+ featureId: string,
33
+ ): RenderedBase {
34
+ return {
35
+ minX: xPos,
36
+ minY: rowTop,
37
+ maxX: xPos + context.scale + GAP_STROKE_OFFSET,
38
+ maxY: rowTop + context.h,
39
+ pos,
40
+ chr,
41
+ sampleId,
42
+ base,
43
+ isMatch,
44
+ isMismatch,
45
+ isGap,
46
+ isInsertion,
47
+ featureId,
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Creates a RenderedBase object for insertions with custom width
53
+ * Uses the actual rendered width instead of the standard scale-based width
54
+ * This ensures accurate spatial queries for different insertion rendering types:
55
+ * - Small insertions: INSERTION_LINE_WIDTH (1px) or INSERTION_BORDER_HEIGHT (5px) with borders
56
+ * - Large insertions (text): measured text width + padding
57
+ * - Large insertions (line): INSERTION_BORDER_WIDTH (2px)
58
+ *
59
+ * @param xPos - X coordinate of the insertion
60
+ * @param rowTop - Y coordinate of the row top
61
+ * @param width - Actual rendered width of the insertion
62
+ * @param context - Rendering context with dimensions
63
+ * @param pos - Genomic coordinate
64
+ * @param chr - Chromosome/sequence name
65
+ * @param sampleId - Sample identifier
66
+ * @param insertionSequence - The insertion sequence
67
+ * @param featureId - Feature identifier
68
+ */
69
+ export function createRenderedInsertion(
70
+ xPos: number,
71
+ rowTop: number,
72
+ width: number,
73
+ context: RenderingContext,
74
+ pos: number,
75
+ chr: string,
76
+ sampleId: string,
77
+ insertionSequence: string,
78
+ featureId: string,
79
+ ): RenderedBase {
80
+ return {
81
+ minX: xPos,
82
+ minY: rowTop,
83
+ maxX: xPos + width,
84
+ maxY: rowTop + context.h,
85
+ pos,
86
+ chr,
87
+ sampleId,
88
+ base: insertionSequence,
89
+ isMatch: false,
90
+ isMismatch: false,
91
+ isGap: false,
92
+ isInsertion: true,
93
+ featureId,
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Checks if an item should be added to the spatial index based on distance filtering
99
+ * Only returns true if the X position is >0.5px away from the last inserted item
100
+ * This reduces spatial index density while maintaining useful spatial queries
101
+ *
102
+ * @param xPos - X position to check
103
+ * @param context - Rendering context with lastInsertedX tracking
104
+ * @param bypassDistanceFilter - If true, always return true (e.g., for insertions)
105
+ * @returns Whether the item should be added to spatial index
106
+ *
107
+ * @example
108
+ * // Items at X positions: 100.0, 100.3, 100.8, 101.5
109
+ * // Only items at 100.0, 100.8, 101.5 would return true (>0.5px apart)
110
+ * // Unless bypassDistanceFilter=true, then all would return true
111
+ */
112
+ export function shouldAddToSpatialIndex(
113
+ xPos: number,
114
+ context: RenderingContext,
115
+ bypassDistanceFilter = false,
116
+ ): boolean {
117
+ return (
118
+ bypassDistanceFilter ||
119
+ Math.abs(xPos - context.lastInsertedX) > MIN_X_DISTANCE
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Adds a rendered base directly to the RBush spatial index
125
+ * Updates the lastInsertedX tracking for distance filtering
126
+ *
127
+ * @param context - Rendering context with spatial index
128
+ * @param renderedBase - The base to add to the spatial index
129
+ */
130
+ export function addToSpatialIndex(
131
+ context: RenderingContext,
132
+ renderedBase: RenderedBase,
133
+ ) {
134
+ context.spatialIndex.insert(renderedBase)
135
+ context.lastInsertedX = renderedBase.minX
136
+ }
@@ -0,0 +1,72 @@
1
+ import { getCharWidthHeight } from '../util'
2
+ import { CHAR_SIZE_WIDTH, VERTICAL_TEXT_OFFSET } from './types'
3
+
4
+ import type { RenderingContext } from './types'
5
+
6
+ function getLetter(a: string, showAsUpperCase: boolean) {
7
+ return showAsUpperCase ? a.toUpperCase() : a
8
+ }
9
+
10
+ /**
11
+ * Renders text labels for bases when zoom level is sufficient
12
+ * Only shows text for mismatches (or all letters when showAllLetters is true)
13
+ * @param context - Rendering context with canvas and styling info
14
+ * @param alignment - The aligned sequence for this sample (lowercase)
15
+ * @param origAlignment - Original alignment preserving case
16
+ * @param seq - The reference sequence
17
+ * @param leftPx - Left pixel position of the feature
18
+ * @param rowTop - Top pixel position of the row
19
+ */
20
+ export function renderText(
21
+ context: RenderingContext,
22
+ alignment: string,
23
+ origAlignment: string,
24
+ seq: string,
25
+ leftPx: number,
26
+ rowTop: number,
27
+ _sampleId: string,
28
+ _featureId: string,
29
+ ) {
30
+ const {
31
+ ctx,
32
+ scale,
33
+ hp2,
34
+ rowHeight,
35
+ showAllLetters,
36
+ mismatchRendering,
37
+ contrastForBase,
38
+ showAsUpperCase,
39
+ } = context
40
+ const { charHeight } = getCharWidthHeight()
41
+
42
+ // Render text labels when zoomed in enough and row is tall enough
43
+ if (scale >= CHAR_SIZE_WIDTH) {
44
+ for (
45
+ let i = 0, genomicOffset = 0, seqLength = alignment.length;
46
+ i < seqLength;
47
+ i++
48
+ ) {
49
+ if (seq[i] !== '-') {
50
+ // Only process non-gap positions in reference
51
+ const xPos = leftPx + scale * genomicOffset
52
+ const textOffset = (scale - CHAR_SIZE_WIDTH) / 2 + 1 // Center text in available space
53
+ const currentChar = alignment[i]!
54
+ // Show text for mismatches or all letters (depending on setting)
55
+ if ((showAllLetters || seq[i] !== currentChar) && currentChar !== '-') {
56
+ ctx.fillStyle = mismatchRendering
57
+ ? (contrastForBase[currentChar] ?? 'white') // Use contrasting color for readability
58
+ : 'black'
59
+ if (rowHeight > charHeight) {
60
+ // Only render if row is tall enough
61
+ ctx.fillText(
62
+ getLetter(origAlignment[i] || '', showAsUpperCase),
63
+ xPos + textOffset,
64
+ hp2 + rowTop + VERTICAL_TEXT_OFFSET,
65
+ )
66
+ }
67
+ }
68
+ genomicOffset++
69
+ }
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,81 @@
1
+ import RBush from 'rbush'
2
+
3
+ // Rendering constants
4
+ export const FONT_CONFIG = 'bold 10px Courier New,monospace'
5
+ export const CHAR_SIZE_WIDTH = 10
6
+ export const GAP_STROKE_OFFSET = 0.4
7
+ export const INSERTION_LINE_WIDTH = 1
8
+ export const INSERTION_BORDER_WIDTH = 2
9
+ export const INSERTION_PADDING = 2
10
+ export const VERTICAL_TEXT_OFFSET = 3
11
+ export const LARGE_INSERTION_THRESHOLD = 10
12
+ export const HIGH_ZOOM_THRESHOLD = 0.2
13
+ export const MIN_ROW_HEIGHT_FOR_BORDERS = 5
14
+ export const HIGH_BP_PER_PX_THRESHOLD = 10
15
+ export const INSERTION_BORDER_HEIGHT = 5
16
+ export const MIN_X_DISTANCE = 0.5
17
+
18
+ export interface Sample {
19
+ id: string
20
+ color?: string
21
+ }
22
+
23
+ export interface GenomicRegion {
24
+ start: number
25
+ end: number
26
+ refName: string
27
+ }
28
+
29
+ /**
30
+ * Represents a rendered letter/base with its spatial and genomic coordinates
31
+ * This structure is designed for insertion into an RBush spatial index
32
+ */
33
+ export interface RenderedBase {
34
+ // Spatial bounding box (required by RBush)
35
+ minX: number
36
+ minY: number
37
+ maxX: number
38
+ maxY: number
39
+ // Genomic information
40
+ pos: number
41
+ chr: string
42
+ sampleId: string
43
+ base: string
44
+ isMatch: boolean
45
+ isMismatch: boolean
46
+ isGap: boolean
47
+ isInsertion: boolean
48
+ // Feature reference
49
+ featureId: string
50
+ }
51
+
52
+ /**
53
+ * Shared rendering context containing all necessary parameters for rendering operations
54
+ */
55
+ export interface RenderingContext {
56
+ ctx: CanvasRenderingContext2D
57
+ scale: number
58
+ canvasWidth: number
59
+ rowHeight: number
60
+ h: number
61
+ hp2: number
62
+ offset: number
63
+ colorForBase: Record<string, string>
64
+ contrastForBase: Record<string, string>
65
+ showAllLetters: boolean
66
+ mismatchRendering: boolean
67
+ showAsUpperCase: boolean
68
+
69
+ // RBush spatial index for efficient spatial queries
70
+ spatialIndex: RBush<RenderedBase>
71
+
72
+ // Track last X position for spatial index optimization
73
+ lastInsertedX: number
74
+ }
75
+
76
+ export interface AlignmentRecord {
77
+ seq: string
78
+ start: number
79
+ strand: number
80
+ chr: string
81
+ }
@@ -81,38 +81,93 @@ export default class MafTabixAdapter extends BaseFeatureDataAdapter {
81
81
  await updateStatus('Processing alignments', statusCallback, () => {
82
82
  let firstAssemblyNameFound = ''
83
83
  const refAssemblyName = this.getConf('refAssemblyName')
84
+
84
85
  for (const feature of features) {
85
86
  const data = (feature.get('field5') as string).split(',')
86
87
  const alignments = {} as Record<string, OrganismRecord>
88
+ const dataLength = data.length
87
89
 
88
- const len = data.length
89
- for (let j = 0; j < len; j++) {
90
+ for (let j = 0; j < dataLength; j++) {
90
91
  const elt = data[j]!
91
- const seq = elt.split(':')[5]!
92
- const ad = elt.split(':')
93
- const ag = ad[0]!.split('.')
94
- const [n1, n2 = '', ...rest] = ag
95
- let assemblyName
96
- let last = ''
97
- if (ag.length === 2) {
98
- assemblyName = n1
99
- last = n2!
100
- } else if (!Number.isNaN(+n2)) {
101
- assemblyName = `${n1}.${n2}`
102
- last = rest.join('.')
92
+ // Cache split result to avoid redundant operations
93
+ const parts = elt.split(':')
94
+
95
+ // Use destructuring for better performance than multiple array access
96
+ const [
97
+ assemblyAndChr,
98
+ startStr,
99
+ srcSizeStr,
100
+ strandStr,
101
+ unknownStr,
102
+ seq,
103
+ ] = parts
104
+
105
+ // Skip if we don't have all required parts
106
+ if (!assemblyAndChr || !seq) {
107
+ continue
108
+ }
109
+
110
+ // Optimized assembly name parsing with simplified logic
111
+ let assemblyName: string
112
+ let chr: string
113
+
114
+ const firstDotIndex = assemblyAndChr.indexOf('.')
115
+ if (firstDotIndex === -1) {
116
+ // No dot found, entire string is assembly name
117
+ assemblyName = assemblyAndChr
118
+ chr = ''
103
119
  } else {
104
- assemblyName = n1
105
- last = [n2, ...rest].join('.')
120
+ const secondDotIndex = assemblyAndChr.indexOf(
121
+ '.',
122
+ firstDotIndex + 1,
123
+ )
124
+ if (secondDotIndex === -1) {
125
+ // Only one dot: assembly.chr
126
+ assemblyName = assemblyAndChr.slice(
127
+ 0,
128
+ Math.max(0, firstDotIndex),
129
+ )
130
+ chr = assemblyAndChr.slice(Math.max(0, firstDotIndex + 1))
131
+ } else {
132
+ // Multiple dots: check if second part is numeric (version number)
133
+ const secondPart = assemblyAndChr.slice(
134
+ firstDotIndex + 1,
135
+ secondDotIndex,
136
+ )
137
+ const isNumeric =
138
+ secondPart.length > 0 && !Number.isNaN(+secondPart)
139
+
140
+ if (isNumeric) {
141
+ // assembly.version.chr format
142
+ assemblyName = assemblyAndChr.slice(
143
+ 0,
144
+ Math.max(0, secondDotIndex),
145
+ )
146
+ chr = assemblyAndChr.slice(Math.max(0, secondDotIndex + 1))
147
+ } else {
148
+ // assembly.chr.more format
149
+ assemblyName = assemblyAndChr.slice(
150
+ 0,
151
+ Math.max(0, firstDotIndex),
152
+ )
153
+ chr = assemblyAndChr.slice(Math.max(0, firstDotIndex + 1))
154
+ }
155
+ }
106
156
  }
157
+
107
158
  if (assemblyName) {
108
- firstAssemblyNameFound = firstAssemblyNameFound || assemblyName
159
+ // Set first assembly name found (only once)
160
+ if (!firstAssemblyNameFound) {
161
+ firstAssemblyNameFound = assemblyName
162
+ }
109
163
 
164
+ // Create alignment record with optimized number conversion
110
165
  alignments[assemblyName] = {
111
- chr: last,
112
- start: +ad[1]!,
113
- srcSize: +ad[2]!,
114
- strand: ad[3] === '-' ? -1 : 1,
115
- unknown: +ad[4]!,
166
+ chr,
167
+ start: +startStr!,
168
+ srcSize: +srcSizeStr!,
169
+ strand: strandStr === '-' ? -1 : 1,
170
+ unknown: +unknownStr!,
116
171
  seq,
117
172
  }
118
173
  }
@@ -1,5 +1,6 @@
1
1
  import { Sample } from '../LinearMafDisplay/types'
2
2
 
3
+ import type { AlignmentRecord } from '../LinearMafRenderer/rendering'
3
4
  import type { Feature, Region } from '@jbrowse/core/util'
4
5
 
5
6
  /**
@@ -27,7 +28,7 @@ export function processFeaturesToFasta({
27
28
  const outputRows = samples.map(() => '-'.repeat(rlen))
28
29
  for (const feature of features.values()) {
29
30
  const leftCoord = feature.get('start')
30
- const vals = feature.get('alignments') as Record<string, { seq: string }>
31
+ const vals = feature.get('alignments') as Record<string, AlignmentRecord>
31
32
  const seq = feature.get('seq')
32
33
  for (const [sample, val] of Object.entries(vals)) {
33
34
  const origAlignment = val.seq