@jbrowse/plugin-breakpoint-split-view 2.14.0 → 2.15.1

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 (35) hide show
  1. package/dist/BreakpointSplitView/BreakpointSplitView.d.ts +29 -1
  2. package/dist/BreakpointSplitView/BreakpointSplitView.js +71 -32
  3. package/dist/BreakpointSplitView/components/AlignmentConnections.js +43 -12
  4. package/dist/BreakpointSplitView/components/BreakpointSplitView.js +12 -10
  5. package/dist/BreakpointSplitView/components/BreakpointSplitViewOverlay.js +6 -5
  6. package/dist/BreakpointSplitView/components/Overlay.js +8 -3
  7. package/dist/BreakpointSplitView/components/PairedFeatures.d.ts +9 -0
  8. package/dist/BreakpointSplitView/components/PairedFeatures.js +100 -0
  9. package/dist/BreakpointSplitView/components/getOrientationColor.d.ts +41 -0
  10. package/dist/BreakpointSplitView/components/getOrientationColor.js +111 -0
  11. package/dist/BreakpointSplitView/components/util.d.ts +1 -0
  12. package/dist/BreakpointSplitView/components/util.js +26 -0
  13. package/dist/BreakpointSplitView/model.d.ts +24 -5
  14. package/dist/BreakpointSplitView/model.js +35 -7
  15. package/dist/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.js +5 -8
  16. package/dist/BreakpointSplitView/util.d.ts +1 -0
  17. package/dist/BreakpointSplitView/util.js +1 -0
  18. package/esm/BreakpointSplitView/BreakpointSplitView.d.ts +29 -1
  19. package/esm/BreakpointSplitView/BreakpointSplitView.js +71 -32
  20. package/esm/BreakpointSplitView/components/AlignmentConnections.js +44 -13
  21. package/esm/BreakpointSplitView/components/BreakpointSplitView.js +11 -9
  22. package/esm/BreakpointSplitView/components/BreakpointSplitViewOverlay.js +6 -5
  23. package/esm/BreakpointSplitView/components/Overlay.js +8 -3
  24. package/esm/BreakpointSplitView/components/PairedFeatures.d.ts +9 -0
  25. package/esm/BreakpointSplitView/components/PairedFeatures.js +75 -0
  26. package/esm/BreakpointSplitView/components/getOrientationColor.d.ts +41 -0
  27. package/esm/BreakpointSplitView/components/getOrientationColor.js +103 -0
  28. package/esm/BreakpointSplitView/components/util.d.ts +1 -0
  29. package/esm/BreakpointSplitView/components/util.js +25 -0
  30. package/esm/BreakpointSplitView/model.d.ts +24 -5
  31. package/esm/BreakpointSplitView/model.js +35 -7
  32. package/esm/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.js +5 -8
  33. package/esm/BreakpointSplitView/util.d.ts +1 -0
  34. package/esm/BreakpointSplitView/util.js +1 -1
  35. package/package.json +4 -4
@@ -6,6 +6,7 @@ exports.hasPairedReads = hasPairedReads;
6
6
  exports.findMatchingAlt = findMatchingAlt;
7
7
  exports.getMatchedBreakendFeatures = getMatchedBreakendFeatures;
8
8
  exports.getMatchedTranslocationFeatures = getMatchedTranslocationFeatures;
9
+ exports.getMatchedPairedFeatures = getMatchedPairedFeatures;
9
10
  const util_1 = require("@jbrowse/core/util");
10
11
  const vcf_1 = require("@gmod/vcf");
11
12
  // this finds candidate alignment features, aimed at plotting split reads from
@@ -115,3 +116,28 @@ function getMatchedTranslocationFeatures(feats) {
115
116
  }
116
117
  return ret;
117
118
  }
