@teselagen/ove 0.8.40 → 0.8.42

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/ove.css CHANGED
@@ -11297,6 +11297,10 @@ path.partWithSelectedTag {
11297
11297
  /* .alignmentViewTrackContainer:hover .alignmentTrackNameDiv {
11298
11298
  opacity: 1 !important;
11299
11299
  } */
11300
+ .ve-alignment-top-bar {
11301
+ align-items: center;
11302
+ }
11303
+
11300
11304
  .ve-alignment-top-bar > * {
11301
11305
  overflow-wrap: normal;
11302
11306
  flex: 0 0 auto;
@@ -11313,6 +11317,82 @@ path.partWithSelectedTag {
11313
11317
  .veAlignmentMismatch {
11314
11318
  opacity: 0.9;
11315
11319
  }
11320
+
11321
+ .veDiffNavigator {
11322
+ display: flex;
11323
+ align-items: center;
11324
+ gap: 4px;
11325
+ }
11326
+
11327
+ /* Menu item content layout */
11328
+ .veDiffMenuItem-inner {
11329
+ display: flex;
11330
+ align-items: center;
11331
+ gap: 6px;
11332
+ }
11333
+
11334
+ /* Navigation pill — groups prev/counter/next into one unit */
11335
+ .veDiffNav {
11336
+ display: flex;
11337
+ align-items: center;
11338
+ border-radius: 3px;
11339
+ background: rgba(92, 112, 128, 0.06);
11340
+ border: 1px solid rgba(92, 112, 128, 0.18);
11341
+ }
11342
+
11343
+ .veDiffNav-center {
11344
+ display: flex;
11345
+ align-items: baseline;
11346
+ gap: 3px;
11347
+ padding: 0 6px;
11348
+ min-width: 64px;
11349
+ justify-content: center;
11350
+ }
11351
+
11352
+ .veDiffNav-fraction {
11353
+ font-size: 11px;
11354
+ font-variant-numeric: tabular-nums;
11355
+ color: #5c7080;
11356
+ line-height: 1;
11357
+ }
11358
+
11359
+ .veDiffNav-sep {
11360
+ margin: 0 1px;
11361
+ opacity: 0.45;
11362
+ }
11363
+
11364
+ /* Position number — monospace fits sequence coordinates */
11365
+ .veDiffNav-pos {
11366
+ font-size: 10px;
11367
+ font-family: monospace;
11368
+ font-variant-numeric: tabular-nums;
11369
+ color: #a7b6c2;
11370
+ line-height: 1;
11371
+ }
11372
+
11373
+ .veDiffNav-empty {
11374
+ font-size: 11px;
11375
+ font-style: italic;
11376
+ color: #a7b6c2;
11377
+ padding: 0 4px;
11378
+ }
11379
+
11380
+ .bp3-dark .veDiffNav {
11381
+ background: rgba(167, 182, 194, 0.05);
11382
+ border-color: rgba(167, 182, 194, 0.14);
11383
+ }
11384
+
11385
+ .bp3-dark .veDiffNav-fraction {
11386
+ color: #5c7080;
11387
+ }
11388
+
11389
+ .bp3-dark .veDiffNav-pos {
11390
+ color: #4f6272;
11391
+ }
11392
+
11393
+ .bp3-dark .veDiffNav-empty {
11394
+ color: #5c7080;
11395
+ }
11316
11396
  .veRowItem:has(.rowViewTextContainer) .veAlignmentMismatch {
11317
11397
  opacity: 0.5;
11318
11398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teselagen/ove",
3
- "version": "0.8.40",
3
+ "version": "0.8.42",
4
4
  "main": "./src/index.js",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/TeselaGen/tg-oss",
@@ -15,7 +15,7 @@
15
15
  "@blueprintjs/core": "3.54.0",
16
16
  "@hello-pangea/dnd": "16.2.0",
17
17
  "@risingstack/react-easy-state": "^6.3.0",
18
- "@teselagen/bio-parsers": "0.4.36",
18
+ "@teselagen/bio-parsers": "0.4.37",
19
19
  "@teselagen/file-utils": "0.3.23",
20
20
  "@teselagen/range-utils": "0.3.20",
21
21
  "@teselagen/react-list": "0.8.18",
@@ -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
- <Tooltip content="Visibility Options">
25
- <Button
26
- className="tg-alignment-visibility-toggle"
27
- small
28
- rightIcon="caret-down"
29
- intent={Intent.PRIMARY}
30
- minimal
31
- icon="eye-open"
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 (!range.isMatch) {
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,183 +1,208 @@
1
1
  /* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
2
- import React, { useEffect, useCallback, useMemo } from "react";
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";
3
13
  import { useSelector } from "react-redux";
4
- import { Button } from "@blueprintjs/core";
14
+ import {
15
+ findAlignmentDifferences,
16
+ groupConsecutiveDifferences
17
+ } from "./findAlignmentDifferences";
5
18
  import { scrollToAlignmentSelection, updateCaretPosition } from "./utils";
6
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
+
7
28
  export function FindMismatches(props) {
8
- const { alignmentJson, id } = props;
29
+ const { alignmentJson, id, onFilterChange } = props;
9
30
  const alignedSeqs = useMemo(
10
31
  () => alignmentJson.map(t => t.alignmentData?.sequence || ""),
11
32
  [alignmentJson]
12
33
  );
13
34
 
14
- // Find mismatch positions
15
- const mismatches = useMemo(() => {
16
- const result = [{ position: 0, bases: [""] }];
17
-
18
- if (alignedSeqs.length > 1 && alignedSeqs[0].length) {
19
- for (let i = 0; i < alignedSeqs[0].length; i++) {
20
- const bases = alignedSeqs.map(seq => seq[i]);
21
- const uniqueBases = new Set(bases);
22
- // Ignore gaps-only columns
23
- if (uniqueBases.size > 1 && !uniqueBases.has("-")) {
24
- result.push({ position: i, bases });
25
- }
26
- }
27
- }
28
- return result;
29
- }, [alignedSeqs]);
35
+ const [activeFilter, setActiveFilter] = React.useState("all");
36
+
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++;
47
+ });
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]);
30
58
 
31
59
  const currentCaretPosition = useSelector(
32
60
  state =>
33
61
  state.VectorEditor.__allEditorsOptions.alignments[id]?.caretPosition
34
62
  );
35
63
 
36
- // State for navigation
37
64
  const [currentIdx, setCurrentIdx] = React.useState(0);
38
- const [disablePrev, setDisablePrev] = React.useState(true);
39
- const [disableNext, setDisableNext] = React.useState(false);
40
-
41
- // Current mismatch info
42
- const currentMismatch = mismatches[currentIdx];
43
-
44
- const handleButtonsState = useCallback(
45
- caret => {
46
- if (mismatches.length <= 1) {
47
- setDisablePrev(true);
48
- setDisableNext(true);
49
- return;
50
- }
51
65
 
52
- const firstMismatchPos = mismatches[1].position;
53
- const lastMismatchPos = mismatches[mismatches.length - 1].position;
66
+ const currentDiff = differences[currentIdx];
67
+ const disablePrev = currentIdx <= 1;
68
+ const disableNext = currentIdx >= differences.length - 1;
54
69
 
55
- setDisablePrev(caret <= firstMismatchPos);
56
- setDisableNext(caret >= lastMismatchPos);
57
- },
58
- [mismatches]
59
- );
70
+ useEffect(() => {
71
+ setCurrentIdx(0);
72
+ }, [activeFilter]);
73
+
74
+ useEffect(() => {
75
+ onFilterChange?.({ activeFilter });
76
+ }, [activeFilter, onFilterChange]);
60
77
 
61
- // Update currentIdx when caret moves to a mismatch
62
78
  useEffect(() => {
63
79
  if (currentCaretPosition !== -1) {
64
- const mismatchIdx = mismatches.findIndex(
65
- m =>
66
- m.position === currentCaretPosition ||
67
- m.position === currentCaretPosition - 1
80
+ const diffIdx = differences.findIndex(
81
+ (d, i) =>
82
+ i > 0 &&
83
+ currentCaretPosition >= d.start &&
84
+ currentCaretPosition <= d.end + 1
68
85
  );
69
- if (mismatchIdx !== -1 && mismatchIdx !== currentIdx) {
70
- handleButtonsState(currentCaretPosition);
71
- setCurrentIdx(mismatchIdx);
86
+ if (diffIdx !== -1 && diffIdx !== currentIdx) {
87
+ setCurrentIdx(diffIdx);
72
88
  }
73
89
  }
74
- }, [currentCaretPosition, mismatches, currentIdx, handleButtonsState]);
75
-
76
- const updateView = mismatch => {
77
- const idx = mismatches.indexOf(mismatch);
78
- const position = mismatch.position;
79
-
80
- handleButtonsState(position);
90
+ }, [currentCaretPosition, differences, currentIdx]);
81
91
 
92
+ const updateView = diff => {
93
+ const idx = differences.indexOf(diff);
94
+ const { start, end } = diff;
82
95
  setCurrentIdx(idx);
83
-
84
- updateCaretPosition({ start: position, end: position });
96
+ updateCaretPosition({ start, end });
85
97
  setTimeout(() => {
86
98
  scrollToAlignmentSelection();
87
99
  }, 0);
88
100
  };
89
101
 
90
- // Handle mismatch navigation
91
- const prevMismatch = () => {
92
- if (currentIdx > 1) {
93
- const newIdx = Math.max(0, currentIdx - 1);
94
- let prev = mismatches[newIdx];
95
-
96
- if (currentCaretPosition > 0) {
97
- prev = [...mismatches]
98
- .reverse()
99
- .find(m => m.position < currentCaretPosition);
100
- }
101
-
102
- if (prev) {
103
- updateView(prev);
104
- }
105
- }
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);
106
111
  };
107
112
 
108
- const nextMismatch = () => {
109
- if (currentIdx < mismatches.length - 1) {
110
- const newIdx = Math.min(mismatches.length - 1, currentIdx + 1);
111
- let next = mismatches[newIdx];
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
+ };
112
121
 
113
- if (currentCaretPosition > 0) {
114
- next = mismatches.find(
115
- m => m.position > currentCaretPosition && m.position > 1
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
+ />
116
145
  );
117
- }
118
-
119
- if (next) {
120
- updateView(next);
121
- }
122
- }
123
- };
146
+ })}
147
+ </Menu>
148
+ );
124
149
 
125
150
  return (
126
- <div
127
- style={{
128
- display: "flex",
129
- flexDirection: "row",
130
- justifyContent: "center",
131
- alignItems: "center",
132
- gap: 10
133
- }}
134
- >
135
- {mismatches.length === 1 ? (
136
- <span style={{ fontStyle: "italic", color: "grey" }}>
137
- no mismatches
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
+ />
169
+
170
+ {/* Navigation control */}
171
+ {noDifferences ? (
172
+ <span className="veDiffNav-empty">
173
+ no{" "}
174
+ {activeFilter === "all" ? "differences" : activeLabel.toLowerCase()}
138
175
  </span>
139
176
  ) : (
140
- <div style={{ display: "flex", flexDirection: "column" }}>
141
- <div
142
- style={{
143
- display: "flex",
144
- alignItems: "center"
145
- }}
146
- >
147
- <strong>Mismatches</strong>
148
- <div style={{ display: "flex", gap: 2 }}>
149
- <Button
150
- intent="primary"
151
- icon="arrow-left"
152
- data-tip="Previous Mismatch"
153
- onClick={prevMismatch}
154
- disabled={disablePrev}
155
- small
156
- minimal
157
- />
158
- <Button
159
- intent="primary"
160
- icon="arrow-right"
161
- data-tip="Next Mismatch"
162
- onClick={nextMismatch}
163
- disabled={disableNext}
164
- small
165
- minimal
166
- />
167
- </div>
168
- </div>
169
- <span
170
- style={{
171
- fontSize: "0.8em",
172
- color: "grey",
173
- lineHeight: "0.8em"
174
- }}
175
- >
176
- {currentMismatch.position > 1 && (
177
- <span>Position: {currentMismatch.position + 1} | </span>
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>
178
195
  )}
179
- ({currentIdx} of {mismatches.length - 1})
180
- </span>
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
+ />
181
206
  </div>
182
207
  )}
183
208
  </div>
@@ -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;