react-msaview 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/bundle/index.js +283 -97265
  2. package/dist/StructureModel.d.ts +9 -0
  3. package/dist/StructureModel.js +11 -0
  4. package/dist/StructureModel.js.map +1 -0
  5. package/dist/UniprotTrack.d.ts +27 -0
  6. package/dist/UniprotTrack.js +52 -0
  7. package/dist/UniprotTrack.js.map +1 -0
  8. package/dist/colorSchemes.d.ts +2 -2
  9. package/dist/colorSchemes.js +24 -29
  10. package/dist/colorSchemes.js.map +1 -0
  11. package/dist/components/AboutDlg.d.ts +0 -1
  12. package/dist/components/AboutDlg.js +38 -48
  13. package/dist/components/AboutDlg.js.map +1 -0
  14. package/dist/components/AddTrackDlg.d.ts +0 -1
  15. package/dist/components/AddTrackDlg.js +13 -13
  16. package/dist/components/AddTrackDlg.js.map +1 -0
  17. package/dist/components/AnnotationDlg.d.ts +0 -1
  18. package/dist/components/AnnotationDlg.js +36 -48
  19. package/dist/components/AnnotationDlg.js.map +1 -0
  20. package/dist/components/BoxTrack.d.ts +3 -4
  21. package/dist/components/BoxTrack.js +51 -48
  22. package/dist/components/BoxTrack.js.map +1 -0
  23. package/dist/components/DetailsDlg.d.ts +0 -1
  24. package/dist/components/DetailsDlg.js +7 -7
  25. package/dist/components/DetailsDlg.js.map +1 -0
  26. package/dist/components/Header.d.ts +0 -1
  27. package/dist/components/Header.js +39 -34
  28. package/dist/components/Header.js.map +1 -0
  29. package/dist/components/ImportForm.d.ts +0 -1
  30. package/dist/components/ImportForm.js +59 -71
  31. package/dist/components/ImportForm.js.map +1 -0
  32. package/dist/components/MSACanvas.d.ts +0 -1
  33. package/dist/components/MSACanvas.js +71 -74
  34. package/dist/components/MSACanvas.js.map +1 -0
  35. package/dist/components/MSAView.d.ts +0 -1
  36. package/dist/components/MSAView.js +19 -38
  37. package/dist/components/MSAView.js.map +1 -0
  38. package/dist/components/MoreInfoDlg.d.ts +2 -3
  39. package/dist/components/MoreInfoDlg.js +5 -5
  40. package/dist/components/MoreInfoDlg.js.map +1 -0
  41. package/dist/components/ResizeHandles.d.ts +2 -3
  42. package/dist/components/ResizeHandles.js +31 -32
  43. package/dist/components/ResizeHandles.js.map +1 -0
  44. package/dist/components/Rubberband.d.ts +2 -1
  45. package/dist/components/Rubberband.js +42 -64
  46. package/dist/components/Rubberband.js.map +1 -0
  47. package/dist/components/Ruler.d.ts +0 -15
  48. package/dist/components/Ruler.js +18 -87
  49. package/dist/components/Ruler.js.map +1 -0
  50. package/dist/components/SettingsDlg.d.ts +0 -1
  51. package/dist/components/SettingsDlg.js +29 -22
  52. package/dist/components/SettingsDlg.js.map +1 -0
  53. package/dist/components/TextTrack.d.ts +3 -4
  54. package/dist/components/TextTrack.js +23 -24
  55. package/dist/components/TextTrack.js.map +1 -0
  56. package/dist/components/Track.d.ts +2 -3
  57. package/dist/components/Track.js +38 -38
  58. package/dist/components/Track.js.map +1 -0
  59. package/dist/components/TrackInfoDlg.d.ts +5 -3
  60. package/dist/components/TrackInfoDlg.js +12 -13
  61. package/dist/components/TrackInfoDlg.js.map +1 -0
  62. package/dist/components/TracklistDlg.d.ts +0 -1
  63. package/dist/components/TracklistDlg.js +9 -9
  64. package/dist/components/TracklistDlg.js.map +1 -0
  65. package/dist/components/TreeCanvas.d.ts +0 -1
  66. package/dist/components/TreeCanvas.js +135 -148
  67. package/dist/components/TreeCanvas.js.map +1 -0
  68. package/dist/components/TreeRuler.d.ts +0 -1
  69. package/dist/components/TreeRuler.js +3 -3
  70. package/dist/components/TreeRuler.js.map +1 -0
  71. package/dist/components/VerticalGuide.d.ts +6 -0
  72. package/dist/components/VerticalGuide.js +30 -0
  73. package/dist/components/VerticalGuide.js.map +1 -0
  74. package/dist/components/data/seq2.d.ts +3 -3
  75. package/dist/components/data/seq2.js +33 -3
  76. package/dist/components/data/seq2.js.map +1 -0
  77. package/{bundle/components/Ruler.d.ts → dist/components/util.d.ts} +1 -6
  78. package/dist/components/util.js +69 -0
  79. package/dist/components/util.js.map +1 -0
  80. package/dist/index.d.ts +2 -4
  81. package/dist/index.js +3 -3
  82. package/dist/index.js.map +1 -0
  83. package/dist/layout.js +14 -20
  84. package/dist/layout.js.map +1 -0
  85. package/dist/model.d.ts +94 -74
  86. package/dist/model.js +232 -473
  87. package/dist/model.js.map +1 -0
  88. package/dist/parseNewick.d.ts +1 -5
  89. package/dist/parseNewick.js +10 -7
  90. package/dist/parseNewick.js.map +1 -0
  91. package/dist/parsers/ClustalMSA.d.ts +6 -18
  92. package/dist/parsers/ClustalMSA.js +55 -64
  93. package/dist/parsers/ClustalMSA.js.map +1 -0
  94. package/dist/parsers/FastaMSA.d.ts +4 -9
  95. package/dist/parsers/FastaMSA.js +55 -64
  96. package/dist/parsers/FastaMSA.js.map +1 -0
  97. package/dist/parsers/StockholmMSA.d.ts +8 -13
  98. package/dist/parsers/StockholmMSA.js +78 -107
  99. package/dist/parsers/StockholmMSA.js.map +1 -0
  100. package/dist/util.d.ts +33 -4
  101. package/dist/util.js +76 -24
  102. package/dist/util.js.map +1 -0
  103. package/dist/version.d.ts +1 -0
  104. package/dist/version.js +2 -0
  105. package/dist/version.js.map +1 -0
  106. package/package.json +30 -30
  107. package/src/StructureModel.ts +11 -0
  108. package/src/UniprotTrack.ts +60 -0
  109. package/src/colorSchemes.ts +520 -0
  110. package/src/components/AboutDlg.tsx +64 -0
  111. package/src/components/AddTrackDlg.tsx +74 -0
  112. package/src/components/AnnotationDlg.tsx +144 -0
  113. package/src/components/BoxTrack.tsx +225 -0
  114. package/src/components/DetailsDlg.tsx +28 -0
  115. package/src/components/Header.tsx +117 -0
  116. package/src/components/ImportForm.tsx +192 -0
  117. package/src/components/MSACanvas.tsx +297 -0
  118. package/src/components/MSAView.tsx +132 -0
  119. package/src/components/MoreInfoDlg.tsx +21 -0
  120. package/src/components/ResizeHandles.tsx +137 -0
  121. package/src/components/Rubberband.tsx +271 -0
  122. package/src/components/Ruler.tsx +122 -0
  123. package/src/components/SettingsDlg.tsx +154 -0
  124. package/src/components/TextTrack.tsx +120 -0
  125. package/src/components/Track.tsx +150 -0
  126. package/src/components/TrackInfoDlg.tsx +59 -0
  127. package/src/components/TracklistDlg.tsx +61 -0
  128. package/src/components/TreeCanvas.tsx +633 -0
  129. package/src/components/TreeRuler.tsx +12 -0
  130. package/src/components/VerticalGuide.tsx +50 -0
  131. package/src/components/data/seq2.ts +35 -0
  132. package/src/components/util.ts +94 -0
  133. package/src/declare.d.ts +2 -0
  134. package/src/index.ts +2 -0
  135. package/src/layout.ts +83 -0
  136. package/src/model.ts +790 -0
  137. package/{bundle/parseNewick.d.ts → src/parseNewick.ts} +36 -5
  138. package/src/parsers/ClustalMSA.ts +79 -0
  139. package/src/parsers/FastaMSA.ts +82 -0
  140. package/src/parsers/StockholmMSA.ts +137 -0
  141. package/src/util.ts +142 -0
  142. package/src/version.ts +1 -0
  143. package/bundle/colorSchemes.d.ts +0 -16
  144. package/bundle/colorSchemes.js +0 -455
  145. package/bundle/components/AboutDlg.d.ts +0 -5
  146. package/bundle/components/AboutDlg.js +0 -47
  147. package/bundle/components/AddTrackDlg.d.ts +0 -8
  148. package/bundle/components/AddTrackDlg.js +0 -26
  149. package/bundle/components/AnnotationDlg.d.ts +0 -11
  150. package/bundle/components/AnnotationDlg.js +0 -77
  151. package/bundle/components/BoxTrack.d.ts +0 -7
  152. package/bundle/components/BoxTrack.js +0 -143
  153. package/bundle/components/DetailsDlg.d.ts +0 -8
  154. package/bundle/components/DetailsDlg.js +0 -12
  155. package/bundle/components/Header.d.ts +0 -6
  156. package/bundle/components/Header.js +0 -63
  157. package/bundle/components/ImportForm.d.ts +0 -6
  158. package/bundle/components/ImportForm.js +0 -89
  159. package/bundle/components/MSACanvas.d.ts +0 -6
  160. package/bundle/components/MSACanvas.js +0 -210
  161. package/bundle/components/MSAView.d.ts +0 -6
  162. package/bundle/components/MSAView.js +0 -88
  163. package/bundle/components/MoreInfoDlg.d.ts +0 -6
  164. package/bundle/components/MoreInfoDlg.js +0 -11
  165. package/bundle/components/ResizeHandles.d.ts +0 -8
  166. package/bundle/components/ResizeHandles.js +0 -110
  167. package/bundle/components/Rubberband.d.ts +0 -7
  168. package/bundle/components/Rubberband.js +0 -196
  169. package/bundle/components/Ruler.js +0 -121
  170. package/bundle/components/SettingsDlg.d.ts +0 -8
  171. package/bundle/components/SettingsDlg.js +0 -40
  172. package/bundle/components/TextTrack.d.ts +0 -7
  173. package/bundle/components/TextTrack.js +0 -72
  174. package/bundle/components/Track.d.ts +0 -11
  175. package/bundle/components/Track.js +0 -81
  176. package/bundle/components/TrackInfoDlg.d.ts +0 -6
  177. package/bundle/components/TrackInfoDlg.js +0 -33
  178. package/bundle/components/TracklistDlg.d.ts +0 -8
  179. package/bundle/components/TracklistDlg.js +0 -18
  180. package/bundle/components/TreeCanvas.d.ts +0 -6
  181. package/bundle/components/TreeCanvas.js +0 -431
  182. package/bundle/components/TreeRuler.d.ts +0 -6
  183. package/bundle/components/TreeRuler.js +0 -8
  184. package/bundle/components/data/seq2.d.ts +0 -3
  185. package/bundle/components/data/seq2.js +0 -3
  186. package/bundle/index.d.ts +0 -4
  187. package/bundle/layout.d.ts +0 -23
  188. package/bundle/layout.js +0 -53
  189. package/bundle/model.d.ts +0 -364
  190. package/bundle/model.js +0 -894
  191. package/bundle/parseNewick.js +0 -94
  192. package/bundle/parsers/ClustalMSA.d.ts +0 -39
  193. package/bundle/parsers/ClustalMSA.js +0 -77
  194. package/bundle/parsers/FastaMSA.d.ts +0 -26
  195. package/bundle/parsers/FastaMSA.js +0 -78
  196. package/bundle/parsers/StockholmMSA.d.ts +0 -75
  197. package/bundle/parsers/StockholmMSA.js +0 -142
  198. package/bundle/util.d.ts +0 -17
  199. package/bundle/util.js +0 -33
  200. package/dist/components/package.json +0 -62
