react-msaview 5.1.0 → 5.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.
Files changed (100) hide show
  1. package/bundle/index.js +35 -135
  2. package/bundle/index.js.map +1 -1
  3. package/dist/calculateBlocks.d.ts +4 -10
  4. package/dist/calculateBlocks.js +12 -20
  5. package/dist/calculateBlocks.js.map +1 -1
  6. package/dist/components/ConservationTrack.js +18 -24
  7. package/dist/components/ConservationTrack.js.map +1 -1
  8. package/dist/components/TextTrack.js +24 -34
  9. package/dist/components/TextTrack.js.map +1 -1
  10. package/dist/components/dialogs/ExportSVGDialog.js +20 -20
  11. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  12. package/dist/components/dialogs/InterProScanDialog.js +6 -1
  13. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  14. package/dist/components/header/FileMenu.js +2 -1
  15. package/dist/components/header/FileMenu.js.map +1 -1
  16. package/dist/components/header/Header.js +1 -1
  17. package/dist/components/header/settingsMenuItems.js +24 -8
  18. package/dist/components/header/settingsMenuItems.js.map +1 -1
  19. package/dist/components/minimap/Minimap.js +1 -6
  20. package/dist/components/minimap/Minimap.js.map +1 -1
  21. package/dist/components/minimap/MinimapSVG.js +4 -8
  22. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  23. package/dist/components/msa/getVisibleLeaves.d.ts +6 -0
  24. package/dist/components/msa/getVisibleLeaves.js +9 -0
  25. package/dist/components/msa/getVisibleLeaves.js.map +1 -0
  26. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
  27. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +5 -6
  28. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  29. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  30. package/dist/components/msa/renderMSABlock.js +98 -84
  31. package/dist/components/msa/renderMSABlock.js.map +1 -1
  32. package/dist/components/msa/renderMSAMouseover.js +3 -3
  33. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  34. package/dist/components/msa/visibleColRange.d.ts +8 -0
  35. package/dist/components/msa/visibleColRange.js +10 -0
  36. package/dist/components/msa/visibleColRange.js.map +1 -0
  37. package/dist/components/renderCtx.d.ts +7 -0
  38. package/dist/components/renderCtx.js +2 -0
  39. package/dist/components/renderCtx.js.map +1 -0
  40. package/dist/components/tracks/renderTracksSvg.d.ts +6 -5
  41. package/dist/components/tracks/renderTracksSvg.js +11 -4
  42. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  43. package/dist/components/tree/TreeCanvas.js.map +1 -1
  44. package/dist/components/tree/TreeCanvasBlock.js +2 -10
  45. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  46. package/dist/components/tree/renderTreeCanvas.d.ts +5 -4
  47. package/dist/components/tree/renderTreeCanvas.js +16 -24
  48. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  49. package/dist/components/util.d.ts +0 -14
  50. package/dist/components/util.js +0 -58
  51. package/dist/components/util.js.map +1 -1
  52. package/dist/hierarchy.d.ts +2 -0
  53. package/dist/hierarchy.js +19 -0
  54. package/dist/hierarchy.js.map +1 -1
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/model.d.ts +26 -30
  58. package/dist/model.js +74 -82
  59. package/dist/model.js.map +1 -1
  60. package/dist/renderToSvg.d.ts +1 -1
  61. package/dist/renderToSvg.js +20 -26
  62. package/dist/renderToSvg.js.map +1 -1
  63. package/dist/rowCoordinateCalculations.d.ts +0 -9
  64. package/dist/rowCoordinateCalculations.js +0 -20
  65. package/dist/rowCoordinateCalculations.js.map +1 -1
  66. package/dist/util.d.ts +1 -0
  67. package/dist/util.js +23 -1
  68. package/dist/util.js.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/dist/version.js +1 -1
  71. package/package.json +5 -5
  72. package/src/calculateBlocks.ts +18 -51
  73. package/src/components/ConservationTrack.tsx +18 -27
  74. package/src/components/TextTrack.tsx +30 -38
  75. package/src/components/dialogs/ExportSVGDialog.tsx +27 -17
  76. package/src/components/dialogs/InterProScanDialog.tsx +6 -1
  77. package/src/components/header/FileMenu.tsx +2 -1
  78. package/src/components/header/Header.tsx +1 -1
  79. package/src/components/header/settingsMenuItems.ts +24 -8
  80. package/src/components/minimap/Minimap.tsx +1 -6
  81. package/src/components/minimap/MinimapSVG.tsx +4 -8
  82. package/src/components/msa/getVisibleLeaves.ts +21 -0
  83. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +9 -9
  84. package/src/components/msa/renderMSABlock.ts +119 -118
  85. package/src/components/msa/renderMSAMouseover.ts +2 -3
  86. package/src/components/msa/visibleColRange.ts +17 -0
  87. package/src/components/renderCtx.ts +29 -0
  88. package/src/components/tracks/renderTracksSvg.ts +18 -9
  89. package/src/components/tree/TreeCanvas.tsx +3 -1
  90. package/src/components/tree/TreeCanvasBlock.tsx +2 -12
  91. package/src/components/tree/renderTreeCanvas.ts +25 -28
  92. package/src/components/util.ts +0 -81
  93. package/src/hierarchy.ts +27 -0
  94. package/src/index.ts +7 -0
  95. package/src/model.ts +81 -98
  96. package/src/renderToSvg.tsx +23 -31
  97. package/src/rowCoordinateCalculations.test.ts +0 -51
  98. package/src/rowCoordinateCalculations.ts +0 -23
  99. package/src/util.ts +25 -1
  100. package/src/version.ts +1 -1
