react-msaview 4.4.5 → 4.5.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/bundle/index.js +9 -9
- package/bundle/index.js.LICENSE.txt +8 -8
- package/bundle/index.js.map +1 -1
- package/dist/colorSchemes.d.ts +0 -6
- package/dist/colorSchemes.js +1 -119
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/ConservationTrack.d.ts +8 -0
- package/dist/components/ConservationTrack.js +54 -0
- package/dist/components/ConservationTrack.js.map +1 -0
- package/dist/components/Loading.js +14 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +36 -0
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -2
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.d.ts +3 -3
- package/dist/components/TextTrack.js +4 -1
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +21 -8
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +19 -3
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/header/GappynessSlider.d.ts +6 -0
- package/dist/components/header/GappynessSlider.js +19 -0
- package/dist/components/header/GappynessSlider.js.map +1 -0
- package/dist/components/header/Header.js +3 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +30 -14
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +4 -3
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +56 -42
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +53 -10
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
- package/dist/components/tracks/renderTracksSvg.js +83 -0
- package/dist/components/tracks/renderTracksSvg.js.map +1 -0
- package/dist/components/tree/TreeCanvasBlock.js +1 -1
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +2 -2
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +1 -1
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +22 -0
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -0
- package/dist/layout.js.map +1 -1
- package/dist/model/msaModel.js +3 -2
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.js +9 -8
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +256 -15
- package/dist/model.js +408 -128
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.d.ts +1 -0
- package/dist/neighborJoining.js +839 -0
- package/dist/neighborJoining.js.map +1 -0
- package/dist/neighborJoining.test.d.ts +1 -0
- package/dist/neighborJoining.test.js +110 -0
- package/dist/neighborJoining.test.js.map +1 -0
- package/dist/parsers/A3mMSA.d.ts +43 -0
- package/dist/parsers/A3mMSA.js +277 -0
- package/dist/parsers/A3mMSA.js.map +1 -0
- package/dist/parsers/A3mMSA.test.d.ts +1 -0
- package/dist/parsers/A3mMSA.test.js +138 -0
- package/dist/parsers/A3mMSA.test.js.map +1 -0
- package/dist/parsers/ClustalMSA.d.ts +4 -4
- package/dist/parsers/ClustalMSA.js +3 -1
- package/dist/parsers/ClustalMSA.js.map +1 -1
- package/dist/parsers/FastaMSA.js +17 -16
- package/dist/parsers/FastaMSA.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -0
- package/dist/renderToSvg.js +48 -18
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.js +3 -5
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/rowCoordinateCalculations.test.js +14 -2
- package/dist/rowCoordinateCalculations.test.js.map +1 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.js +9 -5
- package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.test.js +6 -6
- package/dist/types.d.ts +2 -3
- package/dist/util.js +17 -9
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/src/colorSchemes.ts +1 -179
- package/src/components/ConservationTrack.tsx +104 -0
- package/src/components/Loading.tsx +44 -2
- package/src/components/MSAView.tsx +68 -0
- package/src/components/SequenceTextArea.tsx +3 -2
- package/src/components/TextTrack.tsx +7 -4
- package/src/components/Track.tsx +25 -9
- package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
- package/src/components/header/GappynessSlider.tsx +35 -0
- package/src/components/header/Header.tsx +3 -1
- package/src/components/header/HeaderMenu.tsx +36 -15
- package/src/components/minimap/MinimapSVG.tsx +6 -3
- package/src/components/msa/MSACanvasBlock.tsx +66 -48
- package/src/components/msa/renderMSABlock.ts +82 -22
- package/src/components/tracks/renderTracksSvg.ts +157 -0
- package/src/components/tree/TreeCanvasBlock.tsx +1 -1
- package/src/components/tree/TreeNodeMenu.tsx +2 -2
- package/src/components/tree/renderTreeCanvas.ts +1 -1
- package/src/constants.ts +27 -0
- package/src/layout.ts +1 -6
- package/src/model/msaModel.ts +4 -2
- package/src/model/treeModel.ts +19 -8
- package/src/model.ts +496 -140
- package/src/neighborJoining.test.ts +129 -0
- package/src/neighborJoining.ts +885 -0
- package/src/parsers/A3mMSA.test.ts +164 -0
- package/src/parsers/A3mMSA.ts +321 -0
- package/src/parsers/ClustalMSA.ts +7 -5
- package/src/parsers/FastaMSA.ts +17 -17
- package/src/renderToSvg.tsx +105 -26
- package/src/rowCoordinateCalculations.test.ts +15 -2
- package/src/rowCoordinateCalculations.ts +3 -5
- package/src/seqCoordToRowSpecificGlobalCoord.test.ts +6 -6
- package/src/seqCoordToRowSpecificGlobalCoord.ts +9 -4
- package/src/types.ts +2 -4
- package/src/util.ts +21 -8
- package/src/version.ts +1 -1
- package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
- package/dist/components/dialogs/TracklistDialog.js +0 -23
- package/dist/components/dialogs/TracklistDialog.js.map +0 -1
- package/src/components/dialogs/TracklistDialog.tsx +0 -73
package/src/colorSchemes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { blue, green, orange, red } from '@mui/material/colors'
|
|
|
2
2
|
import { colord, extend } from 'colord'
|
|
3
3
|
import namesPlugin from 'colord/plugins/names'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { transform } from './util'
|
|
6
6
|
|
|
7
7
|
extend([namesPlugin])
|
|
8
8
|
|
|
@@ -353,181 +353,3 @@ export default transform(colorSchemes, ([key, val]) => [
|
|
|
353
353
|
key,
|
|
354
354
|
transform(val, ([letter, color]) => [letter, colord(color).toHex()]),
|
|
355
355
|
])
|
|
356
|
-
|
|
357
|
-
// info http://www.jalview.org/help/html/colourSchemes/clustal.html
|
|
358
|
-
// modifications:
|
|
359
|
-
// reference to clustalX source code scheme modifies what the jalview.org
|
|
360
|
-
// scheme says there the jalview.org colorscheme says WLVIMAFCHP but it
|
|
361
|
-
// should be WLVIMAFCHPY, colprot.xml says e.g. %#ACFHILMVWYPp" which has Y
|
|
362
|
-
export function getClustalXColor(
|
|
363
|
-
stats: Record<string, number>,
|
|
364
|
-
total: number,
|
|
365
|
-
model: { columns: Record<string, string> },
|
|
366
|
-
row: string,
|
|
367
|
-
col: number,
|
|
368
|
-
) {
|
|
369
|
-
const l = model.columns[row]![col]!
|
|
370
|
-
const {
|
|
371
|
-
W = 0,
|
|
372
|
-
L = 0,
|
|
373
|
-
V = 0,
|
|
374
|
-
I = 0,
|
|
375
|
-
M = 0,
|
|
376
|
-
A = 0,
|
|
377
|
-
F = 0,
|
|
378
|
-
C = 0,
|
|
379
|
-
H = 0,
|
|
380
|
-
P = 0,
|
|
381
|
-
R = 0,
|
|
382
|
-
K = 0,
|
|
383
|
-
Q = 0,
|
|
384
|
-
E = 0,
|
|
385
|
-
D = 0,
|
|
386
|
-
T = 0,
|
|
387
|
-
S = 0,
|
|
388
|
-
G = 0,
|
|
389
|
-
Y = 0,
|
|
390
|
-
N = 0,
|
|
391
|
-
} = stats
|
|
392
|
-
|
|
393
|
-
const WLVIMAFCHP = W + L + V + I + M + A + F + C + H + P + Y
|
|
394
|
-
|
|
395
|
-
const KR = K + R
|
|
396
|
-
const QE = Q + E
|
|
397
|
-
const ED = E + D
|
|
398
|
-
const TS = T + S
|
|
399
|
-
|
|
400
|
-
if (WLVIMAFCHP / total > 0.6) {
|
|
401
|
-
if (
|
|
402
|
-
l === 'W' ||
|
|
403
|
-
l === 'L' ||
|
|
404
|
-
l === 'V' ||
|
|
405
|
-
l === 'A' ||
|
|
406
|
-
l === 'I' ||
|
|
407
|
-
l === 'M' ||
|
|
408
|
-
l === 'F' ||
|
|
409
|
-
l === 'C'
|
|
410
|
-
) {
|
|
411
|
-
// blue from jalview.org docs
|
|
412
|
-
return 'rgb(128,179,230)'
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
(l === 'K' || l === 'R') &&
|
|
418
|
-
(KR / total > 0.6 || K / total > 0.8 || R / total > 0.8 || Q / total > 0.8)
|
|
419
|
-
) {
|
|
420
|
-
return '#d88'
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
l === 'E' &&
|
|
425
|
-
(KR / total > 0.6 ||
|
|
426
|
-
QE / total > 0.5 ||
|
|
427
|
-
E / total > 0.8 ||
|
|
428
|
-
Q / total > 0.8 ||
|
|
429
|
-
D / total > 0.8)
|
|
430
|
-
) {
|
|
431
|
-
return 'rgb(192, 72, 192)'
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
l === 'D' &&
|
|
436
|
-
(KR / total > 0.6 ||
|
|
437
|
-
ED / total > 0.5 ||
|
|
438
|
-
K / total > 0.8 ||
|
|
439
|
-
R / total > 0.8 ||
|
|
440
|
-
Q / total > 0.8)
|
|
441
|
-
) {
|
|
442
|
-
return 'rgb(204, 77, 204)'
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (l === 'N' && (N / total > 0.5 || Y / total > 0.85)) {
|
|
446
|
-
return '#8f8'
|
|
447
|
-
}
|
|
448
|
-
if (
|
|
449
|
-
l === 'Q' &&
|
|
450
|
-
(KR / total > 0.6 ||
|
|
451
|
-
QE / total > 0.6 ||
|
|
452
|
-
Q / total > 0.85 ||
|
|
453
|
-
E / total > 0.85 ||
|
|
454
|
-
K / total > 0.85 ||
|
|
455
|
-
R / total > 0.85)
|
|
456
|
-
) {
|
|
457
|
-
return '#8f8'
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
(l === 'S' || l === 'T') &&
|
|
462
|
-
// WLVIMAFCHP modified from 0.6 to 0.55 on page to match what i see in jalview
|
|
463
|
-
(WLVIMAFCHP / total > 0.6 ||
|
|
464
|
-
TS / total > 0.5 ||
|
|
465
|
-
S / total > 0.85 ||
|
|
466
|
-
T / total > 0.85)
|
|
467
|
-
) {
|
|
468
|
-
return 'rgb(26,204,26)'
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (l === 'C' && C / total > 0.85) {
|
|
472
|
-
return 'rgb(240, 128, 128)'
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (l === 'G' && G / total > 0) {
|
|
476
|
-
return 'rgb(240, 144, 72)'
|
|
477
|
-
}
|
|
478
|
-
if (l === 'P' && P / total > 0) {
|
|
479
|
-
return 'rgb(204, 204, 0)'
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
(l === 'H' || l === 'Y') &&
|
|
484
|
-
(WLVIMAFCHP / total > 0.6 ||
|
|
485
|
-
W > 0.85 ||
|
|
486
|
-
Y > 0.85 ||
|
|
487
|
-
A > 0.85 ||
|
|
488
|
-
C > 0.85 ||
|
|
489
|
-
P > 0.85 ||
|
|
490
|
-
Q > 0.85 ||
|
|
491
|
-
F > 0.85 ||
|
|
492
|
-
H > 0.85 ||
|
|
493
|
-
I > 0.85 ||
|
|
494
|
-
L > 0.85 ||
|
|
495
|
-
M > 0.85 ||
|
|
496
|
-
V > 0.85)
|
|
497
|
-
) {
|
|
498
|
-
// cyan from jalview.org docs
|
|
499
|
-
return 'rgb(26, 179, 179)'
|
|
500
|
-
}
|
|
501
|
-
return undefined
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// info http://www.jalview.org/help/html/colourSchemes/clustal.html
|
|
505
|
-
// modifications:
|
|
506
|
-
// reference to clustalX source code scheme modifies what the jalview.org
|
|
507
|
-
// scheme says there the jalview.org colorscheme says WLVIMAFCHP but it should
|
|
508
|
-
// be WLVIMAFCHPY, colprot.xml says e.g. %#ACFHILMVWYPp" which has Y
|
|
509
|
-
export function getPercentIdentityColor(
|
|
510
|
-
stats: Record<string, number>,
|
|
511
|
-
total: number,
|
|
512
|
-
model: { columns: Record<string, string> },
|
|
513
|
-
row: string,
|
|
514
|
-
col: number,
|
|
515
|
-
) {
|
|
516
|
-
const l = model.columns[row]![col]!
|
|
517
|
-
const entries = Object.entries(stats)
|
|
518
|
-
let ent = 0
|
|
519
|
-
let letter = ''
|
|
520
|
-
for (const entry of entries) {
|
|
521
|
-
if (entry[1] > ent && !isBlank(entry[0])) {
|
|
522
|
-
letter = entry[0]
|
|
523
|
-
ent = entry[1]
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const proportion = ent / total
|
|
527
|
-
const thresh = `hsl(240, 30%, ${100 * Math.max(1 - ent / total / 3, 0.3)}%)`
|
|
528
|
-
if (proportion > 0.4) {
|
|
529
|
-
if (l === letter) {
|
|
530
|
-
return thresh
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
|
|
5
|
+
import type { MsaViewModel } from '../model'
|
|
6
|
+
import type { BasicTrack } from '../types'
|
|
7
|
+
|
|
8
|
+
const ConservationBlock = observer(function ({
|
|
9
|
+
model,
|
|
10
|
+
offsetX,
|
|
11
|
+
trackHeight,
|
|
12
|
+
}: {
|
|
13
|
+
model: MsaViewModel
|
|
14
|
+
offsetX: number
|
|
15
|
+
trackHeight: number
|
|
16
|
+
}) {
|
|
17
|
+
const { blockSize, scrollX, colWidth, highResScaleFactor, conservation } =
|
|
18
|
+
model
|
|
19
|
+
|
|
20
|
+
const ref = useRef<HTMLCanvasElement>(null)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!ref.current) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ctx = ref.current.getContext('2d')
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ctx.resetTransform()
|
|
33
|
+
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
34
|
+
ctx.clearRect(0, 0, blockSize, trackHeight)
|
|
35
|
+
ctx.translate(-offsetX, 0)
|
|
36
|
+
|
|
37
|
+
const xStart = Math.max(0, Math.floor(offsetX / colWidth))
|
|
38
|
+
const xEnd = Math.max(0, Math.ceil((offsetX + blockSize) / colWidth))
|
|
39
|
+
|
|
40
|
+
for (let i = xStart; i < xEnd && i < conservation.length; i++) {
|
|
41
|
+
const value = conservation[i]!
|
|
42
|
+
const barHeight = value * trackHeight
|
|
43
|
+
const x = i * colWidth
|
|
44
|
+
|
|
45
|
+
const hue = value * 120
|
|
46
|
+
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`
|
|
47
|
+
ctx.fillRect(x, trackHeight - barHeight, colWidth, barHeight)
|
|
48
|
+
}
|
|
49
|
+
}, [
|
|
50
|
+
blockSize,
|
|
51
|
+
colWidth,
|
|
52
|
+
trackHeight,
|
|
53
|
+
offsetX,
|
|
54
|
+
highResScaleFactor,
|
|
55
|
+
conservation,
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<canvas
|
|
60
|
+
ref={ref}
|
|
61
|
+
height={trackHeight * highResScaleFactor}
|
|
62
|
+
width={blockSize * highResScaleFactor}
|
|
63
|
+
style={{
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
left: scrollX + offsetX,
|
|
66
|
+
width: blockSize,
|
|
67
|
+
height: trackHeight,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const ConservationTrack = observer(function ({
|
|
74
|
+
model,
|
|
75
|
+
track,
|
|
76
|
+
}: {
|
|
77
|
+
model: MsaViewModel
|
|
78
|
+
track: BasicTrack
|
|
79
|
+
}) {
|
|
80
|
+
const { blocksX, msaAreaWidth } = model
|
|
81
|
+
const trackHeight = track.model.height
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
style={{
|
|
86
|
+
position: 'relative',
|
|
87
|
+
height: trackHeight,
|
|
88
|
+
width: msaAreaWidth,
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{blocksX.map(bx => (
|
|
93
|
+
<ConservationBlock
|
|
94
|
+
key={bx}
|
|
95
|
+
model={model}
|
|
96
|
+
offsetX={bx}
|
|
97
|
+
trackHeight={trackHeight}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export default ConservationTrack
|
|
@@ -10,6 +10,45 @@ import ImportForm from './import/ImportForm'
|
|
|
10
10
|
|
|
11
11
|
import type { MsaViewModel } from '../model'
|
|
12
12
|
|
|
13
|
+
function LoadingSpinner() {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 20 }}
|
|
17
|
+
>
|
|
18
|
+
<svg
|
|
19
|
+
width="24"
|
|
20
|
+
height="24"
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
23
|
+
>
|
|
24
|
+
<style>
|
|
25
|
+
{`@keyframes spinner { to { transform: rotate(360deg); } }`}
|
|
26
|
+
</style>
|
|
27
|
+
<circle
|
|
28
|
+
cx="12"
|
|
29
|
+
cy="12"
|
|
30
|
+
r="10"
|
|
31
|
+
stroke="#ccc"
|
|
32
|
+
strokeWidth="3"
|
|
33
|
+
fill="none"
|
|
34
|
+
/>
|
|
35
|
+
<path
|
|
36
|
+
d="M12 2a10 10 0 0 1 10 10"
|
|
37
|
+
stroke="#1976d2"
|
|
38
|
+
strokeWidth="3"
|
|
39
|
+
fill="none"
|
|
40
|
+
strokeLinecap="round"
|
|
41
|
+
style={{
|
|
42
|
+
animation: 'spinner 1s linear infinite',
|
|
43
|
+
transformOrigin: 'center',
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
<Typography variant="h6">Loading...</Typography>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
13
52
|
const Reset = observer(function ({
|
|
14
53
|
model,
|
|
15
54
|
error,
|
|
@@ -34,7 +73,8 @@ const Reset = observer(function ({
|
|
|
34
73
|
})
|
|
35
74
|
|
|
36
75
|
const Loading = observer(function ({ model }: { model: MsaViewModel }) {
|
|
37
|
-
const { isLoading, dataInitialized } = model
|
|
76
|
+
const { isLoading, dataInitialized, msaFilehandle, treeFilehandle } = model
|
|
77
|
+
const hasPendingFilehandle = !!(msaFilehandle || treeFilehandle)
|
|
38
78
|
|
|
39
79
|
return (
|
|
40
80
|
<div>
|
|
@@ -43,10 +83,12 @@ const Loading = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
43
83
|
>
|
|
44
84
|
{dataInitialized ? (
|
|
45
85
|
isLoading ? (
|
|
46
|
-
<
|
|
86
|
+
<LoadingSpinner />
|
|
47
87
|
) : (
|
|
48
88
|
<MSAView model={model} />
|
|
49
89
|
)
|
|
90
|
+
) : hasPendingFilehandle || isLoading ? (
|
|
91
|
+
<LoadingSpinner />
|
|
50
92
|
) : (
|
|
51
93
|
<ImportForm model={model} />
|
|
52
94
|
)}
|
|
@@ -3,6 +3,7 @@ import React, { Suspense } from 'react'
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
5
|
import { HorizontalResizeHandle, VerticalResizeHandle } from './ResizeHandles'
|
|
6
|
+
import Track from './Track'
|
|
6
7
|
import VerticalScrollbar from './VerticalScrollbar'
|
|
7
8
|
import Header from './header/Header'
|
|
8
9
|
import Minimap from './minimap/Minimap'
|
|
@@ -22,6 +23,72 @@ const TopArea = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
23
|
)
|
|
23
24
|
})
|
|
24
25
|
|
|
26
|
+
const TrackColumnIndicator = observer(function ({
|
|
27
|
+
model,
|
|
28
|
+
}: {
|
|
29
|
+
model: MsaViewModel
|
|
30
|
+
}) {
|
|
31
|
+
const {
|
|
32
|
+
mouseCol,
|
|
33
|
+
mouseClickCol,
|
|
34
|
+
colWidth,
|
|
35
|
+
scrollX,
|
|
36
|
+
treeAreaWidth,
|
|
37
|
+
resizeHandleWidth,
|
|
38
|
+
totalTrackAreaHeight,
|
|
39
|
+
} = model
|
|
40
|
+
|
|
41
|
+
const left = treeAreaWidth + resizeHandleWidth
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{mouseCol !== undefined ? (
|
|
46
|
+
<div
|
|
47
|
+
style={{
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
left: left + mouseCol * colWidth + scrollX,
|
|
50
|
+
top: 0,
|
|
51
|
+
width: colWidth,
|
|
52
|
+
height: totalTrackAreaHeight,
|
|
53
|
+
backgroundColor: 'rgba(0,0,0,0.15)',
|
|
54
|
+
pointerEvents: 'none',
|
|
55
|
+
zIndex: 100,
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
) : null}
|
|
59
|
+
{mouseClickCol !== undefined ? (
|
|
60
|
+
<div
|
|
61
|
+
style={{
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
left: left + mouseClickCol * colWidth + scrollX,
|
|
64
|
+
top: 0,
|
|
65
|
+
width: colWidth,
|
|
66
|
+
height: totalTrackAreaHeight,
|
|
67
|
+
backgroundColor: 'rgba(128,128,0,0.2)',
|
|
68
|
+
pointerEvents: 'none',
|
|
69
|
+
zIndex: 100,
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
) : null}
|
|
73
|
+
</>
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const TrackArea = observer(function ({ model }: { model: MsaViewModel }) {
|
|
78
|
+
const { turnedOnTracks } = model
|
|
79
|
+
if (turnedOnTracks.length === 0) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ position: 'relative' }}>
|
|
84
|
+
<TrackColumnIndicator model={model} />
|
|
85
|
+
{turnedOnTracks.map(track => (
|
|
86
|
+
<Track key={track.model.id} model={model} track={track} />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
25
92
|
const MainArea = observer(function ({ model }: { model: MsaViewModel }) {
|
|
26
93
|
const { showVerticalScrollbar } = model
|
|
27
94
|
|
|
@@ -39,6 +106,7 @@ const View = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
39
106
|
return (
|
|
40
107
|
<div style={{ position: 'relative' }}>
|
|
41
108
|
<TopArea model={model} />
|
|
109
|
+
<TrackArea model={model} />
|
|
42
110
|
<MainArea model={model} />
|
|
43
111
|
</div>
|
|
44
112
|
)
|
|
@@ -24,10 +24,11 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
|
|
|
24
24
|
const [showGaps, setShowGaps] = useState(false)
|
|
25
25
|
const [showEmpty, setShowEmpty] = useState(false)
|
|
26
26
|
|
|
27
|
+
const removeGaps = (s: string) => s.replaceAll('-', '').replaceAll('.', '')
|
|
27
28
|
const disp = str
|
|
28
|
-
.map(([s1, s2]) => [s1, showGaps ? s2 : s2
|
|
29
|
+
.map(([s1, s2]) => [s1, showGaps ? s2 : removeGaps(s2)] as const)
|
|
29
30
|
.filter(f => (showEmpty ? true : !!f[1]))
|
|
30
|
-
.map(([s1, s2]) => `>${s1}\n${showGaps ? s2 : s2
|
|
31
|
+
.map(([s1, s2]) => `>${s1}\n${showGaps ? s2 : removeGaps(s2)}`)
|
|
31
32
|
.join('\n')
|
|
32
33
|
return (
|
|
33
34
|
<>
|
|
@@ -6,14 +6,14 @@ import { observer } from 'mobx-react'
|
|
|
6
6
|
import { colorContrast } from '../util'
|
|
7
7
|
|
|
8
8
|
import type { MsaViewModel } from '../model'
|
|
9
|
-
import type {
|
|
9
|
+
import type { BasicTrack } from '../types'
|
|
10
10
|
|
|
11
11
|
const AnnotationBlock = observer(function ({
|
|
12
12
|
track,
|
|
13
13
|
model,
|
|
14
14
|
offsetX,
|
|
15
15
|
}: {
|
|
16
|
-
track:
|
|
16
|
+
track: BasicTrack
|
|
17
17
|
model: MsaViewModel
|
|
18
18
|
offsetX: number
|
|
19
19
|
}) {
|
|
@@ -58,7 +58,7 @@ const AnnotationBlock = observer(function ({
|
|
|
58
58
|
|
|
59
59
|
const xStart = Math.max(0, Math.floor(offsetX / colWidth))
|
|
60
60
|
const xEnd = Math.max(0, Math.ceil((offsetX + blockSize) / colWidth))
|
|
61
|
-
const str = data
|
|
61
|
+
const str = data?.slice(xStart, xEnd)
|
|
62
62
|
for (let i = 0; str && i < str.length; i++) {
|
|
63
63
|
const letter = str[i]!
|
|
64
64
|
const color = colorScheme[letter.toUpperCase()]
|
|
@@ -102,10 +102,13 @@ const AnnotationTrack = observer(function ({
|
|
|
102
102
|
track,
|
|
103
103
|
model,
|
|
104
104
|
}: {
|
|
105
|
-
track:
|
|
105
|
+
track: BasicTrack
|
|
106
106
|
model: MsaViewModel
|
|
107
107
|
}) {
|
|
108
108
|
const { blocksX, msaAreaWidth, rowHeight } = model
|
|
109
|
+
if (!track.model.data) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
109
112
|
return (
|
|
110
113
|
<div
|
|
111
114
|
style={{
|
package/src/components/Track.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import React, { lazy, useEffect, useRef, useState } from 'react'
|
|
|
3
3
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
|
|
4
4
|
import { IconButton, Menu, MenuItem } from '@mui/material'
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
|
-
import normalizeWheel from 'normalize-wheel'
|
|
7
6
|
import { makeStyles } from 'tss-react/mui'
|
|
8
7
|
|
|
9
8
|
import type { MsaViewModel } from '../model'
|
|
@@ -28,8 +27,7 @@ export const TrackLabel = observer(function ({
|
|
|
28
27
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>()
|
|
29
28
|
const { drawLabels, rowHeight, treeAreaWidth: width } = model
|
|
30
29
|
const {
|
|
31
|
-
height,
|
|
32
|
-
model: { name },
|
|
30
|
+
model: { name, height },
|
|
33
31
|
} = track
|
|
34
32
|
const { classes } = useStyles()
|
|
35
33
|
const trackLabelHeight = Math.max(8, rowHeight - 8)
|
|
@@ -101,7 +99,7 @@ const Track = observer(function ({
|
|
|
101
99
|
|
|
102
100
|
track: any
|
|
103
101
|
}) {
|
|
104
|
-
const { resizeHandleWidth } = model
|
|
102
|
+
const { resizeHandleWidth, colWidth, scrollX, numColumns } = model
|
|
105
103
|
const {
|
|
106
104
|
model: { height, error },
|
|
107
105
|
} = track
|
|
@@ -113,9 +111,8 @@ const Track = observer(function ({
|
|
|
113
111
|
if (!curr) {
|
|
114
112
|
return
|
|
115
113
|
}
|
|
116
|
-
function onWheel(
|
|
117
|
-
|
|
118
|
-
deltaX.current += event.pixelX
|
|
114
|
+
function onWheel(event: WheelEvent) {
|
|
115
|
+
deltaX.current += event.deltaX
|
|
119
116
|
|
|
120
117
|
if (!scheduled.current) {
|
|
121
118
|
scheduled.current = true
|
|
@@ -125,18 +122,37 @@ const Track = observer(function ({
|
|
|
125
122
|
scheduled.current = false
|
|
126
123
|
})
|
|
127
124
|
}
|
|
128
|
-
|
|
125
|
+
event.preventDefault()
|
|
129
126
|
}
|
|
130
127
|
curr.addEventListener('wheel', onWheel)
|
|
131
128
|
return () => {
|
|
132
129
|
curr.removeEventListener('wheel', onWheel)
|
|
133
130
|
}
|
|
134
131
|
}, [model])
|
|
132
|
+
|
|
135
133
|
return (
|
|
136
134
|
<div key={track.id} style={{ display: 'flex', height }}>
|
|
137
135
|
<TrackLabel model={model} track={track} />
|
|
138
136
|
<div style={{ width: resizeHandleWidth, flexShrink: 0 }} />
|
|
139
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
ref={ref}
|
|
139
|
+
onMouseMove={event => {
|
|
140
|
+
if (!ref.current) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const { left } = ref.current.getBoundingClientRect()
|
|
144
|
+
const mouseX = event.clientX - left - scrollX
|
|
145
|
+
const col = Math.floor(mouseX / colWidth)
|
|
146
|
+
if (col >= 0 && col < numColumns) {
|
|
147
|
+
model.setMousePos(col, undefined)
|
|
148
|
+
} else {
|
|
149
|
+
model.setMousePos(undefined, undefined)
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
onMouseLeave={() => {
|
|
153
|
+
model.setMousePos(undefined, undefined)
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
140
156
|
{error ? (
|
|
141
157
|
<div style={{ color: 'red', fontSize: 10 }}>{`${error}`}</div>
|
|
142
158
|
) : (
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
|
|
4
4
|
import {
|
|
5
|
+
Alert,
|
|
5
6
|
Button,
|
|
6
7
|
DialogActions,
|
|
7
8
|
DialogContent,
|
|
@@ -26,9 +27,16 @@ export default function ExportSVGDialog({
|
|
|
26
27
|
onClose: () => void
|
|
27
28
|
}) {
|
|
28
29
|
const [includeMinimap, setIncludeMinimap] = useState(true)
|
|
30
|
+
const [includeTracks, setIncludeTracks] = useState(true)
|
|
29
31
|
const [exportType, setExportType] = useState('viewport')
|
|
30
32
|
const [error, setError] = useState<unknown>()
|
|
31
33
|
const theme = useTheme()
|
|
34
|
+
const { totalWidth, totalHeight, treeAreaWidth, turnedOnTracks } = model
|
|
35
|
+
const hasTracks = turnedOnTracks.length > 0
|
|
36
|
+
const entireWidth = totalWidth + treeAreaWidth
|
|
37
|
+
const entireHeight = totalHeight
|
|
38
|
+
const isLargeExport =
|
|
39
|
+
exportType === 'entire' && (entireWidth > 10000 || entireHeight > 10000)
|
|
32
40
|
return (
|
|
33
41
|
<Dialog
|
|
34
42
|
onClose={() => {
|
|
@@ -48,6 +56,15 @@ export default function ExportSVGDialog({
|
|
|
48
56
|
setIncludeMinimap(!includeMinimap)
|
|
49
57
|
}}
|
|
50
58
|
/>
|
|
59
|
+
{hasTracks ? (
|
|
60
|
+
<Checkbox2
|
|
61
|
+
label="Include tracks?"
|
|
62
|
+
checked={includeTracks}
|
|
63
|
+
onChange={() => {
|
|
64
|
+
setIncludeTracks(!includeTracks)
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
51
68
|
<div>
|
|
52
69
|
<FormControl>
|
|
53
70
|
<FormLabel>Export type</FormLabel>
|
|
@@ -70,6 +87,12 @@ export default function ExportSVGDialog({
|
|
|
70
87
|
</RadioGroup>
|
|
71
88
|
</FormControl>
|
|
72
89
|
</div>
|
|
90
|
+
{isLargeExport ? (
|
|
91
|
+
<Alert severity="warning" style={{ marginTop: 8 }}>
|
|
92
|
+
The entire MSA is very large ({Math.round(entireWidth)}x
|
|
93
|
+
{Math.round(entireHeight)} pixels). Export may be slow or fail.
|
|
94
|
+
</Alert>
|
|
95
|
+
) : null}
|
|
73
96
|
</DialogContent>
|
|
74
97
|
<DialogActions>
|
|
75
98
|
<Button
|
|
@@ -83,13 +106,14 @@ export default function ExportSVGDialog({
|
|
|
83
106
|
theme,
|
|
84
107
|
includeMinimap:
|
|
85
108
|
exportType === 'entire' ? false : includeMinimap,
|
|
109
|
+
includeTracks: hasTracks && includeTracks,
|
|
86
110
|
exportType,
|
|
87
111
|
})
|
|
112
|
+
onClose()
|
|
88
113
|
} catch (e) {
|
|
89
114
|
console.error(e)
|
|
90
115
|
setError(e)
|
|
91
116
|
}
|
|
92
|
-
onClose()
|
|
93
117
|
})()
|
|
94
118
|
}}
|
|
95
119
|
>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Slider, Typography } from '@mui/material'
|
|
4
|
+
import { observer } from 'mobx-react'
|
|
5
|
+
|
|
6
|
+
import type { MsaViewModel } from '../../model'
|
|
7
|
+
|
|
8
|
+
const GappynessSlider = observer(function GappynessSlider({
|
|
9
|
+
model,
|
|
10
|
+
}: {
|
|
11
|
+
model: MsaViewModel
|
|
12
|
+
}) {
|
|
13
|
+
const { hideGaps, allowedGappyness } = model
|
|
14
|
+
if (!hideGaps) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
19
|
+
<Typography style={{ whiteSpace: 'nowrap' }}>
|
|
20
|
+
Hide columns w/ >{allowedGappyness}% gaps
|
|
21
|
+
</Typography>
|
|
22
|
+
<Slider
|
|
23
|
+
style={{ width: 100 }}
|
|
24
|
+
min={1}
|
|
25
|
+
max={100}
|
|
26
|
+
value={allowedGappyness}
|
|
27
|
+
onChange={(_, val) => {
|
|
28
|
+
model.setAllowedGappyness(val)
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export default GappynessSlider
|