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.
- package/README.md +1 -1
- package/dist/BigMafAdapter/BigMafAdapter.js +39 -28
- package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
- package/dist/LinearMafDisplay/components/Crosshairs.js +1 -1
- package/dist/LinearMafDisplay/components/Crosshairs.js.map +1 -1
- package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +2 -3
- package/dist/LinearMafDisplay/components/MAFTooltip.js +6 -19
- package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
- package/dist/LinearMafDisplay/stateModel.d.ts +8 -0
- package/dist/LinearMafDisplay/stateModel.js +10 -0
- package/dist/LinearMafDisplay/stateModel.js.map +1 -1
- package/dist/LinearMafDisplay/util.d.ts +20 -0
- package/dist/LinearMafDisplay/util.js +29 -0
- package/dist/LinearMafDisplay/util.js.map +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +3 -0
- package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -2
- package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
- package/dist/LinearMafRenderer/components/ReactComponent.d.ts +3 -0
- package/dist/LinearMafRenderer/components/ReactComponent.js +41 -2
- package/dist/LinearMafRenderer/components/ReactComponent.js.map +1 -1
- package/dist/LinearMafRenderer/components/util.d.ts +1 -0
- package/dist/LinearMafRenderer/components/util.js +13 -0
- package/dist/LinearMafRenderer/components/util.js.map +1 -0
- package/dist/LinearMafRenderer/makeImageData.d.ts +4 -5
- package/dist/LinearMafRenderer/makeImageData.js +28 -146
- package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/features.d.ts +21 -0
- package/dist/LinearMafRenderer/rendering/features.js +58 -0
- package/dist/LinearMafRenderer/rendering/features.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/gaps.d.ts +12 -0
- package/dist/LinearMafRenderer/rendering/gaps.js +35 -0
- package/dist/LinearMafRenderer/rendering/gaps.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/index.d.ts +8 -0
- package/dist/LinearMafRenderer/rendering/index.js +10 -0
- package/dist/LinearMafRenderer/rendering/index.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/insertions.d.ts +14 -0
- package/dist/LinearMafRenderer/rendering/insertions.js +84 -0
- package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/matches.d.ts +13 -0
- package/dist/LinearMafRenderer/rendering/matches.js +41 -0
- package/dist/LinearMafRenderer/rendering/matches.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/mismatches.d.ts +13 -0
- package/dist/LinearMafRenderer/rendering/mismatches.js +47 -0
- package/dist/LinearMafRenderer/rendering/mismatches.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.d.ts +60 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.js +99 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/text.d.ts +12 -0
- package/dist/LinearMafRenderer/rendering/text.js +42 -0
- package/dist/LinearMafRenderer/rendering/text.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/types.d.ts +67 -0
- package/dist/LinearMafRenderer/rendering/types.js +15 -0
- package/dist/LinearMafRenderer/rendering/types.js.map +1 -0
- package/dist/MafTabixAdapter/MafTabixAdapter.js +48 -22
- package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -8
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
- package/dist/out.js +34520 -0
- package/dist/out.js.map +7 -0
- package/dist/util/fastaUtils.js.map +1 -1
- package/package.json +5 -3
- package/src/BigMafAdapter/BigMafAdapter.ts +49 -28
- package/src/LinearMafDisplay/components/Crosshairs.tsx +1 -7
- package/src/LinearMafDisplay/components/MAFTooltip.tsx +14 -33
- package/src/LinearMafDisplay/stateModel.ts +10 -0
- package/src/LinearMafDisplay/util.ts +57 -0
- package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -2
- package/src/LinearMafRenderer/components/ReactComponent.tsx +70 -2
- package/src/LinearMafRenderer/components/util.ts +13 -0
- package/src/LinearMafRenderer/makeImageData.ts +49 -196
- package/src/LinearMafRenderer/rendering/features.ts +138 -0
- package/src/LinearMafRenderer/rendering/gaps.ts +71 -0
- package/src/LinearMafRenderer/rendering/index.ts +9 -0
- package/src/LinearMafRenderer/rendering/insertions.ts +170 -0
- package/src/LinearMafRenderer/rendering/matches.ts +79 -0
- package/src/LinearMafRenderer/rendering/mismatches.ts +125 -0
- package/src/LinearMafRenderer/rendering/spatialIndex.ts +136 -0
- package/src/LinearMafRenderer/rendering/text.ts +72 -0
- package/src/LinearMafRenderer/rendering/types.ts +81 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +77 -22
- 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
|
-
|
|
89
|
-
for (let j = 0; j < len; j++) {
|
|
90
|
+
for (let j = 0; j < dataLength; j++) {
|
|
90
91
|
const elt = data[j]!
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
start: +
|
|
113
|
-
srcSize: +
|
|
114
|
-
strand:
|
|
115
|
-
unknown: +
|
|
166
|
+
chr,
|
|
167
|
+
start: +startStr!,
|
|
168
|
+
srcSize: +srcSizeStr!,
|
|
169
|
+
strand: strandStr === '-' ? -1 : 1,
|
|
170
|
+
unknown: +unknownStr!,
|
|
116
171
|
seq,
|
|
117
172
|
}
|
|
118
173
|
}
|
package/src/util/fastaUtils.ts
CHANGED
|
@@ -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,
|
|
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
|