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.
- package/bundle/index.js +9 -9
- package/bundle/index.js.LICENSE.txt +8 -8
- package/bundle/index.js.map +1 -1
- package/dist/colorSchemes.d.ts +0 -6
- package/dist/colorSchemes.js +1 -119
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/ConservationTrack.d.ts +8 -0
- package/dist/components/ConservationTrack.js +54 -0
- package/dist/components/ConservationTrack.js.map +1 -0
- package/dist/components/Loading.js +14 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +36 -0
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -2
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.d.ts +3 -3
- package/dist/components/TextTrack.js +4 -1
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +21 -8
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +19 -3
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/header/GappynessSlider.d.ts +6 -0
- package/dist/components/header/GappynessSlider.js +19 -0
- package/dist/components/header/GappynessSlider.js.map +1 -0
- package/dist/components/header/Header.js +3 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +30 -14
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +4 -3
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +56 -42
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +71 -26
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +8 -1
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
- package/dist/components/tracks/renderTracksSvg.js +83 -0
- package/dist/components/tracks/renderTracksSvg.js.map +1 -0
- package/dist/components/tree/TreeNodeMenu.js +2 -2
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
- package/dist/components/tree/renderTreeCanvas.js +23 -24
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +22 -0
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -0
- package/dist/layout.js.map +1 -1
- package/dist/model/msaModel.js +3 -2
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.js +9 -8
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +271 -15
- package/dist/model.js +427 -128
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.d.ts +1 -0
- package/dist/neighborJoining.js +839 -0
- package/dist/neighborJoining.js.map +1 -0
- package/dist/neighborJoining.test.d.ts +1 -0
- package/dist/neighborJoining.test.js +110 -0
- package/dist/neighborJoining.test.js.map +1 -0
- package/dist/parsers/A3mMSA.d.ts +43 -0
- package/dist/parsers/A3mMSA.js +277 -0
- package/dist/parsers/A3mMSA.js.map +1 -0
- package/dist/parsers/A3mMSA.test.d.ts +1 -0
- package/dist/parsers/A3mMSA.test.js +138 -0
- package/dist/parsers/A3mMSA.test.js.map +1 -0
- package/dist/parsers/ClustalMSA.d.ts +4 -4
- package/dist/parsers/ClustalMSA.js +3 -1
- package/dist/parsers/ClustalMSA.js.map +1 -1
- package/dist/parsers/FastaMSA.js +17 -16
- package/dist/parsers/FastaMSA.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -0
- package/dist/renderToSvg.js +48 -18
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.js +2 -0
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/types.d.ts +2 -3
- package/dist/util.js +17 -9
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/src/colorSchemes.ts +1 -179
- package/src/components/ConservationTrack.tsx +104 -0
- package/src/components/Loading.tsx +44 -2
- package/src/components/MSAView.tsx +68 -0
- package/src/components/SequenceTextArea.tsx +3 -2
- package/src/components/TextTrack.tsx +7 -4
- package/src/components/Track.tsx +25 -9
- package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
- package/src/components/header/GappynessSlider.tsx +35 -0
- package/src/components/header/Header.tsx +3 -1
- package/src/components/header/HeaderMenu.tsx +36 -15
- package/src/components/minimap/MinimapSVG.tsx +6 -3
- package/src/components/msa/MSACanvasBlock.tsx +66 -48
- package/src/components/msa/renderMSABlock.ts +103 -40
- package/src/components/msa/renderMSAMouseover.ts +9 -0
- package/src/components/tracks/renderTracksSvg.ts +157 -0
- package/src/components/tree/TreeNodeMenu.tsx +2 -2
- package/src/components/tree/renderTreeCanvas.ts +25 -34
- package/src/constants.ts +27 -0
- package/src/layout.ts +1 -6
- package/src/model/msaModel.ts +4 -2
- package/src/model/treeModel.ts +19 -8
- package/src/model.ts +517 -140
- package/src/neighborJoining.test.ts +129 -0
- package/src/neighborJoining.ts +885 -0
- package/src/parsers/A3mMSA.test.ts +164 -0
- package/src/parsers/A3mMSA.ts +321 -0
- package/src/parsers/ClustalMSA.ts +7 -5
- package/src/parsers/FastaMSA.ts +17 -17
- package/src/renderToSvg.tsx +105 -26
- package/src/rowCoordinateCalculations.ts +2 -0
- package/src/types.ts +2 -4
- package/src/util.ts +21 -8
- package/src/version.ts +1 -1
- package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
- package/dist/components/dialogs/TracklistDialog.js +0 -23
- package/dist/components/dialogs/TracklistDialog.js.map +0 -1
- 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
const letter = str[
|
|
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[
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
213
|
-
const letter = str[
|
|
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[
|
|
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,
|
|
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.
|
|
67
|
+
model.toggleCollapsedLeaf(node.id)
|
|
68
68
|
} else {
|
|
69
|
-
model.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 -
|
|
211
|
+
minY: yp - emHeight,
|
|
221
212
|
maxY: yp,
|
|
222
213
|
name,
|
|
223
214
|
id,
|
|
224
215
|
})
|
|
225
216
|
} else {
|
|
226
|
-
|
|
217
|
+
const labelX = noTree ? 2 : xp + d
|
|
218
|
+
ctx.fillText(displayName, labelX, yp)
|
|
227
219
|
clickMap?.insert({
|
|
228
|
-
minX:
|
|
229
|
-
maxX:
|
|
230
|
-
minY: yp -
|
|
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
|
}
|