@teselagen/ove 0.8.39 → 0.8.41
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/AlignmentView/AlignmentSearchBar.d.ts +2 -0
- package/AlignmentView/Mismatches.d.ts +2 -20
- package/AlignmentView/findAlignmentDifferences.d.ts +53 -0
- package/AlignmentView/utils.d.ts +31 -0
- package/index.cjs.js +1063 -164
- package/index.es.js +1063 -164
- package/index.umd.js +1063 -164
- package/ove.css +138 -54
- package/package.json +1 -1
- package/src/AlignmentView/AlignmentSearchBar.js +810 -0
- package/src/AlignmentView/AlignmentVisibilityTool.js +9 -11
- package/src/AlignmentView/Minimap.js +21 -3
- package/src/AlignmentView/Mismatches.js +201 -123
- package/src/AlignmentView/findAlignmentDifferences.js +116 -0
- package/src/AlignmentView/findAlignmentDifferences.test.js +208 -0
- package/src/AlignmentView/index.js +30 -2
- package/src/AlignmentView/style.css +84 -0
- package/src/AlignmentView/utils.js +30 -0
- package/src/PropertySidePanel/index.js +89 -28
- package/src/redux/alignments.js +58 -20
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
Button,
|
|
3
3
|
Popover,
|
|
4
4
|
Intent,
|
|
5
|
-
Tooltip,
|
|
6
5
|
Tag,
|
|
7
6
|
Menu,
|
|
8
7
|
MenuItem
|
|
@@ -21,16 +20,15 @@ export default pureNoFunc(function AlignmentVisibilityTool(props) {
|
|
|
21
20
|
position="bottom"
|
|
22
21
|
content={<VisibilityOptions {...props} />}
|
|
23
22
|
target={
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</Tooltip>
|
|
23
|
+
<Button
|
|
24
|
+
className="tg-alignment-visibility-toggle"
|
|
25
|
+
small
|
|
26
|
+
data-tip="Visibility Options"
|
|
27
|
+
rightIcon="caret-down"
|
|
28
|
+
intent={Intent.PRIMARY}
|
|
29
|
+
minimal
|
|
30
|
+
icon="eye-open"
|
|
31
|
+
/>
|
|
34
32
|
}
|
|
35
33
|
/>
|
|
36
34
|
);
|
|
@@ -23,7 +23,8 @@ export default class Minimap extends React.Component {
|
|
|
23
23
|
"scrollAlignmentView",
|
|
24
24
|
"laneHeight",
|
|
25
25
|
"laneSpacing",
|
|
26
|
-
"isTrackSelected"
|
|
26
|
+
"isTrackSelected",
|
|
27
|
+
"activeFilterType"
|
|
27
28
|
].some(key => props[key] !== newProps[key])
|
|
28
29
|
)
|
|
29
30
|
return true;
|
|
@@ -185,12 +186,14 @@ export default class Minimap extends React.Component {
|
|
|
185
186
|
dimensions: { width = 200 },
|
|
186
187
|
laneHeight,
|
|
187
188
|
laneSpacing = 1,
|
|
188
|
-
isTrackSelected = []
|
|
189
|
+
isTrackSelected = [],
|
|
190
|
+
activeFilterType = "all"
|
|
189
191
|
} = this.props;
|
|
190
192
|
const charWidth = this.getCharWidth();
|
|
191
193
|
|
|
192
194
|
const {
|
|
193
195
|
matchHighlightRanges: _matchHighlightRanges,
|
|
196
|
+
gapRanges = [],
|
|
194
197
|
alignmentData: { trimmedRange } = {}
|
|
195
198
|
} = alignmentTracks[i];
|
|
196
199
|
const matchHighlightRanges = !trimmedRange
|
|
@@ -228,10 +231,25 @@ export default class Minimap extends React.Component {
|
|
|
228
231
|
const toAdd = `M${xStart},${y} L${xStart + width},${y} L${
|
|
229
232
|
xStart + width
|
|
230
233
|
},${y + height} L${xStart},${y + height}`;
|
|
231
|
-
if (
|
|
234
|
+
if (
|
|
235
|
+
!range.isMatch &&
|
|
236
|
+
(activeFilterType === "all" ||
|
|
237
|
+
range.differenceType === activeFilterType)
|
|
238
|
+
) {
|
|
232
239
|
redPath += toAdd;
|
|
233
240
|
}
|
|
234
241
|
});
|
|
242
|
+
if (activeFilterType === "gap") {
|
|
243
|
+
gapRanges.forEach(range => {
|
|
244
|
+
const { xStart, width } = getXStartAndWidthFromNonCircularRange(
|
|
245
|
+
range,
|
|
246
|
+
charWidth
|
|
247
|
+
);
|
|
248
|
+
redPath += `M${xStart},${y} L${xStart + width},${y} L${
|
|
249
|
+
xStart + width
|
|
250
|
+
},${y + height} L${xStart},${y + height}`;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
235
253
|
return (
|
|
236
254
|
<div
|
|
237
255
|
key={i + "-lane"}
|
|
@@ -1,134 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
/* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Intent,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuItem,
|
|
8
|
+
Popover,
|
|
9
|
+
Position,
|
|
10
|
+
Tag
|
|
11
|
+
} from "@blueprintjs/core";
|
|
12
|
+
import React, { useEffect, useMemo } from "react";
|
|
13
|
+
import { useSelector } from "react-redux";
|
|
14
|
+
import {
|
|
15
|
+
findAlignmentDifferences,
|
|
16
|
+
groupConsecutiveDifferences
|
|
17
|
+
} from "./findAlignmentDifferences";
|
|
18
|
+
import { scrollToAlignmentSelection, updateCaretPosition } from "./utils";
|
|
19
|
+
|
|
20
|
+
const FILTER_OPTIONS = [
|
|
21
|
+
{ value: "all", label: "All" },
|
|
22
|
+
{ value: "mismatch", label: "Mismatches" },
|
|
23
|
+
{ value: "insertion", label: "Insertions" },
|
|
24
|
+
{ value: "deletion", label: "Deletions" },
|
|
25
|
+
{ value: "gap", label: "Gaps" }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function FindMismatches(props) {
|
|
29
|
+
const { alignmentJson, id, onFilterChange } = props;
|
|
30
|
+
const alignedSeqs = useMemo(
|
|
31
|
+
() => alignmentJson.map(t => t.alignmentData?.sequence || ""),
|
|
32
|
+
[alignmentJson]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const [activeFilter, setActiveFilter] = React.useState("all");
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
gapsBefore: 0,
|
|
40
|
-
gapsInside: 0
|
|
37
|
+
const allDifferences = useMemo(
|
|
38
|
+
() => groupConsecutiveDifferences(findAlignmentDifferences(alignedSeqs)),
|
|
39
|
+
[alignedSeqs]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const countsByType = useMemo(() => {
|
|
43
|
+
const counts = { all: 0, mismatch: 0, insertion: 0, deletion: 0, gap: 0 };
|
|
44
|
+
allDifferences.forEach(d => {
|
|
45
|
+
counts[d.type] = (counts[d.type] || 0) + 1;
|
|
46
|
+
counts.all++;
|
|
41
47
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
return counts;
|
|
49
|
+
}, [allDifferences]);
|
|
50
|
+
|
|
51
|
+
const differences = useMemo(() => {
|
|
52
|
+
const filtered =
|
|
53
|
+
activeFilter === "all"
|
|
54
|
+
? allDifferences
|
|
55
|
+
: allDifferences.filter(d => d.type === activeFilter);
|
|
56
|
+
return [{ position: -1, start: -1, end: -1, bases: [""] }, ...filtered];
|
|
57
|
+
}, [allDifferences, activeFilter]);
|
|
58
|
+
|
|
59
|
+
const currentCaretPosition = useSelector(
|
|
60
|
+
state =>
|
|
61
|
+
state.VectorEditor.__allEditorsOptions.alignments[id]?.caretPosition
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const [currentIdx, setCurrentIdx] = React.useState(0);
|
|
65
|
+
|
|
66
|
+
const currentDiff = differences[currentIdx];
|
|
67
|
+
const disablePrev = currentIdx <= 1;
|
|
68
|
+
const disableNext = currentIdx >= differences.length - 1;
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setCurrentIdx(0);
|
|
72
|
+
}, [activeFilter]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
onFilterChange?.({ activeFilter });
|
|
76
|
+
}, [activeFilter, onFilterChange]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (currentCaretPosition !== -1) {
|
|
80
|
+
const diffIdx = differences.findIndex(
|
|
81
|
+
(d, i) =>
|
|
82
|
+
i > 0 &&
|
|
83
|
+
currentCaretPosition >= d.start &&
|
|
84
|
+
currentCaretPosition <= d.end + 1
|
|
85
|
+
);
|
|
86
|
+
if (diffIdx !== -1 && diffIdx !== currentIdx) {
|
|
87
|
+
setCurrentIdx(diffIdx);
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
}, [currentCaretPosition, differences, currentIdx]);
|
|
91
|
+
|
|
92
|
+
const updateView = diff => {
|
|
93
|
+
const idx = differences.indexOf(diff);
|
|
94
|
+
const { start, end } = diff;
|
|
95
|
+
setCurrentIdx(idx);
|
|
96
|
+
updateCaretPosition({ start, end });
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
scrollToAlignmentSelection();
|
|
99
|
+
}, 0);
|
|
82
100
|
};
|
|
83
101
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const prevDifference = () => {
|
|
103
|
+
const pivot =
|
|
104
|
+
currentCaretPosition >= 0
|
|
105
|
+
? currentCaretPosition
|
|
106
|
+
: (differences[currentIdx]?.start ?? 0);
|
|
107
|
+
const prev = [...differences]
|
|
108
|
+
.reverse()
|
|
109
|
+
.find(d => d.start >= 0 && d.start < pivot);
|
|
110
|
+
if (prev) updateView(prev);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const nextDifference = () => {
|
|
114
|
+
const pivot =
|
|
115
|
+
currentCaretPosition >= 0
|
|
116
|
+
? currentCaretPosition
|
|
117
|
+
: (differences[currentIdx]?.start ?? -1);
|
|
118
|
+
const next = differences.find(d => d.start > pivot && d.start >= 0);
|
|
119
|
+
if (next) updateView(next);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const noDifferences = differences.length <= 1;
|
|
123
|
+
const activeOption = FILTER_OPTIONS.find(o => o.value === activeFilter);
|
|
124
|
+
const activeLabel = activeOption?.label ?? "Differences";
|
|
125
|
+
|
|
126
|
+
const filterMenu = (
|
|
127
|
+
<Menu>
|
|
128
|
+
{FILTER_OPTIONS.map(({ value, label }) => {
|
|
129
|
+
const count = countsByType[value] ?? 0;
|
|
130
|
+
const isActive = activeFilter === value;
|
|
131
|
+
return (
|
|
132
|
+
<MenuItem
|
|
133
|
+
key={value}
|
|
134
|
+
active={isActive}
|
|
135
|
+
onClick={() => setActiveFilter(value)}
|
|
136
|
+
text={
|
|
137
|
+
<span className="veDiffMenuItem-inner">
|
|
138
|
+
{label}
|
|
139
|
+
<Tag round minimal style={{ marginLeft: 6 }}>
|
|
140
|
+
{count}
|
|
141
|
+
</Tag>
|
|
142
|
+
</span>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
</Menu>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="veDiffNavigator">
|
|
152
|
+
{/* Filter dropdown — single button replacing 5 filter chips */}
|
|
153
|
+
<Popover
|
|
154
|
+
minimal
|
|
155
|
+
position={Position.BOTTOM_LEFT}
|
|
156
|
+
content={filterMenu}
|
|
157
|
+
target={
|
|
158
|
+
<Button
|
|
159
|
+
minimal
|
|
160
|
+
data-tip="Filter Difference Type"
|
|
161
|
+
small
|
|
162
|
+
rightIcon="caret-down"
|
|
163
|
+
className="veDiffFilter-trigger"
|
|
164
|
+
>
|
|
165
|
+
{activeLabel}
|
|
166
|
+
</Button>
|
|
167
|
+
}
|
|
168
|
+
/>
|
|
103
169
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
{/* Navigation control */}
|
|
171
|
+
{noDifferences ? (
|
|
172
|
+
<span className="veDiffNav-empty">
|
|
173
|
+
no{" "}
|
|
174
|
+
{activeFilter === "all" ? "differences" : activeLabel.toLowerCase()}
|
|
175
|
+
</span>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="veDiffNav">
|
|
178
|
+
<Button
|
|
179
|
+
minimal
|
|
180
|
+
small
|
|
181
|
+
data-tip="Previous Difference"
|
|
182
|
+
icon="arrow-left"
|
|
183
|
+
intent={Intent.PRIMARY}
|
|
184
|
+
onClick={prevDifference}
|
|
185
|
+
disabled={disablePrev}
|
|
186
|
+
/>
|
|
187
|
+
<div className="veDiffNav-center">
|
|
188
|
+
<span className="veDiffNav-fraction">
|
|
189
|
+
{currentIdx}
|
|
190
|
+
<span className="veDiffNav-sep">/</span>
|
|
191
|
+
{differences.length - 1}
|
|
192
|
+
</span>
|
|
193
|
+
{currentDiff?.start > -1 && (
|
|
194
|
+
<span className="veDiffNav-pos">:{currentDiff.start + 1}</span>
|
|
195
|
+
)}
|
|
127
196
|
</div>
|
|
197
|
+
<Button
|
|
198
|
+
minimal
|
|
199
|
+
small
|
|
200
|
+
data-tip="Next Difference"
|
|
201
|
+
icon="arrow-right"
|
|
202
|
+
intent={Intent.PRIMARY}
|
|
203
|
+
onClick={nextDifference}
|
|
204
|
+
disabled={disableNext}
|
|
205
|
+
/>
|
|
128
206
|
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
132
210
|
}
|
|
133
211
|
|
|
134
|
-
export default
|
|
212
|
+
export default FindMismatches;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {"mismatch"|"insertion"|"deletion"|"gap"} DifferenceType
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object} AlignmentDifference
|
|
7
|
+
* @property {number} position - 0-based column index in the aligned sequence
|
|
8
|
+
* @property {DifferenceType} type
|
|
9
|
+
* @property {string[]} bases - bases at this column for each track (template first)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Group consecutive same-type differences into regions.
|
|
14
|
+
* Mismatches are never grouped — each is its own entry.
|
|
15
|
+
* Insertions, deletions, and gaps that are side-by-side are collapsed into
|
|
16
|
+
* one entry with a `start` and `end` (both inclusive, 0-based).
|
|
17
|
+
*
|
|
18
|
+
* @param {AlignmentDifference[]} differences
|
|
19
|
+
* @returns {Array<AlignmentDifference & { start: number, end: number }>}
|
|
20
|
+
*/
|
|
21
|
+
export function groupConsecutiveDifferences(differences) {
|
|
22
|
+
const grouped = [];
|
|
23
|
+
|
|
24
|
+
for (const diff of differences) {
|
|
25
|
+
if (diff.type === "mismatch") {
|
|
26
|
+
grouped.push({ ...diff, start: diff.position, end: diff.position });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const last = grouped[grouped.length - 1];
|
|
31
|
+
if (last && last.type === diff.type && last.end === diff.position - 1) {
|
|
32
|
+
grouped[grouped.length - 1] = { ...last, end: diff.position };
|
|
33
|
+
} else {
|
|
34
|
+
grouped.push({ ...diff, start: diff.position, end: diff.position });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return grouped;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify alignment columns into difference types relative to the template track.
|
|
43
|
+
*
|
|
44
|
+
* Template is alignedSeqs[0]. Non-template tracks are alignedSeqs[1+].
|
|
45
|
+
*
|
|
46
|
+
* Classification rules (per column):
|
|
47
|
+
* - "gap" : no non-template track is in its aligned region at this position
|
|
48
|
+
* - "insertion" : template has '-', at least one aligned non-template has a non-gap base
|
|
49
|
+
* - "deletion" : template has a non-gap base, at least one aligned non-template has '-'
|
|
50
|
+
* - "mismatch" : no gaps among aligned tracks, unique base set has more than one member
|
|
51
|
+
*
|
|
52
|
+
* Only tracks whose aligned region covers position i participate in classification.
|
|
53
|
+
* This correctly handles multi-read alignments (e.g. Sanger) where reads cover
|
|
54
|
+
* different sub-ranges of the full alignment.
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} alignedSeqs - Aligned sequence strings, all same length
|
|
57
|
+
* @returns {AlignmentDifference[]}
|
|
58
|
+
*/
|
|
59
|
+
export function findAlignmentDifferences(alignedSeqs) {
|
|
60
|
+
if (alignedSeqs.length < 2 || !alignedSeqs[0]?.length) return [];
|
|
61
|
+
|
|
62
|
+
const template = alignedSeqs[0].toLowerCase();
|
|
63
|
+
const nonTemplates = alignedSeqs.slice(1).map(s => s.toLowerCase());
|
|
64
|
+
|
|
65
|
+
// Compute non-aligned region boundaries for each non-template track.
|
|
66
|
+
// Positions in [0, start) and [end, length) are non-aligned ("gap").
|
|
67
|
+
const trackBounds = nonTemplates.map(seq => {
|
|
68
|
+
const withoutLeading = seq.replace(/^-+/, "");
|
|
69
|
+
const withoutTrailing = seq.replace(/-+$/, "");
|
|
70
|
+
const start = seq.length - withoutLeading.length;
|
|
71
|
+
const end = seq.length - (seq.length - withoutTrailing.length);
|
|
72
|
+
return { start, end };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const differences = [];
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < template.length; i++) {
|
|
78
|
+
const templateBase = template[i];
|
|
79
|
+
const allNonTemplateBases = nonTemplates.map(seq => seq[i]);
|
|
80
|
+
const bases = [templateBase, ...allNonTemplateBases];
|
|
81
|
+
|
|
82
|
+
// Only consider tracks whose aligned region covers position i.
|
|
83
|
+
// Using `some` here would classify a position as a gap whenever any single
|
|
84
|
+
// track hasn't started or has ended, swamping real differences from other
|
|
85
|
+
// tracks that ARE aligned (critical for multi-read Sanger alignments).
|
|
86
|
+
const alignedIndices = trackBounds.reduce((acc, { start, end }, idx) => {
|
|
87
|
+
if (i >= start && i < end) acc.push(idx);
|
|
88
|
+
return acc;
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
if (alignedIndices.length === 0) {
|
|
92
|
+
differences.push({ position: i, type: "gap", bases });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const alignedBases = alignedIndices.map(idx => allNonTemplateBases[idx]);
|
|
97
|
+
const templateIsGap = templateBase === "-";
|
|
98
|
+
const nonTemplateHasBase = alignedBases.some(b => b !== "-");
|
|
99
|
+
const nonTemplateHasGap = alignedBases.some(b => b === "-");
|
|
100
|
+
|
|
101
|
+
if (templateIsGap && nonTemplateHasBase) {
|
|
102
|
+
differences.push({ position: i, type: "insertion", bases });
|
|
103
|
+
} else if (!templateIsGap && nonTemplateHasGap) {
|
|
104
|
+
differences.push({ position: i, type: "deletion", bases });
|
|
105
|
+
} else if (!templateIsGap) {
|
|
106
|
+
const uniqueBases = new Set([templateBase, ...alignedBases]);
|
|
107
|
+
if (uniqueBases.size > 1) {
|
|
108
|
+
differences.push({ position: i, type: "mismatch", bases });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return differences;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default findAlignmentDifferences;
|