react-msaview 6.1.0 → 6.2.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 +92 -92
- package/bundle/index.js.map +4 -4
- package/dist/components/dialogs/ExportSVGDialog.js +48 -20
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +8 -0
- package/dist/components/getVisibleLeaves.js +13 -6
- package/dist/components/getVisibleLeaves.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +1 -1
- package/dist/components/header/settingsMenuItems.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.d.ts +4 -4
- package/dist/components/minimap/MinimapSVG.js +16 -10
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +5 -8
- package/dist/components/minimap/minimapLayout.js +10 -11
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +32 -2
- package/dist/components/msa/msaRaster.js +93 -15
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +22 -19
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/{renderTestEnv.d.ts → headlessRenderEnv.d.ts} +15 -13
- package/dist/{renderTestEnv.js → headlessRenderEnv.js} +22 -18
- package/dist/headlessRenderEnv.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/model/treeModel.d.ts +1 -1
- package/dist/model/treeModel.js +1 -1
- package/dist/renderToSvg.js +115 -36
- package/dist/renderToSvg.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/components/dialogs/ExportSVGDialog.tsx +55 -20
- package/src/components/getVisibleLeaves.ts +25 -9
- package/src/components/header/settingsMenuItems.ts +1 -1
- package/src/components/minimap/Minimap.tsx +1 -4
- package/src/components/minimap/MinimapSVG.tsx +50 -35
- package/src/components/minimap/minimapLayout.test.ts +16 -5
- package/src/components/minimap/minimapLayout.ts +10 -11
- package/src/components/msa/msaRaster.ts +124 -17
- package/src/components/tree/renderTreeCanvas.test.ts +55 -0
- package/src/components/tree/renderTreeCanvas.ts +23 -18
- package/src/{renderTestEnv.ts → headlessRenderEnv.ts} +23 -19
- package/src/impgRender.test.tsx +2 -2
- package/src/index.ts +6 -0
- package/src/model/treeModel.ts +1 -1
- package/src/renderDomainsSvg.test.tsx +2 -2
- package/src/renderMinimapSvg.test.tsx +29 -15
- package/src/renderRasterSvg.test.tsx +216 -0
- package/src/renderSequenceLogoSvg.test.tsx +6 -4
- package/src/renderToSvg.test.tsx +79 -0
- package/src/renderToSvg.tsx +201 -65
- package/src/version.ts +1 -1
- package/dist/renderTestEnv.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* DOM shims for driving `renderToSvg` outside a browser -- the SVG-export
|
|
3
|
+
* tests, the README figure generator, and react-msaview-cli all need them.
|
|
4
4
|
*
|
|
5
5
|
* jsdom implements neither DOMMatrix nor a canvas 2D context, and
|
|
6
6
|
* `@jbrowse/svgcanvas` needs both: it tracks the current transform as a real
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* leaves the rest undefined -- every emitted transform then carries `NaN` in its
|
|
13
13
|
* x components, which no assertion on a drawn x-position can survive.
|
|
14
14
|
*/
|
|
15
|
-
export class
|
|
15
|
+
export class HeadlessDOMMatrix {
|
|
16
16
|
a: number
|
|
17
17
|
b: number
|
|
18
18
|
c: number
|
|
@@ -30,8 +30,8 @@ export class TestDOMMatrix {
|
|
|
30
30
|
this.f = f
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
multiply(o:
|
|
34
|
-
return new
|
|
33
|
+
multiply(o: HeadlessDOMMatrix) {
|
|
34
|
+
return new HeadlessDOMMatrix([
|
|
35
35
|
this.a * o.a + this.c * o.b,
|
|
36
36
|
this.b * o.a + this.d * o.b,
|
|
37
37
|
this.a * o.c + this.c * o.d,
|
|
@@ -42,22 +42,22 @@ export class TestDOMMatrix {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
translate(x: number, y = 0) {
|
|
45
|
-
return this.multiply(new
|
|
45
|
+
return this.multiply(new HeadlessDOMMatrix([1, 0, 0, 1, x, y]))
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
scale(x: number, y = x) {
|
|
49
|
-
return this.multiply(new
|
|
49
|
+
return this.multiply(new HeadlessDOMMatrix([x, 0, 0, y, 0, 0]))
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export class
|
|
53
|
+
export class HeadlessDOMPoint {
|
|
54
54
|
constructor(
|
|
55
55
|
public x = 0,
|
|
56
56
|
public y = 0,
|
|
57
57
|
) {}
|
|
58
58
|
|
|
59
|
-
matrixTransform(m:
|
|
60
|
-
return new
|
|
59
|
+
matrixTransform(m: HeadlessDOMMatrix) {
|
|
60
|
+
return new HeadlessDOMPoint(
|
|
61
61
|
m.a * this.x + m.c * this.y + m.e,
|
|
62
62
|
m.b * this.x + m.d * this.y + m.f,
|
|
63
63
|
)
|
|
@@ -65,18 +65,22 @@ export class TestDOMPoint {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Install the shims
|
|
68
|
+
* Install the shims, into `globalThis` by default. A Node caller that builds
|
|
69
|
+
* its own jsdom passes that window instead, because Node has no global
|
|
70
|
+
* HTMLCanvasElement to patch.
|
|
69
71
|
*
|
|
70
72
|
* `measureText` reports 0.6em per character, close enough to a real sans-serif
|
|
71
|
-
* average for layout
|
|
72
|
-
*
|
|
73
|
+
* average for layout and, more usefully, exactly predictable: a caller can
|
|
74
|
+
* compute the width a renderer will see rather than hardcoding a number
|
|
73
75
|
* measured from one machine's fonts.
|
|
74
76
|
*/
|
|
75
|
-
export function
|
|
77
|
+
export function installHeadlessRenderEnv(win: unknown = globalThis) {
|
|
76
78
|
const g = globalThis as Record<string, unknown>
|
|
77
|
-
g.DOMMatrix =
|
|
78
|
-
g.DOMPoint =
|
|
79
|
-
|
|
79
|
+
g.DOMMatrix = HeadlessDOMMatrix
|
|
80
|
+
g.DOMPoint = HeadlessDOMPoint
|
|
81
|
+
const canvas = (win as { HTMLCanvasElement: typeof HTMLCanvasElement })
|
|
82
|
+
.HTMLCanvasElement
|
|
83
|
+
canvas.prototype.getContext = function () {
|
|
80
84
|
let font = '10px sans-serif'
|
|
81
85
|
return {
|
|
82
86
|
get font() {
|
|
@@ -86,11 +90,11 @@ export function installRenderTestEnv() {
|
|
|
86
90
|
font = v
|
|
87
91
|
},
|
|
88
92
|
measureText: (t: string) => ({
|
|
89
|
-
width: t.length * (Number.parseFloat(font) || 10) *
|
|
93
|
+
width: t.length * (Number.parseFloat(font) || 10) * CHAR_WIDTH_RATIO,
|
|
90
94
|
}),
|
|
91
95
|
} as unknown as CanvasRenderingContext2D
|
|
92
96
|
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
/** the per-character width factor `measureText` above reports */
|
|
96
|
-
export const
|
|
100
|
+
export const CHAR_WIDTH_RATIO = 0.6
|
package/src/impgRender.test.tsx
CHANGED
|
@@ -11,13 +11,13 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
|
11
11
|
import { enableStaticRendering } from 'mobx-react'
|
|
12
12
|
import { beforeAll, expect, test } from 'vitest'
|
|
13
13
|
|
|
14
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
14
15
|
import MSAModelF from './model.ts'
|
|
15
|
-
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
16
16
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
17
17
|
|
|
18
18
|
beforeAll(() => {
|
|
19
19
|
enableStaticRendering(true)
|
|
20
|
-
|
|
20
|
+
installHeadlessRenderEnv()
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
// representative `impg query -o fasta-aln` block: PanSN names with region
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
// members they rely on (e.g. mouseCol/setMousePos, setHighlightedColumns,
|
|
7
7
|
// seqPosToVisibleCol/visibleColToSeqPos) are flagged inline in model.ts.
|
|
8
8
|
export { renderToSvg } from './renderToSvg.tsx'
|
|
9
|
+
// renderToSvg outside a browser needs DOM bits jsdom omits; react-msaview-cli
|
|
10
|
+
// and the README figure generator both drive it that way
|
|
11
|
+
export {
|
|
12
|
+
CHAR_WIDTH_RATIO,
|
|
13
|
+
installHeadlessRenderEnv,
|
|
14
|
+
} from './headlessRenderEnv.ts'
|
|
9
15
|
export { default as MSAView } from './components/Loading.tsx'
|
|
10
16
|
export { default as MSAViewer } from './components/MSAViewer.tsx'
|
|
11
17
|
export { type MsaViewModel, default as MSAModelF } from './model.ts'
|
package/src/model/treeModel.ts
CHANGED
|
@@ -8,13 +8,13 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
|
8
8
|
import { enableStaticRendering } from 'mobx-react'
|
|
9
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
10
|
|
|
11
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
11
12
|
import MSAModelF from './model.ts'
|
|
12
|
-
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
13
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
14
14
|
|
|
15
15
|
beforeAll(() => {
|
|
16
16
|
enableStaticRendering(true)
|
|
17
|
-
|
|
17
|
+
installHeadlessRenderEnv()
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
const msa = `>seq1
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
//
|
|
3
|
-
// The viewport export draws a minimap by default, and nothing covered it.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The viewport export draws a minimap by default, and nothing covered it. It
|
|
4
|
+
// spans the same alignment canvas the live minimap does, so its trapezoid has
|
|
5
|
+
// to reach the right edge of the exported alignment column -- and it is drawn
|
|
6
|
+
// only when the live view shows one at all.
|
|
7
7
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
8
8
|
import { enableStaticRendering } from 'mobx-react'
|
|
9
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
10
10
|
|
|
11
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
11
12
|
import MSAModelF from './model.ts'
|
|
12
|
-
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
13
13
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
14
14
|
|
|
15
15
|
beforeAll(() => {
|
|
16
16
|
enableStaticRendering(true)
|
|
17
|
-
|
|
17
|
+
installHeadlessRenderEnv()
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
const width = 1000
|
|
21
21
|
|
|
22
|
-
function makeModel() {
|
|
23
|
-
const row = 'ACDEFGHIKL'.repeat(
|
|
22
|
+
function makeModel(cols = 400) {
|
|
23
|
+
const row = 'ACDEFGHIKL'.repeat(cols / 10)
|
|
24
24
|
const model = MSAModelF().create({
|
|
25
25
|
type: 'MsaView',
|
|
26
26
|
msaFormat: 'fasta',
|
|
@@ -31,19 +31,24 @@ function makeModel() {
|
|
|
31
31
|
return model
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expect(model.showHorizontalScrollbar).toBe(true)
|
|
37
|
-
|
|
38
|
-
const svg = await renderToSvg(model, {
|
|
34
|
+
function exportViewport(model: ReturnType<typeof makeModel>) {
|
|
35
|
+
return renderToSvg(model, {
|
|
39
36
|
theme: createJBrowseTheme(),
|
|
40
37
|
exportType: 'viewport',
|
|
41
38
|
includeMinimap: true,
|
|
42
39
|
includeTracks: false,
|
|
43
40
|
})
|
|
41
|
+
}
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
const
|
|
43
|
+
test('the exported minimap spans the exported alignment column', async () => {
|
|
44
|
+
const model = makeModel()
|
|
45
|
+
expect(model.showHorizontalScrollbar).toBe(true)
|
|
46
|
+
|
|
47
|
+
const svg = await exportViewport(model)
|
|
48
|
+
|
|
49
|
+
// the minimap column is the alignment canvas, which is what the export lays
|
|
50
|
+
// out beside the tree
|
|
51
|
+
const minimapWidth = model.msaCanvasWidth
|
|
47
52
|
const polygon = /<polygon[^>]*points="([^"]*)"/.exec(svg)?.[1]
|
|
48
53
|
expect(polygon).toBeDefined()
|
|
49
54
|
// bottom edge of the trapezoid reaches the full width it was given
|
|
@@ -53,3 +58,12 @@ test('the exported minimap spans the exported alignment column', async () => {
|
|
|
53
58
|
// and the bar outline is drawn to the same width
|
|
54
59
|
expect(svg).toContain(`width="${minimapWidth}"`)
|
|
55
60
|
})
|
|
61
|
+
|
|
62
|
+
test('no minimap when the alignment already fits across', async () => {
|
|
63
|
+
const model = makeModel(10)
|
|
64
|
+
expect(model.showHorizontalScrollbar).toBe(false)
|
|
65
|
+
|
|
66
|
+
// a minimap here would mark a viewport wider than the bar it sits in, and the
|
|
67
|
+
// live view draws none either
|
|
68
|
+
expect(await exportViewport(model)).not.toContain('<polygon')
|
|
69
|
+
})
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The SVG export's raster background. jsdom has no canvas, so the export falls
|
|
4
|
+
// back to a <rect> per cell there and every other export test exercises that
|
|
5
|
+
// path; this file supplies the canvas the raster needs and checks the export
|
|
6
|
+
// takes it instead.
|
|
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 { renderToSvg } from './renderToSvg.tsx'
|
|
13
|
+
|
|
14
|
+
const dataUrl = 'data:image/png;base64,STUB'
|
|
15
|
+
|
|
16
|
+
// enough of a 2d context for the raster: a color parser, an ImageData that is
|
|
17
|
+
// really backed by bytes, and a canvas that reads back
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
enableStaticRendering(true)
|
|
20
|
+
globalThis.DOMMatrix = class {
|
|
21
|
+
a = 1
|
|
22
|
+
b = 0
|
|
23
|
+
c = 0
|
|
24
|
+
d = 1
|
|
25
|
+
e = 0
|
|
26
|
+
f = 0
|
|
27
|
+
constructor(init?: number[]) {
|
|
28
|
+
const [a = 1, b = 0, c = 0, d = 1, e = 0, f = 0] = init ?? []
|
|
29
|
+
Object.assign(this, { a, b, c, d, e, f })
|
|
30
|
+
}
|
|
31
|
+
multiply(o: any) {
|
|
32
|
+
return new (globalThis.DOMMatrix as any)([
|
|
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
|
|
95
|
+
HTMLCanvasElement.prototype.toDataURL = () => dataUrl
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function makeModel(rows: number, cols: number) {
|
|
99
|
+
const letters = 'ACDEFGHIKLMNPQRSTVWY'
|
|
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({
|
|
108
|
+
id: 'raster',
|
|
109
|
+
type: 'MsaView',
|
|
110
|
+
height: 400,
|
|
111
|
+
msaFormat: 'fasta',
|
|
112
|
+
data: { msa },
|
|
113
|
+
})
|
|
114
|
+
model.setWidth(800)
|
|
115
|
+
return model
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function exportEntire(model: ReturnType<typeof makeModel>) {
|
|
119
|
+
return renderToSvg(model, {
|
|
120
|
+
theme: createJBrowseTheme(),
|
|
121
|
+
exportType: 'entire',
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
test('the background is one image, not a rect per cell', async () => {
|
|
126
|
+
const model = makeModel(40, 300)
|
|
127
|
+
expect(model.bgColor).toBe(true)
|
|
128
|
+
expect(model.actuallyShowDomains).toBe(false)
|
|
129
|
+
|
|
130
|
+
const svg = await exportEntire(model)
|
|
131
|
+
const images = [...svg.matchAll(/<image[^>]*>/g)].map(m => m[0])
|
|
132
|
+
|
|
133
|
+
expect(images).toHaveLength(1)
|
|
134
|
+
expect(images[0]).toContain(dataUrl)
|
|
135
|
+
// the alignment's own rectangle, one pixel per cell scaled up by whole cells
|
|
136
|
+
expect(images[0]).toContain(`width="${300 * model.colWidth}"`)
|
|
137
|
+
expect(images[0]).toContain(`height="${40 * model.rowHeight}"`)
|
|
138
|
+
expect(images[0]).toContain('image-rendering="pixelated"')
|
|
139
|
+
|
|
140
|
+
// 12000 cells would have been 12000 rects
|
|
141
|
+
expect((svg.match(/<rect/g) ?? []).length).toBeLessThan(10)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('letters still draw on top of the raster', async () => {
|
|
145
|
+
const model = makeModel(4, 10)
|
|
146
|
+
expect(model.showMsaLetters).toBe(true)
|
|
147
|
+
|
|
148
|
+
const svg = await exportEntire(model)
|
|
149
|
+
|
|
150
|
+
expect(svg).toContain('<image')
|
|
151
|
+
expect((svg.match(/<text/g) ?? []).length).toBeGreaterThanOrEqual(40)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('the domain overlay keeps the vector path, which paints its own boxes', async () => {
|
|
155
|
+
const model = makeModel(4, 10)
|
|
156
|
+
model.setDomains({
|
|
157
|
+
seq0: {
|
|
158
|
+
xref: [{ id: 'seq0' }],
|
|
159
|
+
matches: [
|
|
160
|
+
{
|
|
161
|
+
signature: {
|
|
162
|
+
entry: { name: 'Kinase', accession: 'PF00069', description: '' },
|
|
163
|
+
},
|
|
164
|
+
locations: [{ start: 1, end: 5 }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
expect(model.actuallyShowDomains).toBe(true)
|
|
170
|
+
|
|
171
|
+
expect(await exportEntire(model)).not.toContain('<image')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('turning background color off leaves nothing for a raster to draw', async () => {
|
|
175
|
+
const model = makeModel(4, 10)
|
|
176
|
+
model.setBgColor(false)
|
|
177
|
+
|
|
178
|
+
expect(await exportEntire(model)).not.toContain('<image')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('the exported minimap carries the alignment thumbnail', async () => {
|
|
182
|
+
const model = makeModel(20, 2000)
|
|
183
|
+
expect(model.showHorizontalScrollbar).toBe(true)
|
|
184
|
+
|
|
185
|
+
const svg = await renderToSvg(model, {
|
|
186
|
+
theme: createJBrowseTheme(),
|
|
187
|
+
exportType: 'viewport',
|
|
188
|
+
includeMinimap: true,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// the bar is the same downsampled alignment the live minimap draws, rather
|
|
192
|
+
// than the empty outline the export used to leave
|
|
193
|
+
const bar = [...svg.matchAll(/<image[^>]*>/g)]
|
|
194
|
+
.map(m => m[0])
|
|
195
|
+
.find(i => i.includes('height="12"'))
|
|
196
|
+
expect(bar).toBeDefined()
|
|
197
|
+
expect(bar).toContain(dataUrl)
|
|
198
|
+
expect(bar).toContain(`width="${model.msaCanvasWidth}"`)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('each track is named in the tree column beside it', async () => {
|
|
202
|
+
const model = makeModel(4, 40)
|
|
203
|
+
model.toggleTrack('sequence-logo')
|
|
204
|
+
const names = model.turnedOnTracks.map(t => t.model.name)
|
|
205
|
+
expect(names.length).toBeGreaterThan(1)
|
|
206
|
+
|
|
207
|
+
const svg = await renderToSvg(model, {
|
|
208
|
+
theme: createJBrowseTheme(),
|
|
209
|
+
exportType: 'entire',
|
|
210
|
+
includeTracks: true,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
expect(svg).toContain(`>${name}</text>`)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
@@ -10,13 +10,16 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
|
10
10
|
import { enableStaticRendering } from 'mobx-react'
|
|
11
11
|
import { beforeAll, expect, test } from 'vitest'
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
CHAR_WIDTH_RATIO,
|
|
15
|
+
installHeadlessRenderEnv,
|
|
16
|
+
} from './headlessRenderEnv.ts'
|
|
13
17
|
import MSAModelF from './model.ts'
|
|
14
|
-
import { TEST_CHAR_WIDTH_RATIO, installRenderTestEnv } from './renderTestEnv.ts'
|
|
15
18
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
16
19
|
|
|
17
20
|
beforeAll(() => {
|
|
18
21
|
enableStaticRendering(true)
|
|
19
|
-
|
|
22
|
+
installHeadlessRenderEnv()
|
|
20
23
|
})
|
|
21
24
|
|
|
22
25
|
// column 0 is fully conserved A, column 1 is a 3:1 split of C over G, and
|
|
@@ -159,8 +162,7 @@ test('letters are stretched to fill their column', async () => {
|
|
|
159
162
|
model.toggleTrack('sequence-logo')
|
|
160
163
|
const glyphs = await glyphsOf(model)
|
|
161
164
|
|
|
162
|
-
const expected =
|
|
163
|
-
colWidth / (model.sequenceLogoTrackHeight * TEST_CHAR_WIDTH_RATIO)
|
|
165
|
+
const expected = colWidth / (model.sequenceLogoTrackHeight * CHAR_WIDTH_RATIO)
|
|
164
166
|
for (const glyph of glyphs) {
|
|
165
167
|
expect(glyph.sx).toBeCloseTo(expected, 5)
|
|
166
168
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Layout and palette of the exported figure itself, as opposed to what any one
|
|
4
|
+
// layer draws inside it.
|
|
5
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
6
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
7
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
10
|
+
import MSAModelF from './model.ts'
|
|
11
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
enableStaticRendering(true)
|
|
15
|
+
installHeadlessRenderEnv()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function makeModel({ rows = 60, cols = 400 } = {}) {
|
|
19
|
+
const letters = 'ACDEFGHIKLMNPQRSTVWY'
|
|
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
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rootRect = (svg: string) => /<rect[^>]*height="100%"[^>]*>/.exec(svg)?.[0]
|
|
38
|
+
|
|
39
|
+
test('the page takes the theme background, not a hardcoded white', async () => {
|
|
40
|
+
const model = makeModel()
|
|
41
|
+
const dark = createJBrowseTheme({ palette: { mode: 'dark' } })
|
|
42
|
+
expect(dark.palette.background.default).not.toBe('white')
|
|
43
|
+
|
|
44
|
+
const svg = await renderToSvg(model, { theme: dark, exportType: 'entire' })
|
|
45
|
+
|
|
46
|
+
// the layers below draw in theme colors: text.primary is near-white in a dark
|
|
47
|
+
// theme, so a white page would export white on white
|
|
48
|
+
expect(rootRect(svg)).toContain(`fill="${dark.palette.background.default}"`)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a viewport export is the alignment canvas, not the whole widget', async () => {
|
|
52
|
+
const model = makeModel()
|
|
53
|
+
expect(model.showHorizontalScrollbar).toBe(true)
|
|
54
|
+
expect(model.showVerticalScrollbar).toBe(true)
|
|
55
|
+
|
|
56
|
+
const svg = await renderToSvg(model, {
|
|
57
|
+
theme: createJBrowseTheme(),
|
|
58
|
+
exportType: 'viewport',
|
|
59
|
+
includeMinimap: true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// the widget box also covers the resize handle and the scrollbars; exporting
|
|
63
|
+
// it drew the rows and columns those hide
|
|
64
|
+
const { treeAreaWidth, msaCanvasWidth, msaAreaHeight, minimapHeight } = model
|
|
65
|
+
expect(svg).toContain(`width="${treeAreaWidth + msaCanvasWidth}"`)
|
|
66
|
+
expect(svg).toContain(`height="${msaAreaHeight + minimapHeight}"`)
|
|
67
|
+
expect(msaAreaHeight + minimapHeight).toBe(model.height)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('no attribute is serialized as the string "undefined"', async () => {
|
|
71
|
+
const model = makeModel({ rows: 4, cols: 20 })
|
|
72
|
+
const svg = await renderToSvg(model, {
|
|
73
|
+
theme: createJBrowseTheme(),
|
|
74
|
+
exportType: 'entire',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(svg).toContain('<text')
|
|
78
|
+
expect(svg).not.toContain('="undefined"')
|
|
79
|
+
})
|