@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
|
|
2
|
+
import * as chai from "chai";
|
|
3
|
+
import {
|
|
4
|
+
findAlignmentDifferences,
|
|
5
|
+
groupConsecutiveDifferences
|
|
6
|
+
} from "./findAlignmentDifferences";
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
const { expect } = chai;
|
|
10
|
+
|
|
11
|
+
describe("findAlignmentDifferences", () => {
|
|
12
|
+
it("returns empty array when fewer than 2 tracks", () => {
|
|
13
|
+
expect(findAlignmentDifferences([])).to.deep.equal([]);
|
|
14
|
+
expect(findAlignmentDifferences(["ACGT"])).to.deep.equal([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns empty array when template is empty", () => {
|
|
18
|
+
expect(findAlignmentDifferences(["", ""])).to.deep.equal([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns empty array when sequences are identical", () => {
|
|
22
|
+
expect(findAlignmentDifferences(["ACGT", "ACGT"])).to.deep.equal([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("detects a single mismatch", () => {
|
|
26
|
+
// pos 1: A vs T
|
|
27
|
+
const diffs = findAlignmentDifferences(["ACGT", "ATGT"]);
|
|
28
|
+
expect(diffs).to.have.lengthOf(1);
|
|
29
|
+
expect(diffs[0]).to.deep.include({ position: 1, type: "mismatch" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("detects multiple mismatches", () => {
|
|
33
|
+
const diffs = findAlignmentDifferences(["AAAA", "TATA"]);
|
|
34
|
+
expect(diffs).to.have.lengthOf(2);
|
|
35
|
+
expect(diffs.map(d => d.position)).to.deep.equal([0, 2]);
|
|
36
|
+
diffs.forEach(d => expect(d.type).to.equal("mismatch"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("detects mismatches in protein sequences (amino acids)", () => {
|
|
40
|
+
const diffs = findAlignmentDifferences(["MAST", "MAST"]);
|
|
41
|
+
expect(diffs).to.have.lengthOf(0);
|
|
42
|
+
|
|
43
|
+
const diffs2 = findAlignmentDifferences(["MAST", "MVST"]);
|
|
44
|
+
expect(diffs2).to.have.lengthOf(1);
|
|
45
|
+
expect(diffs2[0]).to.deep.include({ position: 1, type: "mismatch" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("is case-insensitive", () => {
|
|
49
|
+
const diffs = findAlignmentDifferences(["ACGT", "acgt"]);
|
|
50
|
+
expect(diffs).to.have.lengthOf(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("detects a single-base insertion (gap in template)", () => {
|
|
54
|
+
// template: A-GT, query: ACGT → position 1 is an insertion
|
|
55
|
+
const diffs = findAlignmentDifferences(["A-GT", "ACGT"]);
|
|
56
|
+
const insertions = diffs.filter(d => d.type === "insertion");
|
|
57
|
+
expect(insertions).to.have.lengthOf(1);
|
|
58
|
+
expect(insertions[0].position).to.equal(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("detects a multi-base insertion", () => {
|
|
62
|
+
// template: A---GT, query: ACCCGT
|
|
63
|
+
const diffs = findAlignmentDifferences(["A---GT", "ACCCGT"]);
|
|
64
|
+
const insertions = diffs.filter(d => d.type === "insertion");
|
|
65
|
+
expect(insertions).to.have.lengthOf(3);
|
|
66
|
+
expect(insertions.map(d => d.position)).to.deep.equal([1, 2, 3]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("detects a single-base deletion (gap in non-template)", () => {
|
|
70
|
+
// template: ACGT, query: A-GT → position 1 is a deletion
|
|
71
|
+
const diffs = findAlignmentDifferences(["ACGT", "A-GT"]);
|
|
72
|
+
const deletions = diffs.filter(d => d.type === "deletion");
|
|
73
|
+
expect(deletions).to.have.lengthOf(1);
|
|
74
|
+
expect(deletions[0].position).to.equal(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("detects a multi-base deletion", () => {
|
|
78
|
+
// template: ACCCGT, query: A---GT
|
|
79
|
+
const diffs = findAlignmentDifferences(["ACCCGT", "A---GT"]);
|
|
80
|
+
const deletions = diffs.filter(d => d.type === "deletion");
|
|
81
|
+
expect(deletions).to.have.lengthOf(3);
|
|
82
|
+
expect(deletions.map(d => d.position)).to.deep.equal([1, 2, 3]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("detects leading non-aligned region", () => {
|
|
86
|
+
// query hasn't started yet at position 0-1
|
|
87
|
+
const diffs = findAlignmentDifferences(["ACGT", "--GT"]);
|
|
88
|
+
const gaps = diffs.filter(d => d.type === "gap");
|
|
89
|
+
expect(gaps).to.have.lengthOf(2);
|
|
90
|
+
expect(gaps.map(d => d.position)).to.deep.equal([0, 1]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("detects trailing non-aligned region", () => {
|
|
94
|
+
// query has ended after position 1
|
|
95
|
+
const diffs = findAlignmentDifferences(["ACGT", "AC--"]);
|
|
96
|
+
const gaps = diffs.filter(d => d.type === "gap");
|
|
97
|
+
expect(gaps).to.have.lengthOf(2);
|
|
98
|
+
expect(gaps.map(d => d.position)).to.deep.equal([2, 3]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("detects both leading and trailing non-aligned regions", () => {
|
|
102
|
+
const diffs = findAlignmentDifferences(["ACGTAC", "-CGTA-"]);
|
|
103
|
+
const gaps = diffs.filter(d => d.type === "gap");
|
|
104
|
+
expect(gaps.map(d => d.position)).to.deep.equal([0, 5]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("fully-gapped query returns all positions as gap", () => {
|
|
108
|
+
const diffs = findAlignmentDifferences(["ACGT", "----"]);
|
|
109
|
+
expect(diffs).to.have.lengthOf(4);
|
|
110
|
+
diffs.forEach(d => expect(d.type).to.equal("gap"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("handles mixed difference types in one alignment", () => {
|
|
114
|
+
// template: AACGT
|
|
115
|
+
// query: -ACGG
|
|
116
|
+
// pos 0: gap (leading '-' in query), pos 4: mismatch (T vs G)
|
|
117
|
+
const diffs = findAlignmentDifferences(["AACGT", "-ACGG"]);
|
|
118
|
+
expect(diffs.find(d => d.position === 0)?.type).to.equal("gap");
|
|
119
|
+
expect(diffs.find(d => d.position === 4)?.type).to.equal("mismatch");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("handles multiple non-template tracks", () => {
|
|
123
|
+
// template: ACGT
|
|
124
|
+
// track1: ATGT → mismatch at pos 1
|
|
125
|
+
// track2: AC-T → deletion at pos 2
|
|
126
|
+
const diffs = findAlignmentDifferences(["ACGT", "ATGT", "AC-T"]);
|
|
127
|
+
expect(diffs.find(d => d.position === 1)?.type).to.equal("mismatch");
|
|
128
|
+
expect(diffs.find(d => d.position === 2)?.type).to.equal("deletion");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("includes all track bases in returned diff object", () => {
|
|
132
|
+
// mismatch at position 1: template 'C' vs non-template 'T'
|
|
133
|
+
const diffs = findAlignmentDifferences(["ACGT", "ATGT"]);
|
|
134
|
+
expect(diffs[0].bases).to.have.lengthOf(2);
|
|
135
|
+
expect(diffs[0].bases[0]).to.equal("c"); // template at pos 1 (lowercased)
|
|
136
|
+
expect(diffs[0].bases[1]).to.equal("t"); // non-template at pos 1 (lowercased)
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("groupConsecutiveDifferences", () => {
|
|
141
|
+
it("returns empty array for empty input", () => {
|
|
142
|
+
expect(groupConsecutiveDifferences([])).to.deep.equal([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("does not group mismatches — each stays individual", () => {
|
|
146
|
+
// 4 consecutive mismatches → 4 separate entries
|
|
147
|
+
const diffs = findAlignmentDifferences(["AAAA", "TTTT"]);
|
|
148
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
149
|
+
expect(grouped).to.have.lengthOf(4);
|
|
150
|
+
grouped.forEach((g, i) => {
|
|
151
|
+
expect(g.type).to.equal("mismatch");
|
|
152
|
+
expect(g.start).to.equal(i);
|
|
153
|
+
expect(g.end).to.equal(i);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("groups consecutive deletions into one region", () => {
|
|
158
|
+
// template: ACCCGT, query: A---GT → 3 consecutive deletions
|
|
159
|
+
const diffs = findAlignmentDifferences(["ACCCGT", "A---GT"]);
|
|
160
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
161
|
+
expect(grouped).to.have.lengthOf(1);
|
|
162
|
+
expect(grouped[0]).to.deep.include({ type: "deletion", start: 1, end: 3 });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("groups consecutive insertions into one region", () => {
|
|
166
|
+
// template: A---GT, query: ACCCGT → 3 consecutive insertions
|
|
167
|
+
const diffs = findAlignmentDifferences(["A---GT", "ACCCGT"]);
|
|
168
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
169
|
+
expect(grouped).to.have.lengthOf(1);
|
|
170
|
+
expect(grouped[0]).to.deep.include({ type: "insertion", start: 1, end: 3 });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("groups consecutive gaps into one region", () => {
|
|
174
|
+
// leading 2-base gap + trailing 2-base gap
|
|
175
|
+
const diffs = findAlignmentDifferences(["ACGTAC", "--GT--"]);
|
|
176
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
177
|
+
expect(grouped).to.have.lengthOf(2);
|
|
178
|
+
expect(grouped[0]).to.deep.include({ type: "gap", start: 0, end: 1 });
|
|
179
|
+
expect(grouped[1]).to.deep.include({ type: "gap", start: 4, end: 5 });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("keeps non-consecutive same-type differences separate", () => {
|
|
183
|
+
// two deletions separated by a match; ends with base so trailing '-' is still aligned
|
|
184
|
+
// template: ACACA, query: A-A-A → deletions at pos 1 and 3, matches elsewhere
|
|
185
|
+
const diffs = findAlignmentDifferences(["ACACA", "A-A-A"]);
|
|
186
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
187
|
+
expect(grouped).to.have.lengthOf(2);
|
|
188
|
+
expect(grouped[0]).to.deep.include({ type: "deletion", start: 1, end: 1 });
|
|
189
|
+
expect(grouped[1]).to.deep.include({ type: "deletion", start: 3, end: 3 });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("handles mixed types — groups each type's runs independently", () => {
|
|
193
|
+
// template: A--CGT, query: ACCC-T
|
|
194
|
+
// pos 0: A vs A → match
|
|
195
|
+
// pos 1,2: template '-', query 'C','C' → insertions
|
|
196
|
+
// pos 3: C vs C → match
|
|
197
|
+
// pos 4: G vs '-' → deletion
|
|
198
|
+
// pos 5: T vs T → match
|
|
199
|
+
const diffs = findAlignmentDifferences(["A--CGT", "ACCC-T"]);
|
|
200
|
+
const grouped = groupConsecutiveDifferences(diffs);
|
|
201
|
+
const insertionGroups = grouped.filter(g => g.type === "insertion");
|
|
202
|
+
const deletionGroups = grouped.filter(g => g.type === "deletion");
|
|
203
|
+
expect(insertionGroups).to.have.lengthOf(1);
|
|
204
|
+
expect(insertionGroups[0]).to.deep.include({ start: 1, end: 2 });
|
|
205
|
+
expect(deletionGroups).to.have.lengthOf(1);
|
|
206
|
+
expect(deletionGroups[0]).to.deep.include({ start: 4, end: 4 });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -30,6 +30,8 @@ import ReactDOM from "react-dom";
|
|
|
30
30
|
|
|
31
31
|
import { NonReduxEnhancedLinearView } from "../LinearView";
|
|
32
32
|
import Minimap, { getTrimmedRangesToDisplay } from "./Minimap";
|
|
33
|
+
import FindMismatches from "./Mismatches";
|
|
34
|
+
import AlignmentSearchBar from "./AlignmentSearchBar";
|
|
33
35
|
import { compose, branch, renderComponent } from "recompose";
|
|
34
36
|
import AlignmentVisibilityTool from "./AlignmentVisibilityTool";
|
|
35
37
|
import * as alignmentActions from "../redux/alignments";
|
|
@@ -160,6 +162,12 @@ export const AlignmentView = props => {
|
|
|
160
162
|
const [tempTrimBefore, setTempTrimBefore] = useState({});
|
|
161
163
|
const [tempTrimAfter, setTempTrimAfter] = useState({});
|
|
162
164
|
const [tempTrimmingCaret, setTempTrimmingCaret] = useState({});
|
|
165
|
+
const [searchMatchLayers, setSearchMatchLayers] = React.useState([]);
|
|
166
|
+
const [activeFilterType, setActiveFilterType] = useState("all");
|
|
167
|
+
|
|
168
|
+
const handleFilterChange = useCallback(({ activeFilter }) => {
|
|
169
|
+
setActiveFilterType(activeFilter);
|
|
170
|
+
}, []);
|
|
163
171
|
const bindOutsideChangeHelper = useRef({});
|
|
164
172
|
const alignmentHolder = useRef(null);
|
|
165
173
|
const alignmentHolderTop = useRef(null);
|
|
@@ -1014,7 +1022,16 @@ export const AlignmentView = props => {
|
|
|
1014
1022
|
chromatogramData
|
|
1015
1023
|
})
|
|
1016
1024
|
: linearViewOptions))}
|
|
1017
|
-
additionalSelectionLayers={
|
|
1025
|
+
additionalSelectionLayers={[
|
|
1026
|
+
...(i !== 0
|
|
1027
|
+
? (additionalSelectionLayers || []).filter(layer =>
|
|
1028
|
+
activeFilterType === "all"
|
|
1029
|
+
? layer.differenceType !== "gap"
|
|
1030
|
+
: layer.differenceType === activeFilterType
|
|
1031
|
+
)
|
|
1032
|
+
: additionalSelectionLayers || []),
|
|
1033
|
+
...(searchMatchLayers || [])
|
|
1034
|
+
]}
|
|
1018
1035
|
dimensions={{
|
|
1019
1036
|
width: linearViewWidth
|
|
1020
1037
|
}}
|
|
@@ -1614,7 +1631,7 @@ export const AlignmentView = props => {
|
|
|
1614
1631
|
display: "flex",
|
|
1615
1632
|
minHeight: "32px",
|
|
1616
1633
|
width: "100%",
|
|
1617
|
-
flexWrap: "
|
|
1634
|
+
flexWrap: "wrap",
|
|
1618
1635
|
flexDirection: "row",
|
|
1619
1636
|
flex: "0 0 auto"
|
|
1620
1637
|
}}
|
|
@@ -1747,6 +1764,16 @@ export const AlignmentView = props => {
|
|
|
1747
1764
|
{...alignmentVisibilityToolOptions}
|
|
1748
1765
|
/>
|
|
1749
1766
|
)}
|
|
1767
|
+
<AlignmentSearchBar
|
|
1768
|
+
alignmentTracks={alignmentTracks}
|
|
1769
|
+
id={id}
|
|
1770
|
+
setSearchMatchLayers={setSearchMatchLayers}
|
|
1771
|
+
/>
|
|
1772
|
+
<FindMismatches
|
|
1773
|
+
alignmentJson={alignmentTracks}
|
|
1774
|
+
id={id}
|
|
1775
|
+
onFilterChange={handleFilterChange}
|
|
1776
|
+
/>
|
|
1750
1777
|
{additionalTopEl}
|
|
1751
1778
|
{saveMessage && (
|
|
1752
1779
|
<div
|
|
@@ -1857,6 +1884,7 @@ export const AlignmentView = props => {
|
|
|
1857
1884
|
</>
|
|
1858
1885
|
}
|
|
1859
1886
|
alignmentTracks={alignmentTracks}
|
|
1887
|
+
activeFilterType={activeFilterType}
|
|
1860
1888
|
dimensions={{
|
|
1861
1889
|
width: Math.max(width, 10) || 10
|
|
1862
1890
|
}}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
.alignment-search-bar .bp3-input {
|
|
2
|
+
border-radius: 5px !important;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
.tg-pinch-helper {
|
|
2
6
|
width: 100%;
|
|
3
7
|
}
|
|
@@ -60,6 +64,10 @@
|
|
|
60
64
|
/* .alignmentViewTrackContainer:hover .alignmentTrackNameDiv {
|
|
61
65
|
opacity: 1 !important;
|
|
62
66
|
} */
|
|
67
|
+
.ve-alignment-top-bar {
|
|
68
|
+
align-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
.ve-alignment-top-bar > * {
|
|
64
72
|
overflow-wrap: normal;
|
|
65
73
|
flex: 0 0 auto;
|
|
@@ -76,6 +84,82 @@
|
|
|
76
84
|
.veAlignmentMismatch {
|
|
77
85
|
opacity: 0.9;
|
|
78
86
|
}
|
|
87
|
+
|
|
88
|
+
.veDiffNavigator {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 4px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Menu item content layout */
|
|
95
|
+
.veDiffMenuItem-inner {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 6px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Navigation pill — groups prev/counter/next into one unit */
|
|
102
|
+
.veDiffNav {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
border-radius: 3px;
|
|
106
|
+
background: rgba(92, 112, 128, 0.06);
|
|
107
|
+
border: 1px solid rgba(92, 112, 128, 0.18);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.veDiffNav-center {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: baseline;
|
|
113
|
+
gap: 3px;
|
|
114
|
+
padding: 0 6px;
|
|
115
|
+
min-width: 64px;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.veDiffNav-fraction {
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
font-variant-numeric: tabular-nums;
|
|
122
|
+
color: #5c7080;
|
|
123
|
+
line-height: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.veDiffNav-sep {
|
|
127
|
+
margin: 0 1px;
|
|
128
|
+
opacity: 0.45;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Position number — monospace fits sequence coordinates */
|
|
132
|
+
.veDiffNav-pos {
|
|
133
|
+
font-size: 10px;
|
|
134
|
+
font-family: monospace;
|
|
135
|
+
font-variant-numeric: tabular-nums;
|
|
136
|
+
color: #a7b6c2;
|
|
137
|
+
line-height: 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.veDiffNav-empty {
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-style: italic;
|
|
143
|
+
color: #a7b6c2;
|
|
144
|
+
padding: 0 4px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.bp3-dark .veDiffNav {
|
|
148
|
+
background: rgba(167, 182, 194, 0.05);
|
|
149
|
+
border-color: rgba(167, 182, 194, 0.14);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bp3-dark .veDiffNav-fraction {
|
|
153
|
+
color: #5c7080;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.bp3-dark .veDiffNav-pos {
|
|
157
|
+
color: #4f6272;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.bp3-dark .veDiffNav-empty {
|
|
161
|
+
color: #5c7080;
|
|
162
|
+
}
|
|
79
163
|
.veRowItem:has(.rowViewTextContainer) .veAlignmentMismatch {
|
|
80
164
|
opacity: 0.5;
|
|
81
165
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Mismatch
|
|
5
|
+
* @property {number} position
|
|
6
|
+
* @property {string[]} bases
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} FindMismatchesProps
|
|
11
|
+
* @property {Array<{
|
|
12
|
+
* alignmentData: {
|
|
13
|
+
* sequence: string
|
|
14
|
+
* }
|
|
15
|
+
* }>} alignmentJson
|
|
16
|
+
* @property {string} id
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export function scrollToAlignmentSelection() {
|
|
20
|
+
const el = document.querySelector(".veCaret");
|
|
21
|
+
if (el) {
|
|
22
|
+
el.scrollIntoView({ inline: "center", block: "nearest" });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function updateCaretPosition({ start, end }) {
|
|
27
|
+
if (window.updateAlignmentSelection) {
|
|
28
|
+
window.updateAlignmentSelection({ start, end });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -5,11 +5,15 @@ import {
|
|
|
5
5
|
aminoAcidShortNames
|
|
6
6
|
} from "./calculateAminoAcidFrequency";
|
|
7
7
|
import { Button } from "@blueprintjs/core";
|
|
8
|
+
import {
|
|
9
|
+
scrollToAlignmentSelection,
|
|
10
|
+
updateCaretPosition
|
|
11
|
+
} from "../AlignmentView/utils";
|
|
12
|
+
import { DataTable } from "@teselagen/ui";
|
|
8
13
|
|
|
9
14
|
export default ({ properties, setProperties, style }) => {
|
|
10
15
|
const sidebarRef = React.useRef(null);
|
|
11
16
|
const [mismatchesCount, setMismatchesCount] = React.useState(0);
|
|
12
|
-
const [mismatchesInRange, setMismatchesInRange] = React.useState(0);
|
|
13
17
|
|
|
14
18
|
const { track, isOpen, selection, isPairwise } = properties;
|
|
15
19
|
|
|
@@ -33,6 +37,33 @@ export default ({ properties, setProperties, style }) => {
|
|
|
33
37
|
return isPairwise ? tr.filter(m => m?.color === "red") : tr;
|
|
34
38
|
}, [track, mismatchKey, isPairwise]);
|
|
35
39
|
|
|
40
|
+
const mismatchSchema = useMemo(
|
|
41
|
+
() => ({
|
|
42
|
+
fields: [
|
|
43
|
+
{
|
|
44
|
+
path: "start",
|
|
45
|
+
type: "number",
|
|
46
|
+
displayName: "Start",
|
|
47
|
+
render: val => val + 1
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: "end",
|
|
51
|
+
type: "number",
|
|
52
|
+
displayName: "End",
|
|
53
|
+
render: val => val + 1
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}),
|
|
57
|
+
[]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const mismatchEntities = useMemo(() => {
|
|
61
|
+
return (trackMismatches || []).map((m, i) => ({
|
|
62
|
+
...m,
|
|
63
|
+
id: i.toString()
|
|
64
|
+
}));
|
|
65
|
+
}, [trackMismatches]);
|
|
66
|
+
|
|
36
67
|
useEffect(() => {
|
|
37
68
|
if (!isOpen || sidebarRef.current === null || !track) {
|
|
38
69
|
return;
|
|
@@ -54,24 +85,6 @@ export default ({ properties, setProperties, style }) => {
|
|
|
54
85
|
});
|
|
55
86
|
|
|
56
87
|
setMismatchesCount(mismatchCount);
|
|
57
|
-
setMismatchesInRange(mismatchCount);
|
|
58
|
-
|
|
59
|
-
if (selection && selection.start > -1 && selection.end > -1) {
|
|
60
|
-
let count = 0;
|
|
61
|
-
|
|
62
|
-
trackMismatches?.forEach(tm => {
|
|
63
|
-
if (tm === null || tm.start === null || tm.end === null) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const overlapStart = Math.max(tm.start, selection.start);
|
|
68
|
-
const overlapEnd = Math.min(tm.end, selection.end);
|
|
69
|
-
if (overlapEnd >= overlapStart) {
|
|
70
|
-
count += overlapEnd - overlapStart + 1;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
setMismatchesInRange(count);
|
|
74
|
-
}
|
|
75
88
|
}, [isOpen, track, selection, trackMismatches]);
|
|
76
89
|
|
|
77
90
|
const aminoFreq = useMemo(() => {
|
|
@@ -110,7 +123,7 @@ export default ({ properties, setProperties, style }) => {
|
|
|
110
123
|
width: "100%"
|
|
111
124
|
}}
|
|
112
125
|
></div>
|
|
113
|
-
<
|
|
126
|
+
<HeaderItem title="Track Properties" />
|
|
114
127
|
|
|
115
128
|
<div className="bp3-tab-panel">
|
|
116
129
|
<RowItem item={name} title="Name" />
|
|
@@ -129,10 +142,6 @@ export default ({ properties, setProperties, style }) => {
|
|
|
129
142
|
/>
|
|
130
143
|
</>
|
|
131
144
|
)}
|
|
132
|
-
<RowItem
|
|
133
|
-
item={`${mismatchesInRange}/${mismatchesCount}`}
|
|
134
|
-
title="Mismatches"
|
|
135
|
-
/>
|
|
136
145
|
<RowItem
|
|
137
146
|
item={
|
|
138
147
|
selection && selection.start > -1 ? (
|
|
@@ -145,8 +154,38 @@ export default ({ properties, setProperties, style }) => {
|
|
|
145
154
|
}
|
|
146
155
|
title="Region"
|
|
147
156
|
/>
|
|
157
|
+
<HeaderItem title={`Mismatches (${mismatchesCount})`} />
|
|
158
|
+
|
|
159
|
+
{trackMismatches && trackMismatches.length > 0 && (
|
|
160
|
+
<div
|
|
161
|
+
style={{
|
|
162
|
+
margin: "0px 10px"
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<DataTable
|
|
166
|
+
formName="mismatchesTable"
|
|
167
|
+
isSimple
|
|
168
|
+
noHeader
|
|
169
|
+
noFooter
|
|
170
|
+
withSearch={false}
|
|
171
|
+
noPadding
|
|
172
|
+
compact
|
|
173
|
+
maxHeight={150}
|
|
174
|
+
entities={mismatchEntities}
|
|
175
|
+
schema={mismatchSchema}
|
|
176
|
+
onRowClick={(e, row) => {
|
|
177
|
+
updateCaretPosition({ start: row.start, end: row.end });
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
scrollToAlignmentSelection();
|
|
180
|
+
}, 0);
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
148
185
|
</div>
|
|
149
|
-
<
|
|
186
|
+
<HeaderItem
|
|
187
|
+
title={`${isProtein ? "Amino Acid" : "Base Pair"} Frequencies`}
|
|
188
|
+
/>
|
|
150
189
|
<div className="sidebar-table">
|
|
151
190
|
<div className="sidebar-row">
|
|
152
191
|
<div className="sidebar-cell">
|
|
@@ -225,15 +264,37 @@ export default ({ properties, setProperties, style }) => {
|
|
|
225
264
|
};
|
|
226
265
|
|
|
227
266
|
function RowItem({ item, title, units }) {
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
// Skip rendering when the value is absent (null or undefined),
|
|
268
|
+
// but still allow 0 and other falsy-but-valid values.
|
|
269
|
+
if (item == null) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
230
272
|
const propertyClass = title.split(" ").join("-").toLowerCase();
|
|
231
273
|
return (
|
|
232
274
|
<div className={`ve-flex-row property-${propertyClass}`}>
|
|
233
|
-
<div className="ve-column-left">
|
|
275
|
+
<div style={{ fontWeight: "bold" }} className="ve-column-left">
|
|
276
|
+
{title}
|
|
277
|
+
</div>
|
|
234
278
|
<div className="ve-column-right">
|
|
235
279
|
{item} {units ?? ""}
|
|
236
280
|
</div>
|
|
237
281
|
</div>
|
|
238
282
|
);
|
|
239
283
|
}
|
|
284
|
+
|
|
285
|
+
const HeaderItem = ({ title }) => {
|
|
286
|
+
return (
|
|
287
|
+
<h5
|
|
288
|
+
style={{
|
|
289
|
+
margin: 0,
|
|
290
|
+
fontSize: 15,
|
|
291
|
+
fontWeight: "bold",
|
|
292
|
+
textAlign: "center",
|
|
293
|
+
padding: "5px 0",
|
|
294
|
+
borderBottom: "1px solid #f1f1f1"
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{title}
|
|
298
|
+
</h5>
|
|
299
|
+
);
|
|
300
|
+
};
|