react-msaview 4.4.6 → 4.6.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 (122) hide show
  1. package/bundle/index.js +9 -9
  2. package/bundle/index.js.LICENSE.txt +8 -8
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.d.ts +0 -6
  5. package/dist/colorSchemes.js +1 -119
  6. package/dist/colorSchemes.js.map +1 -1
  7. package/dist/components/ConservationTrack.d.ts +8 -0
  8. package/dist/components/ConservationTrack.js +54 -0
  9. package/dist/components/ConservationTrack.js.map +1 -0
  10. package/dist/components/Loading.js +14 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +36 -0
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/SequenceTextArea.js +3 -2
  15. package/dist/components/SequenceTextArea.js.map +1 -1
  16. package/dist/components/TextTrack.d.ts +3 -3
  17. package/dist/components/TextTrack.js +4 -1
  18. package/dist/components/TextTrack.js.map +1 -1
  19. package/dist/components/Track.js +21 -8
  20. package/dist/components/Track.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +19 -3
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/header/GappynessSlider.d.ts +6 -0
  24. package/dist/components/header/GappynessSlider.js +19 -0
  25. package/dist/components/header/GappynessSlider.js.map +1 -0
  26. package/dist/components/header/Header.js +3 -1
  27. package/dist/components/header/Header.js.map +1 -1
  28. package/dist/components/header/HeaderMenu.js +30 -14
  29. package/dist/components/header/HeaderMenu.js.map +1 -1
  30. package/dist/components/minimap/MinimapSVG.js +4 -3
  31. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  32. package/dist/components/msa/MSACanvasBlock.js +56 -42
  33. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  34. package/dist/components/msa/renderMSABlock.js +71 -26
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/msa/renderMSAMouseover.js +8 -1
  37. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  38. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  39. package/dist/components/tracks/renderTracksSvg.js +83 -0
  40. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  41. package/dist/components/tree/TreeNodeMenu.js +2 -2
  42. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  43. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  44. package/dist/components/tree/renderTreeCanvas.js +23 -24
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +22 -0
  47. package/dist/constants.js +26 -0
  48. package/dist/constants.js.map +1 -0
  49. package/dist/layout.js.map +1 -1
  50. package/dist/model/msaModel.js +3 -2
  51. package/dist/model/msaModel.js.map +1 -1
  52. package/dist/model/treeModel.js +9 -8
  53. package/dist/model/treeModel.js.map +1 -1
  54. package/dist/model.d.ts +271 -15
  55. package/dist/model.js +427 -128
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.d.ts +1 -0
  58. package/dist/neighborJoining.js +839 -0
  59. package/dist/neighborJoining.js.map +1 -0
  60. package/dist/neighborJoining.test.d.ts +1 -0
  61. package/dist/neighborJoining.test.js +110 -0
  62. package/dist/neighborJoining.test.js.map +1 -0
  63. package/dist/parsers/A3mMSA.d.ts +43 -0
  64. package/dist/parsers/A3mMSA.js +277 -0
  65. package/dist/parsers/A3mMSA.js.map +1 -0
  66. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  67. package/dist/parsers/A3mMSA.test.js +138 -0
  68. package/dist/parsers/A3mMSA.test.js.map +1 -0
  69. package/dist/parsers/ClustalMSA.d.ts +4 -4
  70. package/dist/parsers/ClustalMSA.js +3 -1
  71. package/dist/parsers/ClustalMSA.js.map +1 -1
  72. package/dist/parsers/FastaMSA.js +17 -16
  73. package/dist/parsers/FastaMSA.js.map +1 -1
  74. package/dist/renderToSvg.d.ts +1 -0
  75. package/dist/renderToSvg.js +48 -18
  76. package/dist/renderToSvg.js.map +1 -1
  77. package/dist/rowCoordinateCalculations.js +2 -0
  78. package/dist/rowCoordinateCalculations.js.map +1 -1
  79. package/dist/types.d.ts +2 -3
  80. package/dist/util.js +17 -9
  81. package/dist/util.js.map +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +6 -6
  85. package/src/colorSchemes.ts +1 -179
  86. package/src/components/ConservationTrack.tsx +104 -0
  87. package/src/components/Loading.tsx +44 -2
  88. package/src/components/MSAView.tsx +68 -0
  89. package/src/components/SequenceTextArea.tsx +3 -2
  90. package/src/components/TextTrack.tsx +7 -4
  91. package/src/components/Track.tsx +25 -9
  92. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  93. package/src/components/header/GappynessSlider.tsx +35 -0
  94. package/src/components/header/Header.tsx +3 -1
  95. package/src/components/header/HeaderMenu.tsx +36 -15
  96. package/src/components/minimap/MinimapSVG.tsx +6 -3
  97. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  98. package/src/components/msa/renderMSABlock.ts +103 -40
  99. package/src/components/msa/renderMSAMouseover.ts +9 -0
  100. package/src/components/tracks/renderTracksSvg.ts +157 -0
  101. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  102. package/src/components/tree/renderTreeCanvas.ts +25 -34
  103. package/src/constants.ts +27 -0
  104. package/src/layout.ts +1 -6
  105. package/src/model/msaModel.ts +4 -2
  106. package/src/model/treeModel.ts +19 -8
  107. package/src/model.ts +517 -140
  108. package/src/neighborJoining.test.ts +129 -0
  109. package/src/neighborJoining.ts +885 -0
  110. package/src/parsers/A3mMSA.test.ts +164 -0
  111. package/src/parsers/A3mMSA.ts +321 -0
  112. package/src/parsers/ClustalMSA.ts +7 -5
  113. package/src/parsers/FastaMSA.ts +17 -17
  114. package/src/renderToSvg.tsx +105 -26
  115. package/src/rowCoordinateCalculations.ts +2 -0
  116. package/src/types.ts +2 -4
  117. package/src/util.ts +21 -8
  118. package/src/version.ts +1 -1
  119. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  120. package/dist/components/dialogs/TracklistDialog.js +0 -23
  121. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  122. package/src/components/dialogs/TracklistDialog.tsx +0 -73
