react-msaview 6.5.0 → 7.0.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/README.md +6 -5
- package/bundle/index.js +100 -101
- package/bundle/index.js.map +4 -4
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +10 -2
- package/dist/components/MSAViewer.js +90 -2
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -3
- package/dist/components/ResizeHandles.js +28 -21
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
- package/dist/components/dialogs/FeatureDialog.js +13 -9
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +4 -0
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +0 -2
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/LoadWarnings.d.ts +6 -0
- package/dist/components/header/LoadWarnings.js +36 -0
- package/dist/components/header/LoadWarnings.js.map +1 -0
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +9 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
- package/dist/components/header/UnshareableDataWarning.js +42 -0
- package/dist/components/header/UnshareableDataWarning.js.map +1 -0
- package/dist/components/header/getAnnotationsMenu.js +14 -14
- package/dist/components/header/getAnnotationsMenu.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 +2 -6
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/import/util.js +3 -0
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +4 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +13 -3
- package/dist/components/msa/msaRaster.js +173 -61
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +39 -1
- package/dist/components/msa/renderHighlights.js +71 -6
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +1 -2
- package/dist/components/msa/renderMSABlock.js +24 -28
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -30
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.js +10 -11
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +26 -8
- package/dist/components/tracks/drawTracks.js +123 -13
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +2 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +13 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
- package/dist/components/tree/renderTreeCanvas.js +70 -30
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/scaleBar.d.ts +10 -0
- package/dist/components/tree/scaleBar.js +24 -0
- package/dist/components/tree/scaleBar.js.map +1 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.js +18 -1
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +7 -7
- package/dist/fetchUtils.js +4 -32
- package/dist/fetchUtils.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -1
- package/dist/hierarchy.js +16 -10
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/model/DataModel.js +5 -2
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +454 -177
- package/dist/model.js +1180 -511
- package/dist/model.js.map +1 -1
- package/dist/renderToStaticMarkup.d.ts +11 -0
- package/dist/renderToStaticMarkup.js +30 -7
- package/dist/renderToStaticMarkup.js.map +1 -1
- package/dist/renderToSvg.js +25 -19
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +25 -25
- package/dist/rowCoordinateCalculations.js +12 -36
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/seqPosToGlobalCol.js +3 -1
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/svgTestUtil.d.ts +144 -82
- package/dist/types.d.ts +97 -3
- package/dist/util.d.ts +11 -4
- package/dist/util.js +30 -10
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wuss.d.ts +14 -0
- package/dist/wuss.js +55 -0
- package/dist/wuss.js.map +1 -0
- package/package.json +10 -8
- package/src/components/Loading.tsx +12 -2
- package/src/components/MSAViewer.tsx +106 -2
- package/src/components/ResizeHandles.tsx +31 -31
- package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
- package/src/components/dialogs/FeatureDialog.tsx +13 -9
- package/src/components/header/Header.tsx +4 -0
- package/src/components/header/HeaderStatusArea.tsx +0 -5
- package/src/components/header/LoadWarnings.tsx +49 -0
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/header/TreeSettingsMenu.tsx +9 -1
- package/src/components/header/UnshareableDataWarning.tsx +58 -0
- package/src/components/header/getAnnotationsMenu.ts +16 -17
- package/src/components/import/ImportForm.tsx +2 -2
- package/src/components/import/ImportFormExamples.tsx +2 -6
- package/src/components/import/util.ts +3 -0
- package/src/components/msa/MSACanvasBlock.tsx +4 -5
- package/src/components/msa/msaRaster.ts +216 -59
- package/src/components/msa/renderHighlights.ts +103 -5
- package/src/components/msa/renderMSABlock.ts +25 -36
- package/src/components/msa/renderMSAMouseover.ts +9 -39
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/TrackBlocks.tsx +10 -13
- package/src/components/tracks/drawTracks.ts +165 -17
- package/src/components/tree/TreeCanvas.tsx +2 -3
- package/src/components/tree/TreeCanvasBlock.tsx +4 -2
- package/src/components/tree/TreeRuler.tsx +38 -3
- package/src/components/tree/renderTreeCanvas.ts +79 -35
- package/src/components/tree/scaleBar.ts +23 -0
- package/src/constants.ts +20 -1
- package/src/fetchUtils.ts +7 -40
- package/src/hierarchy.ts +20 -11
- package/src/index.ts +18 -2
- package/src/model/DataModel.ts +6 -2
- package/src/model.ts +1352 -616
- package/src/rawImports.d.ts +9 -0
- package/src/renderToStaticMarkup.ts +34 -7
- package/src/renderToSvg.tsx +33 -27
- package/src/rowCoordinateCalculations.ts +15 -59
- package/src/seqPosToGlobalCol.ts +3 -1
- package/src/types.ts +131 -4
- package/src/util.ts +31 -14
- package/src/version.ts +1 -1
- package/src/wuss.ts +62 -0
- package/dist/components/dialogs/InterProScanDialog.js +0 -137
- package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
- package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
- package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
- package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
- package/dist/launchInterProScan.d.ts +0 -13
- package/dist/launchInterProScan.js +0 -110
- package/dist/launchInterProScan.js.map +0 -1
- package/dist/useColorContrast.d.ts +0 -10
- package/dist/useColorContrast.js +0 -13
- package/dist/useColorContrast.js.map +0 -1
- package/src/clustalX.test.ts +0 -74
- package/src/colSpace.test.ts +0 -67
- package/src/collapseLogic.test.ts +0 -115
- package/src/colorSchemes.docs.test.ts +0 -57
- package/src/columnCounts.test.ts +0 -73
- package/src/columnStats.test.ts +0 -94
- package/src/columnTracks.test.ts +0 -97
- package/src/components/dialogs/InterProScanDialog.tsx +0 -214
- package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
- package/src/components/minimap/minimapLayout.test.ts +0 -71
- package/src/components/msa/msaRaster.test.ts +0 -145
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
- package/src/components/tree/renderTreeCanvas.test.ts +0 -296
- package/src/dataInitialized.test.ts +0 -42
- package/src/domainBands.test.ts +0 -122
- package/src/domainLetterColor.test.ts +0 -80
- package/src/exportFileName.test.ts +0 -40
- package/src/fetchUtils.test.ts +0 -83
- package/src/flatToTree.test.ts +0 -51
- package/src/hierarchy.test.ts +0 -228
- package/src/highlights.test.ts +0 -70
- package/src/impgRender.test.tsx +0 -68
- package/src/largeTree.test.ts +0 -67
- package/src/launchInterProScan.ts +0 -167
- package/src/loadErrorDisplay.test.tsx +0 -102
- package/src/modelFilehandleLoaders.test.ts +0 -137
- package/src/modelReset.test.ts +0 -120
- package/src/neighborJoining.test.ts +0 -164
- package/src/parseAsn1.test.ts +0 -36
- package/src/propertyConservation.test.ts +0 -36
- package/src/renderColumnTracksSvg.test.tsx +0 -108
- package/src/renderDomainsSvg.test.tsx +0 -118
- package/src/renderHighlightsSvg.test.tsx +0 -85
- package/src/renderMinimapSvg.test.tsx +0 -62
- package/src/renderRasterSvg.test.tsx +0 -144
- package/src/renderSequenceLogoSvg.test.tsx +0 -165
- package/src/renderToSvg.test.tsx +0 -65
- package/src/reset.test.ts +0 -61
- package/src/rowCoordinateCalculations.test.ts +0 -211
- package/src/rowSpace.test.ts +0 -46
- package/src/segmentOverlay.test.ts +0 -70
- package/src/seqPosToGlobalCol.test.ts +0 -59
- package/src/sequenceLogo.test.ts +0 -88
- package/src/setFontSize.test.ts +0 -33
- package/src/stripDefault.test.ts +0 -48
- package/src/stripDefaultSnapshot.test.ts +0 -87
- package/src/treeMetadata.test.ts +0 -39
- package/src/useCanvasAutorun.test.tsx +0 -98
- package/src/useColorContrast.ts +0 -18
- package/src/viewChrome.test.tsx +0 -134
- package/src/wheelZoom.test.ts +0 -46
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { expect, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import MSAModelF from '../../model.ts'
|
|
5
|
-
import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
|
|
6
|
-
|
|
7
|
-
// wide enough to overflow horizontally, tall enough to raise the vertical
|
|
8
|
-
// scrollbar — which takes 20px out of the width the alignment actually draws on
|
|
9
|
-
function makeModel() {
|
|
10
|
-
const names = Array.from({ length: 60 }, (_, i) => `s${i}`)
|
|
11
|
-
const model = MSAModelF().create({
|
|
12
|
-
type: 'MsaView',
|
|
13
|
-
msaFormat: 'fasta',
|
|
14
|
-
height: 400,
|
|
15
|
-
data: { msa: names.map(n => `>${n}\n${'ACGT'.repeat(50)}`).join('\n') },
|
|
16
|
-
})
|
|
17
|
-
model.setWidth(1000)
|
|
18
|
-
return model
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
test('the viewport rectangle scales by the canvas width, not the msa area', () => {
|
|
22
|
-
const model = makeModel()
|
|
23
|
-
expect(model.showVerticalScrollbar).toBe(true)
|
|
24
|
-
expect(model.msaCanvasWidth).toBe(model.msaAreaWidth - 20)
|
|
25
|
-
|
|
26
|
-
const { unit, s, w } = getMinimapLayout(model)
|
|
27
|
-
expect(unit).toBeCloseTo(model.msaCanvasWidth / model.totalWidth)
|
|
28
|
-
// unscrolled, the rectangle starts at the left edge and covers the fraction
|
|
29
|
-
// of the alignment on screen. Sizing it by msaAreaWidth instead overstated
|
|
30
|
-
// that fraction by the scrollbar's width.
|
|
31
|
-
expect(s).toBeCloseTo(0)
|
|
32
|
-
expect(w).toBeCloseTo(
|
|
33
|
-
(model.msaCanvasWidth / model.totalWidth) * model.msaCanvasWidth,
|
|
34
|
-
)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('the rectangle tracks scrollX and the trapezoid spans the bar', () => {
|
|
38
|
-
const model = makeModel()
|
|
39
|
-
model.setScrollX(-300)
|
|
40
|
-
|
|
41
|
-
const { msaCanvasWidth: width } = model
|
|
42
|
-
const { s, polygonHeight, polygonPoints } = getMinimapLayout(model)
|
|
43
|
-
expect(s).toBeCloseTo((300 / model.totalWidth) * width)
|
|
44
|
-
expect(polygonHeight).toBe(model.minimapHeight - MINIMAP_BAR_HEIGHT)
|
|
45
|
-
// bottom edge spans the full minimap, top edge is the viewport rectangle
|
|
46
|
-
expect(
|
|
47
|
-
polygonPoints.endsWith(`0,${polygonHeight} ${width},${polygonHeight}`),
|
|
48
|
-
).toBe(true)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('an alignment that already fits never overflows the bar', () => {
|
|
52
|
-
const model = MSAModelF().create({
|
|
53
|
-
type: 'MsaView',
|
|
54
|
-
msaFormat: 'fasta',
|
|
55
|
-
data: { msa: '>a\nACGT\n>b\nACGT' },
|
|
56
|
-
})
|
|
57
|
-
model.setWidth(1000)
|
|
58
|
-
expect(model.totalWidth).toBeLessThan(model.msaCanvasWidth)
|
|
59
|
-
expect(getMinimapLayout(model).w).toBe(model.msaCanvasWidth)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('an empty alignment collapses to a zero-width unit instead of dividing by zero', () => {
|
|
63
|
-
const model = MSAModelF().create({ type: 'MsaView' })
|
|
64
|
-
model.setWidth(1000)
|
|
65
|
-
expect(model.totalWidth).toBe(0)
|
|
66
|
-
const { unit, s, w } = getMinimapLayout(model)
|
|
67
|
-
expect(unit).toBe(0)
|
|
68
|
-
expect(s).toBeCloseTo(0)
|
|
69
|
-
// still clamped to the minimum grabbable width
|
|
70
|
-
expect(w).toBe(20)
|
|
71
|
-
})
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { beforeAll, expect, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import MSAModelF from '../../model.ts'
|
|
5
|
-
import { drawMsaRaster, rasterPixels } from './msaRaster.ts'
|
|
6
|
-
|
|
7
|
-
import type { RasterSpec } from './msaRaster.ts'
|
|
8
|
-
|
|
9
|
-
// the stub context resolves a color by parsing '#rrggbb', so every color the
|
|
10
|
-
// raster asks about in this file is written that way
|
|
11
|
-
function hexBytes(css: string) {
|
|
12
|
-
const hex = css.replace('#', '')
|
|
13
|
-
return [
|
|
14
|
-
Number.parseInt(hex.slice(0, 2), 16),
|
|
15
|
-
Number.parseInt(hex.slice(2, 4), 16),
|
|
16
|
-
Number.parseInt(hex.slice(4, 6), 16),
|
|
17
|
-
255,
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const drawn: unknown[] = []
|
|
22
|
-
|
|
23
|
-
beforeAll(() => {
|
|
24
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
25
|
-
let fillStyle = '#000000'
|
|
26
|
-
return {
|
|
27
|
-
get fillStyle() {
|
|
28
|
-
return fillStyle
|
|
29
|
-
},
|
|
30
|
-
set fillStyle(v: string) {
|
|
31
|
-
fillStyle = v
|
|
32
|
-
},
|
|
33
|
-
imageSmoothingEnabled: true,
|
|
34
|
-
font: '10px sans-serif',
|
|
35
|
-
measureText: (t: string) => ({ width: t.length * 6 }),
|
|
36
|
-
clearRect: () => {},
|
|
37
|
-
fillRect: () => {},
|
|
38
|
-
resetTransform: () => {},
|
|
39
|
-
scale: () => {},
|
|
40
|
-
translate: () => {},
|
|
41
|
-
getImageData: () => ({
|
|
42
|
-
data: Uint8ClampedArray.from(hexBytes(fillStyle)),
|
|
43
|
-
}),
|
|
44
|
-
createImageData: (width: number, height: number) => ({
|
|
45
|
-
data: new Uint8ClampedArray(width * height * 4),
|
|
46
|
-
width,
|
|
47
|
-
height,
|
|
48
|
-
}),
|
|
49
|
-
putImageData: () => {},
|
|
50
|
-
drawImage: (image: unknown) => {
|
|
51
|
-
drawn.push(image)
|
|
52
|
-
},
|
|
53
|
-
} as unknown as CanvasRenderingContext2D
|
|
54
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const spec: RasterSpec = {
|
|
58
|
-
rowNames: ['a', 'b', 'c'],
|
|
59
|
-
columns: new Map([
|
|
60
|
-
['a', 'MKL'],
|
|
61
|
-
['b', 'MKL'],
|
|
62
|
-
['c', 'MK'],
|
|
63
|
-
]),
|
|
64
|
-
relativeTo: 'a',
|
|
65
|
-
colorAt: (_col, letter) => (letter === 'M' ? '#ff0000' : undefined),
|
|
66
|
-
bg: '#000000',
|
|
67
|
-
hover: '#00ff00',
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ids: Record<string, number> = { '#ff0000': 1, '#000000': 2, '#00ff00': 3 }
|
|
71
|
-
const toPixel = (css: string) => ids[css]!
|
|
72
|
-
|
|
73
|
-
test('one pixel per cell, reference matches and short rows', () => {
|
|
74
|
-
const px = rasterPixels({
|
|
75
|
-
spec,
|
|
76
|
-
col0: 0,
|
|
77
|
-
row0: 0,
|
|
78
|
-
width: 3,
|
|
79
|
-
height: 4,
|
|
80
|
-
toPixel,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// the reference row itself keeps its scheme colors
|
|
84
|
-
expect([...px.slice(0, 3)]).toEqual([1, 2, 2])
|
|
85
|
-
// every cell of an identical row reads as a match
|
|
86
|
-
expect([...px.slice(3, 6)]).toEqual([3, 3, 3])
|
|
87
|
-
// past the end of a short row, and past the last row, stays transparent
|
|
88
|
-
expect([...px.slice(6, 9)]).toEqual([3, 3, 0])
|
|
89
|
-
expect([...px.slice(9, 12)]).toEqual([0, 0, 0])
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('sampling steps over columns and rows', () => {
|
|
93
|
-
const px = rasterPixels({
|
|
94
|
-
spec,
|
|
95
|
-
col0: 0,
|
|
96
|
-
row0: 0,
|
|
97
|
-
width: 2,
|
|
98
|
-
height: 2,
|
|
99
|
-
colStep: 2,
|
|
100
|
-
rowStep: 2,
|
|
101
|
-
toPixel,
|
|
102
|
-
})
|
|
103
|
-
// the second sampled column falls past the end of the short row 'c'
|
|
104
|
-
expect([...px]).toEqual([1, 2, 3, 0])
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
function make() {
|
|
108
|
-
const model = MSAModelF().create({
|
|
109
|
-
id: 'raster',
|
|
110
|
-
type: 'MsaView',
|
|
111
|
-
msaFormat: 'fasta',
|
|
112
|
-
data: { msa: '>a\nMKLVIL\n>b\nMRLVIL\n>c\nMKLAIL' },
|
|
113
|
-
})
|
|
114
|
-
model.setWidth(1000)
|
|
115
|
-
return model
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function tilesFor(model: ReturnType<typeof make>) {
|
|
119
|
-
drawn.length = 0
|
|
120
|
-
const ctx = document.createElement('canvas').getContext('2d')!
|
|
121
|
-
drawMsaRaster({ ctx, model, theme: theme(), offsetX: 0, offsetY: 0 })
|
|
122
|
-
return [...drawn]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// only the two palette entries the raster reads
|
|
126
|
-
const theme = () =>
|
|
127
|
-
({
|
|
128
|
-
palette: {
|
|
129
|
-
action: { hover: '#00ff00' },
|
|
130
|
-
background: { default: '#ffffff' },
|
|
131
|
-
},
|
|
132
|
-
}) as never
|
|
133
|
-
|
|
134
|
-
test('a zoom reuses the tiles, a recolor rebuilds them', () => {
|
|
135
|
-
const model = make()
|
|
136
|
-
const tile = tilesFor(model)[0]
|
|
137
|
-
expect(tile).toBeDefined()
|
|
138
|
-
|
|
139
|
-
model.setColWidth(3)
|
|
140
|
-
model.setRowHeight(2)
|
|
141
|
-
expect(tilesFor(model)[0]).toBe(tile)
|
|
142
|
-
|
|
143
|
-
model.setColorSchemeName('clustalx_protein_dynamic')
|
|
144
|
-
expect(tilesFor(model)[0]).not.toBe(tile)
|
|
145
|
-
})
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import stateModelFactory from '../../model.ts'
|
|
4
|
-
import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
|
|
5
|
-
|
|
6
|
-
import type { RenderCtx } from '../renderCtx.ts'
|
|
7
|
-
|
|
8
|
-
const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
|
|
9
|
-
|
|
10
|
-
// only `human` carries the gene model; `mouse` is there to push it off the top
|
|
11
|
-
// of the block
|
|
12
|
-
const msa = `>mouse\n${row}\n>human\n${row}`
|
|
13
|
-
|
|
14
|
-
const gff = `##gff-version 3
|
|
15
|
-
human src exon 1 20 . . . ID=human.exon1;Name=exon-1
|
|
16
|
-
human src exon 21 40 . . . ID=human.exon2;Name=exon-2
|
|
17
|
-
human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
|
|
18
|
-
|
|
19
|
-
function drawnLabels() {
|
|
20
|
-
const model = stateModelFactory().create({
|
|
21
|
-
type: 'MsaView',
|
|
22
|
-
data: { msa },
|
|
23
|
-
})
|
|
24
|
-
model.setWidth(1000)
|
|
25
|
-
model.setRowHeight(20)
|
|
26
|
-
// segment numbers are drawn in place of residue letters, never on top of them
|
|
27
|
-
model.setDrawMsaLetters(false)
|
|
28
|
-
model.applyGFFText(gff)
|
|
29
|
-
expect(model.rowNames).toEqual(['mouse', 'human'])
|
|
30
|
-
|
|
31
|
-
const labels: string[] = []
|
|
32
|
-
const ctx = {
|
|
33
|
-
font: '12px sans-serif',
|
|
34
|
-
resetTransform() {},
|
|
35
|
-
scale() {},
|
|
36
|
-
translate() {},
|
|
37
|
-
fillRect() {},
|
|
38
|
-
strokeRect() {},
|
|
39
|
-
measureText: (text: string) => ({ width: text.length * 6 }),
|
|
40
|
-
fillText(text: string) {
|
|
41
|
-
labels.push(text)
|
|
42
|
-
},
|
|
43
|
-
} as unknown as RenderCtx
|
|
44
|
-
|
|
45
|
-
renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
|
|
46
|
-
return labels
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
|
|
50
|
-
expect(drawnLabels()).toEqual(['1', '2', '3'])
|
|
51
|
-
})
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { calcDepthToLeaf, findMaxBranchLen } from '../../hierarchy.ts'
|
|
4
|
-
import stateModelFactory from '../../model.ts'
|
|
5
|
-
import { ClickMapIndex } from './clickMap.ts'
|
|
6
|
-
import { getNodeX, renderTreeCanvas } from './renderTreeCanvas.ts'
|
|
7
|
-
|
|
8
|
-
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
9
|
-
import type { RenderCtx } from '../renderCtx.ts'
|
|
10
|
-
import type { Theme } from '@mui/material'
|
|
11
|
-
|
|
12
|
-
function leaf(id: string, len?: number): HierarchyNode {
|
|
13
|
-
return {
|
|
14
|
-
data: { id, name: id, children: [] },
|
|
15
|
-
children: null,
|
|
16
|
-
parent: null,
|
|
17
|
-
depth: 0,
|
|
18
|
-
height: 0,
|
|
19
|
-
len,
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function internal(
|
|
24
|
-
id: string,
|
|
25
|
-
children: HierarchyNode[],
|
|
26
|
-
len?: number,
|
|
27
|
-
): HierarchyNode {
|
|
28
|
-
const node: HierarchyNode = {
|
|
29
|
-
data: { id, name: id, children: children.map(c => c.data) },
|
|
30
|
-
children,
|
|
31
|
-
parent: null,
|
|
32
|
-
depth: 0,
|
|
33
|
-
height: 1,
|
|
34
|
-
len,
|
|
35
|
-
}
|
|
36
|
-
for (const child of children) {
|
|
37
|
-
child.parent = node
|
|
38
|
-
}
|
|
39
|
-
return node
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('calcDepthToLeaf', () => {
|
|
43
|
-
it('is 0 for a leaf', () => {
|
|
44
|
-
expect(calcDepthToLeaf(leaf('a'))).toBe(0)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('counts steps to the deepest tip', () => {
|
|
48
|
-
const tips = [leaf('a'), leaf('b')]
|
|
49
|
-
const intermediate = internal('int', tips)
|
|
50
|
-
const root = internal('root', [intermediate])
|
|
51
|
-
|
|
52
|
-
expect(calcDepthToLeaf(tips[0]!)).toBe(0)
|
|
53
|
-
expect(calcDepthToLeaf(intermediate)).toBe(1)
|
|
54
|
-
expect(calcDepthToLeaf(root)).toBe(2)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('memoizes without re-walking the subtree', () => {
|
|
58
|
-
// the renderer asks for every node's depth on every pass, so the memo has to
|
|
59
|
-
// short-circuit the traversal itself, not just the arithmetic. A sentinel
|
|
60
|
-
// planted on an already-computed child must survive a second root call.
|
|
61
|
-
const child = leaf('a')
|
|
62
|
-
const root = internal('root', [child])
|
|
63
|
-
expect(calcDepthToLeaf(root)).toBe(1)
|
|
64
|
-
|
|
65
|
-
child.depthToLeaf = 99
|
|
66
|
-
expect(calcDepthToLeaf(root)).toBe(1)
|
|
67
|
-
expect(child.depthToLeaf).toBe(99)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('findMaxBranchLen', () => {
|
|
72
|
-
it('uses the node itself when it is a leaf', () => {
|
|
73
|
-
expect(findMaxBranchLen(leaf('a', 1.5))).toBe(1.5)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('takes the max across descendants', () => {
|
|
77
|
-
expect(
|
|
78
|
-
findMaxBranchLen(internal('p', [leaf('a', 0.5), leaf('b', 1.5)], 0.3)),
|
|
79
|
-
).toBe(1.5)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('treats a missing len as 0', () => {
|
|
83
|
-
expect(findMaxBranchLen(leaf('a'))).toBe(0)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('getNodeX cladogram positioning', () => {
|
|
88
|
-
it('aligns every tip at the rightmost x', () => {
|
|
89
|
-
const tips = [leaf('a'), leaf('b')]
|
|
90
|
-
calcDepthToLeaf(internal('root', tips))
|
|
91
|
-
|
|
92
|
-
const xs = tips.map(tip => getNodeX(tip, false, 100, 1))
|
|
93
|
-
expect(xs).toEqual([100, 100])
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('puts the root at the leftmost x and internal nodes in between', () => {
|
|
97
|
-
const tips = [leaf('a'), leaf('b')]
|
|
98
|
-
const intermediate = internal('int', tips)
|
|
99
|
-
const root = internal('root', [intermediate])
|
|
100
|
-
calcDepthToLeaf(root)
|
|
101
|
-
|
|
102
|
-
const xRoot = getNodeX(root, false, 100, 2)!
|
|
103
|
-
const xInt = getNodeX(intermediate, false, 100, 2)!
|
|
104
|
-
const xTip = getNodeX(tips[0]!, false, 100, 2)!
|
|
105
|
-
|
|
106
|
-
expect(xRoot).toBe(0)
|
|
107
|
-
expect(xInt).toBeGreaterThan(xRoot)
|
|
108
|
-
expect(xTip).toBeGreaterThan(xInt)
|
|
109
|
-
expect(xTip).toBe(100)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('collapses to the root x when there is no topological depth', () => {
|
|
113
|
-
expect(getNodeX(leaf('a'), false, 100, 0)).toBe(0)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('uses branch length in phylogram mode', () => {
|
|
117
|
-
expect(getNodeX(leaf('a', 2.5), true, 100, 1)).toBe(2.5)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
// the x values the tree pass actually strokes, for a model built from newick,
|
|
122
|
-
// alongside the tree width they are expected to span
|
|
123
|
-
function drawnTree(newick: string) {
|
|
124
|
-
const model = stateModelFactory().create({
|
|
125
|
-
type: 'MsaView',
|
|
126
|
-
data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
|
|
127
|
-
})
|
|
128
|
-
model.setWidth(1000)
|
|
129
|
-
// labels would need a canvas to measure; the branch geometry is the subject
|
|
130
|
-
model.setDrawLabels(false)
|
|
131
|
-
model.setDrawNodeBubbles(false)
|
|
132
|
-
|
|
133
|
-
const xs: number[] = []
|
|
134
|
-
const ctx = {
|
|
135
|
-
font: '12px sans-serif',
|
|
136
|
-
beginPath() {},
|
|
137
|
-
stroke() {},
|
|
138
|
-
resetTransform() {},
|
|
139
|
-
scale() {},
|
|
140
|
-
translate() {},
|
|
141
|
-
moveTo(x: number) {
|
|
142
|
-
xs.push(x)
|
|
143
|
-
},
|
|
144
|
-
lineTo(x: number) {
|
|
145
|
-
xs.push(x)
|
|
146
|
-
},
|
|
147
|
-
} as unknown as RenderCtx
|
|
148
|
-
|
|
149
|
-
renderTreeCanvas({
|
|
150
|
-
model,
|
|
151
|
-
ctx,
|
|
152
|
-
offsetY: 0,
|
|
153
|
-
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
154
|
-
})
|
|
155
|
-
return { xs, model }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
describe('renderTreeCanvas horizontal extent', () => {
|
|
159
|
-
it('spreads a cladogram across the tree area when branch lengths are absent', () => {
|
|
160
|
-
// a lengthless newick forces cladogram mode; scaling it by the (zero) max
|
|
161
|
-
// branch length would stack every node on x=0 as one vertical line
|
|
162
|
-
const { xs, model } = drawnTree('((a,b),(c,d));')
|
|
163
|
-
expect(Math.max(...xs)).toBe(model.treeWidth)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('scales a phylogram by branch length', () => {
|
|
167
|
-
const { xs, model } = drawnTree('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
|
|
168
|
-
expect(Math.max(...xs)).toBeCloseTo(model.treeWidth)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// labels off means no label gutter to reserve, so the tips reach the far edge
|
|
172
|
-
// of the tree area rather than stopping at the default treeWidth
|
|
173
|
-
it('hands the label gutter to the tree when labels are off', () => {
|
|
174
|
-
const { model } = drawnTree('((a,b),(c,d));')
|
|
175
|
-
expect(model.treeWidth).toBe(model.treeAreaWidth - 10 - model.marginLeft)
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
describe('node bubble click targets', () => {
|
|
180
|
-
const bounds = { minX: 0, minY: 0, maxX: 10000, maxY: 10000 }
|
|
181
|
-
|
|
182
|
-
// "draw clickable bubbles" only controls the painting; the branches have to
|
|
183
|
-
// stay clickable with it off
|
|
184
|
-
function renderWithBubbles(drawNodeBubbles: boolean) {
|
|
185
|
-
const model = stateModelFactory().create({
|
|
186
|
-
type: 'MsaView',
|
|
187
|
-
data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: '((a,b),(c,d));' },
|
|
188
|
-
})
|
|
189
|
-
model.setWidth(1000)
|
|
190
|
-
model.setDrawLabels(false)
|
|
191
|
-
model.setDrawNodeBubbles(drawNodeBubbles)
|
|
192
|
-
|
|
193
|
-
let arcs = 0
|
|
194
|
-
const ctx = {
|
|
195
|
-
font: '12px sans-serif',
|
|
196
|
-
beginPath() {},
|
|
197
|
-
stroke() {},
|
|
198
|
-
fill() {},
|
|
199
|
-
resetTransform() {},
|
|
200
|
-
scale() {},
|
|
201
|
-
translate() {},
|
|
202
|
-
moveTo() {},
|
|
203
|
-
lineTo() {},
|
|
204
|
-
arc() {
|
|
205
|
-
arcs++
|
|
206
|
-
},
|
|
207
|
-
} as unknown as RenderCtx
|
|
208
|
-
|
|
209
|
-
const clickMap = new ClickMapIndex()
|
|
210
|
-
renderTreeCanvas({
|
|
211
|
-
model,
|
|
212
|
-
ctx,
|
|
213
|
-
clickMap,
|
|
214
|
-
offsetY: 0,
|
|
215
|
-
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
216
|
-
})
|
|
217
|
-
return { arcs, hits: clickMap.search(bounds) }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
it('indexes the internal nodes when the bubbles are drawn', () => {
|
|
221
|
-
const { arcs, hits } = renderWithBubbles(true)
|
|
222
|
-
expect(arcs).toBe(3)
|
|
223
|
-
expect(hits.filter(h => h.branch).length).toBe(3)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('indexes them just the same when the bubbles are off', () => {
|
|
227
|
-
const { arcs, hits } = renderWithBubbles(false)
|
|
228
|
-
expect(arcs).toBe(0)
|
|
229
|
-
expect(hits.filter(h => h.branch).length).toBe(3)
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
describe('leaf label click targets', () => {
|
|
234
|
-
const bounds = { minX: 0, minY: 0, maxX: 10000, maxY: 10000 }
|
|
235
|
-
// the recorded fillText x is in content coordinates -- the fake ctx applies
|
|
236
|
-
// the same translate the real one does -- so it is directly comparable to the
|
|
237
|
-
// clickMap box, which is what the hit test searches
|
|
238
|
-
function renderLabels(labelsAlignRight: boolean) {
|
|
239
|
-
const model = stateModelFactory().create({
|
|
240
|
-
type: 'MsaView',
|
|
241
|
-
data: { msa: '>a\nA\n>b\nA', tree: '(a,b);' },
|
|
242
|
-
})
|
|
243
|
-
model.setWidth(1000)
|
|
244
|
-
model.setDrawTree(false)
|
|
245
|
-
model.setLabelsAlignRight(labelsAlignRight)
|
|
246
|
-
|
|
247
|
-
let tx = 0
|
|
248
|
-
const drawn: number[] = []
|
|
249
|
-
const ctx = {
|
|
250
|
-
font: '12px sans-serif',
|
|
251
|
-
measureText: (text: string) => ({ width: text.length * 6 }),
|
|
252
|
-
beginPath() {},
|
|
253
|
-
stroke() {},
|
|
254
|
-
setLineDash() {},
|
|
255
|
-
resetTransform() {
|
|
256
|
-
tx = 0
|
|
257
|
-
},
|
|
258
|
-
scale() {},
|
|
259
|
-
translate(x: number) {
|
|
260
|
-
tx += x
|
|
261
|
-
},
|
|
262
|
-
moveTo() {},
|
|
263
|
-
lineTo() {},
|
|
264
|
-
fillText(_text: string, x: number) {
|
|
265
|
-
drawn.push(x + tx)
|
|
266
|
-
},
|
|
267
|
-
} as unknown as RenderCtx
|
|
268
|
-
|
|
269
|
-
const clickMap = new ClickMapIndex()
|
|
270
|
-
renderTreeCanvas({
|
|
271
|
-
model,
|
|
272
|
-
ctx,
|
|
273
|
-
clickMap,
|
|
274
|
-
offsetY: 0,
|
|
275
|
-
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
276
|
-
})
|
|
277
|
-
return { drawn, hits: clickMap.search(bounds).filter(h => !h.branch) }
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
it('boxes a right-aligned label at the edge it is drawn against', () => {
|
|
281
|
-
const { drawn, hits } = renderLabels(true)
|
|
282
|
-
expect(hits.length).toBe(drawn.length)
|
|
283
|
-
// textAlign is 'right' here, so the drawn x is the label's right edge
|
|
284
|
-
for (const hit of hits) {
|
|
285
|
-
expect(drawn).toContain(hit.maxX)
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
it('boxes a left-aligned label at the edge it is drawn from', () => {
|
|
290
|
-
const { drawn, hits } = renderLabels(false)
|
|
291
|
-
expect(hits.length).toBe(drawn.length)
|
|
292
|
-
for (const hit of hits) {
|
|
293
|
-
expect(drawn).toContain(hit.minX)
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
})
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { getSnapshot } from '@jbrowse/mobx-state-tree'
|
|
2
|
-
import { expect, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import stateModelFactory from './model.ts'
|
|
5
|
-
|
|
6
|
-
const MsaView = stateModelFactory()
|
|
7
|
-
|
|
8
|
-
test('an empty model is not initialized', () => {
|
|
9
|
-
expect(MsaView.create({ type: 'MsaView' }).dataInitialized).toBe(false)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('inline data initializes the view', () => {
|
|
13
|
-
const model = MsaView.create({
|
|
14
|
-
type: 'MsaView',
|
|
15
|
-
data: { msa: '>a\nACGT\n>b\nACGA\n', tree: '' },
|
|
16
|
-
})
|
|
17
|
-
expect(model.dataInitialized).toBe(true)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
// DataModel's postProcessSnapshot drops a document over 50kb rather than
|
|
21
|
-
// inlining it into a session or a shared URL, so it comes back `undefined`
|
|
22
|
-
// rather than ''. That has to read as "no data" -- otherwise the restored
|
|
23
|
-
// session renders an empty alignment instead of the import form
|
|
24
|
-
test('a dropped oversize inline msa restores as uninitialized', () => {
|
|
25
|
-
const msa = `>a\n${'A'.repeat(60_000)}\n>b\n${'C'.repeat(60_000)}\n`
|
|
26
|
-
const model = MsaView.create({ type: 'MsaView', data: { msa, tree: '' } })
|
|
27
|
-
expect(model.dataInitialized).toBe(true)
|
|
28
|
-
|
|
29
|
-
const restored = MsaView.create(getSnapshot(model))
|
|
30
|
-
expect(restored.data.msa).toBeUndefined()
|
|
31
|
-
expect(restored.MSA).toBe(null)
|
|
32
|
-
expect(restored.dataInitialized).toBe(false)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('an error keeps the view uninitialized', () => {
|
|
36
|
-
const model = MsaView.create({
|
|
37
|
-
type: 'MsaView',
|
|
38
|
-
data: { msa: '>a\nACGT\n', tree: '' },
|
|
39
|
-
})
|
|
40
|
-
model.setError(new Error('boom'))
|
|
41
|
-
expect(model.dataInitialized).toBe(false)
|
|
42
|
-
})
|