@jbrowse/plugin-linear-comparative-view 2.11.1 → 2.12.0
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/dist/LGVSyntenyDisplay/components/util.js +1 -1
- package/dist/LGVSyntenyDisplay/configSchemaF.d.ts +5 -0
- package/dist/LGVSyntenyDisplay/model.d.ts +7 -3
- package/dist/LinearComparativeView/model.d.ts +11 -36
- package/dist/LinearComparativeView/model.js +0 -23
- package/dist/LinearSyntenyDisplay/components/LinearSyntenyRendering.js +54 -101
- package/dist/LinearSyntenyDisplay/components/util.d.ts +10 -0
- package/dist/LinearSyntenyDisplay/components/util.js +87 -1
- package/dist/LinearSyntenyView/components/ImportForm/index.js +1 -0
- package/dist/LinearSyntenyView/model.d.ts +39 -139
- package/dist/LinearSyntenyView/model.js +0 -8
- package/dist/SyntenyFeatureDetail/SyntenyFeatureDetail.d.ts +5 -3
- package/dist/SyntenyFeatureDetail/SyntenyFeatureDetail.js +2 -4
- package/esm/LGVSyntenyDisplay/components/util.js +1 -1
- package/esm/LGVSyntenyDisplay/configSchemaF.d.ts +5 -0
- package/esm/LGVSyntenyDisplay/model.d.ts +7 -3
- package/esm/LinearComparativeView/model.d.ts +11 -36
- package/esm/LinearComparativeView/model.js +1 -24
- package/esm/LinearSyntenyDisplay/components/LinearSyntenyRendering.js +54 -101
- package/esm/LinearSyntenyDisplay/components/util.d.ts +10 -0
- package/esm/LinearSyntenyDisplay/components/util.js +84 -1
- package/esm/LinearSyntenyView/components/ImportForm/index.js +1 -0
- package/esm/LinearSyntenyView/model.d.ts +39 -139
- package/esm/LinearSyntenyView/model.js +0 -8
- package/esm/SyntenyFeatureDetail/SyntenyFeatureDetail.d.ts +5 -3
- package/esm/SyntenyFeatureDetail/SyntenyFeatureDetail.js +2 -4
- package/package.json +2 -2
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { observer } from 'mobx-react';
|
|
3
|
-
import {
|
|
3
|
+
import { getContainingView } from '@jbrowse/core/util';
|
|
4
4
|
import { transaction } from 'mobx';
|
|
5
5
|
import { makeStyles } from 'tss-react/mui';
|
|
6
6
|
// locals
|
|
7
7
|
import SyntenyTooltip from './SyntenyTooltip';
|
|
8
8
|
import { getId, MAX_COLOR_RANGE } from '../drawSynteny';
|
|
9
9
|
import SyntenyContextMenu from './SyntenyContextMenu';
|
|
10
|
+
import { getTooltip, onSynClick, onSynContextClick } from './util';
|
|
10
11
|
const useStyles = makeStyles()({
|
|
11
12
|
pix: {
|
|
12
13
|
imageRendering: 'pixelated',
|
|
@@ -31,30 +32,44 @@ const LinearSyntenyRendering = observer(function ({ model, }) {
|
|
|
31
32
|
const view = getContainingView(model);
|
|
32
33
|
const height = view.middleComparativeHeight;
|
|
33
34
|
const width = view.width;
|
|
35
|
+
const delta = useRef(0);
|
|
36
|
+
const timeout = useRef();
|
|
34
37
|
const [anchorEl, setAnchorEl] = useState();
|
|
35
38
|
const [tooltip, setTooltip] = useState('');
|
|
36
39
|
const [currX, setCurrX] = useState();
|
|
37
40
|
const [mouseCurrDownX, setMouseCurrDownX] = useState();
|
|
38
41
|
const [mouseInitialDownX, setMouseInitialDownX] = useState();
|
|
39
42
|
const [currY, setCurrY] = useState();
|
|
43
|
+
const { mouseoverId } = model;
|
|
40
44
|
// these useCallbacks avoid new refs from being created on any mouseover, etc.
|
|
41
45
|
const k1 = useCallback((ref) => model.setMouseoverCanvasRef(ref),
|
|
42
46
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
47
|
[model, height, width]);
|
|
44
|
-
const k2 = useCallback((ref) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const k2 = useCallback((ref) => {
|
|
49
|
+
model.setMainCanvasRef(ref);
|
|
50
|
+
function onWheel(event) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
if (event.ctrlKey === true) {
|
|
53
|
+
delta.current += event.deltaY / 500;
|
|
54
|
+
for (const v of view.views) {
|
|
55
|
+
v.setScaleFactor(delta.current < 0 ? 1 - delta.current : 1 / (1 + delta.current));
|
|
56
|
+
}
|
|
57
|
+
if (timeout.current) {
|
|
58
|
+
clearTimeout(timeout.current);
|
|
59
|
+
}
|
|
60
|
+
timeout.current = setTimeout(() => {
|
|
61
|
+
for (const v of view.views) {
|
|
62
|
+
v.setScaleFactor(1);
|
|
63
|
+
v.zoomTo(delta.current > 0
|
|
64
|
+
? v.bpPerPx * (1 + delta.current)
|
|
65
|
+
: v.bpPerPx / (1 - delta.current), event.clientX - ((ref === null || ref === void 0 ? void 0 : ref.getBoundingClientRect().left) || 0));
|
|
66
|
+
}
|
|
67
|
+
delta.current = 0;
|
|
68
|
+
}, 300);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
56
71
|
if (Math.abs(event.deltaY) < Math.abs(event.deltaX)) {
|
|
57
|
-
xOffset.current += event.deltaX;
|
|
72
|
+
xOffset.current += event.deltaX / 2;
|
|
58
73
|
}
|
|
59
74
|
if (currScrollFrame.current === undefined) {
|
|
60
75
|
currScrollFrame.current = requestAnimationFrame(() => {
|
|
@@ -67,7 +82,28 @@ const LinearSyntenyRendering = observer(function ({ model, }) {
|
|
|
67
82
|
});
|
|
68
83
|
});
|
|
69
84
|
}
|
|
70
|
-
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (ref) {
|
|
88
|
+
ref.addEventListener('wheel', onWheel);
|
|
89
|
+
// this is a react 19-ism to have a cleanup in the ref callback
|
|
90
|
+
// https://react.dev/blog/2024/04/25/react-19#cleanup-functions-for-refs
|
|
91
|
+
// note: it warns in earlier versions of react
|
|
92
|
+
return () => ref.removeEventListener('wheel', onWheel);
|
|
93
|
+
}
|
|
94
|
+
return () => { };
|
|
95
|
+
},
|
|
96
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
|
+
[model, height, width]);
|
|
98
|
+
const k3 = useCallback((ref) => model.setClickMapCanvasRef(ref),
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
[model, height, width]);
|
|
101
|
+
const k4 = useCallback((ref) => model.setCigarClickMapCanvasRef(ref),
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
[model, height, width]);
|
|
104
|
+
return (React.createElement("div", { className: classes.rel },
|
|
105
|
+
React.createElement("canvas", { ref: k1, width: width, height: height, className: cx(classes.abs, classes.none) }),
|
|
106
|
+
React.createElement("canvas", { ref: k2, onMouseMove: event => {
|
|
71
107
|
var _a;
|
|
72
108
|
if (mouseCurrDownX !== undefined) {
|
|
73
109
|
xOffset.current += mouseCurrDownX - event.clientX;
|
|
@@ -127,95 +163,12 @@ const LinearSyntenyRendering = observer(function ({ model, }) {
|
|
|
127
163
|
setMouseCurrDownX(undefined);
|
|
128
164
|
if (mouseInitialDownX !== undefined &&
|
|
129
165
|
Math.abs(evt.clientX - mouseInitialDownX) < 5) {
|
|
130
|
-
|
|
166
|
+
onSynClick(evt, model);
|
|
131
167
|
}
|
|
132
|
-
}, onContextMenu: evt =>
|
|
133
|
-
onSyntenyContextClick(evt, model, setAnchorEl);
|
|
134
|
-
}, "data-testid": "synteny_canvas", className: classes.abs, width: width, height: height }),
|
|
168
|
+
}, onContextMenu: evt => onSynContextClick(evt, model, setAnchorEl), "data-testid": "synteny_canvas", className: classes.abs, width: width, height: height }),
|
|
135
169
|
React.createElement("canvas", { ref: k3, className: classes.pix, width: width, height: height }),
|
|
136
170
|
React.createElement("canvas", { ref: k4, className: classes.pix, width: width, height: height }),
|
|
137
|
-
|
|
171
|
+
mouseoverId && tooltip && currX && currY ? (React.createElement(SyntenyTooltip, { title: tooltip })) : null,
|
|
138
172
|
anchorEl ? (React.createElement(SyntenyContextMenu, { model: model, anchorEl: anchorEl, onClose: () => setAnchorEl(undefined) })) : null));
|
|
139
173
|
});
|
|
140
|
-
function onSyntenyClick(event, model) {
|
|
141
|
-
const ref1 = model.clickMapCanvas;
|
|
142
|
-
const ref2 = model.cigarClickMapCanvas;
|
|
143
|
-
if (!ref1 || !ref2) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const rect = ref1.getBoundingClientRect();
|
|
147
|
-
const ctx1 = ref1.getContext('2d');
|
|
148
|
-
const ctx2 = ref2.getContext('2d');
|
|
149
|
-
if (!ctx1 || !ctx2) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const x = event.clientX - rect.left;
|
|
153
|
-
const y = event.clientY - rect.top;
|
|
154
|
-
const [r1, g1, b1] = ctx1.getImageData(x, y, 1, 1).data;
|
|
155
|
-
const unitMultiplier = Math.floor(MAX_COLOR_RANGE / model.numFeats);
|
|
156
|
-
const id = getId(r1, g1, b1, unitMultiplier);
|
|
157
|
-
const feat = model.featPositions[id];
|
|
158
|
-
if (feat) {
|
|
159
|
-
const { f } = feat;
|
|
160
|
-
model.setClickId(f.id());
|
|
161
|
-
const session = getSession(model);
|
|
162
|
-
if (isSessionModelWithWidgets(session)) {
|
|
163
|
-
session.showWidget(session.addWidget('SyntenyFeatureWidget', 'syntenyFeature', {
|
|
164
|
-
featureData: {
|
|
165
|
-
feature1: f.toJSON(),
|
|
166
|
-
feature2: f.get('mate'),
|
|
167
|
-
},
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return feat;
|
|
172
|
-
}
|
|
173
|
-
function onSyntenyContextClick(event, model, setAnchorEl) {
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
const ref1 = model.clickMapCanvas;
|
|
176
|
-
const ref2 = model.cigarClickMapCanvas;
|
|
177
|
-
if (!ref1 || !ref2) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const rect = ref1.getBoundingClientRect();
|
|
181
|
-
const ctx1 = ref1.getContext('2d');
|
|
182
|
-
const ctx2 = ref2.getContext('2d');
|
|
183
|
-
if (!ctx1 || !ctx2) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const { clientX, clientY } = event;
|
|
187
|
-
const x = clientX - rect.left;
|
|
188
|
-
const y = clientY - rect.top;
|
|
189
|
-
const [r1, g1, b1] = ctx1.getImageData(x, y, 1, 1).data;
|
|
190
|
-
const unitMultiplier = Math.floor(MAX_COLOR_RANGE / model.numFeats);
|
|
191
|
-
const id = getId(r1, g1, b1, unitMultiplier);
|
|
192
|
-
const f = model.featPositions[id];
|
|
193
|
-
if (f) {
|
|
194
|
-
model.setClickId(f.f.id());
|
|
195
|
-
setAnchorEl({ clientX, clientY, feature: f });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
function getTooltip(f, cigarOp, cigarOpLen) {
|
|
199
|
-
// @ts-expect-error
|
|
200
|
-
const f1 = f.toJSON();
|
|
201
|
-
const f2 = f1.mate;
|
|
202
|
-
const l1 = f1.end - f1.start;
|
|
203
|
-
const l2 = f2.end - f2.start;
|
|
204
|
-
const identity = f1.identity;
|
|
205
|
-
const n1 = f1.name;
|
|
206
|
-
const n2 = f2.name;
|
|
207
|
-
return [
|
|
208
|
-
`Loc1: ${assembleLocString(f1)}`,
|
|
209
|
-
`Loc2: ${assembleLocString(f2)}`,
|
|
210
|
-
`Inverted: ${f1.strand === -1}`,
|
|
211
|
-
`Query len: ${l1.toLocaleString('en-US')}`,
|
|
212
|
-
`Target len: ${l2.toLocaleString('en-US')}`,
|
|
213
|
-
identity ? `Identity: ${identity.toPrecision(2)}` : '',
|
|
214
|
-
cigarOp ? `CIGAR operator: ${cigarOp}${cigarOpLen}` : '',
|
|
215
|
-
n1 ? `Name 1: ${n1}` : '',
|
|
216
|
-
n2 ? `Name 1: ${n2}` : '',
|
|
217
|
-
]
|
|
218
|
-
.filter(f => !!f)
|
|
219
|
-
.join('<br/>');
|
|
220
|
-
}
|
|
221
174
|
export default LinearSyntenyRendering;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { Feature } from '@jbrowse/core/util';
|
|
3
|
+
import { LinearSyntenyDisplayModel } from '../model';
|
|
2
4
|
interface Pos {
|
|
3
5
|
offsetPx: number;
|
|
4
6
|
}
|
|
7
|
+
export interface ClickCoord {
|
|
8
|
+
clientX: number;
|
|
9
|
+
clientY: number;
|
|
10
|
+
feature: any;
|
|
11
|
+
}
|
|
5
12
|
interface FeatPos {
|
|
6
13
|
p11: Pos;
|
|
7
14
|
p12: Pos;
|
|
@@ -24,4 +31,7 @@ export declare function drawMatchSimple({ feature, ctx, offsets, cb, height, dra
|
|
|
24
31
|
export declare function draw(ctx: CanvasRenderingContext2D, x1: number, x2: number, y1: number, x3: number, x4: number, y2: number, mid: number, drawCurves?: boolean): void;
|
|
25
32
|
export declare function drawBox(ctx: CanvasRenderingContext2D, x1: number, x2: number, y1: number, x3: number, x4: number, y2: number): void;
|
|
26
33
|
export declare function drawBezierBox(ctx: CanvasRenderingContext2D, x1: number, x2: number, y1: number, x3: number, x4: number, y2: number, mid: number): void;
|
|
34
|
+
export declare function onSynClick(event: React.MouseEvent, model: LinearSyntenyDisplayModel): import("../model").FeatPos | undefined;
|
|
35
|
+
export declare function onSynContextClick(event: React.MouseEvent, model: LinearSyntenyDisplayModel, setAnchorEl: (arg: ClickCoord) => void): void;
|
|
36
|
+
export declare function getTooltip(f: Feature, cigarOp?: string, cigarOpLen?: string): string;
|
|
27
37
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { doesIntersect2 } from '@jbrowse/core/util';
|
|
1
|
+
import { assembleLocString, doesIntersect2, getSession, isSessionModelWithWidgets, } from '@jbrowse/core/util';
|
|
2
|
+
// locals
|
|
3
|
+
import { getId, MAX_COLOR_RANGE } from '../drawSynteny';
|
|
2
4
|
export function drawMatchSimple({ feature, ctx, offsets, cb, height, drawCurves, oobLimit, viewWidth, hideTiny, }) {
|
|
3
5
|
const { p11, p12, p21, p22 } = feature;
|
|
4
6
|
const x11 = p11.offsetPx - offsets[0];
|
|
@@ -74,3 +76,84 @@ export function drawBezierBox(ctx, x1, x2, y1, x3, x4, y2, mid) {
|
|
|
74
76
|
ctx.closePath();
|
|
75
77
|
ctx.fill();
|
|
76
78
|
}
|
|
79
|
+
export function onSynClick(event, model) {
|
|
80
|
+
const ref1 = model.clickMapCanvas;
|
|
81
|
+
const ref2 = model.cigarClickMapCanvas;
|
|
82
|
+
if (!ref1 || !ref2) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const rect = ref1.getBoundingClientRect();
|
|
86
|
+
const ctx1 = ref1.getContext('2d');
|
|
87
|
+
const ctx2 = ref2.getContext('2d');
|
|
88
|
+
if (!ctx1 || !ctx2) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const x = event.clientX - rect.left;
|
|
92
|
+
const y = event.clientY - rect.top;
|
|
93
|
+
const [r1, g1, b1] = ctx1.getImageData(x, y, 1, 1).data;
|
|
94
|
+
const unitMultiplier = Math.floor(MAX_COLOR_RANGE / model.numFeats);
|
|
95
|
+
const id = getId(r1, g1, b1, unitMultiplier);
|
|
96
|
+
const feat = model.featPositions[id];
|
|
97
|
+
if (feat) {
|
|
98
|
+
const { f } = feat;
|
|
99
|
+
model.setClickId(f.id());
|
|
100
|
+
const session = getSession(model);
|
|
101
|
+
if (isSessionModelWithWidgets(session)) {
|
|
102
|
+
session.showWidget(session.addWidget('SyntenyFeatureWidget', 'syntenyFeature', {
|
|
103
|
+
featureData: {
|
|
104
|
+
feature1: f.toJSON(),
|
|
105
|
+
feature2: f.get('mate'),
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return feat;
|
|
111
|
+
}
|
|
112
|
+
export function onSynContextClick(event, model, setAnchorEl) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
const ref1 = model.clickMapCanvas;
|
|
115
|
+
const ref2 = model.cigarClickMapCanvas;
|
|
116
|
+
if (!ref1 || !ref2) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const rect = ref1.getBoundingClientRect();
|
|
120
|
+
const ctx1 = ref1.getContext('2d');
|
|
121
|
+
const ctx2 = ref2.getContext('2d');
|
|
122
|
+
if (!ctx1 || !ctx2) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { clientX, clientY } = event;
|
|
126
|
+
const x = clientX - rect.left;
|
|
127
|
+
const y = clientY - rect.top;
|
|
128
|
+
const [r1, g1, b1] = ctx1.getImageData(x, y, 1, 1).data;
|
|
129
|
+
const unitMultiplier = Math.floor(MAX_COLOR_RANGE / model.numFeats);
|
|
130
|
+
const id = getId(r1, g1, b1, unitMultiplier);
|
|
131
|
+
const f = model.featPositions[id];
|
|
132
|
+
if (f) {
|
|
133
|
+
model.setClickId(f.f.id());
|
|
134
|
+
setAnchorEl({ clientX, clientY, feature: f });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function getTooltip(f, cigarOp, cigarOpLen) {
|
|
138
|
+
// @ts-expect-error
|
|
139
|
+
const f1 = f.toJSON();
|
|
140
|
+
const f2 = f1.mate;
|
|
141
|
+
const l1 = f1.end - f1.start;
|
|
142
|
+
const l2 = f2.end - f2.start;
|
|
143
|
+
const identity = f1.identity;
|
|
144
|
+
const n1 = f1.name;
|
|
145
|
+
const n2 = f2.name;
|
|
146
|
+
return [
|
|
147
|
+
`Loc1: ${assembleLocString(f1)}`,
|
|
148
|
+
`Loc2: ${assembleLocString(f2)}`,
|
|
149
|
+
`Inverted: ${f1.strand === -1}`,
|
|
150
|
+
`Query len: ${l1.toLocaleString('en-US')}`,
|
|
151
|
+
`Target len: ${l2.toLocaleString('en-US')}`,
|
|
152
|
+
identity ? `Identity: ${identity.toPrecision(2)}` : '',
|
|
153
|
+
cigarOp ? `CIGAR operator: ${cigarOp}${cigarOpLen}` : '',
|
|
154
|
+
n1 ? `Name 1: ${n1}` : '',
|
|
155
|
+
n2 ? `Name 1: ${n2}` : '',
|
|
156
|
+
]
|
|
157
|
+
.filter(f => !!f)
|
|
158
|
+
.join('<br/>');
|
|
159
|
+
}
|
|
@@ -65,6 +65,7 @@ const LinearSyntenyViewImportForm = observer(function ({ model, }) {
|
|
|
65
65
|
};
|
|
66
66
|
})));
|
|
67
67
|
model.views.forEach(view => view.setWidth(model.width));
|
|
68
|
+
model.views.forEach(view => view.showAllRegions());
|
|
68
69
|
if (sessionTrackData) {
|
|
69
70
|
session.addTrackConf(sessionTrackData);
|
|
70
71
|
model.toggleTrack(sessionTrackData.trackId);
|