@@ -0,0 +1,633 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { Menu, MenuItem } from '@mui/material'
3
+ import normalizeWheel from 'normalize-wheel'
4
+ import { observer } from 'mobx-react'
5
+ import RBush from 'rbush'
6
+
7
+ // locals
8
+ import { MsaViewModel } from '../model'
9
+ import MoreInfoDlg from './MoreInfoDlg'
10
+
11
+ const extendBounds = 5
12
+ const radius = 3.5
13
+ const d = radius * 2
14
+
15
+ const padding = 600
16
+
17
+ interface TooltipData {
18
+ name: string
19
+ id: string
20
+ x: number
21
+ y: number
22
+ }
23
+
24
+ interface ClickEntry {
25
+ name: string
26
+ id: string
27
+ branch?: boolean
28
+ minX: number
29
+ maxX: number
30
+ minY: number
31
+ maxY: number
32
+ }
33
+
34
+ const TreeMenu = observer(function ({
35
+ node,
36
+ onClose,
37
+ model,
38
+ }: {
39
+ node: { x: number; y: number; name: string }
40
+ model: MsaViewModel
41
+ onClose: () => void
42
+ }) {
43
+ const { structures } = model
44
+ const nodeDetails = node ? model.getRowData(node.name) : undefined
45
+
46
+ return (
47
+ <>
48
+ <Menu
49
+ anchorReference="anchorPosition"
50
+ anchorPosition={{
51
+ top: node.y,
52
+ left: node.x,
53
+ }}
54
+ transitionDuration={0}
55
+ keepMounted
56
+ open={Boolean(node)}
57
+ onClose={onClose}
58
+ >
59
+ <MenuItem dense disabled>
60
+ {node.name}
61
+ </MenuItem>
62
+
63
+ <MenuItem
64
+ dense
65
+ onClick={() => {
66
+ model.setDialogComponent(MoreInfoDlg, {
67
+ info: model.getRowData(node.name),
68
+ })
69
+ onClose()
70
+ }}
71
+ >
72
+ More info...
73
+ </MenuItem>
74
+
75
+ {structures[node.name]?.map(entry => {
76
+ return !model.selectedStructures.some(n => n.id === node.name) ? (
77
+ <MenuItem
78
+ key={JSON.stringify(entry)}
79
+ dense
80
+ onClick={() => {
81
+ model.addStructureToSelection({
82
+ structure: entry,
83
+ id: node.name,
84
+ })
85
+ onClose()
86
+ }}
87
+ >
88
+ Add PDB to selection ({entry.pdb})
89
+ </MenuItem>
90
+ ) : (
91
+ <MenuItem
92
+ key={JSON.stringify(entry)}
93
+ dense
94
+ onClick={() => {
95
+ model.removeStructureFromSelection({
96
+ structure: entry,
97
+ id: node.name,
98
+ })
99
+ onClose()
100
+ }}
101
+ >
102
+ Remove PDB from selection ({entry.pdb})
103
+ </MenuItem>
104
+ )
105
+ })}
106
+
107
+ {
108
+ // @ts-expect-error
109
+ nodeDetails?.data.accession?.map((accession: string) => (
110
+ <MenuItem
111
+ dense
112
+ key={accession}
113
+ onClick={() => {
114
+ model.addUniprotTrack({
115
+ // @ts-expect-error
116
+ name: nodeDetails?.data.name,
117
+ accession,
118
+ })
119
+ onClose()
120
+ }}
121
+ >
122
+ Open UniProt track ({accession})
123
+ </MenuItem>
124
+ ))
125
+ }
126
+ </Menu>
127
+ </>
128
+ )
129
+ })
130
+ interface Node {
131
+ x: number
132
+ y: number
133
+ name: string
134
+ id: string
135
+ }
136
+
137
+ const TreeBranchMenu = observer(function ({
138
+ node,
139
+ model,
140
+ onClose,
141
+ }: {
142
+ node: Node
143
+ model: MsaViewModel
144
+ onClose: () => void
145
+ }) {
146
+ return (
147
+ <Menu
148
+ anchorReference="anchorPosition"
149
+ anchorPosition={{
150
+ left: node.x,
151
+ top: node.y,
152
+ }}
153
+ transitionDuration={0}
154
+ keepMounted
155
+ open={Boolean(node)}
156
+ onClose={onClose}
157
+ >
158
+ <MenuItem dense disabled>
159
+ {node.name}
160
+ </MenuItem>
161
+ <MenuItem
162
+ dense
163
+ onClick={() => {
164
+ model.toggleCollapsed(node.id)
165
+ onClose()
166
+ }}
167
+ >
168
+ {model.collapsed.includes(node.id)
169
+ ? 'Expand this node'
170
+ : 'Collapse this node'}
171
+ </MenuItem>
172
+ <MenuItem
173
+ dense
174
+ onClick={() => {
175
+ model.showOnly === node.id
176
+ ? model.setShowOnly(undefined)
177
+ : model.setShowOnly(node.id)
178
+ onClose()
179
+ }}
180
+ >
181
+ {model.showOnly === node.id
182
+ ? 'Disable show only this node'
183
+ : 'Show only this node'}
184
+ </MenuItem>
185
+ </Menu>
186
+ )
187
+ })
188
+
189
+ const TreeBlock = observer(function ({
190
+ model,
191
+ offsetY,
192
+ }: {
193
+ model: MsaViewModel
194
+ offsetY: number
195
+ }) {
196
+ const ref = useRef<HTMLCanvasElement>(null)
197
+ const clickMap = useRef(new RBush<ClickEntry>())
198
+ const mouseoverRef = useRef<HTMLCanvasElement>(null)
199
+ const [branchMenu, setBranchMenu] = useState<TooltipData>()
200
+ const [toggleNodeMenu, setToggleNodeMenu] = useState<TooltipData>()
201
+ const [hoverElt, setHoverElt] = useState<ClickEntry>()
202
+
203
+ const {
204
+ hierarchy,
205
+ rowHeight,
206
+ scrollY,
207
+ treeWidth,
208
+ showBranchLen,
209
+ collapsed,
210
+ margin,
211
+ labelsAlignRight,
212
+ noTree,
213
+ blockSize,
214
+ drawNodeBubbles,
215
+ drawTree,
216
+ treeAreaWidth,
217
+ structures,
218
+ highResScaleFactor,
219
+ } = model
220
+
221
+ useEffect(() => {
222
+ clickMap.current.clear()
223
+
224
+ if (!ref.current) {
225
+ return
226
+ }
227
+ const ctx = ref.current.getContext('2d')
228
+ if (!ctx) {
229
+ return
230
+ }
231
+
232
+ ctx.resetTransform()
233
+ ctx.scale(highResScaleFactor, highResScaleFactor)
234
+ ctx.clearRect(0, 0, treeWidth + padding, blockSize)
235
+ ctx.translate(margin.left, -offsetY)
236
+
237
+ const font = ctx.font
238
+ ctx.font = font.replace(/\d+px/, `${Math.max(8, rowHeight - 8)}px`)
239
+
240
+ if (!noTree && drawTree) {
241
+ hierarchy.links().forEach(({ source, target }) => {
242
+ const y = showBranchLen ? 'len' : 'y'
243
+ // @ts-expect-error
244
+ const { x: sy, [y]: sx } = source
245
+ // @ts-expect-error
246
+ const { x: ty, [y]: tx } = target
247
+
248
+ const y1 = Math.min(sy, ty)
249
+ const y2 = Math.max(sy, ty)
250
+ // 1d line intersection to check if line crosses block at all, this is
251
+ // an optimization that allows us to skip drawing most tree links
252
+ // outside the block
253
+ if (offsetY + blockSize >= y1 && y2 >= offsetY) {
254
+ ctx.beginPath()
255
+ ctx.moveTo(sx, sy)
256
+ ctx.lineTo(sx, ty)
257
+ ctx.lineTo(tx, ty)
258
+ ctx.stroke()
259
+ }
260
+ })
261
+
262
+ if (drawNodeBubbles) {
263
+ hierarchy.descendants().forEach(node => {
264
+ const val = showBranchLen ? 'len' : 'y'
265
+ const {
266
+ // @ts-expect-error
267
+ x: y,
268
+ // @ts-expect-error
269
+ [val]: x,
270
+ data,
271
+ } = node
272
+ const { id = '', name = '' } = data
273
+
274
+ if (
275
+ y > offsetY - extendBounds &&
276
+ y < offsetY + blockSize + extendBounds
277
+ ) {
278
+ ctx.strokeStyle = 'black'
279
+ ctx.fillStyle = collapsed.includes(id) ? 'black' : 'white'
280
+ ctx.beginPath()
281
+ ctx.arc(x, y, radius, 0, 2 * Math.PI)
282
+ ctx.fill()
283
+ ctx.stroke()
284
+
285
+ clickMap.current.insert({
286
+ minX: x - radius,
287
+ maxX: x - radius + d,
288
+ minY: y - radius,
289
+ maxY: y - radius + d,
290
+ branch: true,
291
+ id,
292
+ name,
293
+ })
294
+ }
295
+ })
296
+ }
297
+ }
298
+
299
+ if (rowHeight >= 10) {
300
+ if (labelsAlignRight) {
301
+ ctx.textAlign = 'right'
302
+ ctx.setLineDash([1, 3])
303
+ } else {
304
+ ctx.textAlign = 'start'
305
+ }
306
+ hierarchy.leaves().forEach(node => {
307
+ const {
308
+ // @ts-expect-error
309
+ x: y,
310
+ // @ts-expect-error
311
+ y: x,
312
+ data: { name, id },
313
+ // @ts-expect-error
314
+ len,
315
+ } = node
316
+
317
+ if (
318
+ y > offsetY - extendBounds &&
319
+ y < offsetY + blockSize + extendBounds
320
+ ) {
321
+ // note: +rowHeight/4 matches with -rowHeight/4 in msa
322
+ const yp = y + rowHeight / 4
323
+ const xp = showBranchLen ? len : x
324
+
325
+ const { width } = ctx.measureText(name)
326
+ const height = ctx.measureText('M').width // use an 'em' for height
327
+
328
+ const hasStructure = structures[name]
329
+ ctx.fillStyle = hasStructure ? 'blue' : 'black'
330
+
331
+ if (!drawTree && !labelsAlignRight) {
332
+ ctx.fillText(name, 0, yp)
333
+ clickMap.current.insert({
334
+ minX: 0,
335
+ maxX: width,
336
+ minY: yp - height,
337
+ maxY: yp,
338
+ name,
339
+ id,
340
+ })
341
+ } else if (labelsAlignRight) {
342
+ const smallPadding = 2
343
+ const offset = treeAreaWidth - smallPadding - margin.left
344
+ if (drawTree && !noTree) {
345
+ const { width } = ctx.measureText(name)
346
+ ctx.moveTo(xp + radius + 2, y)
347
+ ctx.lineTo(offset - smallPadding - width, y)
348
+ ctx.stroke()
349
+ }
350
+ ctx.fillText(name, offset, yp)
351
+ clickMap.current.insert({
352
+ minX: treeAreaWidth - margin.left - width,
353
+ maxX: treeAreaWidth - margin.left,
354
+ minY: yp - height,
355
+ maxY: yp,
356
+ name,
357
+ id,
358
+ })
359
+ } else {
360
+ ctx.fillText(name, xp + d, yp)
361
+ clickMap.current.insert({
362
+ minX: xp + d,
363
+ maxX: xp + d + width,
364
+ minY: yp - height,
365
+ maxY: yp,
366
+ name,
367
+ id,
368
+ })
369
+ }
370
+ }
371
+ })
372
+ ctx.setLineDash([])
373
+ }
374
+ }, [
375
+ collapsed,
376
+ rowHeight,
377
+ margin.left,
378
+ hierarchy,
379
+ offsetY,
380
+ treeWidth,
381
+ showBranchLen,
382
+ noTree,
383
+ blockSize,
384
+ drawNodeBubbles,
385
+ drawTree,
386
+ labelsAlignRight,
387
+ treeAreaWidth,
388
+ structures,
389
+ highResScaleFactor,
390
+ ])
391
+
392
+ useEffect(() => {
393
+ const canvas = mouseoverRef.current
394
+ if (!canvas) {
395
+ return
396
+ }
397
+ const ctx = canvas.getContext('2d')
398
+ if (!ctx) {
399
+ return
400
+ }
401
+
402
+ ctx.resetTransform()
403
+ ctx.clearRect(0, 0, treeWidth + padding, blockSize)
404
+ ctx.translate(margin.left, -offsetY)
405
+
406
+ if (hoverElt) {
407
+ const { minX, maxX, minY, maxY } = hoverElt
408
+
409
+ ctx.fillStyle = 'rgba(0,0,0,0.1)'
410
+ ctx.fillRect(minX, minY, maxX - minX, maxY - minY)
411
+ }
412
+ }, [hoverElt, margin.left, offsetY, blockSize, treeWidth])
413
+
414
+ function hoverBranchClickMap(event: React.MouseEvent) {
415
+ const x = event.nativeEvent.offsetX - margin.left
416
+ const y = event.nativeEvent.offsetY
417
+
418
+ const [entry] = clickMap.current.search({
419
+ minX: x,
420
+ maxX: x + 1,
421
+ minY: y + offsetY,
422
+ maxY: y + 1 + offsetY,
423
+ })
424
+
425
+ return entry && entry.branch
426
+ ? { ...entry, x: event.clientX, y: event.clientY }
427
+ : undefined
428
+ }
429
+
430
+ function hoverNameClickMap(event: React.MouseEvent) {
431
+ const x = event.nativeEvent.offsetX - margin.left
432
+ const y = event.nativeEvent.offsetY
433
+ const [entry] = clickMap.current.search({
434
+ minX: x,
435
+ maxX: x + 1,
436
+ minY: y + offsetY,
437
+ maxY: y + 1 + offsetY,
438
+ })
439
+
440
+ return entry && !entry.branch
441
+ ? { ...entry, x: event.clientX, y: event.clientY }
442
+ : undefined
443
+ }
444
+
445
+ return (
446
+ <>
447
+ {branchMenu?.id ? (
448
+ <TreeBranchMenu
449
+ node={branchMenu}
450
+ model={model}
451
+ onClose={() => setBranchMenu(undefined)}
452
+ />
453
+ ) : null}
454
+
455
+ {toggleNodeMenu?.id ? (
456
+ <TreeMenu
457
+ node={toggleNodeMenu}
458
+ model={model}
459
+ onClose={() => setToggleNodeMenu(undefined)}
460
+ />
461
+ ) : null}
462
+
463
+ <canvas
464
+ width={(treeWidth + padding) * highResScaleFactor}
465
+ height={blockSize * highResScaleFactor}
466
+ style={{
467
+ width: treeWidth + padding,
468
+ height: blockSize,
469
+ top: scrollY + offsetY,
470
+ left: 0,
471
+ position: 'absolute',
472
+ }}
473
+ onMouseMove={event => {
474
+ if (!ref.current) {
475
+ return
476
+ }
477
+
478
+ const ret = hoverNameClickMap(event) || hoverBranchClickMap(event)
479
+
480
+ ref.current.style.cursor = ret ? 'pointer' : 'default'
481
+
482
+ setHoverElt(hoverNameClickMap(event))
483
+ }}
484
+ onClick={event => {
485
+ const { clientX: x, clientY: y } = event
486
+
487
+ const data = hoverBranchClickMap(event)
488
+ if (data?.id) {
489
+ setBranchMenu({ ...data, x, y })
490
+ }
491
+
492
+ const data2 = hoverNameClickMap(event)
493
+ if (data2?.id) {
494
+ setToggleNodeMenu({ ...data2, x, y })
495
+ }
496
+ }}
497
+ ref={ref}
498
+ />
499
+ <canvas
500
+ style={{
501
+ width: treeWidth + padding,
502
+ height: blockSize,
503
+ top: scrollY + offsetY,
504
+ left: 0,
505
+ position: 'absolute',
506
+ pointerEvents: 'none',
507
+ zIndex: 100,
508
+ }}
509
+ width={treeWidth + padding}
510
+ height={blockSize}
511
+ ref={mouseoverRef}
512
+ />
513
+ </>
514
+ )
515
+ })
516
+ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
517
+ const ref = useRef<HTMLDivElement>(null)
518
+ const scheduled = useRef(false)
519
+ const deltaY = useRef(0)
520
+ const prevY = useRef<number>(0)
521
+ const { treeWidth, height, blocksY } = model
522
+ const [mouseDragging, setMouseDragging] = useState(false)
523
+
524
+ useEffect(() => {
525
+ const curr = ref.current
526
+ if (!curr) {
527
+ return
528
+ }
529
+ function onWheel(origEvent: WheelEvent) {
530
+ const event = normalizeWheel(origEvent)
531
+ deltaY.current += event.pixelY
532
+
533
+ if (!scheduled.current) {
534
+ scheduled.current = true
535
+ requestAnimationFrame(() => {
536
+ model.doScrollY(-deltaY.current)
537
+ deltaY.current = 0
538
+ scheduled.current = false
539
+ })
540
+ }
541
+ origEvent.preventDefault()
542
+ }
543
+ curr.addEventListener('wheel', onWheel)
544
+ return () => {
545
+ curr.removeEventListener('wheel', onWheel)
546
+ }
547
+ }, [model])
548
+
549
+ useEffect(() => {
550
+ let cleanup = () => {}
551
+
552
+ function globalMouseMove(event: MouseEvent) {
553
+ event.preventDefault()
554
+ const currY = event.clientY
555
+ const distanceY = currY - prevY.current
556
+ if (distanceY) {
557
+ // use rAF to make it so multiple event handlers aren't fired per-frame
558
+ // see https://calendar.perfplanet.com/2013/the-runtime-performance-checklist/
559
+ if (!scheduled.current) {
560
+ scheduled.current = true
561
+ window.requestAnimationFrame(() => {
562
+ model.doScrollY(distanceY)
563
+ scheduled.current = false
564
+ prevY.current = event.clientY
565
+ })
566
+ }
567
+ }
568
+ }
569
+
570
+ function globalMouseUp() {
571
+ prevY.current = 0
572
+ if (mouseDragging) {
573
+ setMouseDragging(false)
574
+ }
575
+ }
576
+
577
+ if (mouseDragging) {
578
+ window.addEventListener('mousemove', globalMouseMove, true)
579
+ window.addEventListener('mouseup', globalMouseUp, true)
580
+ cleanup = () => {
581
+ window.removeEventListener('mousemove', globalMouseMove, true)
582
+ window.removeEventListener('mouseup', globalMouseUp, true)
583
+ }
584
+ }
585
+ return cleanup
586
+ }, [model, mouseDragging])
587
+
588
+ function mouseDown(event: React.MouseEvent) {
589
+ // check if clicking a draggable element or a resize handle
590
+ const target = event.target as HTMLElement
591
+ if (target.draggable || target.dataset.resizer) {
592
+ return
593
+ }
594
+
595
+ // otherwise do click and drag scroll
596
+ if (event.button === 0) {
597
+ prevY.current = event.clientY
598
+ setMouseDragging(true)
599
+ }
600
+ }
601
+
602
+ // this local mouseup is used in addition to the global because sometimes
603
+ // the global add/remove are not called in time, resulting in issue #533
604
+ function mouseUp(event: React.MouseEvent) {
605
+ event.preventDefault()
606
+ setMouseDragging(false)
607
+ }
608
+
609
+ function mouseLeave(event: React.MouseEvent) {
610
+ event.preventDefault()
611
+ }
612
+
613
+ return (
614
+ <div
615
+ ref={ref}
616
+ onMouseDown={mouseDown}
617
+ onMouseUp={mouseUp}
618
+ onMouseLeave={mouseLeave}
619
+ style={{
620
+ height,
621
+ position: 'relative',
622
+ overflow: 'hidden',
623
+ width: treeWidth + padding,
624
+ }}
625
+ >
626
+ {blocksY.map(block => (
627
+ <TreeBlock key={block} model={model} offsetY={block} />
628
+ ))}
629
+ </div>
630
+ )
631
+ })
632
+
633
+ export default TreeCanvas
@@ -0,0 +1,12 @@
1
+ import React from 'react'
2
+ import { observer } from 'mobx-react'
3
+
4
+ // locals
5
+ import { MsaViewModel } from '../model'
6
+
7
+ const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
8
+ const { treeWidth } = model
9
+ return <div style={{ width: treeWidth }} />
10
+ })
11
+
12
+ export default TreeRuler
@@ -0,0 +1,50 @@
1
+ import React from 'react'
2
+
3
+ import { observer } from 'mobx-react'
4
+ import { makeStyles } from 'tss-react/mui'
5
+ import { Tooltip } from '@mui/material'
6
+
7
+ // icons
8
+ import { MsaViewModel } from '../model'
9
+
10
+ const useStyles = makeStyles()({
11
+ guide: {
12
+ pointerEvents: 'none',
13
+ height: '100%',
14
+ width: 1,
15
+ position: 'absolute',
16
+ zIndex: 10,
17
+ },
18
+ })
19
+
20
+ const VerticalGuide = observer(function ({
21
+ model,
22
+ coordX,
23
+ }: {
24
+ model: MsaViewModel
25
+ coordX: number
26
+ }) {
27
+ const { treeAreaWidth } = model
28
+ const { classes } = useStyles()
29
+ return (
30
+ <>
31
+ <Tooltip open placement="top" title={`${model.pxToBp(coordX) + 1}`} arrow>
32
+ <div
33
+ style={{
34
+ left: coordX + treeAreaWidth,
35
+ position: 'absolute',
36
+ height: 1,
37
+ }}
38
+ />
39
+ </Tooltip>
40
+ <div
41
+ className={classes.guide}
42
+ style={{
43
+ left: coordX + treeAreaWidth,
44
+ background: 'red',
45
+ }}
46
+ />
47
+ </>
48
+ )
49
+ })
50
+ export default VerticalGuide