@@ -1,5 +1,6 @@
1
- import React, { useEffect, useMemo, useRef } from 'react'
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
2
 
3
+ import { BaseTooltip } from '@jbrowse/core/ui'
3
4
  import { useTheme } from '@mui/material'
4
5
  import { autorun } from 'mobx'
5
6
  import { observer } from 'mobx-react'
@@ -79,55 +80,72 @@ const MSACanvasBlock = observer(function ({
79
80
  contrastScheme,
80
81
  ])
81
82
 
83
+ const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>()
84
+ const { hoveredInsertion } = model
85
+
82
86
  return (
83
- <canvas
84
- ref={ref}
85
- onMouseMove={event => {
86
- if (!ref.current) {
87
- return
88
- }
89
- const { left, top } = ref.current.getBoundingClientRect()
90
- const mouseX = event.clientX - left + offsetX
91
- const mouseY = event.clientY - top + offsetY
92
- const x = Math.floor(mouseX / colWidth)
93
- const y = Math.floor(mouseY / rowHeight)
87
+ <>
88
+ <canvas
89
+ ref={ref}
90
+ onMouseMove={event => {
91
+ if (!ref.current) {
92
+ return
93
+ }
94
+ setMousePosition({ x: event.clientX, y: event.clientY })
95
+ const { left, top } = ref.current.getBoundingClientRect()
96
+ const mouseX = event.clientX - left + offsetX
97
+ const mouseY = event.clientY - top + offsetY
98
+ const x = Math.floor(mouseX / colWidth)
99
+ const y = Math.floor(mouseY / rowHeight)
94
100
 
95
- // Only set mouse position if within valid MSA bounds
96
- if (x >= 0 && x < model.numColumns && y >= 0 && y < model.numRows) {
97
- model.setMousePos(x, y)
98
- } else {
99
- // Clear mouse position when outside bounds
100
- model.setMousePos(undefined, undefined)
101
- }
102
- }}
103
- onClick={event => {
104
- if (!ref.current) {
105
- return
106
- }
107
- const { left, top } = ref.current.getBoundingClientRect()
108
- const mouseX = event.clientX - left + offsetX
109
- const mouseY = event.clientY - top + offsetY
110
- const x = Math.floor(mouseX / colWidth)
111
- const y = Math.floor(mouseY / rowHeight)
112
- if (x === mouseClickCol && y === mouseClickRow) {
113
- model.setMouseClickPos(undefined, undefined)
114
- } else {
115
- model.setMouseClickPos(x, y)
116
- }
117
- }}
118
- onMouseLeave={() => {
119
- model.setMousePos()
120
- }}
121
- width={blockSize * highResScaleFactor}
122
- height={blockSize * highResScaleFactor}
123
- style={{
124
- position: 'absolute',
125
- top: scrollY + offsetY,
126
- left: scrollX + offsetX,
127
- width: blockSize,
128
- height: blockSize,
129
- }}
130
- />
101
+ // Only set mouse position if within valid MSA bounds
102
+ if (x >= 0 && x < model.numColumns && y >= 0 && y < model.numRows) {
103
+ model.setMousePos(x, y)
104
+ } else {
105
+ // Clear mouse position when outside bounds
106
+ model.setMousePos(undefined, undefined)
107
+ }
108
+ }}
109
+ onClick={event => {
110
+ if (!ref.current) {
111
+ return
112
+ }
113
+ const { left, top } = ref.current.getBoundingClientRect()
114
+ const mouseX = event.clientX - left + offsetX
115
+ const mouseY = event.clientY - top + offsetY
116
+ const x = Math.floor(mouseX / colWidth)
117
+ const y = Math.floor(mouseY / rowHeight)
118
+ if (x === mouseClickCol && y === mouseClickRow) {
119
+ model.setMouseClickPos(undefined, undefined)
120
+ } else {
121
+ model.setMouseClickPos(x, y)
122
+ }
123
+ }}
124
+ onMouseLeave={() => {
125
+ model.setMousePos()
126
+ setMousePosition(undefined)
127
+ }}
128
+ width={blockSize * highResScaleFactor}
129
+ height={blockSize * highResScaleFactor}
130
+ style={{
131
+ position: 'absolute',
132
+ top: scrollY + offsetY,
133
+ left: scrollX + offsetX,
134
+ width: blockSize,
135
+ height: blockSize,
136
+ }}
137
+ />
138
+ {hoveredInsertion && mousePosition ? (
139
+ <BaseTooltip
140
+ clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
141
+ >
142
+ Insertion ({hoveredInsertion.letters.length}bp):{' '}
143
+ {hoveredInsertion.letters.length > 20
144
+ ? `${hoveredInsertion.letters.slice(0, 20)}...`
145
+ : hoveredInsertion.letters}
146
+ </BaseTooltip>
147
+ ) : null}
148
+ </>
131
149
  )
132
150
  })
