react-msaview 6.2.2 → 6.3.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 +104 -104
- package/bundle/index.js.map +4 -4
- package/dist/components/MSAViewer.d.ts +10 -1
- package/dist/components/MSAViewer.js +3 -1
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/minimap/Minimap.js +1 -1
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +2 -2
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +4 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +5 -4
- package/dist/components/msa/msaRaster.js +10 -6
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +31 -0
- package/dist/components/msa/renderHighlights.js +49 -0
- package/dist/components/msa/renderHighlights.js.map +1 -0
- package/dist/components/msa/renderMSAMouseover.d.ts +3 -1
- package/dist/components/msa/renderMSAMouseover.js +11 -1
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tracks/drawTracks.js +4 -0
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +31 -0
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/exportFileName.d.ts +6 -0
- package/dist/exportFileName.js +15 -0
- package/dist/exportFileName.js.map +1 -0
- package/dist/headlessRenderEnv.d.ts +4 -1
- package/dist/headlessRenderEnv.js +6 -3
- package/dist/headlessRenderEnv.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +172 -9
- package/dist/model.js +134 -3
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +30 -6
- package/dist/renderToSvg.js.map +1 -1
- package/dist/svgTestUtil.d.ts +1662 -0
- package/dist/svgTestUtil.js +28 -0
- package/dist/svgTestUtil.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/umd.d.ts +1 -0
- package/dist/umd.js +3 -0
- package/dist/umd.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.js +7 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/columnTracks.test.ts +97 -0
- package/src/components/MSAViewer.tsx +13 -0
- package/src/components/minimap/Minimap.tsx +1 -1
- package/src/components/minimap/MinimapSVG.tsx +2 -2
- package/src/components/msa/MSAMouseoverCanvas.tsx +4 -2
- package/src/components/msa/msaRaster.ts +10 -6
- package/src/components/msa/renderHighlights.ts +86 -0
- package/src/components/msa/renderMSAMouseover.ts +14 -0
- package/src/components/tracks/drawTracks.ts +4 -0
- package/src/components/tree/renderTreeCanvas.ts +56 -0
- package/src/exportFileName.test.ts +40 -0
- package/src/exportFileName.ts +20 -0
- package/src/headlessRenderEnv.ts +20 -11
- package/src/highlights.test.ts +70 -0
- package/src/impgRender.test.tsx +2 -4
- package/src/index.ts +4 -0
- package/src/model.ts +162 -10
- package/src/renderColumnTracksSvg.test.tsx +108 -0
- package/src/renderDomainsSvg.test.tsx +25 -10
- package/src/renderHighlightsSvg.test.tsx +85 -0
- package/src/renderMinimapSvg.test.tsx +6 -13
- package/src/renderRasterSvg.test.tsx +21 -93
- package/src/renderSequenceLogoSvg.test.tsx +4 -17
- package/src/renderToSvg.test.tsx +7 -21
- package/src/renderToSvg.tsx +32 -6
- package/src/svgTestUtil.ts +40 -0
- package/src/types.ts +39 -0
- package/src/umd.ts +3 -0
- package/src/util.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The SVG export draws the `highlights` layer: a bordered band with its label
|
|
4
|
+
// over the alignment, a residue span at the column the row's gaps put it in,
|
|
5
|
+
// and a row set tinted across both the tree and the alignment.
|
|
6
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
7
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
8
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
11
|
+
import MSAModelF from './model.ts'
|
|
12
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
13
|
+
|
|
14
|
+
import type { Highlight } from './types.ts'
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
enableStaticRendering(true)
|
|
18
|
+
installHeadlessRenderEnv()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const msa = `>seq1
|
|
22
|
+
ACDEFGHIKL
|
|
23
|
+
>seq2
|
|
24
|
+
AC--FGHIKL
|
|
25
|
+
>seq3
|
|
26
|
+
ACDEFGHIKL`
|
|
27
|
+
|
|
28
|
+
async function exportWith(highlights: Highlight[]) {
|
|
29
|
+
const model = MSAModelF().create({
|
|
30
|
+
id: 'highlight-svg-test',
|
|
31
|
+
type: 'MsaView',
|
|
32
|
+
height: 400,
|
|
33
|
+
msaFormat: 'fasta',
|
|
34
|
+
data: { msa },
|
|
35
|
+
highlights,
|
|
36
|
+
})
|
|
37
|
+
model.setWidth(800)
|
|
38
|
+
const svg = await renderToSvg(model, {
|
|
39
|
+
theme: createJBrowseTheme(),
|
|
40
|
+
exportType: 'entire',
|
|
41
|
+
})
|
|
42
|
+
return { model, svg }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rects = (svg: string) => svg.match(/<rect[^>]*>/g) ?? []
|
|
46
|
+
|
|
47
|
+
test('a labeled column span draws a band and its label', async () => {
|
|
48
|
+
const { model, svg } = await exportWith([
|
|
49
|
+
{ start: 3, end: 5, label: 'motif', color: 'rgb(1, 2, 3)' },
|
|
50
|
+
])
|
|
51
|
+
const { colWidth, totalHeight } = model
|
|
52
|
+
const band = rects(svg).find(
|
|
53
|
+
r => r.includes(`x="${2 * colWidth}"`) && r.includes('rgb(1, 2, 3)'),
|
|
54
|
+
)
|
|
55
|
+
expect(band).toBeDefined()
|
|
56
|
+
expect(band).toContain(`width="${3 * colWidth}"`)
|
|
57
|
+
expect(band).toContain(`height="${totalHeight}"`)
|
|
58
|
+
expect(svg).toContain('>motif<')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('a residue span lands where the row has its residues', async () => {
|
|
62
|
+
const { model, svg } = await exportWith([
|
|
63
|
+
{ row: 'seq2', start: 3, end: 3, color: 'rgb(9, 9, 9)' },
|
|
64
|
+
])
|
|
65
|
+
expect(model.resolvedHighlights[0]).toMatchObject({
|
|
66
|
+
startCol: 4,
|
|
67
|
+
endCol: 4,
|
|
68
|
+
})
|
|
69
|
+
const band = rects(svg).find(r => r.includes('rgb(9, 9, 9)'))
|
|
70
|
+
expect(band).toContain(`x="${4 * model.colWidth}"`)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('a row set tints the row in the tree and the alignment', async () => {
|
|
74
|
+
const { model, svg } = await exportWith([
|
|
75
|
+
{ rows: ['seq3'], label: 'odd one out', color: 'rgb(7, 7, 7)' },
|
|
76
|
+
])
|
|
77
|
+
const { rowHeight, treeAreaWidth, totalWidth } = model
|
|
78
|
+
const tints = rects(svg).filter(
|
|
79
|
+
r => r.includes('rgb(7, 7, 7)') && r.includes(`y="${2 * rowHeight}"`),
|
|
80
|
+
)
|
|
81
|
+
expect(tints.map(r => /width="([^"]+)"/.exec(r)?.[1])).toEqual(
|
|
82
|
+
expect.arrayContaining([`${treeAreaWidth}`, `${totalWidth}`]),
|
|
83
|
+
)
|
|
84
|
+
expect(svg).toContain('>odd one out<')
|
|
85
|
+
})
|
|
@@ -5,30 +5,23 @@
|
|
|
5
5
|
// to reach the right edge of the exported alignment column -- and it is drawn
|
|
6
6
|
// only when the live view shows one at all.
|
|
7
7
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
8
|
-
import { enableStaticRendering } from 'mobx-react'
|
|
9
8
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
9
|
|
|
11
|
-
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
12
|
-
import MSAModelF from './model.ts'
|
|
13
10
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
11
|
+
import { createTestModel, installSvgTestEnv } from './svgTestUtil.ts'
|
|
14
12
|
|
|
15
13
|
beforeAll(() => {
|
|
16
|
-
|
|
17
|
-
installHeadlessRenderEnv()
|
|
14
|
+
installSvgTestEnv()
|
|
18
15
|
})
|
|
19
16
|
|
|
20
17
|
const width = 1000
|
|
21
18
|
|
|
22
19
|
function makeModel(cols = 400) {
|
|
23
20
|
const row = 'ACDEFGHIKL'.repeat(cols / 10)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
data: { msa: `>seq1\n${row}\n>seq2\n${row}` },
|
|
29
|
-
})
|
|
30
|
-
model.setWidth(width)
|
|
31
|
-
return model
|
|
21
|
+
return createTestModel(
|
|
22
|
+
{ height: 300, data: { msa: `>seq1\n${row}\n>seq2\n${row}` } },
|
|
23
|
+
width,
|
|
24
|
+
)
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
function exportViewport(model: ReturnType<typeof makeModel>) {
|
|
@@ -5,114 +5,42 @@
|
|
|
5
5
|
// path; this file supplies the canvas the raster needs and checks the export
|
|
6
6
|
// takes it instead.
|
|
7
7
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
8
|
-
import { enableStaticRendering } from 'mobx-react'
|
|
9
8
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
9
|
|
|
11
|
-
import MSAModelF from './model.ts'
|
|
12
10
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
11
|
+
import {
|
|
12
|
+
createTestModel,
|
|
13
|
+
installSvgTestEnv,
|
|
14
|
+
syntheticProteinMsa,
|
|
15
|
+
} from './svgTestUtil.ts'
|
|
13
16
|
|
|
14
17
|
const dataUrl = 'data:image/png;base64,STUB'
|
|
15
18
|
|
|
16
19
|
// enough of a 2d context for the raster: a color parser, an ImageData that is
|
|
17
20
|
// really backed by bytes, and a canvas that reads back
|
|
18
21
|
beforeAll(() => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.a * o.a + this.c * o.b,
|
|
34
|
-
this.b * o.a + this.d * o.b,
|
|
35
|
-
this.a * o.c + this.c * o.d,
|
|
36
|
-
this.b * o.c + this.d * o.d,
|
|
37
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
38
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
39
|
-
])
|
|
40
|
-
}
|
|
41
|
-
translate(x: number, y = 0) {
|
|
42
|
-
return this.multiply(
|
|
43
|
-
new (globalThis.DOMMatrix as any)([1, 0, 0, 1, x, y]),
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
scale(x: number, y = x) {
|
|
47
|
-
return this.multiply(
|
|
48
|
-
new (globalThis.DOMMatrix as any)([x, 0, 0, y, 0, 0]),
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
} as any
|
|
52
|
-
globalThis.DOMPoint = class {
|
|
53
|
-
constructor(
|
|
54
|
-
public x = 0,
|
|
55
|
-
public y = 0,
|
|
56
|
-
) {}
|
|
57
|
-
matrixTransform(m: any) {
|
|
58
|
-
return new (globalThis.DOMPoint as any)(
|
|
59
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
60
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
} as any
|
|
64
|
-
|
|
65
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
66
|
-
let font = '10px sans-serif'
|
|
67
|
-
let fillStyle = '#000000'
|
|
68
|
-
return {
|
|
69
|
-
get font() {
|
|
70
|
-
return font
|
|
71
|
-
},
|
|
72
|
-
set font(v: string) {
|
|
73
|
-
font = v
|
|
74
|
-
},
|
|
75
|
-
get fillStyle() {
|
|
76
|
-
return fillStyle
|
|
77
|
-
},
|
|
78
|
-
set fillStyle(v: string) {
|
|
79
|
-
fillStyle = v
|
|
80
|
-
},
|
|
81
|
-
measureText: (t: string) => ({
|
|
82
|
-
width: t.length * (Number.parseFloat(font) || 10) * 0.6,
|
|
83
|
-
}),
|
|
84
|
-
clearRect: () => {},
|
|
85
|
-
fillRect: () => {},
|
|
86
|
-
createImageData: (width: number, height: number) => ({
|
|
87
|
-
data: new Uint8ClampedArray(width * height * 4),
|
|
88
|
-
width,
|
|
89
|
-
height,
|
|
90
|
-
}),
|
|
91
|
-
putImageData: () => {},
|
|
92
|
-
getImageData: () => ({ data: Uint8ClampedArray.from([1, 2, 3, 255]) }),
|
|
93
|
-
} as unknown as CanvasRenderingContext2D
|
|
94
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
22
|
+
installSvgTestEnv({
|
|
23
|
+
fillStyle: '#000000',
|
|
24
|
+
clearRect: () => {},
|
|
25
|
+
fillRect: () => {},
|
|
26
|
+
createImageData: ((width: number, height: number) => ({
|
|
27
|
+
data: new Uint8ClampedArray(width * height * 4),
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
})) as unknown as CanvasRenderingContext2D['createImageData'],
|
|
31
|
+
putImageData: () => {},
|
|
32
|
+
getImageData: (() => ({
|
|
33
|
+
data: Uint8ClampedArray.from([1, 2, 3, 255]),
|
|
34
|
+
})) as unknown as CanvasRenderingContext2D['getImageData'],
|
|
35
|
+
})
|
|
95
36
|
HTMLCanvasElement.prototype.toDataURL = () => dataUrl
|
|
96
37
|
})
|
|
97
38
|
|
|
98
39
|
function makeModel(rows: number, cols: number) {
|
|
99
|
-
|
|
100
|
-
const msa = Array.from({ length: rows }, (_, r) => {
|
|
101
|
-
let s = ''
|
|
102
|
-
for (let c = 0; c < cols; c++) {
|
|
103
|
-
s += letters[(r * 7 + c * 3) % letters.length]
|
|
104
|
-
}
|
|
105
|
-
return `>seq${r}\n${s}`
|
|
106
|
-
}).join('\n')
|
|
107
|
-
const model = MSAModelF().create({
|
|
40
|
+
return createTestModel({
|
|
108
41
|
id: 'raster',
|
|
109
|
-
|
|
110
|
-
height: 400,
|
|
111
|
-
msaFormat: 'fasta',
|
|
112
|
-
data: { msa },
|
|
42
|
+
data: { msa: syntheticProteinMsa(rows, cols) },
|
|
113
43
|
})
|
|
114
|
-
model.setWidth(800)
|
|
115
|
-
return model
|
|
116
44
|
}
|
|
117
45
|
|
|
118
46
|
function exportEntire(model: ReturnType<typeof makeModel>) {
|
|
@@ -7,19 +7,14 @@
|
|
|
7
7
|
// until asked for, the stack is ordered and scaled, and the glyphs land inside
|
|
8
8
|
// the track's own band.
|
|
9
9
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
10
|
-
import { enableStaticRendering } from 'mobx-react'
|
|
11
10
|
import { beforeAll, expect, test } from 'vitest'
|
|
12
11
|
|
|
13
|
-
import {
|
|
14
|
-
CHAR_WIDTH_RATIO,
|
|
15
|
-
installHeadlessRenderEnv,
|
|
16
|
-
} from './headlessRenderEnv.ts'
|
|
17
|
-
import MSAModelF from './model.ts'
|
|
12
|
+
import { CHAR_WIDTH_RATIO } from './headlessRenderEnv.ts'
|
|
18
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
14
|
+
import { createTestModel, installSvgTestEnv } from './svgTestUtil.ts'
|
|
19
15
|
|
|
20
16
|
beforeAll(() => {
|
|
21
|
-
|
|
22
|
-
installHeadlessRenderEnv()
|
|
17
|
+
installSvgTestEnv()
|
|
23
18
|
})
|
|
24
19
|
|
|
25
20
|
// column 0 is fully conserved A, column 1 is a 3:1 split of C over G, and
|
|
@@ -28,15 +23,7 @@ const msa = `>s1\nACA\n>s2\nACC\n>s3\nACG\n>s4\nAGT\n`
|
|
|
28
23
|
const colWidth = 40
|
|
29
24
|
|
|
30
25
|
function makeModel() {
|
|
31
|
-
|
|
32
|
-
type: 'MsaView',
|
|
33
|
-
msaFormat: 'fasta',
|
|
34
|
-
height: 400,
|
|
35
|
-
colWidth,
|
|
36
|
-
data: { msa },
|
|
37
|
-
})
|
|
38
|
-
model.setWidth(1000)
|
|
39
|
-
return model
|
|
26
|
+
return createTestModel({ colWidth, data: { msa } }, 1000)
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
async function exportSvg(model: ReturnType<typeof makeModel>) {
|
package/src/renderToSvg.test.tsx
CHANGED
|
@@ -3,35 +3,21 @@
|
|
|
3
3
|
// Layout and palette of the exported figure itself, as opposed to what any one
|
|
4
4
|
// layer draws inside it.
|
|
5
5
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
6
|
-
import { enableStaticRendering } from 'mobx-react'
|
|
7
6
|
import { beforeAll, expect, test } from 'vitest'
|
|
8
7
|
|
|
9
|
-
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
10
|
-
import MSAModelF from './model.ts'
|
|
11
8
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
9
|
+
import {
|
|
10
|
+
createTestModel,
|
|
11
|
+
installSvgTestEnv,
|
|
12
|
+
syntheticProteinMsa,
|
|
13
|
+
} from './svgTestUtil.ts'
|
|
12
14
|
|
|
13
15
|
beforeAll(() => {
|
|
14
|
-
|
|
15
|
-
installHeadlessRenderEnv()
|
|
16
|
+
installSvgTestEnv()
|
|
16
17
|
})
|
|
17
18
|
|
|
18
19
|
function makeModel({ rows = 60, cols = 400 } = {}) {
|
|
19
|
-
|
|
20
|
-
const msa = Array.from({ length: rows }, (_, r) => {
|
|
21
|
-
const seq = Array.from(
|
|
22
|
-
{ length: cols },
|
|
23
|
-
(_, c) => letters[(r * 7 + c * 3) % letters.length],
|
|
24
|
-
).join('')
|
|
25
|
-
return `>seq${r}\n${seq}`
|
|
26
|
-
}).join('\n')
|
|
27
|
-
const model = MSAModelF().create({
|
|
28
|
-
type: 'MsaView',
|
|
29
|
-
msaFormat: 'fasta',
|
|
30
|
-
height: 400,
|
|
31
|
-
data: { msa },
|
|
32
|
-
})
|
|
33
|
-
model.setWidth(800)
|
|
34
|
-
return model
|
|
20
|
+
return createTestModel({ data: { msa: syntheticProteinMsa(rows, cols) } })
|
|
35
21
|
}
|
|
36
22
|
|
|
37
23
|
const rootRect = (svg: string) => /<rect[^>]*height="100%"[^>]*>/.exec(svg)?.[0]
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { visibleRowRange } from './components/getVisibleLeaves.ts'
|
|
|
8
8
|
import MinimapSVG from './components/minimap/MinimapSVG.tsx'
|
|
9
9
|
import { rasterImageHref, rasterSupported } from './components/msa/msaRaster.ts'
|
|
10
10
|
import { renderBoxFeatureCanvasBlock } from './components/msa/renderBoxFeatureCanvasBlock.ts'
|
|
11
|
+
import { renderHighlights } from './components/msa/renderHighlights.ts'
|
|
11
12
|
import { renderMSABlock } from './components/msa/renderMSABlock.ts'
|
|
12
13
|
import { visibleColRange } from './components/msa/visibleColRange.ts'
|
|
13
14
|
import { renderAllTracks } from './components/tracks/drawTracks.ts'
|
|
@@ -45,6 +46,10 @@ function getLegendWidth(model: MsaViewModel) {
|
|
|
45
46
|
return Math.min(LEGEND_MAX_W, Math.max(120, w))
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function legendHeight(domainTypes: unknown[]) {
|
|
50
|
+
return LEGEND_PAD * 2 + domainTypes.length * LEGEND_ROW_H
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
// resolved sizes/offsets (in svg user units) for the chosen export, shared by
|
|
49
54
|
// every layer; 'entire' renders the whole alignment unscrolled, 'viewport'
|
|
50
55
|
// mirrors the live scroll position
|
|
@@ -119,7 +124,12 @@ function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
|
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
export async function renderToSvg(model: MsaViewModel, opts: ExportSvgOptions) {
|
|
122
|
-
|
|
127
|
+
// dataInitialized stays false while the model holds a load error, so waiting
|
|
128
|
+
// on it alone would never settle
|
|
129
|
+
await when(() => model.dataInitialized || !!model.error)
|
|
130
|
+
if (model.error) {
|
|
131
|
+
throw model.error
|
|
132
|
+
}
|
|
123
133
|
const { Context } = await import('@jbrowse/svgcanvas')
|
|
124
134
|
return renderToStaticMarkup(
|
|
125
135
|
<MsaSvg
|
|
@@ -142,10 +152,16 @@ function MsaSvg({
|
|
|
142
152
|
Context: typeof ContextType
|
|
143
153
|
layout: Layout
|
|
144
154
|
}) {
|
|
145
|
-
const { width,
|
|
146
|
-
const { treeAreaWidth, minimapHeight } = model
|
|
155
|
+
const { width, trackHeight, includeMinimap, legendWidth } = layout
|
|
156
|
+
const { treeAreaWidth, minimapHeight, visibleDomainTypes } = model
|
|
147
157
|
const totalWidth = width + legendWidth
|
|
148
158
|
const legendTop = includeMinimap ? minimapHeight : 0
|
|
159
|
+
// a short alignment with many domain types has a key taller than its rows,
|
|
160
|
+
// and the page has to hold the whole key
|
|
161
|
+
const height =
|
|
162
|
+
legendWidth > 0
|
|
163
|
+
? Math.max(layout.height, legendTop + legendHeight(visibleDomainTypes))
|
|
164
|
+
: layout.height
|
|
149
165
|
const contrastScheme = colorContrast(model.colorScheme, theme)
|
|
150
166
|
const props = { Context, model, theme, layout, contrastScheme }
|
|
151
167
|
|
|
@@ -207,7 +223,7 @@ function LegendSVG({
|
|
|
207
223
|
width: number
|
|
208
224
|
}) {
|
|
209
225
|
const { visibleDomainTypes, fillPalette, strokePalette } = model
|
|
210
|
-
const boxHeight =
|
|
226
|
+
const boxHeight = legendHeight(visibleDomainTypes)
|
|
211
227
|
// the column is capped at LEGEND_MAX_W, so a name too long for it has to be
|
|
212
228
|
// clipped here -- text that overruns the reserved width runs off the figure
|
|
213
229
|
const maxChars = Math.floor(
|
|
@@ -221,7 +237,7 @@ function LegendSVG({
|
|
|
221
237
|
width={width - 4}
|
|
222
238
|
height={boxHeight}
|
|
223
239
|
fill={theme.palette.background.paper}
|
|
224
|
-
stroke=
|
|
240
|
+
stroke={theme.palette.divider}
|
|
225
241
|
rx={2}
|
|
226
242
|
/>
|
|
227
243
|
{visibleDomainTypes.map((d, i) => {
|
|
@@ -307,6 +323,16 @@ function CoreRendering({
|
|
|
307
323
|
highResScaleFactorOverride: 1,
|
|
308
324
|
rasterTiles: !!raster,
|
|
309
325
|
})
|
|
326
|
+
msaCtx.resetTransform()
|
|
327
|
+
renderHighlights({
|
|
328
|
+
ctx: msaCtx,
|
|
329
|
+
model,
|
|
330
|
+
theme,
|
|
331
|
+
offsetX,
|
|
332
|
+
offsetY,
|
|
333
|
+
width: msaAreaWidth,
|
|
334
|
+
height: contentHeight,
|
|
335
|
+
})
|
|
310
336
|
|
|
311
337
|
return (
|
|
312
338
|
<>
|
|
@@ -364,7 +390,7 @@ function rasterBackground({
|
|
|
364
390
|
blockSizeY: contentHeight,
|
|
365
391
|
})
|
|
366
392
|
const numCols = Math.min(xEnd, model.numColumns) - xStart
|
|
367
|
-
const numRows = Math.min(yEnd, model.
|
|
393
|
+
const numRows = Math.min(yEnd, model.numRows) - yStart
|
|
368
394
|
const href = rasterImageHref({
|
|
369
395
|
model,
|
|
370
396
|
theme,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shared setup for the tests that drive renderToSvg headlessly.
|
|
2
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
3
|
+
|
|
4
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
5
|
+
import MSAModelF from './model.ts'
|
|
6
|
+
|
|
7
|
+
type Snapshot = NonNullable<
|
|
8
|
+
Parameters<ReturnType<typeof MSAModelF>['create']>[0]
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
export function installSvgTestEnv(
|
|
12
|
+
contextExtras?: Partial<CanvasRenderingContext2D>,
|
|
13
|
+
) {
|
|
14
|
+
enableStaticRendering(true)
|
|
15
|
+
installHeadlessRenderEnv(globalThis, contextExtras)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createTestModel(snapshot: Omit<Snapshot, 'type'>, width = 800) {
|
|
19
|
+
const model = MSAModelF().create({
|
|
20
|
+
type: 'MsaView',
|
|
21
|
+
msaFormat: 'fasta',
|
|
22
|
+
height: 400,
|
|
23
|
+
...snapshot,
|
|
24
|
+
})
|
|
25
|
+
model.setWidth(width)
|
|
26
|
+
return model
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// a protein alignment with no two neighbouring cells alike, so any color or
|
|
30
|
+
// letter landing in the wrong cell shows up
|
|
31
|
+
export function syntheticProteinMsa(rows: number, cols: number) {
|
|
32
|
+
const letters = 'ACDEFGHIKLMNPQRSTVWY'
|
|
33
|
+
return Array.from({ length: rows }, (_, r) => {
|
|
34
|
+
const seq = Array.from(
|
|
35
|
+
{ length: cols },
|
|
36
|
+
(_, c) => letters[(r * 7 + c * 3) % letters.length],
|
|
37
|
+
).join('')
|
|
38
|
+
return `>seq${r}\n${seq}`
|
|
39
|
+
}).join('\n')
|
|
40
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -34,6 +34,22 @@ export interface BasicTrack {
|
|
|
34
34
|
model: TextTrackModel & BarTrackModel
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// a track supplied as data in the snapshot rather than computed from the
|
|
38
|
+
// alignment. `values` (bar) or `data` (text) index alignment columns, or the
|
|
39
|
+
// 1-based residues of `row` when it names one
|
|
40
|
+
export interface ColumnTrackSpec {
|
|
41
|
+
id: string
|
|
42
|
+
name: string
|
|
43
|
+
kind: 'bar' | 'text'
|
|
44
|
+
values?: number[]
|
|
45
|
+
data?: string
|
|
46
|
+
max?: number
|
|
47
|
+
color?: string
|
|
48
|
+
colors?: Record<string, string>
|
|
49
|
+
row?: string
|
|
50
|
+
height?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
// the overlay annotation itself lives in msa-parsers, alongside the adapters
|
|
38
54
|
// that build it. TidyDomainAnnotation is its former name, kept because
|
|
39
55
|
// downstream plugins name it in their emitted declarations.
|
|
@@ -50,6 +66,29 @@ export interface DomainBand {
|
|
|
50
66
|
stackIndex: number
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A persistent, labeled highlight. Coordinates are 1-based and inclusive, as
|
|
71
|
+
* GFF's are. `row` makes `start`/`end` residues of that row; without it they
|
|
72
|
+
* are alignment columns. `rows` marks whole rows instead.
|
|
73
|
+
*/
|
|
74
|
+
export interface Highlight {
|
|
75
|
+
row?: string
|
|
76
|
+
rows?: string[]
|
|
77
|
+
start?: number
|
|
78
|
+
end?: number
|
|
79
|
+
label?: string
|
|
80
|
+
color?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** a Highlight resolved to visible column indices and row indices */
|
|
84
|
+
export interface ResolvedHighlight {
|
|
85
|
+
startCol?: number
|
|
86
|
+
endCol?: number
|
|
87
|
+
rowIndices: number[]
|
|
88
|
+
label?: string
|
|
89
|
+
color?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
53
92
|
export interface Node {
|
|
54
93
|
children?: Node[]
|
|
55
94
|
name?: string
|
package/src/umd.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
|
|
3
|
+
// a host that swaps models (the R htmlwidget on re-render) has to destroy the
|
|
4
|
+
// old one, or the disposers its autoruns registered never run
|
|
5
|
+
export { destroy } from '@jbrowse/mobx-state-tree'
|
|
3
6
|
export { default as MSAView } from './components/Loading.tsx'
|
|
4
7
|
export { type MsaViewModel, default as MSAModelF } from './model.ts'
|
|
5
8
|
|
package/src/util.ts
CHANGED
|
@@ -46,6 +46,14 @@ export function colorContrast(
|
|
|
46
46
|
])
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function dropBlanks<T>(blanks: number[], arr: T[]) {
|
|
50
|
+
if (blanks.length === 0) {
|
|
51
|
+
return arr
|
|
52
|
+
}
|
|
53
|
+
const set = new Set(blanks)
|
|
54
|
+
return arr.filter((_, i) => !set.has(i))
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export function skipBlanks(blanks: number[], str: string) {
|
|
50
58
|
if (blanks.length === 0) {
|
|
51
59
|
return str
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '6.
|
|
1
|
+
export const version = '6.3.0'
|