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.
Files changed (129) hide show
  1. package/bundle/index.js +9 -9
  2. package/bundle/index.js.LICENSE.txt +8 -8
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.d.ts +0 -6
  5. package/dist/colorSchemes.js +1 -119
  6. package/dist/colorSchemes.js.map +1 -1
  7. package/dist/components/ConservationTrack.d.ts +8 -0
  8. package/dist/components/ConservationTrack.js +54 -0
  9. package/dist/components/ConservationTrack.js.map +1 -0
  10. package/dist/components/Loading.js +14 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +36 -0
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/SequenceTextArea.js +3 -2
  15. package/dist/components/SequenceTextArea.js.map +1 -1
  16. package/dist/components/TextTrack.d.ts +3 -3
  17. package/dist/components/TextTrack.js +4 -1
  18. package/dist/components/TextTrack.js.map +1 -1
  19. package/dist/components/Track.js +21 -8
  20. package/dist/components/Track.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +19 -3
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/header/GappynessSlider.d.ts +6 -0
  24. package/dist/components/header/GappynessSlider.js +19 -0
  25. package/dist/components/header/GappynessSlider.js.map +1 -0
  26. package/dist/components/header/Header.js +3 -1
  27. package/dist/components/header/Header.js.map +1 -1
  28. package/dist/components/header/HeaderMenu.js +30 -14
  29. package/dist/components/header/HeaderMenu.js.map +1 -1
  30. package/dist/components/minimap/MinimapSVG.js +4 -3
  31. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  32. package/dist/components/msa/MSACanvasBlock.js +56 -42
  33. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  34. package/dist/components/msa/renderMSABlock.js +53 -10
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  37. package/dist/components/tracks/renderTracksSvg.js +83 -0
  38. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  39. package/dist/components/tree/TreeCanvasBlock.js +1 -1
  40. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  41. package/dist/components/tree/TreeNodeMenu.js +2 -2
  42. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  43. package/dist/components/tree/renderTreeCanvas.js +1 -1
  44. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  45. package/dist/constants.d.ts +22 -0
  46. package/dist/constants.js +26 -0
  47. package/dist/constants.js.map +1 -0
  48. package/dist/layout.js.map +1 -1
  49. package/dist/model/msaModel.js +3 -2
  50. package/dist/model/msaModel.js.map +1 -1
  51. package/dist/model/treeModel.js +9 -8
  52. package/dist/model/treeModel.js.map +1 -1
  53. package/dist/model.d.ts +256 -15
  54. package/dist/model.js +408 -128
  55. package/dist/model.js.map +1 -1
  56. package/dist/neighborJoining.d.ts +1 -0
  57. package/dist/neighborJoining.js +839 -0
  58. package/dist/neighborJoining.js.map +1 -0
  59. package/dist/neighborJoining.test.d.ts +1 -0
  60. package/dist/neighborJoining.test.js +110 -0
  61. package/dist/neighborJoining.test.js.map +1 -0
  62. package/dist/parsers/A3mMSA.d.ts +43 -0
  63. package/dist/parsers/A3mMSA.js +277 -0
  64. package/dist/parsers/A3mMSA.js.map +1 -0
  65. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  66. package/dist/parsers/A3mMSA.test.js +138 -0
  67. package/dist/parsers/A3mMSA.test.js.map +1 -0
  68. package/dist/parsers/ClustalMSA.d.ts +4 -4
  69. package/dist/parsers/ClustalMSA.js +3 -1
  70. package/dist/parsers/ClustalMSA.js.map +1 -1
  71. package/dist/parsers/FastaMSA.js +17 -16
  72. package/dist/parsers/FastaMSA.js.map +1 -1
  73. package/dist/renderToSvg.d.ts +1 -0
  74. package/dist/renderToSvg.js +48 -18
  75. package/dist/renderToSvg.js.map +1 -1
  76. package/dist/rowCoordinateCalculations.js +3 -5
  77. package/dist/rowCoordinateCalculations.js.map +1 -1
  78. package/dist/rowCoordinateCalculations.test.js +14 -2
  79. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  80. package/dist/seqCoordToRowSpecificGlobalCoord.js +9 -5
  81. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -1
  82. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +6 -6
  83. package/dist/types.d.ts +2 -3
  84. package/dist/util.js +17 -9
  85. package/dist/util.js.map +1 -1
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/package.json +6 -6
  89. package/src/colorSchemes.ts +1 -179
  90. package/src/components/ConservationTrack.tsx +104 -0
  91. package/src/components/Loading.tsx +44 -2
  92. package/src/components/MSAView.tsx +68 -0
  93. package/src/components/SequenceTextArea.tsx +3 -2
  94. package/src/components/TextTrack.tsx +7 -4
  95. package/src/components/Track.tsx +25 -9
  96. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  97. package/src/components/header/GappynessSlider.tsx +35 -0
  98. package/src/components/header/Header.tsx +3 -1
  99. package/src/components/header/HeaderMenu.tsx +36 -15
  100. package/src/components/minimap/MinimapSVG.tsx +6 -3
  101. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  102. package/src/components/msa/renderMSABlock.ts +82 -22
  103. package/src/components/tracks/renderTracksSvg.ts +157 -0
  104. package/src/components/tree/TreeCanvasBlock.tsx +1 -1
  105. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  106. package/src/components/tree/renderTreeCanvas.ts +1 -1
  107. package/src/constants.ts +27 -0
  108. package/src/layout.ts +1 -6
  109. package/src/model/msaModel.ts +4 -2
  110. package/src/model/treeModel.ts +19 -8
  111. package/src/model.ts +496 -140
  112. package/src/neighborJoining.test.ts +129 -0
  113. package/src/neighborJoining.ts +885 -0
  114. package/src/parsers/A3mMSA.test.ts +164 -0
  115. package/src/parsers/A3mMSA.ts +321 -0
  116. package/src/parsers/ClustalMSA.ts +7 -5
  117. package/src/parsers/FastaMSA.ts +17 -17
  118. package/src/renderToSvg.tsx +105 -26
  119. package/src/rowCoordinateCalculations.test.ts +15 -2
  120. package/src/rowCoordinateCalculations.ts +3 -5
  121. package/src/seqCoordToRowSpecificGlobalCoord.test.ts +6 -6
  122. package/src/seqCoordToRowSpecificGlobalCoord.ts +9 -4
  123. package/src/types.ts +2 -4
  124. package/src/util.ts +21 -8
  125. package/src/version.ts +1 -1
  126. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  127. package/dist/components/dialogs/TracklistDialog.js +0 -23
  128. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  129. package/src/components/dialogs/TracklistDialog.tsx +0 -73
@@ -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 { isBlank, transform } from './util'
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
- <Typography variant="h4">Loading...</Typography>
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.replaceAll('-', '')] as const)
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.replaceAll('-', '')}`)
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 { ITextTrack } from '../types'
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: ITextTrack
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.slice(xStart, xEnd)
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: ITextTrack
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={{
@@ -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(origEvent: WheelEvent) {
117
- const event = normalizeWheel(origEvent)
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
- origEvent.preventDefault()
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 ref={ref}>
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/ &gt;{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