react-msaview 5.10.0 → 6.1.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 +99 -99
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.js +22 -22
- package/dist/clustalX.js.map +1 -1
- package/dist/columnCounts.d.ts +7 -0
- package/dist/columnCounts.js +18 -0
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/CopyButton.d.ts +9 -0
- package/dist/components/CopyButton.js +23 -0
- package/dist/components/CopyButton.js.map +1 -0
- package/dist/components/DragHandle.d.ts +25 -0
- package/dist/components/DragHandle.js +49 -0
- package/dist/components/DragHandle.js.map +1 -0
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +2 -2
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +4 -2
- package/dist/components/ResizeHandles.js +35 -54
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -10
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +11 -25
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +3 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +52 -129
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
- package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
- package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
- package/dist/components/dialogs/TrackInfoDialog.js +8 -14
- package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
- package/dist/components/header/FileMenu.js +3 -3
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +1 -1
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
- package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
- package/dist/components/header/getAnnotationsMenu.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +15 -0
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +66 -57
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +40 -23
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.d.ts +2 -1
- package/dist/components/minimap/MinimapSVG.js +3 -4
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +10 -1
- package/dist/components/minimap/minimapLayout.js +15 -10
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
- package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
- package/dist/components/msa/AnnotationLegend.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +9 -3
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +12 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +1 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/MSAPanel.js +2 -2
- package/dist/components/msa/MSAPanel.js.map +1 -1
- package/dist/components/msa/domainBandCursor.d.ts +17 -0
- package/dist/components/msa/domainBandCursor.js +39 -0
- package/dist/components/msa/domainBandCursor.js.map +1 -0
- package/dist/components/msa/msaRaster.d.ts +63 -0
- package/dist/components/msa/msaRaster.js +252 -0
- package/dist/components/msa/msaRaster.js.map +1 -0
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +27 -41
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -2
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/tileColor.d.ts +6 -0
- package/dist/components/msa/tileColor.js +35 -0
- package/dist/components/msa/tileColor.js.map +1 -0
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.d.ts +14 -0
- package/dist/components/tracks/TrackBlocks.js +65 -0
- package/dist/components/tracks/TrackBlocks.js.map +1 -0
- package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
- package/dist/components/tracks/drawTracks.js +193 -0
- package/dist/components/tracks/drawTracks.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -2
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +7 -4
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +5 -2
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +5 -2
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -1
- package/dist/ggplotPalettes.d.ts +0 -1
- package/dist/ggplotPalettes.js +0 -3
- package/dist/ggplotPalettes.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -6
- package/dist/hierarchy.js +4 -6
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/launchInterProScan.d.ts +0 -23
- package/dist/launchInterProScan.js +2 -5
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +21 -6
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/DataModel.d.ts +1 -4
- package/dist/model/DataModel.js +8 -10
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +86 -107
- package/dist/model.js +238 -162
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +34 -18
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderTestEnv.d.ts +43 -0
- package/dist/renderTestEnv.js +88 -0
- package/dist/renderTestEnv.js.map +1 -0
- package/dist/renderToSvg.js +3 -3
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +13 -17
- package/dist/rowCoordinateCalculations.js +45 -39
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/sequenceLogo.d.ts +28 -0
- package/dist/sequenceLogo.js +47 -0
- package/dist/sequenceLogo.js.map +1 -0
- package/dist/types.d.ts +6 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/clustalX.ts +22 -22
- package/src/colSpace.test.ts +27 -0
- package/src/columnCounts.test.ts +15 -0
- package/src/columnCounts.ts +19 -0
- package/src/components/CopyButton.tsx +44 -0
- package/src/components/DragHandle.tsx +81 -0
- package/src/components/Loading.tsx +10 -1
- package/src/components/MSAView.tsx +2 -3
- package/src/components/ResizeHandles.tsx +70 -87
- package/src/components/SequenceTextArea.tsx +3 -16
- package/src/components/VerticalScrollbar.tsx +20 -33
- package/src/components/dialogs/FeatureDialog.tsx +3 -4
- package/src/components/dialogs/InterProScanDialog.tsx +54 -131
- package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
- package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
- package/src/components/header/FileMenu.tsx +3 -3
- package/src/components/header/GappynessSlider.tsx +4 -1
- package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
- package/src/components/header/settingsMenuItems.ts +18 -0
- package/src/components/import/ImportForm.tsx +1 -1
- package/src/components/import/ImportFormExamples.tsx +82 -133
- package/src/components/minimap/Minimap.tsx +67 -32
- package/src/components/minimap/MinimapSVG.tsx +32 -31
- package/src/components/minimap/minimapLayout.test.ts +60 -0
- package/src/components/minimap/minimapLayout.ts +15 -10
- package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
- package/src/components/msa/MSACanvas.tsx +24 -11
- package/src/components/msa/MSACanvasBlock.tsx +13 -3
- package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
- package/src/components/msa/MSAPanel.tsx +2 -2
- package/src/components/msa/domainBandCursor.ts +41 -0
- package/src/components/msa/msaRaster.test.ts +145 -0
- package/src/components/msa/msaRaster.ts +350 -0
- package/src/components/msa/renderMSABlock.ts +34 -43
- package/src/components/msa/renderMSAMouseover.ts +1 -3
- package/src/components/msa/tileColor.ts +37 -0
- package/src/components/renderCtx.ts +2 -0
- package/src/components/tracks/TrackBlocks.tsx +111 -0
- package/src/components/tracks/drawTracks.ts +323 -0
- package/src/components/tree/TreeCanvas.tsx +16 -4
- package/src/components/tree/TreeCanvasBlock.tsx +18 -11
- package/src/components/tree/TreeRuler.tsx +7 -2
- package/src/components/tree/renderTreeCanvas.test.ts +15 -7
- package/src/components/tree/renderTreeCanvas.ts +5 -1
- package/src/constants.ts +10 -0
- package/src/domainBands.test.ts +24 -0
- package/src/domainLetterColor.test.ts +80 -0
- package/src/ggplotPalettes.ts +0 -4
- package/src/hierarchy.test.ts +7 -7
- package/src/hierarchy.ts +6 -9
- package/src/impgRender.test.tsx +2 -57
- package/src/index.ts +4 -1
- package/src/largeTree.test.ts +39 -4
- package/src/launchInterProScan.ts +3 -30
- package/src/loadErrorDisplay.test.tsx +102 -0
- package/src/measureTextCanvas.ts +22 -6
- package/src/model/DataModel.ts +11 -11
- package/src/model.ts +262 -172
- package/src/neighborJoining.ts +36 -18
- package/src/renderDomainsSvg.test.tsx +2 -57
- package/src/renderMinimapSvg.test.tsx +55 -0
- package/src/renderSequenceLogoSvg.test.tsx +176 -0
- package/src/renderTestEnv.ts +96 -0
- package/src/renderToSvg.tsx +3 -3
- package/src/rowCoordinateCalculations.ts +48 -44
- package/src/sequenceLogo.test.ts +88 -0
- package/src/sequenceLogo.ts +63 -0
- package/src/types.ts +17 -16
- package/src/version.ts +1 -1
- package/src/viewChrome.test.tsx +134 -0
- package/dist/components/ConservationTrack.d.ts +0 -8
- package/dist/components/ConservationTrack.js +0 -49
- package/dist/components/ConservationTrack.js.map +0 -1
- package/dist/components/TextTrack.d.ts +0 -8
- package/dist/components/TextTrack.js +0 -58
- package/dist/components/TextTrack.js.map +0 -1
- package/dist/components/dialogs/DomainDialog.d.ts +0 -6
- package/dist/components/dialogs/DomainDialog.js +0 -19
- package/dist/components/dialogs/DomainDialog.js.map +0 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
- package/dist/components/header/SettingsMenu.d.ts +0 -6
- package/dist/components/header/SettingsMenu.js +0 -40
- package/dist/components/header/SettingsMenu.js.map +0 -1
- package/dist/components/header/getDomainsMenu.js.map +0 -1
- package/dist/components/msa/DomainLegend.js.map +0 -1
- package/dist/components/tracks/renderTracksSvg.js +0 -126
- package/dist/components/tracks/renderTracksSvg.js.map +0 -1
- package/src/components/ConservationTrack.tsx +0 -99
- package/src/components/TextTrack.tsx +0 -103
- package/src/components/dialogs/DomainDialog.tsx +0 -45
- package/src/components/header/SettingsMenu.tsx +0 -57
- package/src/components/tracks/renderTracksSvg.ts +0 -247
package/src/neighborJoining.ts
CHANGED
|
@@ -31,25 +31,43 @@ const BLOSUM62_DATA = [
|
|
|
31
31
|
[-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4, 1],
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const NUM_SYMBOLS = BLOSUM62_KEYS.length
|
|
35
|
+
const BLOSUM62 = Int8Array.from(BLOSUM62_DATA.flat())
|
|
36
|
+
|
|
37
|
+
// charCode -> row/column in BLOSUM62, -1 for anything the matrix does not name.
|
|
38
|
+
// A flat table rather than a Map of Maps keyed by an upper-cased character, for
|
|
39
|
+
// the reason columnCounts.ts gives: the distance matrix is O(rows^2 * columns)
|
|
40
|
+
// lookups, and on a 120x800 alignment the Map form spent nearly all of its time
|
|
41
|
+
// allocating one-character strings in toUpperCase: 2.6s to build a tree, versus
|
|
42
|
+
// 0.34s here for a byte-identical newick.
|
|
43
|
+
const symbolOfCode = new Int8Array(128).fill(-1)
|
|
44
|
+
for (let i = 0; i < NUM_SYMBOLS; i++) {
|
|
45
|
+
const key = BLOSUM62_KEYS[i]!
|
|
46
|
+
symbolOfCode[key.charCodeAt(0)] = i
|
|
47
|
+
symbolOfCode[key.toLowerCase().charCodeAt(0)] = i
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
50
|
+
// gap, or past the end of a row shorter than the alignment
|
|
51
|
+
const GAP = -2
|
|
46
52
|
|
|
47
53
|
// stockholm and a3m write gaps as '.', not just '-'. Scoring a '.' as a residue
|
|
48
54
|
// charges the pair the unknown-symbol penalty on every padded column, so two
|
|
49
55
|
// sequences that differ only in where an unrelated row forced padding come out
|
|
50
56
|
// as divergent.
|
|
51
|
-
function
|
|
52
|
-
|
|
57
|
+
function symbolAt(seq: string, i: number) {
|
|
58
|
+
if (i >= seq.length) {
|
|
59
|
+
return GAP
|
|
60
|
+
}
|
|
61
|
+
const code = seq.charCodeAt(i)
|
|
62
|
+
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
63
|
+
if ((code - 45) >>> 0 <= 1) {
|
|
64
|
+
return GAP
|
|
65
|
+
}
|
|
66
|
+
return code < 128 ? symbolOfCode[code]! : -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function blosumScore(a: number, b: number) {
|
|
70
|
+
return a < 0 || b < 0 ? -4 : BLOSUM62[a * NUM_SYMBOLS + b]!
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
function computePairwiseDistance(seq1: string, seq2: string) {
|
|
@@ -63,11 +81,11 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
63
81
|
let maxPossibleScore = 0
|
|
64
82
|
|
|
65
83
|
for (let i = 0; i < len; i++) {
|
|
66
|
-
const a = seq1
|
|
67
|
-
const b = seq2
|
|
84
|
+
const a = symbolAt(seq1, i)
|
|
85
|
+
const b = symbolAt(seq2, i)
|
|
68
86
|
|
|
69
|
-
const aGap =
|
|
70
|
-
const bGap =
|
|
87
|
+
const aGap = a === GAP
|
|
88
|
+
const bGap = b === GAP
|
|
71
89
|
if (aGap && bGap) {
|
|
72
90
|
continue
|
|
73
91
|
}
|
|
@@ -77,8 +95,8 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
77
95
|
continue
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
totalScore +=
|
|
81
|
-
maxPossibleScore += Math.max(
|
|
98
|
+
totalScore += blosumScore(a, b)
|
|
99
|
+
maxPossibleScore += Math.max(blosumScore(a, a), blosumScore(b, b))
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
if (compared === 0) {
|
|
@@ -9,67 +9,12 @@ import { enableStaticRendering } from 'mobx-react'
|
|
|
9
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
10
|
|
|
11
11
|
import MSAModelF from './model.ts'
|
|
12
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
12
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
13
14
|
|
|
14
|
-
class Mat {
|
|
15
|
-
constructor(
|
|
16
|
-
public a = 1,
|
|
17
|
-
public b = 0,
|
|
18
|
-
public c = 0,
|
|
19
|
-
public d = 1,
|
|
20
|
-
public e = 0,
|
|
21
|
-
public f = 0,
|
|
22
|
-
) {}
|
|
23
|
-
multiply(o: Mat) {
|
|
24
|
-
return new Mat(
|
|
25
|
-
this.a * o.a + this.c * o.b,
|
|
26
|
-
this.b * o.a + this.d * o.b,
|
|
27
|
-
this.a * o.c + this.c * o.d,
|
|
28
|
-
this.b * o.c + this.d * o.d,
|
|
29
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
30
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
translate(x: number, y = 0) {
|
|
34
|
-
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
35
|
-
}
|
|
36
|
-
scale(x: number, y = x) {
|
|
37
|
-
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
class Pt {
|
|
41
|
-
constructor(
|
|
42
|
-
public x = 0,
|
|
43
|
-
public y = 0,
|
|
44
|
-
) {}
|
|
45
|
-
matrixTransform(m: Mat) {
|
|
46
|
-
return new Pt(
|
|
47
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
48
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
15
|
beforeAll(() => {
|
|
54
16
|
enableStaticRendering(true)
|
|
55
|
-
|
|
56
|
-
g.DOMMatrix = Mat
|
|
57
|
-
g.DOMPoint = Pt
|
|
58
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
59
|
-
let font = '10px sans-serif'
|
|
60
|
-
return {
|
|
61
|
-
get font() {
|
|
62
|
-
return font
|
|
63
|
-
},
|
|
64
|
-
set font(v: string) {
|
|
65
|
-
font = v
|
|
66
|
-
},
|
|
67
|
-
measureText(t: string) {
|
|
68
|
-
const size = Number.parseFloat(font) || 10
|
|
69
|
-
return { width: t.length * size * 0.6 } as TextMetrics
|
|
70
|
-
},
|
|
71
|
-
} as unknown as CanvasRenderingContext2D
|
|
72
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
17
|
+
installRenderTestEnv()
|
|
73
18
|
})
|
|
74
19
|
|
|
75
20
|
const msa = `>seq1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The viewport export draws a minimap by default, and nothing covered it. The
|
|
4
|
+
// export lays the figure out on its own grid rather than the on-screen one, so
|
|
5
|
+
// the minimap has to be told how wide to draw -- reading a width off the model
|
|
6
|
+
// puts its trapezoid short of the alignment column beside it.
|
|
7
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
8
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
9
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import MSAModelF from './model.ts'
|
|
12
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
13
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
enableStaticRendering(true)
|
|
17
|
+
installRenderTestEnv()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const width = 1000
|
|
21
|
+
|
|
22
|
+
function makeModel() {
|
|
23
|
+
const row = 'ACDEFGHIKL'.repeat(40)
|
|
24
|
+
const model = MSAModelF().create({
|
|
25
|
+
type: 'MsaView',
|
|
26
|
+
msaFormat: 'fasta',
|
|
27
|
+
height: 300,
|
|
28
|
+
data: { msa: `>seq1\n${row}\n>seq2\n${row}` },
|
|
29
|
+
})
|
|
30
|
+
model.setWidth(width)
|
|
31
|
+
return model
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('the exported minimap spans the exported alignment column', async () => {
|
|
35
|
+
const model = makeModel()
|
|
36
|
+
expect(model.showHorizontalScrollbar).toBe(true)
|
|
37
|
+
|
|
38
|
+
const svg = await renderToSvg(model, {
|
|
39
|
+
theme: createJBrowseTheme(),
|
|
40
|
+
exportType: 'viewport',
|
|
41
|
+
includeMinimap: true,
|
|
42
|
+
includeTracks: false,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// the minimap column runs from the tree area to the right edge of the figure
|
|
46
|
+
const minimapWidth = width - model.treeAreaWidth
|
|
47
|
+
const polygon = /<polygon[^>]*points="([^"]*)"/.exec(svg)?.[1]
|
|
48
|
+
expect(polygon).toBeDefined()
|
|
49
|
+
// bottom edge of the trapezoid reaches the full width it was given
|
|
50
|
+
expect(polygon!.endsWith(`${minimapWidth},${model.minimapHeight - 12}`)).toBe(
|
|
51
|
+
true,
|
|
52
|
+
)
|
|
53
|
+
// and the bar outline is drawn to the same width
|
|
54
|
+
expect(svg).toContain(`width="${minimapWidth}"`)
|
|
55
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The logo track is the first track kind whose letters are drawn under a
|
|
4
|
+
// per-letter transform rather than at a fixed size, so it is also the first that
|
|
5
|
+
// can silently draw nothing if the export context loses that transform. These
|
|
6
|
+
// tests go through the real SVG export to pin the whole path: the track is off
|
|
7
|
+
// until asked for, the stack is ordered and scaled, and the glyphs land inside
|
|
8
|
+
// the track's own band.
|
|
9
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
10
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
11
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
12
|
+
|
|
13
|
+
import MSAModelF from './model.ts'
|
|
14
|
+
import { TEST_CHAR_WIDTH_RATIO, installRenderTestEnv } from './renderTestEnv.ts'
|
|
15
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
enableStaticRendering(true)
|
|
19
|
+
installRenderTestEnv()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// column 0 is fully conserved A, column 1 is a 3:1 split of C over G, and
|
|
23
|
+
// column 2 is an even four-way split carrying no information at all
|
|
24
|
+
const msa = `>s1\nACA\n>s2\nACC\n>s3\nACG\n>s4\nAGT\n`
|
|
25
|
+
const colWidth = 40
|
|
26
|
+
|
|
27
|
+
function makeModel() {
|
|
28
|
+
const model = MSAModelF().create({
|
|
29
|
+
type: 'MsaView',
|
|
30
|
+
msaFormat: 'fasta',
|
|
31
|
+
height: 400,
|
|
32
|
+
colWidth,
|
|
33
|
+
data: { msa },
|
|
34
|
+
})
|
|
35
|
+
model.setWidth(1000)
|
|
36
|
+
return model
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function exportSvg(model: ReturnType<typeof makeModel>) {
|
|
40
|
+
return renderToSvg(model, {
|
|
41
|
+
theme: createJBrowseTheme(),
|
|
42
|
+
exportType: 'entire',
|
|
43
|
+
includeTracks: true,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The logo's glyphs are the only <text> drawn at the track's own font size, and
|
|
48
|
+
// the only ones carrying a non-unit vertical scale, which is what separates them
|
|
49
|
+
// from the alignment letters and tree labels in the same document.
|
|
50
|
+
function logoGlyphs(svg: string, fontSize: number) {
|
|
51
|
+
return [
|
|
52
|
+
...svg.matchAll(
|
|
53
|
+
/<text[^>]*font-size="([\d.]+)px"[^>]*transform="matrix\(([^)]*)\)"[^>]*>([A-Z])<\/text>/g,
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
.filter(m => Number(m[1]) === fontSize)
|
|
57
|
+
.map(m => {
|
|
58
|
+
const [sx, , , sy, tx, ty] = m[2]!.split(/[\s,]+/).map(Number)
|
|
59
|
+
return { letter: m[3]!, sx: sx!, sy: sy!, x: tx!, y: ty! }
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function glyphsOf(model: ReturnType<typeof makeModel>) {
|
|
64
|
+
return logoGlyphs(await exportSvg(model), model.sequenceLogoTrackHeight)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test('the logo track is hidden until it is asked for', () => {
|
|
68
|
+
const model = makeModel()
|
|
69
|
+
expect(model.tracks.map(t => t.model.id)).toContain('sequence-logo')
|
|
70
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).not.toContain(
|
|
71
|
+
'sequence-logo',
|
|
72
|
+
)
|
|
73
|
+
// and being default-off writes nothing into the shareable snapshot
|
|
74
|
+
expect(model.turnedOffTracks.size).toBe(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('toggling it on shows it, and off again hides it', () => {
|
|
78
|
+
const model = makeModel()
|
|
79
|
+
model.toggleTrack('sequence-logo')
|
|
80
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).toContain('sequence-logo')
|
|
81
|
+
model.toggleTrack('sequence-logo')
|
|
82
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).not.toContain(
|
|
83
|
+
'sequence-logo',
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('an on-by-default track still toggles off and back on', () => {
|
|
88
|
+
const model = makeModel()
|
|
89
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).toContain('conservation')
|
|
90
|
+
model.toggleTrack('conservation')
|
|
91
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).not.toContain(
|
|
92
|
+
'conservation',
|
|
93
|
+
)
|
|
94
|
+
model.toggleTrack('conservation')
|
|
95
|
+
expect(model.turnedOnTracks.map(t => t.model.id)).toContain('conservation')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('the conserved column draws one full-height letter', async () => {
|
|
99
|
+
const model = makeModel()
|
|
100
|
+
model.toggleTrack('sequence-logo')
|
|
101
|
+
const col0 = (await glyphsOf(model)).filter(g => g.x === 0)
|
|
102
|
+
|
|
103
|
+
expect(col0.map(g => g.letter)).toEqual(['A'])
|
|
104
|
+
// a fully conserved nucleotide column is 2 of 2 bits, so the glyph stretches
|
|
105
|
+
// to the whole track height over the cap height of the reference font
|
|
106
|
+
expect(col0[0]!.sy).toBeCloseTo(1 / 0.72, 5)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('a split column stacks the minor letter below the major one', async () => {
|
|
110
|
+
const model = makeModel()
|
|
111
|
+
model.toggleTrack('sequence-logo')
|
|
112
|
+
const col1 = (await glyphsOf(model)).filter(g => g.x === colWidth)
|
|
113
|
+
|
|
114
|
+
// ascending order means C (3 of 4) is drawn last, so it ends up on top
|
|
115
|
+
expect(col1.map(g => g.letter)).toEqual(['G', 'C'])
|
|
116
|
+
const [g, c] = col1
|
|
117
|
+
expect(c!.sy).toBeGreaterThan(g!.sy)
|
|
118
|
+
// and the taller letter sits higher up the track (smaller y is nearer the top)
|
|
119
|
+
expect(c!.y).toBeLessThan(g!.y)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('the uninformative column draws nothing', async () => {
|
|
123
|
+
const model = makeModel()
|
|
124
|
+
model.toggleTrack('sequence-logo')
|
|
125
|
+
const glyphs = await glyphsOf(model)
|
|
126
|
+
expect(glyphs.filter(g => g.x === colWidth * 2)).toEqual([])
|
|
127
|
+
// the other two columns did draw, so an empty column 2 is the logo's own
|
|
128
|
+
// verdict rather than the whole track missing
|
|
129
|
+
expect(glyphs).toHaveLength(3)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('every glyph stays inside the track band', async () => {
|
|
133
|
+
const model = makeModel()
|
|
134
|
+
model.toggleTrack('sequence-logo')
|
|
135
|
+
|
|
136
|
+
// the export stacks the tracks, so the logo's band is offset by whatever is
|
|
137
|
+
// drawn above it -- assert against that band rather than against 0..height,
|
|
138
|
+
// which would pass only while the logo happened to be the first track
|
|
139
|
+
const above = model.turnedOnTracks
|
|
140
|
+
.slice(
|
|
141
|
+
0,
|
|
142
|
+
model.turnedOnTracks.findIndex(t => t.model.id === 'sequence-logo'),
|
|
143
|
+
)
|
|
144
|
+
.reduce((a, t) => a + t.model.height, 0)
|
|
145
|
+
const bandTop = above
|
|
146
|
+
const bandBottom = above + model.sequenceLogoTrackHeight
|
|
147
|
+
|
|
148
|
+
const glyphs = await glyphsOf(model)
|
|
149
|
+
expect(glyphs.length).toBeGreaterThan(0)
|
|
150
|
+
for (const glyph of glyphs) {
|
|
151
|
+
// baselines run from the band's bottom edge upward, never past either end
|
|
152
|
+
expect(glyph.y).toBeGreaterThan(bandTop)
|
|
153
|
+
expect(glyph.y).toBeLessThanOrEqual(bandBottom)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('letters are stretched to fill their column', async () => {
|
|
158
|
+
const model = makeModel()
|
|
159
|
+
model.toggleTrack('sequence-logo')
|
|
160
|
+
const glyphs = await glyphsOf(model)
|
|
161
|
+
|
|
162
|
+
const expected =
|
|
163
|
+
colWidth / (model.sequenceLogoTrackHeight * TEST_CHAR_WIDTH_RATIO)
|
|
164
|
+
for (const glyph of glyphs) {
|
|
165
|
+
expect(glyph.sx).toBeCloseTo(expected, 5)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('the logo appears in the export only when the track is on', async () => {
|
|
170
|
+
const off = makeModel()
|
|
171
|
+
expect(await glyphsOf(off)).toEqual([])
|
|
172
|
+
|
|
173
|
+
const on = makeModel()
|
|
174
|
+
on.toggleTrack('sequence-logo')
|
|
175
|
+
expect((await glyphsOf(on)).length).toBeGreaterThan(0)
|
|
176
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsdom shims for the SVG-export tests. Test-only -- nothing in the viewer
|
|
3
|
+
* imports this.
|
|
4
|
+
*
|
|
5
|
+
* jsdom implements neither DOMMatrix nor a canvas 2D context, and
|
|
6
|
+
* `@jbrowse/svgcanvas` needs both: it tracks the current transform as a real
|
|
7
|
+
* matrix, and the renderers ask a throwaway canvas to measure text.
|
|
8
|
+
*
|
|
9
|
+
* The matrix stub has to accept the ARRAY constructor. svgcanvas normalizes
|
|
10
|
+
* every transform through `new DOMMatrix([a, b, c, d, e, f])`, so a stub that
|
|
11
|
+
* only takes six positional arguments silently binds the whole array to `a` and
|
|
12
|
+
* leaves the rest undefined -- every emitted transform then carries `NaN` in its
|
|
13
|
+
* x components, which no assertion on a drawn x-position can survive.
|
|
14
|
+
*/
|
|
15
|
+
export class TestDOMMatrix {
|
|
16
|
+
a: number
|
|
17
|
+
b: number
|
|
18
|
+
c: number
|
|
19
|
+
d: number
|
|
20
|
+
e: number
|
|
21
|
+
f: number
|
|
22
|
+
|
|
23
|
+
constructor(init?: number[]) {
|
|
24
|
+
const [a = 1, b = 0, c = 0, d = 1, e = 0, f = 0] = init ?? []
|
|
25
|
+
this.a = a
|
|
26
|
+
this.b = b
|
|
27
|
+
this.c = c
|
|
28
|
+
this.d = d
|
|
29
|
+
this.e = e
|
|
30
|
+
this.f = f
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
multiply(o: TestDOMMatrix) {
|
|
34
|
+
return new TestDOMMatrix([
|
|
35
|
+
this.a * o.a + this.c * o.b,
|
|
36
|
+
this.b * o.a + this.d * o.b,
|
|
37
|
+
this.a * o.c + this.c * o.d,
|
|
38
|
+
this.b * o.c + this.d * o.d,
|
|
39
|
+
this.a * o.e + this.c * o.f + this.e,
|
|
40
|
+
this.b * o.e + this.d * o.f + this.f,
|
|
41
|
+
])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
translate(x: number, y = 0) {
|
|
45
|
+
return this.multiply(new TestDOMMatrix([1, 0, 0, 1, x, y]))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
scale(x: number, y = x) {
|
|
49
|
+
return this.multiply(new TestDOMMatrix([x, 0, 0, y, 0, 0]))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class TestDOMPoint {
|
|
54
|
+
constructor(
|
|
55
|
+
public x = 0,
|
|
56
|
+
public y = 0,
|
|
57
|
+
) {}
|
|
58
|
+
|
|
59
|
+
matrixTransform(m: TestDOMMatrix) {
|
|
60
|
+
return new TestDOMPoint(
|
|
61
|
+
m.a * this.x + m.c * this.y + m.e,
|
|
62
|
+
m.b * this.x + m.d * this.y + m.f,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Install the shims. Call from `beforeAll` in any test that exports SVG.
|
|
69
|
+
*
|
|
70
|
+
* `measureText` reports 0.6em per character, close enough to a real sans-serif
|
|
71
|
+
* average for layout assertions and, more usefully, exactly predictable: a test
|
|
72
|
+
* can compute the width a renderer will see rather than hardcoding a number
|
|
73
|
+
* measured from one machine's fonts.
|
|
74
|
+
*/
|
|
75
|
+
export function installRenderTestEnv() {
|
|
76
|
+
const g = globalThis as Record<string, unknown>
|
|
77
|
+
g.DOMMatrix = TestDOMMatrix
|
|
78
|
+
g.DOMPoint = TestDOMPoint
|
|
79
|
+
HTMLCanvasElement.prototype.getContext = function () {
|
|
80
|
+
let font = '10px sans-serif'
|
|
81
|
+
return {
|
|
82
|
+
get font() {
|
|
83
|
+
return font
|
|
84
|
+
},
|
|
85
|
+
set font(v: string) {
|
|
86
|
+
font = v
|
|
87
|
+
},
|
|
88
|
+
measureText: (t: string) => ({
|
|
89
|
+
width: t.length * (Number.parseFloat(font) || 10) * 0.6,
|
|
90
|
+
}),
|
|
91
|
+
} as unknown as CanvasRenderingContext2D
|
|
92
|
+
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** the per-character width factor `measureText` above reports */
|
|
96
|
+
export const TEST_CHAR_WIDTH_RATIO = 0.6
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { when } from 'mobx'
|
|
|
7
7
|
import MinimapSVG from './components/minimap/MinimapSVG.tsx'
|
|
8
8
|
import { renderBoxFeatureCanvasBlock } from './components/msa/renderBoxFeatureCanvasBlock.ts'
|
|
9
9
|
import { renderMSABlock } from './components/msa/renderMSABlock.ts'
|
|
10
|
-
import { renderAllTracks } from './components/tracks/
|
|
10
|
+
import { renderAllTracks } from './components/tracks/drawTracks.ts'
|
|
11
11
|
import { renderTreeCanvas } from './components/tree/renderTreeCanvas.ts'
|
|
12
12
|
import { colorContrast } from './util.ts'
|
|
13
13
|
|
|
@@ -161,7 +161,7 @@ function MsaSvg({
|
|
|
161
161
|
{includeMinimap ? (
|
|
162
162
|
<>
|
|
163
163
|
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
164
|
-
<MinimapSVG model={model} />
|
|
164
|
+
<MinimapSVG model={model} width={width - treeAreaWidth} />
|
|
165
165
|
</g>
|
|
166
166
|
<g transform={`translate(0 ${minimapHeight})`}>{body}</g>
|
|
167
167
|
</>
|
|
@@ -178,7 +178,7 @@ function MsaSvg({
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// the domain color key drawn as a reserved column to the right of the
|
|
181
|
-
// alignment, mirroring the on-screen
|
|
181
|
+
// alignment, mirroring the on-screen AnnotationLegend overlay
|
|
182
182
|
function LegendSVG({ model, width }: { model: MsaViewModel; width: number }) {
|
|
183
183
|
const { visibleDomainTypes, fillPalette, strokePalette } = model
|
|
184
184
|
const boxHeight = LEGEND_PAD * 2 + visibleDomainTypes.length * LEGEND_ROW_H
|
|
@@ -18,6 +18,25 @@ import { isBlank } from './util.ts'
|
|
|
18
18
|
* Each row can have different seqPos values for the same globalCol due to gaps.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Index of the first element in `[0, len)` for which `pred` holds, over a range
|
|
23
|
+
* where `pred` is false up to some point and true from there on. Both
|
|
24
|
+
* conversions below are that shape, since `blanks` is sorted ascending.
|
|
25
|
+
*/
|
|
26
|
+
function partitionPoint(len: number, pred: (i: number) => boolean) {
|
|
27
|
+
let left = 0
|
|
28
|
+
let right = len
|
|
29
|
+
while (left < right) {
|
|
30
|
+
const mid = (left + right) >>> 1
|
|
31
|
+
if (pred(mid)) {
|
|
32
|
+
right = mid
|
|
33
|
+
} else {
|
|
34
|
+
left = mid + 1
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return left
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/**
|
|
22
41
|
* Convert a visible column index to a global column index.
|
|
23
42
|
* This is used when translating mouse/screen coordinates to MSA coordinates.
|
|
@@ -27,32 +46,12 @@ import { isBlank } from './util.ts'
|
|
|
27
46
|
* @returns The corresponding global column index in the full MSA
|
|
28
47
|
*/
|
|
29
48
|
export function visibleColToGlobalCol(blanks: number[], visibleCol: number) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
while (blankArrayIndex < blanksLen && blanks[blankArrayIndex] === globalCol) {
|
|
37
|
-
blankArrayIndex++
|
|
38
|
-
globalCol++
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
while (currentVisibleCol < visibleCol) {
|
|
42
|
-
currentVisibleCol++
|
|
43
|
-
globalCol++
|
|
44
|
-
|
|
45
|
-
// Skip any blank columns after incrementing
|
|
46
|
-
while (
|
|
47
|
-
blankArrayIndex < blanksLen &&
|
|
48
|
-
blanks[blankArrayIndex] === globalCol
|
|
49
|
-
) {
|
|
50
|
-
blankArrayIndex++
|
|
51
|
-
globalCol++
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return globalCol
|
|
49
|
+
// `blanks[i] - i` counts the visible columns before the i-th blank, so the
|
|
50
|
+
// first blank with more than visibleCol of them in front of it is the first
|
|
51
|
+
// one sitting past this column. Its index is how many blanks shift it right.
|
|
52
|
+
return (
|
|
53
|
+
visibleCol + partitionPoint(blanks.length, i => blanks[i]! - i > visibleCol)
|
|
54
|
+
)
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -67,25 +66,30 @@ export function globalColToVisibleCol(
|
|
|
67
66
|
blanks: number[],
|
|
68
67
|
globalCol: number,
|
|
69
68
|
): number | undefined {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const mid = Math.floor((left + right) / 2)
|
|
76
|
-
if (blanks[mid] === globalCol) {
|
|
77
|
-
return undefined // Column is hidden
|
|
78
|
-
}
|
|
79
|
-
if (blanks[mid]! < globalCol) {
|
|
80
|
-
left = mid + 1
|
|
81
|
-
} else {
|
|
82
|
-
right = mid - 1
|
|
83
|
-
}
|
|
84
|
-
}
|
|
69
|
+
const blanksBefore = countBlanksBefore(blanks, globalCol)
|
|
70
|
+
return blanks[blanksBefore] === globalCol
|
|
71
|
+
? undefined // Column is hidden
|
|
72
|
+
: globalCol - blanksBefore
|
|
73
|
+
}
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
function countBlanksBefore(blanks: number[], globalCol: number) {
|
|
76
|
+
return partitionPoint(blanks.length, i => blanks[i]! >= globalCol)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many visible columns come before a global column. For a visible column
|
|
81
|
+
* that is its own visible index; for a hidden one it is the index of the next
|
|
82
|
+
* visible column, i.e. where the hidden column collapses to.
|
|
83
|
+
*
|
|
84
|
+
* That total answer is what spans want: resolving both ends of a span through
|
|
85
|
+
* globalColToVisibleCol instead loses the whole span whenever either end lands
|
|
86
|
+
* in a hidden column.
|
|
87
|
+
*
|
|
88
|
+
* @param blanks - Sorted array of global column indices that are hidden
|
|
89
|
+
* @param globalCol - The global column index in the full MSA
|
|
90
|
+
*/
|
|
91
|
+
export function visibleColsBefore(blanks: number[], globalCol: number) {
|
|
92
|
+
return globalCol - countBlanksBefore(blanks, globalCol)
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|