119
+ // Getting "matched" TRA means just return all TRA
120
+ function getMatchedPairedFeatures(feats) {
121
+ const candidates = new Map();
122
+ const alreadySeen = new Set();
123
+ for (const f of feats.values()) {
124
+ if (!alreadySeen.has(f.id()) && f.get('type') === 'paired_feature') {
125
+ const r1 = f.id().replace('-r1', '');
126
+ const r2 = f.id().replace('-r2', '');
127
+ if (f.id().endsWith('-r1')) {
128
+ if (!candidates.get(r1)) {
129
+ candidates.set(r1, []);
130
+ }
131
+ candidates.get(r1).push(f);
132
+ }
133
+ else if (f.id().endsWith('-r2')) {
134
+ if (!candidates.get(r2)) {
135
+ candidates.set(r2, []);
136
+ }
137
+ candidates.get(r2).push(f);
138
+ }
139
+ }
140
+ alreadySeen.add(f.id());
141
+ }
142
+ return [...candidates.values()].filter(v => v.length > 1);
143
+ }
@@ -282,9 +282,14 @@ export default function stateModelFactory(pluginManager: PluginManager): import(
282
282
  } & {
283
283
  /**
284
284
  * #getter
285
- * Find all track ids that match across multiple views
285
+ * Find all track ids that match across multiple views, or return just
286
+ * the single view's track if only a single row is used
286
287
  */
287
- readonly matchedTracks: any[];
288
+ readonly matchedTracks: (import("mobx-state-tree").IMSTArray<import("mobx-state-tree").IAnyType> & import("mobx-state-tree").IStateTreeNode<import("mobx-state-tree").IArrayType<import("mobx-state-tree").IAnyType>>) | {
289
+ configuration: {
290
+ trackId: string;
291
+ };
292
+ }[];
288
293
  /**
289
294
  * #method
290
295
  * Get tracks with a given trackId across multiple views
@@ -292,11 +297,15 @@ export default function stateModelFactory(pluginManager: PluginManager): import(
292
297
  getMatchedTracks(trackConfigId: string): any[];
293
298
  /**
294
299
  * #method
295
- *
296
- * Translocation features are handled differently
297
- * since they do not have a mate e.g. they are one sided
300
+ * Translocation features are handled differently since they do not have
301
+ * a mate e.g. they are one sided
298
302
  */
299
303
  hasTranslocations(trackConfigId: string): Feature | undefined;
304
+ /**
305
+ * #method
306
+ * Paired features similar to breakends, but simpler, like BEDPE
307
+ */
308
+ hasPairedFeatures(trackConfigId: string): Feature | undefined;
300
309
  /**
301
310
  * #method
302
311
  * Get a composite map of featureId-\>feature map for a track across
@@ -338,6 +347,10 @@ export default function stateModelFactory(pluginManager: PluginManager): import(
338
347
  * #action
339
348
  */
340
349
  setMatchedTrackFeatures(obj: Record<string, Feature[][]>): void;
350
+ /**
351
+ * #action
352
+ */
353
+ reverseViewOrder(): void;
341
354
  } & {
342
355
  afterAttach(): void;
343
356
  /**
@@ -346,6 +359,12 @@ export default function stateModelFactory(pluginManager: PluginManager): import(
346
359
  menuItems(): ({
347
360
  label: string;
348
361
  subMenu: import("@jbrowse/core/ui").MenuItem[];
362
+ } | {
363
+ label: string;
364
+ onClick: () => void;
365
+ type?: undefined;
366
+ checked?: undefined;
367
+ icon?: undefined;
349
368
  } | {
350
369
  label: string;
351
370
  type: string;
@@ -46,9 +46,13 @@ function calc(track, f) {
46
46
  return (_b = (_a = track.displays[0]).searchFeatureByID) === null || _b === void 0 ? void 0 : _b.call(_a, f.id());
47
47
  }
48
48
  async function getBlockFeatures(model, track) {
49
+ var _a;
49
50
  const { views } = model;
50
51
  const { rpcManager, assemblyManager } = (0, util_1.getSession)(model);
51
- const assemblyName = model.views[0].assemblyNames[0];
52
+ const assemblyName = (_a = model.views[0]) === null || _a === void 0 ? void 0 : _a.assemblyNames[0];
53
+ if (!assemblyName) {
54
+ return undefined;
55
+ }
52
56
  const assembly = await assemblyManager.waitForAssembly(assemblyName);
53
57
  if (!assembly) {
54
58
  return undefined; // throw new Error(`assembly not found: "${assemblyName}"`)
@@ -119,10 +123,13 @@ function stateModelFactory(pluginManager) {
119
123
  .views(self => ({
120
124
  /**
121
125
  * #getter
122
- * Find all track ids that match across multiple views
126
+ * Find all track ids that match across multiple views, or return just
127
+ * the single view's track if only a single row is used
123
128
  */
124
129
  get matchedTracks() {
125
- return (0, util_2.intersect)(elt => elt.configuration.trackId, ...self.views.map(view => view.tracks));
130
+ return self.views.length === 1
131
+ ? self.views[0].tracks
132
+ : (0, util_2.intersect)(elt => elt.configuration.trackId, ...self.views.map(view => view.tracks));
126
133
  },
127
134
  /**
128
135
  * #method
@@ -135,13 +142,19 @@ function stateModelFactory(pluginManager) {
135
142
  },
136
143
  /**
137
144
  * #method
138
- *
139
- * Translocation features are handled differently
140
- * since they do not have a mate e.g. they are one sided
145
+ * Translocation features are handled differently since they do not have
146
+ * a mate e.g. they are one sided
141
147
  */
142
148
  hasTranslocations(trackConfigId) {
143
149
  return [...this.getTrackFeatures(trackConfigId).values()].find(f => f.get('type') === 'translocation');
144
150
  },
151
+ /**
152
+ * #method
153
+ * Paired features similar to breakends, but simpler, like BEDPE
154
+ */
155
+ hasPairedFeatures(trackConfigId) {
156
+ return [...this.getTrackFeatures(trackConfigId).values()].find(f => f.get('type') === 'paired_feature');
157
+ },
145
158
  /**
146
159
  * #method
147
160
  * Get a composite map of featureId-\>feature map for a track across
@@ -239,6 +252,12 @@ function stateModelFactory(pluginManager) {
239
252
  setMatchedTrackFeatures(obj) {
240
253
  self.matchedTrackFeatures = obj;
241
254
  },
255
+ /**
256
+ * #action
257
+ */
258
+ reverseViewOrder() {
259
+ self.views.reverse();
260
+ },
242
261
  }))
