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.
Files changed (80) hide show
  1. package/bundle/index.js +104 -104
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/MSAViewer.d.ts +10 -1
  4. package/dist/components/MSAViewer.js +3 -1
  5. package/dist/components/MSAViewer.js.map +1 -1
  6. package/dist/components/minimap/Minimap.js +1 -1
  7. package/dist/components/minimap/Minimap.js.map +1 -1
  8. package/dist/components/minimap/MinimapSVG.js +2 -2
  9. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  10. package/dist/components/msa/MSAMouseoverCanvas.js +4 -2
  11. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  12. package/dist/components/msa/msaRaster.d.ts +5 -4
  13. package/dist/components/msa/msaRaster.js +10 -6
  14. package/dist/components/msa/msaRaster.js.map +1 -1
  15. package/dist/components/msa/renderHighlights.d.ts +31 -0
  16. package/dist/components/msa/renderHighlights.js +49 -0
  17. package/dist/components/msa/renderHighlights.js.map +1 -0
  18. package/dist/components/msa/renderMSAMouseover.d.ts +3 -1
  19. package/dist/components/msa/renderMSAMouseover.js +11 -1
  20. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  21. package/dist/components/tracks/drawTracks.js +4 -0
  22. package/dist/components/tracks/drawTracks.js.map +1 -1
  23. package/dist/components/tree/renderTreeCanvas.js +31 -0
  24. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  25. package/dist/exportFileName.d.ts +6 -0
  26. package/dist/exportFileName.js +15 -0
  27. package/dist/exportFileName.js.map +1 -0
  28. package/dist/headlessRenderEnv.d.ts +4 -1
  29. package/dist/headlessRenderEnv.js +6 -3
  30. package/dist/headlessRenderEnv.js.map +1 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/model.d.ts +172 -9
  34. package/dist/model.js +134 -3
  35. package/dist/model.js.map +1 -1
  36. package/dist/renderToSvg.js +30 -6
  37. package/dist/renderToSvg.js.map +1 -1
  38. package/dist/svgTestUtil.d.ts +1662 -0
  39. package/dist/svgTestUtil.js +28 -0
  40. package/dist/svgTestUtil.js.map +1 -0
  41. package/dist/types.d.ts +33 -0
  42. package/dist/umd.d.ts +1 -0
  43. package/dist/umd.js +3 -0
  44. package/dist/umd.js.map +1 -1
  45. package/dist/util.d.ts +1 -0
  46. package/dist/util.js +7 -0
  47. package/dist/util.js.map +1 -1
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +3 -3
  51. package/src/columnTracks.test.ts +97 -0
  52. package/src/components/MSAViewer.tsx +13 -0
  53. package/src/components/minimap/Minimap.tsx +1 -1
  54. package/src/components/minimap/MinimapSVG.tsx +2 -2
  55. package/src/components/msa/MSAMouseoverCanvas.tsx +4 -2
  56. package/src/components/msa/msaRaster.ts +10 -6
  57. package/src/components/msa/renderHighlights.ts +86 -0
  58. package/src/components/msa/renderMSAMouseover.ts +14 -0
  59. package/src/components/tracks/drawTracks.ts +4 -0
  60. package/src/components/tree/renderTreeCanvas.ts +56 -0
  61. package/src/exportFileName.test.ts +40 -0
  62. package/src/exportFileName.ts +20 -0
  63. package/src/headlessRenderEnv.ts +20 -11
  64. package/src/highlights.test.ts +70 -0
  65. package/src/impgRender.test.tsx +2 -4
  66. package/src/index.ts +4 -0
  67. package/src/model.ts +162 -10
  68. package/src/renderColumnTracksSvg.test.tsx +108 -0
  69. package/src/renderDomainsSvg.test.tsx +25 -10
  70. package/src/renderHighlightsSvg.test.tsx +85 -0
  71. package/src/renderMinimapSvg.test.tsx +6 -13
  72. package/src/renderRasterSvg.test.tsx +21 -93
  73. package/src/renderSequenceLogoSvg.test.tsx +4 -17
  74. package/src/renderToSvg.test.tsx +7 -21
  75. package/src/renderToSvg.tsx +32 -6
  76. package/src/svgTestUtil.ts +40 -0
  77. package/src/types.ts +39 -0
  78. package/src/umd.ts +3 -0
  79. package/src/util.ts +8 -0
  80. 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
- enableStaticRendering(true)
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
- 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
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
- 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
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
- 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({
40
+ return createTestModel({
108
41
  id: 'raster',
109
- type: 'MsaView',
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
- enableStaticRendering(true)
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
- const model = MSAModelF().create({
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>) {
@@ -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
- enableStaticRendering(true)
15
- installHeadlessRenderEnv()
16
+ installSvgTestEnv()
16
17
  })
17
18
 
18
19
  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
20
+ return createTestModel({ data: { msa: syntheticProteinMsa(rows, cols) } })
35
21
  }
36
22
 
37
23
  const rootRect = (svg: string) => /<rect[^>]*height="100%"[^>]*>/.exec(svg)?.[0]
@@ -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
- await when(() => model.dataInitialized)
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, height, trackHeight, includeMinimap, legendWidth } = layout
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 = LEGEND_PAD * 2 + visibleDomainTypes.length * LEGEND_ROW_H
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="#ccc"
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.leaves.length) - yStart
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.2.2'
1
+ export const version = '6.3.0'