133
151
 
@@ -1,5 +1,3 @@
1
- import { getClustalXColor, getPercentIdentityColor } from '../../colorSchemes'
2
-
3
1
  import type { MsaViewModel } from '../../model'
4
2
  import type { NodeWithIdsAndLength } from '../../types'
5
3
  import type { Theme } from '@mui/material'
@@ -57,7 +55,6 @@ export function renderMSABlock({
57
55
  ctx,
58
56
  theme,
59
57
  offsetX,
60
- offsetY,
61
58
  xStart,
62
59
  xEnd,
63
60
  visibleLeaves,
@@ -68,7 +65,13 @@ export function renderMSABlock({
68
65
  ctx,
69
66
  offsetX,
70
67
  contrastScheme,
71
- theme,
68
+ xStart,
69
+ xEnd,
70
+ visibleLeaves,
71
+ })
72
+ drawInsertionIndicators({
73
+ model,
74
+ ctx,
72
75
  xStart,
73
76
  xEnd,
74
77
  visibleLeaves,
@@ -88,7 +91,6 @@ function drawTiles({
88
91
  model: MsaViewModel
89
92
  offsetX: number
90
93
  theme: Theme
91
- offsetY: number
92
94
  ctx: CanvasRenderingContext2D
93
95
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
94
96
  xStart: number
@@ -109,6 +111,10 @@ function drawTiles({
109
111
  ? columns[relativeTo]?.slice(xStart, xEnd)
110
112
  : null
111
113
 
114
+ const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
115
+ const isPercentIdentity = colorSchemeName === 'percent_identity_dynamic'
116
+ const offsetXAligned = offsetX - (offsetX % colWidth)
117
+
112
118
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
113
119
  const node = visibleLeaves[i]!
114
120
  const {
@@ -117,44 +123,29 @@ function drawTiles({
117
123
  const y = node.x!
118
124
  const str = columns[name]?.slice(xStart, xEnd)
119
125
  if (str) {
120
- for (let i = 0, l2 = str.length; i < l2; i++) {
121
- const letter = str[i]!
126
+ for (let j = 0, l2 = str.length; j < l2; j++) {
127
+ const letter = str[j]!
122
128
 
123
129
  // Use a muted background for positions that match reference
124
130
  const isMatchingReference =
125
- referenceSeq && name !== relativeTo && letter === referenceSeq[i]
126
-
127
- const r1 = colorSchemeName === 'clustalx_protein_dynamic'
128
- const r2 = colorSchemeName === 'percent_identity_dynamic'
129
- const color = r1
130
- ? getClustalXColor(
131
- // use model.colStats dot notation here: delay use of colStats
132
- // until absolutely needed
133
- model.colStats[xStart + i]!,
134
- model.colStatsSums[xStart + i]!,
135
- model,
136
- name,
137
- xStart + i,
138
- )
139
- : r2
140
- ? getPercentIdentityColor(
141
- // use model.colStats dot notation here: delay use of
142
- // colStats until absolutely needed
143
- model.colStats[xStart + i]!,
144
- model.colStatsSums[xStart + i]!,
145
- model,
146
- name,
147
- xStart + i,
148
- )
131
+ referenceSeq && name !== relativeTo && letter === referenceSeq[j]
132
+
133
+ const color = isClustalX
134
+ ? model.colClustalX[xStart + j]![letter]
135
+ : isPercentIdentity
136
+ ? (() => {
137
+ const consensus = model.colConsensus[xStart + j]!
138
+ return letter === consensus.letter ? consensus.color : undefined
139
+ })()
149
140
  : colorScheme[letter.toUpperCase()]
150
- if (bgColor || r1 || r2) {
141
+ if (bgColor || isClustalX || isPercentIdentity) {
151
142
  // Use a very light background for matching positions in relative mode
152
143
  const finalColor = isMatchingReference
153
144
  ? theme.palette.action.hover
154
145
  : color || theme.palette.background.default
155
146
  ctx.fillStyle = finalColor
156
147
  ctx.fillRect(
157
- i * colWidth + offsetX - (offsetX % colWidth),
148
+ j * colWidth + offsetXAligned,
158
149
  y - rowHeight,
159
150
  colWidth,
160
151
  rowHeight,
@@ -177,7 +168,6 @@ function drawText({
177
168
  offsetX: number
178
169
  model: MsaViewModel
179
170
  contrastScheme: Record<string, string>
180
- theme: Theme
181
171
  ctx: CanvasRenderingContext2D
182
172
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
183
173
  xStart: number
@@ -201,20 +191,24 @@ function drawText({
201
191
  : null
202
192
 
203
193
  if (showMsaLetters) {
194
+ const offsetXAligned = offsetX - (offsetX % colWidth)
195
+ const halfColWidth = colWidth / 2
196
+ const quarterRowHeight = rowHeight / 4
197
+
204
198
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
205
199
  const node = visibleLeaves[i]!
206
200
  const {
207
201
  data: { name },
208
202
  } = node
209
- const y = node.x!
203
+ const y = node.x! - quarterRowHeight
210
204
  const str = columns[name]?.slice(xStart, xEnd)
211
205
  if (str) {
212
- for (let i = 0, l2 = str.length; i < l2; i++) {
213
- const letter = str[i]!
206
+ for (let j = 0, l2 = str.length; j < l2; j++) {
207
+ const letter = str[j]!
214
208
 
215
209
  // Check if this position matches the reference
216
210
  const isMatchingReference =
217
- referenceSeq && name !== relativeTo && letter === referenceSeq[i]
211
+ referenceSeq && name !== relativeTo && letter === referenceSeq[j]
218
212
 
219
213
  // Show dot for matching positions, original letter for differences
220
214
  const displayLetter = isMatchingReference ? '.' : letter
@@ -223,7 +217,6 @@ function drawText({
223
217
  const contrast = contrastLettering
224
218
  ? contrastScheme[letter.toUpperCase()] || 'black'
225
219
  : 'black'
226
- const x = i * colWidth + offsetX - (offsetX % colWidth)
227
220
 
228
221
  // note: -rowHeight/4 matches +rowHeight/4 in tree
229
222
  ctx.fillStyle = actuallyShowDomains
@@ -231,7 +224,77 @@ function drawText({
231
224
  : bgColor
232
225
  ? contrast
233
226
  : color || 'black'
234
- ctx.fillText(displayLetter, x + colWidth / 2, y - rowHeight / 4)
227
+ ctx.fillText(displayLetter, j * colWidth + offsetXAligned + halfColWidth, y)
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ function drawInsertionIndicators({
235
+ model,
236
+ ctx,
237
+ visibleLeaves,
238
+ xStart,
239
+ xEnd,
240
+ }: {
241
+ model: MsaViewModel
242
+ ctx: CanvasRenderingContext2D
243
+ visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
244
+ xStart: number
245
+ xEnd: number
246
+ }) {
247
+ const { bgColor, hideGapsEffective } = model
248
+ if (!hideGapsEffective) {
249
+ return
250
+ }
251
+
252
+ ctx.lineWidth = 1
253
+ ctx.strokeStyle = '#f0f'
254
+ drawZigZag({ visibleLeaves, xStart, ctx, model, xEnd, offset: 0 })
255
+ ctx.strokeStyle = !bgColor ? '#000' : '#fff'
256
+ drawZigZag({ visibleLeaves, xStart, ctx, model, xEnd, offset: -1 })
257
+ }
258
+
259
+ function drawZigZag({
260
+ model,
261
+ ctx,
262
+ visibleLeaves,
263
+ xStart,
264
+ xEnd,
265
+ offset,
266
+ }: {
267
+ model: MsaViewModel
268
+ ctx: CanvasRenderingContext2D
269
+ visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
270
+ xStart: number
271
+ xEnd: number
272
+ offset: number
273
+ }) {
274
+ const zigSize = 1
275
+ const { colWidth, rowHeight, insertionPositions } = model
276
+ for (const node of visibleLeaves) {
277
+ const { name } = node.data
278
+ const insertions = insertionPositions.get(name)
279
+ if (insertions) {
280
+ const y = node.x!
281
+ for (const { pos } of insertions) {
282
+ if (pos >= xStart && pos < xEnd) {
283
+ const x = pos * colWidth
284
+ const top = y - rowHeight
285
+ const bottom = y
286
+ ctx.beginPath()
287
+ ctx.moveTo(x + offset, top + offset)
288
+ let currentY = top
289
+ let goRight = true
290
+ while (currentY < bottom) {
291
+ const nextY = Math.min(currentY + zigSize * 2, bottom)
292
+ const nextX = goRight ? x + zigSize : x - zigSize
293
+ ctx.lineTo(nextX + offset, nextY + offset)
294
+ currentY = nextY
295
+ goRight = !goRight
296
+ }
297
+ ctx.stroke()
235
298
  }
236
299
  }
237
300
  }
@@ -28,6 +28,7 @@ export function renderMouseover({
28
28
  relativeTo,
29
29
  rowNamesSet,
30
30
  hoveredTreeNode,
31
+ highlightedColumns,
31
32
  } = model
32
33
  ctx.resetTransform()
33
34
  ctx.clearRect(0, 0, width, height)
@@ -52,6 +53,14 @@ export function renderMouseover({
52
53
  }
53
54
  }
54
55
 
56
+ // Highlight multiple columns
57
+ if (highlightedColumns?.length) {
58
+ ctx.fillStyle = highlightColor
59
+ for (const col of highlightedColumns) {
60
+ ctx.fillRect(col * colWidth + scrollX, 0, colWidth, height)
61
+ }
62
+ }
63
+
55
64
  if (mouseCol !== undefined) {
56
65
  ctx.fillStyle = hoverColor
57
66
  ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
@@ -0,0 +1,157 @@
1
+ import type { MsaViewModel } from '../../model'
2
+ import type { BasicTrack } from '../../types'
3
+
4
+ export function renderConservationTrack({
5
+ model,
6
+ ctx,
7
+ offsetX,
8
+ offsetY,
9
+ trackHeight,
10
+ blockSizeXOverride,
11
+ highResScaleFactorOverride,
12
+ }: {
13
+ model: MsaViewModel
14
+ ctx: CanvasRenderingContext2D
15
+ offsetX: number
16
+ offsetY: number
17
+ trackHeight: number
18
+ blockSizeXOverride?: number
19
+ highResScaleFactorOverride?: number
20
+ }) {
21
+ const { blockSize, colWidth, highResScaleFactor, conservation } = model
22
+ const bx = blockSizeXOverride ?? blockSize
23
+ const k = highResScaleFactorOverride ?? highResScaleFactor
24
+
25
+ ctx.resetTransform()
26
+ ctx.scale(k, k)
27
+ ctx.translate(-offsetX, offsetY)
28
+
29
+ const xStart = Math.max(0, Math.floor(offsetX / colWidth))
30
+ const xEnd = Math.max(0, Math.ceil((offsetX + bx) / colWidth))
31
+
32
+ for (let i = xStart; i < xEnd && i < conservation.length; i++) {
33
+ const value = conservation[i]!
34
+ const barHeight = value * trackHeight
35
+ const x = i * colWidth
36
+
37
+ const hue = value * 120
38
+ ctx.fillStyle = `hsl(${hue}, 70%, 50%)`
39
+ ctx.fillRect(x, trackHeight - barHeight, colWidth, barHeight)
40
+ }
41
+
42
+ ctx.resetTransform()
43
+ }
44
+
45
+ export function renderTextTrack({
46
+ model,
47
+ ctx,
48
+ track,
49
+ offsetX,
50
+ offsetY,
51
+ contrastScheme,
52
+ blockSizeXOverride,
53
+ highResScaleFactorOverride,
54
+ }: {
55
+ model: MsaViewModel
56
+ ctx: CanvasRenderingContext2D
57
+ track: BasicTrack
58
+ offsetX: number
59
+ offsetY: number
60
+ contrastScheme: Record<string, string>
61
+ blockSizeXOverride?: number
62
+ highResScaleFactorOverride?: number
63
+ }) {
64
+ const {
65
+ blockSize,
66
+ bgColor,
67
+ colorScheme: modelColorScheme,
68
+ colWidth,
69
+ fontSize,
70
+ rowHeight,
71
+ highResScaleFactor,
72
+ } = model
73
+
74
+ const { customColorScheme, data } = track.model
75
+ const colorScheme = customColorScheme ?? modelColorScheme
76
+ const bx = blockSizeXOverride ?? blockSize
77
+ const k = highResScaleFactorOverride ?? highResScaleFactor
78
+
79
+ ctx.resetTransform()
80
+ ctx.scale(k, k)
81
+ ctx.translate(-offsetX, offsetY)
82
+ ctx.textAlign = 'center'
83
+ ctx.font = ctx.font.replace(/\d+px/, `${fontSize}px`)
84
+
85
+ const xStart = Math.max(0, Math.floor(offsetX / colWidth))
86
+ const xEnd = Math.max(0, Math.ceil((offsetX + bx) / colWidth))
87
+ const str = data?.slice(xStart, xEnd)
88
+
89
+ for (let i = 0; str && i < str.length; i++) {
90
+ const letter = str[i]!
91
+ const color = colorScheme[letter.toUpperCase()]
92
+ const x = i * colWidth + offsetX - (offsetX % colWidth)
93
+
94
+ if (bgColor && color) {
95
+ ctx.fillStyle = color
96
+ ctx.fillRect(x, 0, colWidth, rowHeight)
97
+ }
98
+
99
+ if (rowHeight >= 10 && colWidth >= rowHeight / 2) {
100
+ ctx.fillStyle =
101
+ bgColor && color
102
+ ? (contrastScheme[letter.toUpperCase()] ?? 'black')
103
+ : 'black'
104
+ ctx.fillText(letter, x + colWidth / 2, rowHeight / 2 + 1)
105
+ }
106
+ }
107
+
108
+ ctx.resetTransform()
109
+ }
110
+
111
+ export function renderAllTracks({
112
+ model,
113
+ ctx,
114
+ offsetX,
115
+ contrastScheme,
116
+ blockSizeXOverride,
117
+ highResScaleFactorOverride,
118
+ }: {
119
+ model: MsaViewModel
120
+ ctx: CanvasRenderingContext2D
121
+ offsetX: number
122
+ contrastScheme: Record<string, string>
123
+ blockSizeXOverride?: number
124
+ highResScaleFactorOverride?: number
125
+ }) {
126
+ const { turnedOnTracks } = model
127
+ let currentY = 0
128
+
129
+ for (const track of turnedOnTracks) {
130
+ const trackHeight = track.model.height
131
+
132
+ if (track.model.id === 'conservation') {
133
+ renderConservationTrack({
134
+ model,
135
+ ctx,
136
+ offsetX,
137
+ offsetY: currentY,
138
+ trackHeight,
139
+ blockSizeXOverride,
140
+ highResScaleFactorOverride,
141
+ })
142
+ } else {
143
+ renderTextTrack({
144
+ model,
145
+ ctx,
146
+ track,
147
+ offsetX,
148
+ offsetY: currentY,
149
+ contrastScheme,
150
+ blockSizeXOverride,
151
+ highResScaleFactorOverride,
152
+ })
153
+ }
154
+
155
+ currentY += trackHeight
156
+ }
157
+ }
@@ -64,9 +64,9 @@ const TreeMenu = observer(function ({
64
64
  model.toggleCollapsed(node.id)
65
65
  } else {
66
66
  if (node.id.endsWith('-leafnode')) {
67
- model.toggleCollapsed2(node.id)
67
+ model.toggleCollapsedLeaf(node.id)
68
68
  } else {
69
- model.toggleCollapsed2(`${node.id}-leafnode`)
69
+ model.toggleCollapsedLeaf(`${node.id}-leafnode`)
70
70
  }
71
71
  }
72
72
  onClose()
@@ -42,15 +42,9 @@ export function renderTree({
42
42
  theme: Theme
43
43
  blockSizeYOverride?: number
44
44
  }) {
45
- const {
46
- hierarchy,
47
- allBranchesLength0,
48
- showBranchLen: showBranchLenPre,
49
- blockSize,
50
- } = model
45
+ const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
51
46
  const by = blockSizeYOverride || blockSize
52
47
  ctx.strokeStyle = theme.palette.text.primary
53
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
54
48
  for (const link of hierarchy.links()) {
55
49
  const { source, target } = link
56
50
  if (target.height === 0 && !showBranchLen) {
@@ -89,25 +83,22 @@ export function renderNodeBubbles({
89
83
  clickMap?: ClickMapIndex
90
84
  offsetY: number
91
85
  model: MsaViewModel
92
- theme: Theme
93
86
  blockSizeYOverride?: number
94
87
  }) {
95
88
  const {
96
89
  hierarchy,
97
- showBranchLen: showBranchLenPre,
98
- allBranchesLength0,
90
+ showBranchLenEffective: showBranchLen,
99
91
  collapsed,
100
92
  blockSize,
101
93
  marginLeft: ml,
102
94
  } = model
103
95
  const by = blockSizeYOverride || blockSize
104
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
105
96
  for (const node of hierarchy.descendants()) {
106
97
  const val = showBranchLen ? 'len' : 'y'
107
98
  // @ts-expect-error
108
99
  const { [val]: x, data } = node
109
100
  const y = node.x!
110
- const { id = '', name = '' } = data
101
+ const { id, name } = data
111
102
  if (
112
103
  node.height > 1 &&
113
104
  y > offsetY - extendBounds &&
@@ -150,8 +141,7 @@ export function renderTreeLabels({
150
141
  }) {
151
142
  const {
152
143
  fontSize,
153
- showBranchLen: showBranchLenPre,
154
- allBranchesLength0,
144
+ showBranchLenEffective: showBranchLen,
155
145
  treeMetadata,
156
146
  hierarchy,
157
147
  collapsed,
@@ -167,13 +157,13 @@ export function renderTreeLabels({
167
157
  noTree,
168
158
  } = model
169
159
  const by = blockSizeYOverride || blockSize
160
+ const emHeight = ctx.measureText('M').width
170
161
  if (labelsAlignRight) {
171
162
  ctx.textAlign = 'right'
172
163
  ctx.setLineDash([1, 3])
173
164
  } else {
174
165
  ctx.textAlign = 'start'
175
166
  }
176
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
177
167
  for (const node of leaves) {
178
168
  const {
179
169
  data: { name, id },
@@ -187,28 +177,29 @@ export function renderTreeLabels({
187
177
  if (y > offsetY - extendBounds && y < offsetY + by + extendBounds) {
188
178
  // note: +rowHeight/4 matches with -rowHeight/4 in msa
189
179
  const yp = y + fontSize / 4
190
- let xp = (showBranchLen ? len : x) || 0
191
- if (
192
- !showBranchLen &&
193
- !collapsed.includes(id) &&
194
- !collapsedLeaves.includes(id)
195
- ) {
196
- // this subtraction is a hack to compensate for the leafnode rendering
197
- // glitch (issue #71). the context is that an extra leaf node is added
198
- // so that 'collapsing/hiding leaf nodes is possible' but this causes
199
- // weird workarounds
200
- xp -= treeWidth / hierarchy.height
180
+ let xp = 0
181
+ if (!noTree) {
182
+ xp = (showBranchLen ? len : x) || 0
183
+ if (
184
+ !showBranchLen &&
185
+ !collapsed.includes(id) &&
186
+ !collapsedLeaves.includes(id)
187
+ ) {
188
+ // this subtraction is a hack to compensate for the leafnode rendering
189
+ // glitch (issue #71). the context is that an extra leaf node is added
190
+ // so that 'collapsing/hiding leaf nodes is possible' but this causes
191
+ // weird workarounds
192
+ xp -= treeWidth / hierarchy.height
193
+ }
201
194
  }
202
195
 
203
196
  const { width } = ctx.measureText(displayName)
204
- const height = ctx.measureText('M').width // use an 'em' for height
205
197
 
206
198
  ctx.fillStyle = theme.palette.text.primary
207
199
  if (labelsAlignRight) {
208
200
  const smallPadding = 2
209
201
  const offset = treeAreaWidthMinusMargin - smallPadding
210
202
  if (drawTree && !noTree) {
211
- const { width } = ctx.measureText(displayName)
212
203
  ctx.moveTo(xp + radius + 2, y)
213
204
  ctx.lineTo(offset - smallPadding - width, y)
214
205
  ctx.stroke()
@@ -217,17 +208,18 @@ export function renderTreeLabels({
217
208
  clickMap?.insert({
218
209
  minX: treeAreaWidth - width,
219
210
  maxX: treeAreaWidth,
220
- minY: yp - height,
211
+ minY: yp - emHeight,
221
212
  maxY: yp,
222
213
  name,
223
214
  id,
224
215
  })
225
216
  } else {
226
- ctx.fillText(displayName, xp + d, yp)
217
+ const labelX = noTree ? 2 : xp + d
218
+ ctx.fillText(displayName, labelX, yp)
227
219
  clickMap?.insert({
228
- minX: xp + d + marginLeft,
229
- maxX: xp + d + width + marginLeft,
230
- minY: yp - height,
220
+ minX: labelX + marginLeft,
221
+ maxX: labelX + width + marginLeft,
222
+ minY: yp - emHeight,
231
223
  maxY: yp,
232
224
  name,
233
225
  id,
@@ -303,7 +295,6 @@ export function renderTreeCanvas({
303
295
  offsetY,
304
296
  clickMap,
305
297
  model,
306
- theme,
307
298
  blockSizeYOverride,
308
299
  })
309
300
  }