243
262
  .actions(self => ({
244
263
  afterAttach() {
@@ -265,7 +284,16 @@ function stateModelFactory(pluginManager) {
265
284
  return [
266
285
  ...self.views
267
286
  .map((view, idx) => [idx, view.menuItems()])
268
- .map(f => ({ label: `View ${f[0] + 1} Menu`, subMenu: f[1] })),
287
+ .map(f => ({
288
+ label: `Row ${f[0] + 1} view menu`,
289
+ subMenu: f[1],
290
+ })),
291
+ {
292
+ label: 'Reverse view order',
293
+ onClick: () => {
294
+ self.reverseViewOrder();
295
+ },
296
+ },
269
297
  {
270
298
  label: 'Show intra-view links',
271
299
  type: 'checkbox',
@@ -38,10 +38,7 @@ async function renderToSvg(model, opts) {
38
38
  const trackLabelMaxLen = (0, util_2.getTrackNameMaxLen)(views, fontSize, session) + 40;
39
39
  const trackLabelOffset = trackLabels === 'left' ? trackLabelMaxLen : 0;
40
40
  const textOffset = trackLabels === 'offset' ? textHeight : 0;
41
- const trackOffsets = [
42
- (0, util_2.getTrackOffsets)(views[0], textOffset, fontSize + offset),
43
- (0, util_2.getTrackOffsets)(views[1], textOffset, fontSize + heights[0] + offset),
44
- ];
41
+ const trackOffsets = views.map((view, idx) => (0, util_2.getTrackOffsets)(view, textOffset, fontSize + (idx > 0 ? heights[idx - 1] : 0) + offset));
45
42
  const w = width + trackLabelOffset;
46
43
  const t = (0, ui_1.createJBrowseTheme)(theme);
47
44
  // the xlink namespace is used for rendering <image> tag
@@ -49,16 +46,16 @@ async function renderToSvg(model, opts) {
49
46
  react_1.default.createElement(Wrapper, null,
50
47
  react_1.default.createElement("svg", { width: width, height: totalHeightSvg, xmlns: "http://www.w3.org/2000/svg", xmlnsXlink: "http://www.w3.org/1999/xlink", viewBox: [0, 0, w + shift * 2, totalHeightSvg].toString() },
51
48
  react_1.default.createElement(SVGBackground_1.default, { width: w, height: totalHeightSvg, shift: shift }),
52
- react_1.default.createElement("g", { transform: `translate(${shift} ${fontSize})` },
49
+ views[0] ? (react_1.default.createElement("g", { transform: `translate(${shift} ${fontSize})` },
53
50
  react_1.default.createElement("g", { transform: `translate(${trackLabelOffset})` },
54
51
  react_1.default.createElement("text", { x: 0, fontSize: fontSize, fill: t.palette.text.primary }, views[0].assemblyNames.join(', ')),
55
52
  react_1.default.createElement(plugin_linear_genome_view_1.SVGRuler, { model: displayResults[0].view, fontSize: fontSize })),
56
- react_1.default.createElement(plugin_linear_genome_view_1.SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: displayResults[0].view, displayResults: displayResults[0].data, offset: offset, trackLabelOffset: trackLabelOffset })),
57
- react_1.default.createElement("g", { transform: `translate(${shift} ${fontSize + heights[0]})` },
53
+ react_1.default.createElement(plugin_linear_genome_view_1.SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: displayResults[0].view, displayResults: displayResults[0].data, offset: offset, trackLabelOffset: trackLabelOffset }))) : null,
54
+ views[1] ? (react_1.default.createElement("g", { transform: `translate(${shift} ${fontSize + heights[0]})` },
58
55
  react_1.default.createElement("g", { transform: `translate(${trackLabelOffset})` },
59
56
  react_1.default.createElement("text", { x: 0, fontSize: fontSize, fill: t.palette.text.primary }, views[1].assemblyNames.join(', ')),
60
57
  react_1.default.createElement(plugin_linear_genome_view_1.SVGRuler, { model: displayResults[1].view, fontSize: fontSize })),
61
- react_1.default.createElement(plugin_linear_genome_view_1.SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: displayResults[1].view, displayResults: displayResults[1].data, offset: offset, trackLabelOffset: trackLabelOffset })),
58
+ react_1.default.createElement(plugin_linear_genome_view_1.SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: displayResults[1].view, displayResults: displayResults[1].data, offset: offset, trackLabelOffset: trackLabelOffset }))) : null,
62
59
  react_1.default.createElement("defs", null,
63
60
  react_1.default.createElement("clipPath", { id: "clip-bsv" },
64
61
  react_1.default.createElement("rect", { x: 0, y: 0, width: width, height: totalHeightSvg }))),
@@ -11,6 +11,7 @@ interface Display {
11
11
  interface Track {
12
12
  displays: Display[];
13
13
  }
14
+ export declare function heightFromSpecificLevel(views: LGV[], trackId: string, level: number, getYPosOverride?: (trackId: string, level: number) => number): number;
14
15
  export declare function getPxFromCoordinate(view: LGV, refName: string, coord: number): number;
15
16
  export declare function yPos(trackId: string, level: number, views: LGV[], tracks: Track[], c: LayoutRecord, getYPosOverride?: (trackId: string, level: number) => number): number;
16
17
  export declare const useNextFrame: (variable: unknown) => void;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useNextFrame = void 0;
4
+ exports.heightFromSpecificLevel = heightFromSpecificLevel;
4
5
  exports.getPxFromCoordinate = getPxFromCoordinate;
5
6
  exports.yPos = yPos;
6
7
  exports.intersect = intersect;
@@ -1,8 +1,18 @@
1
1
  import { Feature, Region } from '@jbrowse/core/util';
2
2
  import ViewType from '@jbrowse/core/pluggableElementTypes/ViewType';
3
3
  import { IStateTreeNode } from 'mobx-state-tree';
4
+ import { Assembly } from '@jbrowse/core/assemblyManager/assembly';
4
5
  export default class BreakpointSplitViewType extends ViewType {
5
- snapshotFromBreakendFeature(feature: Feature, view: {
6
+ getBreakendCoveringRegions({ feature, assembly, }: {
7
+ feature: Feature;
8
+ assembly: Assembly;
9
+ }): {
10
+ pos: number;
11
+ refName: string | undefined;
12
+ mateRefName: string | undefined;
13
+ matePos: number;
14
+ };
15
+ singleLevelSnapshotFromBreakendFeature(feature: Feature, view: {
6
16
  displayedRegions: Region[];
7
17
  } & IStateTreeNode): {
8
18
  type: string;
@@ -21,4 +31,22 @@ export default class BreakpointSplitViewType extends ViewType {
21
31
  displayName: string;
22
32
  featureData: unknown;
23
33
  };
34
+ snapshotFromBreakendFeature(feature: Feature, view: {
35
+ displayedRegions: Region[];
36
+ } & IStateTreeNode): {
37
+ type: string;
38
+ views: {
39
+ type: string;
40
+ displayedRegions: {
41
+ start: number;
42
+ end: number;
43
+ refName: string;
44
+ assemblyName: string;
45
+ }[];
46
+ hideHeader: boolean;
47
+ bpPerPx: number;
48
+ offsetPx: number;
49
+ }[];
50
+ displayName: string;
51
+ };
24
52
  }
@@ -2,49 +2,29 @@ import { getSession } from '@jbrowse/core/util';
2
2
  import ViewType from '@jbrowse/core/pluggableElementTypes/ViewType';
3
3
  import { parseBreakend } from '@gmod/vcf';
4
4
  export default class BreakpointSplitViewType extends ViewType {
5
- snapshotFromBreakendFeature(feature, view) {
5
+ getBreakendCoveringRegions({ feature, assembly, }) {
6
6
  var _a;
7
7
  const alt = (_a = feature.get('ALT')) === null || _a === void 0 ? void 0 : _a[0];
8
8
  const bnd = alt ? parseBreakend(alt) : undefined;
9
9
  const startPos = feature.get('start');
10
+ const refName = feature.get('refName');
10
11
  let endPos;
11
- const bpPerPx = 10;
12
- // TODO: Figure this out for multiple assembly names
13
- const { assemblyName } = view.displayedRegions[0];
14
- const { assemblyManager } = getSession(view);
15
- const assembly = assemblyManager.get(assemblyName);
16
- if (!assembly) {
17
- throw new Error(`assembly ${assemblyName} not found`);
18
- }
19
- if (!assembly.regions) {
20
- throw new Error(`assembly ${assemblyName} regions not loaded`);
21
- }
22
- const featureRefName = assembly.getCanonicalRefName(feature.get('refName'));
23
- const topRegion = assembly.regions.find(f => f.refName === featureRefName);
24
12
  let mateRefName;
25
- let startMod = 0;
26
- let endMod = 0;
27
13
  // a VCF breakend feature
28
14
  if (alt === '<TRA>') {
29
15
  const INFO = feature.get('INFO');
30
16
  endPos = INFO.END[0] - 1;
31
- mateRefName = assembly.getCanonicalRefName(INFO.CHR2[0]);
17
+ mateRefName = INFO.CHR2[0];
32
18
  }
33
19
  else if (bnd === null || bnd === void 0 ? void 0 : bnd.MatePosition) {
34
20
  const matePosition = bnd.MatePosition.split(':');
35
21
  endPos = +matePosition[1] - 1;
36
- mateRefName = assembly.getCanonicalRefName(matePosition[0]);
37
- if (bnd.Join === 'left') {
38
- startMod = -1;
39
- }
40
- if (bnd.MateDirection === 'left') {
41
- endMod = -1;
42
- }
22
+ mateRefName = matePosition[0];
43
23
  }
44
24
  else if (feature.get('mate')) {
45
25
  // a generic 'mate' feature
46
26
  const mate = feature.get('mate');
47
- mateRefName = assembly.getCanonicalRefName(mate.refName);
27
+ mateRefName = mate.refName;
48
28
  endPos = mate.start;
49
29
  }
50
30
  else {
@@ -53,16 +33,76 @@ export default class BreakpointSplitViewType extends ViewType {
53
33
  if (!mateRefName) {
54
34
  throw new Error(`unable to resolve mate refName ${mateRefName} in reference genome`);
55
35
  }
36
+ return {
37
+ pos: startPos,
38
+ refName: assembly.getCanonicalRefName(refName),
39
+ mateRefName: assembly.getCanonicalRefName(mateRefName),
40
+ matePos: endPos,
41
+ };
42
+ }
43
+ singleLevelSnapshotFromBreakendFeature(feature, view) {
44
+ const session = getSession(view);
45
+ const bpPerPx = 10;
46
+ const { assemblyName } = view.displayedRegions[0];
47
+ const { assemblyManager } = session;
48
+ const assembly = assemblyManager.get(assemblyName);
49
+ if (!assembly) {
50
+ throw new Error(`assembly ${assemblyName} not found`);
51
+ }
52
+ if (!assembly.regions) {
53
+ throw new Error(`assembly ${assemblyName} regions not loaded`);
54
+ }
55
+ const { refName, pos: startPos, mateRefName, matePos: endPos, } = this.getBreakendCoveringRegions({
56
+ feature,
57
+ assembly,
58
+ });
59
+ const topRegion = assembly.regions.find(f => f.refName === refName);
56
60
  const bottomRegion = assembly.regions.find(f => f.refName === mateRefName);
57
- if (!topRegion || !bottomRegion) {
58
- throw new Error('unable to find the refName for the top or bottom of the breakpoint view');
61
+ const topMarkedRegion = [{ ...topRegion }, { ...topRegion }];
62
+ const bottomMarkedRegion = [{ ...bottomRegion }, { ...bottomRegion }];
63
+ topMarkedRegion[0].end = startPos;
64
+ topMarkedRegion[1].start = startPos;
65
+ bottomMarkedRegion[0].end = endPos;
66
+ bottomMarkedRegion[1].start = endPos;
67
+ return {
68
+ type: 'BreakpointSplitView',
69
+ views: [
70
+ {
71
+ type: 'LinearGenomeView',
72
+ displayedRegions: topMarkedRegion,
73
+ hideHeader: true,
74
+ bpPerPx,
75
+ offsetPx: (topRegion.start + feature.get('start')) / bpPerPx,
76
+ },
77
+ ],
78
+ displayName: `${feature.get('name') || feature.get('id') || 'breakend'} split detail`,
79
+ featureData: undefined,
80
+ };
81
+ }
82
+ snapshotFromBreakendFeature(feature, view) {
83
+ const session = getSession(view);
84
+ const bpPerPx = 10;
85
+ const { assemblyName } = view.displayedRegions[0];
86
+ const { assemblyManager } = session;
87
+ const assembly = assemblyManager.get(assemblyName);
88
+ if (!assembly) {
89
+ throw new Error(`assembly ${assemblyName} not found`);
90
+ }
91
+ if (!assembly.regions) {
92
+ throw new Error(`assembly ${assemblyName} regions not loaded`);
59
93
  }
94
+ const { refName, pos: startPos, mateRefName, matePos: endPos, } = this.getBreakendCoveringRegions({
95
+ feature,
96
+ assembly,
97
+ });
98
+ const topRegion = assembly.regions.find(f => f.refName === refName);
99
+ const bottomRegion = assembly.regions.find(f => f.refName === mateRefName);
60
100
  const topMarkedRegion = [{ ...topRegion }, { ...topRegion }];
61
101
  const bottomMarkedRegion = [{ ...bottomRegion }, { ...bottomRegion }];
62
- topMarkedRegion[0].end = startPos + startMod;
63
- topMarkedRegion[1].start = startPos + startMod;
64
- bottomMarkedRegion[0].end = endPos + endMod;
65
- bottomMarkedRegion[1].start = endPos + endMod;
102
+ topMarkedRegion[0].end = startPos;
103
+ topMarkedRegion[1].start = startPos;
104
+ bottomMarkedRegion[0].end = endPos;
105
+ bottomMarkedRegion[1].start = endPos;
66
106
  return {
67
107
  type: 'BreakpointSplitView',
68
108
  views: [
@@ -82,7 +122,6 @@ export default class BreakpointSplitViewType extends ViewType {
82
122
  },
83
123
  ],
84
124
  displayName: `${feature.get('name') || feature.get('id') || 'breakend'} split detail`,
85
- featureData: undefined,
86
125
  };
87
126
  }
88
127
  }
@@ -5,7 +5,8 @@ import { useTheme } from '@mui/material';
5
5
  import { getSession, getStrokeProps } from '@jbrowse/core/util';
6
6
  // locals
7
7
  import { getBadlyPairedAlignments, getMatchedAlignmentFeatures, hasPairedReads, } from './util';
8
- import { yPos, useNextFrame, getPxFromCoordinate } from '../util';
8
+ import { yPos, useNextFrame, getPxFromCoordinate, heightFromSpecificLevel, } from '../util';
9
+ import { getLongReadOrientationAbnormal, getLongReadOrientationColorOrDefault, getPairedOrientationColor, isAbnormalOrientation, } from './getOrientationColor';
9
10
  const [LEFT, , RIGHT] = [0, 1, 2, 3];
10
11
  const AlignmentConnections = observer(function ({ model, trackId, parentRef, getTrackYPosOverride, }) {
11
12
  const { views, showIntraviewLinks } = model;
@@ -13,7 +14,8 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
13
14
  const session = getSession(model);
14
15
  const snap = getSnapshot(model);
15
16
  const { assemblyManager } = session;
16
- const assembly = assemblyManager.get(views[0].assemblyNames[0]);
17
+ const v0 = views[0];
18
+ const assembly = v0 ? assemblyManager.get(v0.assemblyNames[0]) : undefined;
17
19
  useNextFrame(snap);
18
20
  const allFeatures = model.getTrackFeatures(trackId);
19
21
  const hasPaired = useMemo(() => hasPairedReads(allFeatures), [allFeatures]);
@@ -34,7 +36,7 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
34
36
  const rect = parentRef.current.getBoundingClientRect();
35
37
  yOffset = rect.top;
36
38
  }
37
- return assembly ? (React.createElement("g", { fill: "none", ...getStrokeProps(theme.palette.text.disabled), "data-testid": layoutMatches.length ? `${trackId}-loaded` : trackId }, layoutMatches.map(chunk => {
39
+ return assembly ? (React.createElement("g", { fill: "none", "data-testid": layoutMatches.length ? `${trackId}-loaded` : trackId }, layoutMatches.map(chunk => {
38
40
  const ret = [];
39
41
  // we follow a path in the list of chunks, not from top to bottom, just in series
40
42
  // following x1,y1 -> x2,y2
@@ -45,7 +47,7 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
45
47
  console.warn('received null layout for a overlay feature');
46
48
  return null;
47
49
  }
48
- // disable rendering connections in a single level
50
+ // disable rendering connections in a single row
49
51
  if (!showIntraviewLinks && level1 === level2) {
50
52
  return null;
51
53
  }
@@ -54,8 +56,25 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
54
56
  if (!f1ref || !f2ref) {
55
57
  throw new Error(`unable to find ref for ${f1ref || f2ref}`);
56
58
  }
59
+ const r = {
60
+ pair_orientation: f1.get('pair_orientation'),
61
+ };
57
62
  const s1 = f1.get('strand');
58
63
  const s2 = f2.get('strand');
64
+ const sameRef = f1ref === f2ref;
65
+ const checkOrientation = sameRef;
66
+ let orientationColor = '';
67
+ let isAbnormal = false;
68
+ if (checkOrientation) {
69
+ if (hasPaired) {
70
+ orientationColor = getPairedOrientationColor(r);
71
+ isAbnormal = isAbnormalOrientation(r);
72
+ }
73
+ else {
74
+ orientationColor = getLongReadOrientationColorOrDefault(s1, s2);
75
+ isAbnormal = getLongReadOrientationAbnormal(s1, s2);
76
+ }
77
+ }
59
78
  const p1 = c1[s1 === -1 ? LEFT : RIGHT];
60
79
  const sn1 = s2 === -1;
61
80
  const p2 = hasPaired ? c2[sn1 ? LEFT : RIGHT] : c2[sn1 ? RIGHT : LEFT];
@@ -63,11 +82,20 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
63
82
  const x2 = getPxFromCoordinate(views[level2], f2ref, p2);
64
83
  const reversed1 = views[level1].pxToBp(x1).reversed;
65
84
  const reversed2 = views[level2].pxToBp(x2).reversed;
85
+ const rf1 = reversed1 ? -1 : 1;
86
+ const rf2 = reversed2 ? -1 : 1;
66
87
  const tracks = views.map(v => v.getTrack(trackId));
67
88
  const y1 = yPos(trackId, level1, views, tracks, c1, getTrackYPosOverride) -
68
89
  yOffset;
69
90
  const y2 = yPos(trackId, level2, views, tracks, c2, getTrackYPosOverride) -
70
91
  yOffset;
92
+ const sameLevel = level1 === level2;
93
+ const abnormalSpecialRenderFlag = sameLevel && isAbnormal;
94
+ const trackHeight = abnormalSpecialRenderFlag
95
+ ? tracks[level1].displays[0].height
96
+ : 0;
97
+ const pf1 = hasPaired ? -1 : 1;
98
+ const y0 = heightFromSpecificLevel(views, trackId, level1, getTrackYPosOverride);
71
99
  // possible todo: use totalCurveHeight to possibly make alternative
72
100
  // squiggle if the S is too small
73
101
  const path = [
@@ -75,19 +103,22 @@ const AlignmentConnections = observer(function ({ model, trackId, parentRef, get
75
103
  x1,
76
104
  y1,
77
105
  'C',
78
- x1 + 200 * f1.get('strand') * (reversed1 ? -1 : 1),
79
- y1,
80
- x2 -
81
- 200 *
82
- f2.get('strand') *
83
- (reversed2 ? -1 : 1) *
84
- (hasPaired ? -1 : 1),
85
- y2,
106
+ // first bezier x,y
107
+ x1 + 200 * f1.get('strand') * rf1,
108
+ abnormalSpecialRenderFlag
109
+ ? Math.min(y0 - yOffset + trackHeight, y1 + trackHeight)
110
+ : y1,
111
+ // second bezier x,y
112
+ x2 - 200 * f2.get('strand') * rf2 * pf1,
113
+ abnormalSpecialRenderFlag
114
+ ? Math.min(y0 - yOffset + trackHeight, y2 + trackHeight)
115
+ : y2,
116
+ // third bezier x,y
86
117
  x2,
87
118
  y2,
88
119
  ].join(' ');
89
120
  const id = `${f1.id()}-${f2.id()}`;
90
- ret.push(React.createElement("path", { d: path, key: id, "data-testid": "r1", strokeWidth: mouseoverElt === id ? 5 : 1, onClick: () => {
121
+ ret.push(React.createElement("path", { d: path, key: id, "data-testid": "r1", strokeWidth: mouseoverElt === id ? 5 : 1, ...getStrokeProps(orientationColor || theme.palette.text.disabled), onClick: () => {
91
122
  var _a, _b;
92
123
  const featureWidget = (_a = session.addWidget) === null || _a === void 0 ? void 0 : _a.call(session, 'BreakpointAlignmentsWidget', 'breakpointAlignments', {
93
124
  featureData: {
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { observer } from 'mobx-react';
3
- import { getEnv } from 'mobx-state-tree';
3
+ import { getEnv } from '@jbrowse/core/util';
4
4
  import { makeStyles } from 'tss-react/mui';
5
5
  import BreakpointSplitViewOverlay from './BreakpointSplitViewOverlay';
6
6
  const useStyles = makeStyles()(theme => ({
@@ -14,22 +14,24 @@ const useStyles = makeStyles()(theme => ({
14
14
  content: {
15
15
  gridArea: '1/1',
16
16
  },
17
+ rel: {
18
+ position: 'relative',
19
+ },
17
20
  }));
18
21
  const BreakpointSplitViewLevels = observer(function ({ model, }) {
19
22
  const { classes } = useStyles();
20
23
  const { views } = model;
21
24
  const { pluginManager } = getEnv(model);
22
25
  return (React.createElement("div", { className: classes.content },
23
- React.createElement("div", { style: { position: 'relative' } }, views.map((view, idx) => {
26
+ React.createElement("div", { className: classes.rel }, views.map((view, idx) => {
24
27
  const { ReactComponent } = pluginManager.getViewType(view.type);
25
28
  const viewComponent = React.createElement(ReactComponent, { key: view.id, model: view });
26
- if (idx === views.length - 1) {
27
- return viewComponent;
28
- }
29
- return [
30
- viewComponent,
31
- React.createElement("div", { key: `${view.id}-divider`, className: classes.viewDivider }),
32
- ];
29
+ return idx === views.length - 1
30
+ ? viewComponent
31
+ : [
32
+ viewComponent,
33
+ React.createElement("div", { key: `${view.id}-divider`, className: classes.viewDivider }),
34
+ ];
33
35
  }))));
34
36
  });
35
37
  const BreakpointSplitView = observer(function ({ model, }) {
@@ -23,11 +23,12 @@ const BreakpointSplitViewOverlay = observer(function ({ model, }) {
23
23
  zIndex: 10,
24
24
  pointerEvents: interactToggled ? undefined : 'none',
25
25
  } }, matchedTracks.map(track => (
26
- // note: we must pass ref down, because the child component needs to
27
- // getBoundingClientRect on the this components SVG, and we cannot
28
- // rely on using getBoundingClientRect in this component to make
29
- // sure this works because if it gets shifted around by another
30
- // element, this will not re-render necessarily
26
+ // note: we must pass ref down, because:
27
+ // - the child component needs to getBoundingClientRect on the this
28
+ // components SVG, and...
29
+ // - we cannot rely on using getBoundingClientRect in this component
30
+ // to make sure this works because if it gets shifted around by
31
+ // another element, this will not re-render necessarily
31
32
  React.createElement(Overlay, { parentRef: ref, key: track.configuration.trackId, model: model, trackId: track.configuration.trackId }))))));
32
33
  });
33
34
  export default BreakpointSplitViewOverlay;
@@ -3,16 +3,21 @@ import { observer } from 'mobx-react';
3
3
  import AlignmentConnections from './AlignmentConnections';
4
4
  import Breakends from './Breakends';
5
5
  import Translocations from './Translocations';
6
+ import PairedFeatures from './PairedFeatures';
6
7
  const Overlay = observer(function (props) {
7
8
  var _a, _b;
8
9
  const { model, trackId } = props;
9
10
  const tracks = model.getMatchedTracks(trackId);
11
+ // curvy line type arcs
10
12
  if (((_a = tracks[0]) === null || _a === void 0 ? void 0 : _a.type) === 'AlignmentsTrack') {
11
13
  return React.createElement(AlignmentConnections, { ...props });
12
14
  }
13
- if (((_b = tracks[0]) === null || _b === void 0 ? void 0 : _b.type) === 'VariantTrack') {
14
- return model.hasTranslocations(trackId) ? (React.createElement(Translocations, { ...props })) : (React.createElement(Breakends, { ...props }));
15
+ // translocation type arcs
16
+ else if (((_b = tracks[0]) === null || _b === void 0 ? void 0 : _b.type) === 'VariantTrack') {
17
+ return model.hasTranslocations(trackId) ? (React.createElement(Translocations, { ...props })) : model.hasPairedFeatures(trackId) ? (React.createElement(PairedFeatures, { ...props })) : (React.createElement(Breakends, { ...props }));
18
+ }
19
+ else {
20
+ return null;
15
21
  }
16
- return null;
17
22
  });
18
23
  export default Overlay;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { BreakpointViewModel } from '../model';
3
+ declare const PairedFeatures: ({ model, trackId, parentRef: ref, getTrackYPosOverride, }: {
4
+ model: BreakpointViewModel;
5
+ trackId: string;
6
+ parentRef: React.RefObject<SVGSVGElement>;
7
+ getTrackYPosOverride?: (trackId: string, level: number) => number;
8
+ }) => React.JSX.Element | null;
9
+ export default PairedFeatures;