@@ -1,7 +1,12 @@
1
- import { calcDepthToLeaf, descendants, links } from '../../hierarchy.ts'
1
+ import {
2
+ calcDepthToLeaf,
3
+ forEachDescendant,
4
+ forEachLink,
5
+ } from '../../hierarchy.ts'
2
6
 
3
7
  import type { HierarchyNode } from '../../hierarchy.ts'
4
8
  import type { MsaViewModel } from '../../model.ts'
9
+ import type { RenderCtx } from '../renderCtx.ts'
5
10
  import type { Theme } from '@mui/material'
6
11
 
7
12
  export const padding = 600
@@ -59,7 +64,7 @@ export function renderTree({
59
64
  blockSizeYOverride,
60
65
  }: {
61
66
  offsetY: number
62
- ctx: CanvasRenderingContext2D
67
+ ctx: RenderCtx
63
68
  model: MsaViewModel
64
69
  theme: Theme
65
70
  maxBranchLen: number
@@ -67,16 +72,15 @@ export function renderTree({
67
72
  blockSizeYOverride?: number
68
73
  }) {
69
74
  const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
70
- const by = blockSizeYOverride || blockSize
75
+ const by = blockSizeYOverride ?? blockSize
71
76
  ctx.strokeStyle = theme.palette.text.primary
72
- for (const link of links(hierarchy)) {
73
- const { source, target } = link
77
+ forEachLink(hierarchy, (source, target) => {
74
78
  const sy = source.x!
75
79
  const ty = target.x!
76
80
  const tx = getNodeX(target, showBranchLen, maxBranchLen, maxDepthToLeaf)
77
81
  const sx = getNodeX(source, showBranchLen, maxBranchLen, maxDepthToLeaf)
78
82
  if (tx === undefined || sx === undefined) {
79
- continue
83
+ return
80
84
  }
81
85
 
82
86
  const y1 = Math.min(sy, ty)
@@ -91,7 +95,7 @@ export function renderTree({
91
95
  ctx.lineTo(tx, ty)
92
96
  ctx.stroke()
93
97
  }
94
- }
98
+ })
95
99
  }
96
100
 
97
101
  export function renderNodeBubbles({
@@ -103,7 +107,7 @@ export function renderNodeBubbles({
103
107
  maxDepthToLeaf,
104
108
  blockSizeYOverride,
105
109
  }: {
106
- ctx: CanvasRenderingContext2D
110
+ ctx: RenderCtx
107
111
  clickMap?: ClickMapIndex
108
112
  offsetY: number
109
113
  model: MsaViewModel
@@ -118,11 +122,11 @@ export function renderNodeBubbles({
118
122
  blockSize,
119
123
  marginLeft: ml,
120
124
  } = model
121
- const by = blockSizeYOverride || blockSize
122
- for (const node of descendants(hierarchy)) {
125
+ const by = blockSizeYOverride ?? blockSize
126
+ forEachDescendant(hierarchy, node => {
123
127
  const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
124
128
  if (x === undefined) {
125
- continue
129
+ return
126
130
  }
127
131
  const { data } = node
128
132
  const y = node.x!
@@ -149,7 +153,7 @@ export function renderNodeBubbles({
149
153
  name,
150
154
  })
151
155
  }
152
- }
156
+ })
153
157
  }
154
158
 
155
159
  export function renderTreeLabels({
@@ -164,7 +168,7 @@ export function renderTreeLabels({
164
168
  }: {
165
169
  model: MsaViewModel
166
170
  offsetY: number
167
- ctx: CanvasRenderingContext2D
171
+ ctx: RenderCtx
168
172
  clickMap?: ClickMapIndex
169
173
  theme: Theme
170
174
  maxBranchLen: number
@@ -183,8 +187,9 @@ export function renderTreeLabels({
183
187
  marginLeft,
184
188
  leaves,
185
189
  noTree,
190
+ labelWidthMap,
186
191
  } = model
187
- const by = blockSizeYOverride || blockSize
192
+ const by = blockSizeYOverride ?? blockSize
188
193
  const emHeight = ctx.measureText('M').width
189
194
  if (labelsAlignRight) {
190
195
  ctx.textAlign = 'right'
@@ -204,10 +209,11 @@ export function renderTreeLabels({
204
209
  const yp = y + fontSize / 4
205
210
  let xp = 0
206
211
  if (!noTree) {
207
- xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) || 0
212
+ xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) ?? 0
208
213
  }
209
214
 
210
- const { width } = ctx.measureText(displayName)
215
+ const width =
216
+ labelWidthMap.get(name) ?? ctx.measureText(displayName).width
211
217
 
212
218
  ctx.fillStyle = theme.palette.text.primary
213
219
  if (labelsAlignRight) {
@@ -255,7 +261,7 @@ export function renderTreeCanvas({
255
261
  }: {
256
262
  model: MsaViewModel
257
263
  offsetY: number
258
- ctx: CanvasRenderingContext2D
264
+ ctx: RenderCtx
259
265
  clickMap?: ClickMapIndex
260
266
  theme: Theme
261
267
  highResScaleFactorOverride?: number
@@ -272,22 +278,13 @@ export function renderTreeCanvas({
272
278
  fontSize,
273
279
  showTreeText,
274
280
  marginLeft,
275
- nref,
276
281
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
277
- rowHeight: _rowHeight, // this is needed for redrawing after zoom change
282
+ rowHeight: _rowHeight, // read so the draw autorun re-fires after a zoom change
278
283
  } = model
279
284
 
280
285
  ctx.resetTransform()
281
286
 
282
- // this is a bogus use of nref, it is never less than 0. we just are using
283
- // it in this statement because otherwise it would be an unused variable,
284
- // we just need to use nref to indicate a redraw in an autorun when canvas
285
- // ref is updated and in order to convince bundlers like not to delete
286
- // unused usage with propertyReadSideEffects
287
- const k =
288
- nref < 0
289
- ? Number.NEGATIVE_INFINITY
290
- : highResScaleFactorOverride || highResScaleFactor
287
+ const k = highResScaleFactorOverride ?? highResScaleFactor
291
288
  ctx.scale(k, k)
292
289
  ctx.translate(marginLeft, -offsetY)
293
290
 
@@ -1,84 +1,3 @@
1
- /**
2
- * Given a scale ( bp/px ) and minimum distances (px) between major and minor
3
- * gridlines, return an object like `{ majorPitch: bp, minorPitch: bp }` giving
4
- * the gridline pitches to use.
5
- */
6
- export function chooseGridPitch(
7
- scale: number,
8
- minMajorPitchPx: number,
9
- minMinorPitchPx: number,
10
- ) {
11
- scale = Math.abs(scale)
12
- const minMajorPitchBp = minMajorPitchPx * scale
13
- const majorMagnitude = Number.parseInt(
14
- minMajorPitchBp.toExponential().split(/e/i)[1]!,
15
- 10,
16
- )
17
-
18
- let majorPitch = 10 ** majorMagnitude
19
- while (majorPitch < minMajorPitchBp) {
20
- majorPitch *= 2
21
- if (majorPitch >= minMajorPitchBp) {
22
- break
23
- }
24
- majorPitch *= 2.5
25
- }
26
-
27
- majorPitch = Math.max(majorPitch, 5)
28
-
29
- const majorPitchPx = majorPitch / scale
30
-
31
- let minorPitch = 0
32
- if (!(majorPitch % 10) && majorPitchPx / 10 >= minMinorPitchPx) {
33
- minorPitch = majorPitch / 10
34
- } else if (!(majorPitch % 5) && majorPitchPx / 5 >= minMinorPitchPx) {
35
- minorPitch = majorPitch / 5
36
- } else if (!(majorPitch % 2) && majorPitchPx / 2 >= minMinorPitchPx) {
37
- minorPitch = majorPitch / 2
38
- }
39
-
40
- return { majorPitch, minorPitch }
41
- }
42
- export function makeTicks(
43
- start: number,
44
- end: number,
45
- bpPerPx: number,
46
- emitMajor = true,
47
- emitMinor = true,
48
- ) {
49
- const gridPitch = chooseGridPitch(bpPerPx, 60, 15)
50
-
51
- let minBase = start
52
- let maxBase = end
53
-
54
- if (bpPerPx < 0) {
55
- ;[minBase, maxBase] = [maxBase, minBase]
56
- }
57
-
58
- // add 20px additional on the right and left to allow us to draw the ends of
59
- // labels that lie a little outside our region
60
- minBase -= Math.abs(20 * bpPerPx)
61
- maxBase += Math.abs(20 * bpPerPx) + 1
62
-
63
- const iterPitch = gridPitch.minorPitch || gridPitch.majorPitch
64
- let index = 0
65
- const ticks = []
66
- for (
67
- let base = Math.ceil(minBase / iterPitch) * iterPitch;
68
- base < maxBase;
69
- base += iterPitch
70
- ) {
71
- if (emitMinor && base % (gridPitch.majorPitch * 2)) {
72
- ticks.push({ type: 'minor', base: base - 1, index })
73
- index += 1
74
- } else if (emitMajor && !(base % (gridPitch.majorPitch * 2))) {
75
- ticks.push({ type: 'major', base: base - 1, index })
76
- index += 1
77
- }
78
- }
79
- return ticks
80
- }
81
-
82
1
  export function mathPower(num: number): string {
83
2
  if (num < 999) {
84
3
  return String(num)
package/src/hierarchy.ts CHANGED
@@ -157,6 +157,33 @@ export function links<T>(node: HierarchyNode<T>): HierarchyLink<T>[] {
157
157
  return result
158
158
  }
159
159
 
160
+ export function forEachLink<T>(
161
+ node: HierarchyNode<T>,
162
+ cb: (source: HierarchyNode<T>, target: HierarchyNode<T>) => void,
163
+ ) {
164
+ function visit(n: HierarchyNode<T>) {
165
+ if (n.children) {
166
+ for (const child of n.children) {
167
+ cb(n, child)
168
+ visit(child)
169
+ }
170
+ }
171
+ }
172
+ visit(node)
173
+ }
174
+
175
+ export function forEachDescendant<T>(
176
+ node: HierarchyNode<T>,
177
+ cb: (n: HierarchyNode<T>) => void,
178
+ ) {
179
+ cb(node)
180
+ if (node.children) {
181
+ for (const child of node.children) {
182
+ forEachDescendant(child, cb)
183
+ }
184
+ }
185
+ }
186
+
160
187
  export function clusterLayout<T>(
161
188
  root: HierarchyNode<T>,
162
189
  sizeX: number,
package/src/index.ts CHANGED
@@ -1,3 +1,10 @@
1
+ // PUBLIC API — consumed by external JBrowse 2 plugins. Changing or removing any
2
+ // export here is a breaking change for downstream repos:
3
+ // - jbrowse-plugin-msaview (wraps MSAModelF + MSAView into a JBrowse view)
4
+ // - jbrowse-plugin-protein3d (drives MSA<->structure hover/highlight sync)
5
+ // Those plugins also reach into the MsaViewModel instance at runtime; the model
6
+ // members they rely on (e.g. mouseCol/setMousePos, setHighlightedColumns,
7
+ // seqPosToVisibleCol/visibleColToSeqPos) are flagged inline in model.ts.
1
8
  export { default as MSAView } from './components/Loading.tsx'
2
9
  export { default as MSAViewer } from './components/MSAViewer.tsx'
3
10
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
package/src/model.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  parseNewick,
20
20
  } from 'msa-parsers'
21
21
 
22
- import { blocksX, blocksY } from './calculateBlocks.ts'
22
+ import { calculateBlocks } from './calculateBlocks.ts'
23
23
  import { clustalXColumnColors } from './clustalX.ts'
24
24
  import colorSchemes from './colorSchemes.ts'
25
25
  import ConservationTrack from './components/ConservationTrack.tsx'
@@ -317,13 +317,6 @@ function stateModelFactory() {
317
317
  */
318
318
  highlightedColumns: undefined as number[] | undefined,
319
319
 
320
- /**
321
- * #volatile
322
- * a dummy variable that is incremented when ref changes so autorun for
323
- * drawing canvas commands will run
324
- */
325
- nref: 0,
326
-
327
320
  /**
328
321
  * #volatile
329
322
  */
@@ -408,6 +401,10 @@ function stateModelFactory() {
408
401
  /**
409
402
  * #action
410
403
  * set mouse position (row, column) in the MSA
404
+ *
405
+ * CROSS-REPO CONTRACT: jbrowse-plugin-protein3d calls this (and reads the
406
+ * `mouseCol` volatile) to sync MSA<->3D-structure hover. Keep the name and
407
+ * signature stable; see that repo's ProteinToMsaHoverSync.tsx.
411
408
  */
412
409
  setMousePos(col?: number, row?: number) {
413
410
  self.mouseCol = col
@@ -440,6 +437,10 @@ function stateModelFactory() {
440
437
  /**
441
438
  * #action
442
439
  * set highlighted columns
440
+ *
441
+ * CROSS-REPO CONTRACT: called by jbrowse-plugin-msaview
442
+ * (afterCreateAutoruns.ts) to highlight alignment columns. It has no
443
+ * in-repo caller, so do not flag it as dead code — it is public API.
443
444
  */
444
445
  setHighlightedColumns(columns?: number[]) {
445
446
  self.highlightedColumns = columns
@@ -625,14 +626,14 @@ function stateModelFactory() {
625
626
  * #getter
626
627
  */
627
628
  get header() {
628
- return (this.MSA?.getHeader() || {}) as Record<string, unknown>
629
+ return (this.MSA?.getHeader() ?? {}) as Record<string, unknown>
629
630
  },
630
631
 
631
632
  /**
632
633
  * #getter
633
634
  */
634
635
  get alignmentNames() {
635
- return this.MSA?.alignmentNames || []
636
+ return this.MSA?.alignmentNames ?? []
636
637
  },
637
638
  /**
638
639
  * #getter
@@ -665,7 +666,7 @@ function stateModelFactory() {
665
666
  * #getter
666
667
  */
667
668
  get numColumns() {
668
- return (this.MSA?.getWidth() || 0) - this.blanks.length
669
+ return (this.MSA?.getWidth() ?? 0) - this.blanks.length
669
670
  },
670
671
 
671
672
  /**
@@ -693,15 +694,32 @@ function stateModelFactory() {
693
694
  /**
694
695
  * #getter
695
696
  */
697
+ /**
698
+ * #getter
699
+ * Returns the list of row (sequence) names in display order.
700
+ * Part of the public API used by downstream consumers (e.g. jbrowse plugins).
701
+ */
696
702
  get rowNames(): string[] {
697
703
  return this.leaves.map(n => n.data.name)
698
704
  },
705
+ /**
706
+ * #getter
707
+ */
708
+ get rowNamesSet() {
709
+ const map = new Map<string, number>()
710
+ this.leaves.forEach((leaf, index) => {
711
+ map.set(leaf.data.name, index)
712
+ })
713
+ return map
714
+ },
699
715
  /**
700
716
  * #getter
701
717
  */
702
718
  get mouseOverRowName() {
703
719
  const { mouseRow } = self
704
- return mouseRow === undefined ? undefined : this.rowNames[mouseRow]
720
+ return mouseRow === undefined
721
+ ? undefined
722
+ : this.leaves[mouseRow]?.data.name
705
723
  },
706
724
  /**
707
725
  * #getter
@@ -712,7 +730,7 @@ function stateModelFactory() {
712
730
  if (mouseCol === undefined || mouseRow === undefined) {
713
731
  return undefined
714
732
  }
715
- const rowName = this.rowNames[mouseRow]
733
+ const rowName = this.leaves[mouseRow]?.data.name
716
734
  if (!rowName) {
717
735
  return undefined
718
736
  }
@@ -898,7 +916,7 @@ function stateModelFactory() {
898
916
  * #getter
899
917
  */
900
918
  get columns() {
901
- return Object.fromEntries(
919
+ return new Map(
902
920
  this.rows.map(
903
921
  (row, index) => [row[0], this.columns2d[index]!] as const,
904
922
  ),
@@ -911,7 +929,12 @@ function stateModelFactory() {
911
929
  const { hideGapsEffective } = self
912
930
  return this.rows
913
931
  .map(r => r[1])
914
- .map(str => (hideGapsEffective ? skipBlanks(this.blanks, str) : str))
932
+ .map(str =>
933
+ (hideGapsEffective
934
+ ? skipBlanks(this.blanks, str)
935
+ : str
936
+ ).toUpperCase(),
937
+ )
915
938
  },
916
939
  /**
917
940
  * #getter
@@ -939,7 +962,13 @@ function stateModelFactory() {
939
962
  * #getter
940
963
  */
941
964
  get colStatsSums() {
942
- return this.colStats.map(val => sum(Object.values(val)))
965
+ return this.colStats.map(col => {
966
+ let s = 0
967
+ for (const k in col) {
968
+ s += col[k]!
969
+ }
970
+ return s
971
+ })
943
972
  },
944
973
 
945
974
  /**
@@ -951,9 +980,8 @@ function stateModelFactory() {
951
980
  const letters = new Set<string>()
952
981
  for (const stats of this.colStats) {
953
982
  for (const letter of Object.keys(stats)) {
954
- const upper = letter.toUpperCase()
955
- if (upper !== '-' && upper !== '.') {
956
- letters.add(upper)
983
+ if (letter !== '-' && letter !== '.') {
984
+ letters.add(letter)
957
985
  }
958
986
  }
959
987
  }
@@ -1029,24 +1057,18 @@ function stateModelFactory() {
1029
1057
  return 0
1030
1058
  }
1031
1059
 
1032
- const gapCount = (stats['-'] || 0) + (stats['.'] || 0)
1060
+ const gapCount = (stats['-'] ?? 0) + (stats['.'] ?? 0)
1033
1061
  const nonGapTotal = total - gapCount
1034
1062
  if (nonGapTotal === 0) {
1035
1063
  return 0
1036
1064
  }
1037
1065
 
1038
- const merged: Record<string, number> = {}
1066
+ let entropy = 0
1039
1067
  for (const letter of Object.keys(stats)) {
1040
1068
  if (letter === '-' || letter === '.') {
1041
1069
  continue
1042
1070
  }
1043
- const upper = letter.toUpperCase()
1044
- merged[upper] = (merged[upper] || 0) + stats[letter]!
1045
- }
1046
-
1047
- let entropy = 0
1048
- for (const key of Object.keys(merged)) {
1049
- const freq = merged[key]! / nonGapTotal
1071
+ const freq = stats[letter]! / nonGapTotal
1050
1072
  if (freq > 0) {
1051
1073
  entropy -= freq * Math.log2(freq)
1052
1074
  }
@@ -1134,22 +1156,22 @@ function stateModelFactory() {
1134
1156
  * #getter
1135
1157
  */
1136
1158
  get blocksX() {
1137
- return blocksX({
1138
- viewportWidth: self.msaAreaWidth,
1139
- viewportX: -self.scrollX,
1159
+ return calculateBlocks({
1160
+ viewportSize: self.msaAreaWidth,
1161
+ viewportPos: -self.scrollX,
1140
1162
  blockSize: self.blockSize,
1141
- mapWidth: self.totalWidth,
1163
+ mapSize: self.totalWidth,
1142
1164
  })
1143
1165
  },
1144
1166
  /**
1145
1167
  * #getter
1146
1168
  */
1147
1169
  get blocksY() {
1148
- return blocksY({
1149
- viewportHeight: self.height,
1150
- viewportY: -self.scrollY,
1170
+ return calculateBlocks({
1171
+ viewportSize: self.height,
1172
+ viewportPos: -self.scrollY,
1151
1173
  blockSize: self.blockSize,
1152
- mapHeight: self.totalHeight,
1174
+ mapSize: self.totalHeight,
1153
1175
  })
1154
1176
  },
1155
1177
  }))
@@ -1272,7 +1294,7 @@ function stateModelFactory() {
1272
1294
  self.scrollY = clamp(self.scrollY + deltaY, -self.totalHeight + 10, 0)
1273
1295
  },
1274
1296
 
1275
- setInterProAnnotations(data: Record<string, InterProScanResults>) {
1297
+ setInterProAnnotations(data?: Record<string, InterProScanResults>) {
1276
1298
  self.interProAnnotations = data
1277
1299
  },
1278
1300
 
@@ -1318,24 +1340,25 @@ function stateModelFactory() {
1318
1340
  /**
1319
1341
  * #getter
1320
1342
  */
1321
- get labelsWidth() {
1343
+ get labelWidthMap() {
1322
1344
  const { rowHeight, leaves, treeMetadata, fontSize } = self
1323
1345
  if (rowHeight <= 5) {
1324
- return 0
1346
+ return new Map<string, number>()
1325
1347
  }
1326
- return leaves.reduce(
1327
- (max, node) =>
1328
- Math.max(
1329
- max,
1330
- measureTextCanvas(
1331
- treeMetadata[node.data.name]?.genome || node.data.name,
1332
- fontSize,
1333
- ),
1334
- ),
1335
- 0,
1348
+ return new Map(
1349
+ leaves.map(node => {
1350
+ const { name } = node.data
1351
+ const displayName = treeMetadata[name]?.genome || name
1352
+ return [name, measureTextCanvas(displayName, fontSize)] as const
1353
+ }),
1336
1354
  )
1337
1355
  },
1338
1356
 
1357
+ get labelsWidth() {
1358
+ const widths = this.labelWidthMap
1359
+ return widths.size === 0 ? 0 : Math.max(...widths.values())
1360
+ },
1361
+
1339
1362
  /**
1340
1363
  * #getter
1341
1364
  */
@@ -1402,13 +1425,6 @@ function stateModelFactory() {
1402
1425
  return self.msaAreaWidth < self.totalWidth
1403
1426
  },
1404
1427
 
1405
- /**
1406
- * #getter
1407
- */
1408
- get rowNamesSet() {
1409
- return new Map(self.rowNames.map((r, idx) => [r, idx]))
1410
- },
1411
-
1412
1428
  /**
1413
1429
  * #method
1414
1430
  * Return a row-specific letter at a visible column, or undefined if gap.
@@ -1427,6 +1443,11 @@ function stateModelFactory() {
1427
1443
  * Convert a visible column to a row-specific sequence position (0-based).
1428
1444
  * Returns undefined if the position is a gap in the sequence.
1429
1445
  *
1446
+ * CROSS-REPO CONTRACT: this and the sibling coordinate converters
1447
+ * (seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) are used
1448
+ * by jbrowse-plugin-protein3d to translate between alignment columns and
1449
+ * structure/sequence residue positions across gaps. Keep them stable.
1450
+ *
1430
1451
  * @param rowName - The name of the row
1431
1452
  * @param visibleCol - The visible column index
1432
1453
  * @returns The sequence position (0-based), or undefined if it's a gap
@@ -1480,14 +1501,8 @@ function stateModelFactory() {
1480
1501
  * @returns The global column index in the full MSA
1481
1502
  */
1482
1503
  seqPosToGlobalCol(rowName: string, seqPos: number) {
1483
- const { rows } = self
1484
- const index = this.rowNamesSet.get(rowName)
1485
- return index !== undefined && rows[index]
1486
- ? seqPosToGlobalCol({
1487
- row: rows[index][1],
1488
- seqPos,
1489
- })
1490
- : 0
1504
+ const seq = self.rowMap.get(rowName)
1505
+ return seq ? seqPosToGlobalCol({ row: seq, seqPos }) : 0
1491
1506
  },
1492
1507
 
1493
1508
  /**
@@ -1506,30 +1521,6 @@ function stateModelFactory() {
1506
1521
  }))
1507
1522
 
1508
1523
  .views(self => ({
1509
- /**
1510
- * #getter
1511
- * Returns information about the currently hovered cell
1512
- */
1513
- get hoveredCell() {
1514
- const { mouseCol, mouseRow } = self
1515
- if (mouseCol === undefined || mouseRow === undefined) {
1516
- return undefined
1517
- }
1518
- const rowName = self.rowNames[mouseRow]
1519
- if (!rowName) {
1520
- return undefined
1521
- }
1522
- const seq = self.columns[rowName]
1523
- const base = seq?.[mouseCol]
1524
- const seqPos = self.visibleColToSeqPosOneBased(rowName, mouseCol)
1525
- return {
1526
- rowName,
1527
- col: mouseCol,
1528
- base,
1529
- seqPos,
1530
- }
1531
- },
1532
-
1533
1524
  /**
1534
1525
  * #getter
1535
1526
  * widget width minus the tree area gives the space for the MSA
@@ -1650,7 +1641,7 @@ function stateModelFactory() {
1650
1641
  self.setTreeFilehandle(undefined)
1651
1642
  self.setMSAFilehandle(undefined)
1652
1643
  self.setGFFFilehandle(undefined)
1653
- self.setInterProAnnotations({})
1644
+ self.setInterProAnnotations(undefined)
1654
1645
  self.setShowDomains(false)
1655
1646
  },
1656
1647
  /**
@@ -1660,21 +1651,13 @@ function stateModelFactory() {
1660
1651
  theme: Theme
1661
1652
  includeMinimap?: boolean
1662
1653
  includeTracks?: boolean
1663
- exportType: string
1654
+ exportType: 'entire' | 'viewport'
1664
1655
  }) {
1665
1656
  const { renderToSvg } = await import('./renderToSvg.tsx')
1666
1657
  const html = await renderToSvg(self as MsaViewModel, opts)
1667
1658
  const blob = new Blob([html], { type: 'image/svg+xml' })
1668
1659
  saveAs(blob, 'image.svg')
1669
1660
  },
1670
- /**
1671
- * #action
1672
- * internal, used for drawing to canvas
1673
- */
1674
- incrementRef() {
1675
- self.nref++
1676
- },
1677
-
1678
1661
  initFilter(arg: string) {
1679
1662
  if (!self.featureFilters.has(arg)) {
1680
1663
  self.featureFilters.set(arg, true)