react-msaview 8.0.0 → 8.1.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 (86) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.map +3 -3
  3. package/dist/calculateBlocks.js +5 -3
  4. package/dist/calculateBlocks.js.map +1 -1
  5. package/dist/components/Loading.js +2 -8
  6. package/dist/components/Loading.js.map +1 -1
  7. package/dist/components/SequenceTextArea.js +0 -1
  8. package/dist/components/SequenceTextArea.js.map +1 -1
  9. package/dist/components/Track.js +2 -2
  10. package/dist/components/Track.js.map +1 -1
  11. package/dist/components/dialogs/AboutDialog.js +6 -6
  12. package/dist/components/dialogs/AboutDialog.js.map +1 -1
  13. package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
  14. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  15. package/dist/components/dialogs/ExportSVGDialog.js +1 -1
  16. package/dist/components/dialogs/FeatureDialog.js +8 -3
  17. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  18. package/dist/components/header/Header.js +1 -1
  19. package/dist/components/header/Header.js.map +1 -1
  20. package/dist/components/header/LoadWarnings.js +3 -19
  21. package/dist/components/header/LoadWarnings.js.map +1 -1
  22. package/dist/components/header/settingsMenuItems.js +3 -3
  23. package/dist/components/header/settingsMenuItems.js.map +1 -1
  24. package/dist/components/import/ImportForm.js +1 -1
  25. package/dist/components/import/ImportForm.js.map +1 -1
  26. package/dist/components/msa/MSACanvas.js +1 -3
  27. package/dist/components/msa/MSACanvas.js.map +1 -1
  28. package/dist/components/msa/drawFeatureSpans.d.ts +7 -1
  29. package/dist/components/msa/drawFeatureSpans.js +58 -23
  30. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  31. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -8
  32. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  33. package/dist/components/msa/renderMSABlock.js +8 -9
  34. package/dist/components/msa/renderMSABlock.js.map +1 -1
  35. package/dist/components/tracks/drawTracks.js +8 -5
  36. package/dist/components/tracks/drawTracks.js.map +1 -1
  37. package/dist/components/tree/TreeBranchMenu.js +1 -1
  38. package/dist/components/tree/TreeBranchMenu.js.map +1 -1
  39. package/dist/components/tree/TreeCanvas.js +1 -3
  40. package/dist/components/tree/TreeCanvas.js.map +1 -1
  41. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  42. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  43. package/dist/components/tree/TreeNodeMenu.js +1 -1
  44. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  45. package/dist/components/tree/renderTreeCanvas.js +2 -1
  46. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  47. package/dist/constants.d.ts +1 -0
  48. package/dist/constants.js +4 -0
  49. package/dist/constants.js.map +1 -1
  50. package/dist/model.d.ts +31 -32
  51. package/dist/model.js +184 -141
  52. package/dist/model.js.map +1 -1
  53. package/dist/svgTestUtil.d.ts +18 -22
  54. package/dist/types.d.ts +2 -0
  55. package/dist/useWheelScroll.js +0 -4
  56. package/dist/useWheelScroll.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +3 -3
  60. package/src/calculateBlocks.ts +5 -3
  61. package/src/components/Loading.tsx +2 -30
  62. package/src/components/SequenceTextArea.tsx +0 -1
  63. package/src/components/Track.tsx +2 -1
  64. package/src/components/dialogs/AboutDialog.tsx +35 -6
  65. package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
  66. package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
  67. package/src/components/dialogs/FeatureDialog.tsx +11 -3
  68. package/src/components/header/Header.tsx +1 -1
  69. package/src/components/header/LoadWarnings.tsx +8 -21
  70. package/src/components/header/settingsMenuItems.ts +3 -3
  71. package/src/components/import/ImportForm.tsx +3 -5
  72. package/src/components/msa/MSACanvas.tsx +0 -3
  73. package/src/components/msa/drawFeatureSpans.ts +80 -25
  74. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
  75. package/src/components/msa/renderMSABlock.ts +9 -8
  76. package/src/components/tracks/drawTracks.ts +8 -16
  77. package/src/components/tree/TreeBranchMenu.tsx +1 -2
  78. package/src/components/tree/TreeCanvas.tsx +0 -3
  79. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  80. package/src/components/tree/TreeNodeMenu.tsx +1 -2
  81. package/src/components/tree/renderTreeCanvas.ts +2 -1
  82. package/src/constants.ts +5 -0
  83. package/src/model.ts +232 -161
  84. package/src/types.ts +2 -0
  85. package/src/useWheelScroll.ts +0 -4
  86. package/src/version.ts +1 -1
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  import { ErrorMessage } from '@jbrowse/core/ui'
4
4
  import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary'
5
- import { Button, Typography } from '@mui/material'
5
+ import { Button, CircularProgress, Typography } from '@mui/material'
6
6
  import { observer } from 'mobx-react'
7
7
 
8
8
  import MSAView from './MSAView.tsx'
@@ -16,35 +16,7 @@ const LoadingSpinner = observer(function ({ model }: { model: MsaViewModel }) {
16
16
  <div
17
17
  style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 20 }}
18
18
  >
19
- <svg
20
- width="24"
21
- height="24"
22
- viewBox="0 0 24 24"
23
- xmlns="http://www.w3.org/2000/svg"
24
- >
25
- <style>
26
- {`@keyframes spinner { to { transform: rotate(360deg); } }`}
27
- </style>
28
- <circle
29
- cx="12"
30
- cy="12"
31
- r="10"
32
- stroke="#ccc"
33
- strokeWidth="3"
34
- fill="none"
35
- />
36
- <path
37
- d="M12 2a10 10 0 0 1 10 10"
38
- stroke="#1976d2"
39
- strokeWidth="3"
40
- fill="none"
41
- strokeLinecap="round"
42
- style={{
43
- animation: 'spinner 1s linear infinite',
44
- transformOrigin: 'center',
45
- }}
46
- />
47
- </svg>
19
+ <CircularProgress size={24} />
48
20
  <Typography variant="h6">{status?.msg ?? 'Loading...'}</Typography>
49
21
  {status?.onCancel ? (
50
22
  <Button
@@ -15,7 +15,6 @@ const useStyles = makeStyles()(theme => ({
15
15
  dialogContent: {
16
16
  background: theme.palette.action.selected,
17
17
  margin: 4,
18
- minWidth: '80em',
19
18
  },
20
19
  }))
21
20
 
@@ -78,6 +78,7 @@ const TrackLabel = observer(function TrackLabel({
78
78
  </span>
79
79
  <IconButton
80
80
  className={classes.button}
81
+ aria-label={`${name} track menu`}
81
82
  style={{
82
83
  width: trackLabelHeight,
83
84
  height: trackLabelHeight,
@@ -105,7 +106,7 @@ const TrackLabel = observer(function TrackLabel({
105
106
  closeMenu()
106
107
  }}
107
108
  >
108
- Close
109
+ Hide track
109
110
  </MenuItem>
110
111
  <MenuItem
111
112
  dense
@@ -17,16 +17,33 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
17
17
  <DialogContent>
18
18
  <Typography>
19
19
  MSAView {version} (
20
- <Link href="https://github.com/gmod/react-msaview">Github</Link>)
20
+ <Link
21
+ target="_blank"
22
+ rel="noopener"
23
+ href="https://github.com/gmod/react-msaview"
24
+ >
25
+ Github
26
+ </Link>
27
+ )
21
28
  </Typography>
22
29
 
23
30
  <ul>
24
31
  <li>
25
32
  <Typography>
26
33
  We use some color schemes from the{' '}
27
- <Link href="https://github.com/biotite-dev/biotite">biotite</Link>{' '}
34
+ <Link
35
+ target="_blank"
36
+ rel="noopener"
37
+ href="https://github.com/biotite-dev/biotite"
38
+ >
39
+ biotite
40
+ </Link>{' '}
28
41
  project, and their license is reproduced{' '}
29
- <Link href="https://github.com/biotite-dev/biotite/blob/master/LICENSE.rst">
42
+ <Link
43
+ target="_blank"
44
+ rel="noopener"
45
+ href="https://github.com/biotite-dev/biotite/blob/master/LICENSE.rst"
46
+ >
30
47
  here
31
48
  </Link>
32
49
  </Typography>
@@ -34,7 +51,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
34
51
  <li>
35
52
  <Typography>
36
53
  See this page for some information on jalview colorings{' '}
37
- <Link href="https://www.jalview.org/help/html/colourSchemes/">
54
+ <Link
55
+ target="_blank"
56
+ rel="noopener"
57
+ href="https://www.jalview.org/help/html/colourSchemes/"
58
+ >
38
59
  here
39
60
  </Link>
40
61
  </Typography>
@@ -43,7 +64,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
43
64
  <Typography>
44
65
  See this page for some info on the clustal, cinema, maeditor, and
45
66
  lesk color schemes{' '}
46
- <Link href="http://www.bioinformatics.nl/~berndb/aacolour.html">
67
+ <Link
68
+ target="_blank"
69
+ rel="noopener"
70
+ href="http://www.bioinformatics.nl/~berndb/aacolour.html"
71
+ >
47
72
  here
48
73
  </Link>
49
74
  </Typography>
@@ -51,7 +76,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
51
76
  <li>
52
77
  <Typography>
53
78
  See this paper about the flower color scheme{' '}
54
- <Link href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7245768/">
79
+ <Link
80
+ target="_blank"
81
+ rel="noopener"
82
+ href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7245768/"
83
+ >
55
84
  here
56
85
  </Link>
57
86
  </Typography>
@@ -126,7 +126,7 @@ const AnnotationFileDialog = observer(function ({
126
126
  <Button
127
127
  variant="contained"
128
128
  color="primary"
129
- disabled={loading}
129
+ disabled={loading || (choice === 'file' ? !file : !url.trim())}
130
130
  onClick={() => {
131
131
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
132
132
  ;(async () => {
@@ -154,6 +154,15 @@ const AnnotationFileDialog = observer(function ({
154
154
  >
155
155
  Open annotations
156
156
  </Button>
157
+ <Button
158
+ variant="contained"
159
+ color="secondary"
160
+ onClick={() => {
161
+ handleClose()
162
+ }}
163
+ >
164
+ Cancel
165
+ </Button>
157
166
  </DialogActions>
158
167
  </Dialog>
159
168
  )
@@ -167,7 +167,7 @@ export default function ExportSVGDialog({
167
167
  })
168
168
  }}
169
169
  >
170
- {exporting ? 'Exporting…' : 'Submit'}
170
+ {exporting ? 'Exporting…' : 'Export'}
171
171
  </Button>
172
172
  <Button
173
173
  variant="contained"
@@ -47,10 +47,18 @@ const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
47
47
  })
48
48
 
49
49
  const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
50
- const { annotationTypes, annotations, fillPalette } = model
50
+ const { annotationTypes, annotations, featureColors, fillPalette } = model
51
51
  const counts = new Map<string, number>()
52
+ const swatches = new Map<string, string>()
52
53
  for (const annot of annotations) {
53
- counts.set(annot.accession, (counts.get(annot.accession) ?? 0) + 1)
54
+ const { accession } = annot
55
+ counts.set(accession, (counts.get(accession) ?? 0) + 1)
56
+ if (!swatches.has(accession)) {
57
+ swatches.set(
58
+ accession,
59
+ featureColors.get(annot)?.fill ?? fillPalette[accession]!,
60
+ )
61
+ }
54
62
  }
55
63
  return (
56
64
  <>
@@ -86,7 +94,7 @@ const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
86
94
  style={{
87
95
  width: 20,
88
96
  height: 20,
89
- background: fillPalette[accession],
97
+ background: swatches.get(accession),
90
98
  }}
91
99
  />
92
100
  </TableCell>
@@ -34,7 +34,7 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
34
34
  [model],
35
35
  )
36
36
  return (
37
- <div ref={ref} style={{ display: 'flex' }}>
37
+ <div ref={ref} style={{ display: 'flex', flexWrap: 'wrap' }}>
38
38
  <FileMenu model={model} />
39
39
  <ColorSchemeMenu model={model} />
40
40
  <TreeSettingsMenu model={model} />
@@ -1,46 +1,33 @@
1
1
  import React from 'react'
2
2
 
3
- import { makeStyles } from '@jbrowse/core/util/tss-react'
4
3
  import Warning from '@mui/icons-material/Warning'
5
- import { Tooltip, Typography } from '@mui/material'
4
+ import { Button, Tooltip } from '@mui/material'
6
5
  import { observer } from 'mobx-react'
7
6
 
8
7
  import type { MsaViewModel } from '../../model.ts'
9
8
 
10
- const useStyles = makeStyles()(theme => ({
11
- warning: {
12
- display: 'flex',
13
- alignItems: 'center',
14
- gap: 4,
15
- margin: 'auto',
16
- marginLeft: 10,
17
- whiteSpace: 'nowrap',
18
- color: theme.palette.warning.main,
19
- cursor: 'pointer',
20
- },
21
- }))
22
-
23
9
  // A failed optional layer, such as a 404 on the annotations or row metadata
24
10
  // that is not JSON, shows here as a dismissible warning. Setting `error` would
25
11
  // clear dataInitialized and hide the loaded alignment.
26
12
  const LoadWarnings = observer(function ({ model }: { model: MsaViewModel }) {
27
13
  const { warnings } = model
28
- const { classes } = useStyles()
29
14
  if (warnings.length === 0) {
30
15
  return null
31
16
  }
32
17
  return (
33
18
  <Tooltip title={`${warnings.join(' ')} (click to dismiss)`}>
34
- <Typography
35
- component="div"
36
- className={classes.warning}
19
+ <Button
20
+ aria-label="Dismiss warnings"
21
+ color="warning"
22
+ size="small"
23
+ startIcon={<Warning fontSize="small" />}
24
+ style={{ margin: 'auto', marginLeft: 10, whiteSpace: 'nowrap' }}
37
25
  onClick={() => {
38
26
  model.clearWarnings()
39
27
  }}
40
28
  >
41
- <Warning fontSize="small" />
42
29
  {warnings.length > 1 ? `${warnings.length} warnings` : 'Warning'}
43
- </Typography>
30
+ </Button>
44
31
  </Tooltip>
45
32
  )
46
33
  })
@@ -12,7 +12,7 @@ function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
12
12
  }
13
13
  }
14
14
 
15
- // the only place in the UI to turn a closed track back on
15
+ // the only place in the UI to turn a hidden track back on
16
16
  function tracksSubMenu(model: MsaViewModel): MenuItem[] {
17
17
  const shown = new Set(model.turnedOnTracks.map(t => t.model.id))
18
18
  return model.tracks.map(({ model: { id, name } }) =>
@@ -38,10 +38,10 @@ export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
38
38
  model.setDrawMsaLetters(arg)
39
39
  }),
40
40
  // inverted: bgColor colors the tile, and off colors the letter
41
- toggle('Color letters instead of background of tiles', !bgColor, arg => {
41
+ toggle('Color letters, not cells', !bgColor, arg => {
42
42
  model.setBgColor(!arg)
43
43
  }),
44
- toggle('Enable hiding gappy columns?', hideGaps, arg => {
44
+ toggle('Hide gappy columns', hideGaps, arg => {
45
45
  model.setHideGaps(arg)
46
46
  }),
47
47
  ]
@@ -24,11 +24,9 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
24
24
  Open an MSA file (FASTA, Stockholm, Clustal, A3M or EMF format) and/or
25
25
  a tree file (Newick format).
26
26
  </Typography>
27
- <Typography color="error">
28
- Note: you can open up just an MSA or just a tree, both are not
29
- required. Some MSA files e.g. stockholm format have an embedded tree
30
- also and this is fine, and opening a separate tree file is not
31
- required.
27
+ <Typography color="text.secondary">
28
+ An MSA alone or a tree alone is enough, and a Stockholm file with an
29
+ embedded tree needs no separate tree file.
32
30
  </Typography>
33
31
  </div>
34
32
 
@@ -54,9 +54,6 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
54
54
  data-testid="msa_canvas"
55
55
  onMouseDown={onMouseDown}
56
56
  onMouseUp={onMouseUp}
57
- onMouseLeave={event => {
58
- event.preventDefault()
59
- }}
60
57
  style={{
61
58
  position: 'relative',
62
59
  height,
@@ -1,3 +1,5 @@
1
+ import { minFeatureLabelHeight } from '../../constants.ts'
2
+
1
3
  import type { HierarchyNode } from '../../hierarchy.ts'
2
4
  import type { Annotation, NodeWithIdsAndLength } from '../../types.ts'
3
5
  import type { RenderCtx } from '../renderCtx.ts'
@@ -25,8 +27,14 @@ export interface SpanLayout {
25
27
  height: (laneCount: number) => number
26
28
  /** the band's top edge, from its row baseline, its lane and its height */
27
29
  top: (y: number, lane: number, height: number) => number
28
- /** how far a strand head reaches past the body */
30
+ /** how far back from the feature's end the head starts */
29
31
  headLength: (height: number) => number
32
+ /**
33
+ * how far the head rises above the band. A bar too thin to taper reads its
34
+ * direction off a head taller than itself, and the bar sits on the row's
35
+ * bottom edge, so the room is above.
36
+ */
37
+ headRise?: (height: number) => number
30
38
  }
31
39
 
32
40
  /** the rows of `bandsByRow` that `leaves` covers, in display order */
@@ -76,9 +84,26 @@ export function drawFeatureSpans<T extends SpanBand>({
76
84
  xMin: number
77
85
  xMax: number
78
86
  }) {
87
+ // one row's spans are drawn before any of its labels. Bands are ordered
88
+ // longest-first so a nested one lands on top, and drawing each label with its
89
+ // own span put the child's box over the parent's text: `S` covered `RBD`, and
90
+ // `RBD` read as `RB`. Reused across rows rather than allocated per row.
91
+ const placed: {
92
+ band: T
93
+ xStart: number
94
+ w: number
95
+ t: number
96
+ head: number
97
+ fill: string
98
+ }[] = []
99
+
79
100
  for (const { y, bands } of rows) {
80
101
  const h = layout.height(bands[0]!.laneCount)
81
102
  const headLen = layout.headLength(h)
103
+ const rise = layout.headRise?.(h) ?? 0
104
+ const labelled = labelOf !== undefined && h >= minFeatureLabelHeight
105
+ placed.length = 0
106
+
82
107
  for (const band of bands) {
83
108
  const [xStart, xEnd] = xOf(band)
84
109
  const w = xEnd - xStart
@@ -90,21 +115,33 @@ export function drawFeatureSpans<T extends SpanBand>({
90
115
  ctx.fillStyle = fill
91
116
  ctx.strokeStyle = stroke
92
117
  const { strand } = band.annotation
118
+ const head = strand === undefined ? 0 : Math.min(headLen, w)
93
119
  if (strand === undefined) {
94
120
  ctx.fillRect(xStart, t, w, h)
95
121
  ctx.strokeRect(xStart, t, w, h)
96
122
  } else {
97
- drawGeneArrow({ ctx, x: xStart, t, w, h, headLen, strand })
123
+ drawGeneArrow({ ctx, x: xStart, t, w, h, head, rise, strand })
98
124
  }
99
- const label = labelOf?.(band)
100
- if (label !== undefined) {
101
- const fontSize = Math.min(h - 2, 11)
102
- ctx.font = `${fontSize}px sans-serif`
103
- if (ctx.measureText(label).width + 2 <= w) {
125
+ if (labelled) {
126
+ placed.push({ band, xStart, w, t, head, fill })
127
+ }
128
+ }
129
+
130
+ if (placed.length > 0) {
131
+ ctx.font = `${Math.min(h - 2, 11)}px sans-serif`
132
+ ctx.textAlign = 'center'
133
+ ctx.textBaseline = 'middle'
134
+ for (const { band, xStart, w, t, head, fill } of placed) {
135
+ // asking for the label here rather than in the pass above keeps the
136
+ // once-per-block segment numbering in drawing order
137
+ const label = labelOf!(band)
138
+ // the head narrows to a point, so only about half of it holds text
139
+ if (
140
+ label !== undefined &&
141
+ ctx.measureText(label).width + 2 <= w - head / 2
142
+ ) {
104
143
  labelDrawn?.(band)
105
144
  ctx.fillStyle = contrastText(fill)
106
- ctx.textAlign = 'center'
107
- ctx.textBaseline = 'middle'
108
145
  ctx.fillText(label, xStart + w / 2, t + h / 2)
109
146
  }
110
147
  }
@@ -112,19 +149,24 @@ export function drawFeatureSpans<T extends SpanBand>({
112
149
  }
113
150
  }
114
151
 
115
- // A gene arrow: a full-width rectangular body spanning the feature's
116
- // start..end columns, plus a triangular head that points *beyond* that end in
117
- // the strand direction (right for +, left for -). Keeping the body aligned to
118
- // the exact start/end columns means + and - strand features read as having the
119
- // same boundaries; the arrow lives outside them purely to show transcription
120
- // direction, preserving the column-by-column homology down the rows.
152
+ /**
153
+ * A gene arrow, drawn the way gggenes and gggenomes draw one: the head is the
154
+ * last `head` pixels *of* the feature, tapering to a point at its end, and a
155
+ * feature shorter than the head is all head. The glyph therefore covers its
156
+ * start..end span and nothing else, so adjacent genes butt together instead of
157
+ * biting triangles out of each other, a neighbour never draws over a label, and
158
+ * a gene 20 columns wide reads as 20 columns wide.
159
+ *
160
+ * `rise` lifts the head above the band, for a bar too thin to taper.
161
+ */
121
162
  function drawGeneArrow({
122
163
  ctx,
123
164
  x,
124
165
  t,
125
166
  w,
126
167
  h,
127
- headLen,
168
+ head,
169
+ rise,
128
170
  strand,
129
171
  }: {
130
172
  ctx: RenderCtx
@@ -132,18 +174,31 @@ function drawGeneArrow({
132
174
  t: number
133
175
  w: number
134
176
  h: number
135
- headLen: number
177
+ head: number
178
+ rise: number
136
179
  strand: number
137
180
  }) {
138
- const dir = strand > 0 ? 1 : -1
139
- const bodyStart = strand > 0 ? x : x + w
140
- const bodyEnd = strand > 0 ? x + w : x
181
+ const tip = strand > 0 ? x + w : x
182
+ const tail = strand > 0 ? x : x + w
183
+ const flange = strand > 0 ? tip - head : tip + head
184
+ const bottom = t + h
185
+ const crown = t - rise
186
+ const point = (crown + bottom) / 2
141
187
  ctx.beginPath()
142
- ctx.moveTo(bodyStart, t)
143
- ctx.lineTo(bodyEnd, t)
144
- ctx.lineTo(bodyEnd + dir * headLen, t + h / 2)
145
- ctx.lineTo(bodyEnd, t + h)
146
- ctx.lineTo(bodyStart, t + h)
188
+ if (head >= w) {
189
+ ctx.moveTo(tail, crown)
190
+ ctx.lineTo(tip, point)
191
+ ctx.lineTo(tail, bottom)
192
+ } else {
193
+ ctx.moveTo(tail, t)
194
+ ctx.lineTo(flange, t)
195
+ if (rise > 0) {
196
+ ctx.lineTo(flange, crown)
197
+ }
198
+ ctx.lineTo(tip, point)
199
+ ctx.lineTo(flange, bottom)
200
+ ctx.lineTo(tail, bottom)
201
+ }
147
202
  ctx.closePath()
148
203
  ctx.fill()
149
204
  ctx.stroke()
@@ -60,16 +60,12 @@ export function renderBoxFeatureCanvasBlock({
60
60
  // out); when letters show, the alternating shades alone mark the boundaries
61
61
  // and a number would collide with the sequence. A featureLabel encoding names
62
62
  // what every span carries instead
63
- const drawSegmentLabels =
64
- !featureLabels && !showMsaLetters && !subFeatureRows && barHeight >= 9
63
+ const drawSegmentLabels = !featureLabels && !showMsaLetters && !subFeatureRows
65
64
  // a segment (exon) number labels its band once per block, on the topmost
66
65
  // visible row carrying that segment, so it reads as a column header for the
67
66
  // whole band. Keyed by accession rather than drawn on row 0, because the rows
68
67
  // carrying the gene model are often not the first ones in the alignment
69
68
  const labelled = drawSegmentLabels ? new Set<string>() : undefined
70
- // gene arrow heads stick out up to a row height past the band, so pad the
71
- // cull window enough that a band just outside the block still draws its head
72
- const cull = rowHeight + colWidth
73
69
 
74
70
  drawFeatureSpans<DomainBand>({
75
71
  ctx,
@@ -88,9 +84,11 @@ export function renderBoxFeatureCanvasBlock({
88
84
  y -
89
85
  rowHeight +
90
86
  (subFeatureRows ? lane * h : domainUnderline ? rowHeight - h : 0),
91
- // the head keeps its full size on an underline bar, since a head as short
92
- // as the bar reads as a nub rather than as a direction
93
87
  headLength: h => (domainUnderline ? rowHeight / 2 : h),
88
+ // a head only as tall as an underline bar tapers over ten pixels of
89
+ // three, which reads as nothing; it takes the lower two fifths of the
90
+ // row instead, leaving the letters above it
91
+ headRise: h => (domainUnderline ? Math.max(0, rowHeight * 0.4 - h) : 0),
94
92
  },
95
93
  xOf: band => [band.startCol * colWidth, band.endCol * colWidth],
96
94
  colors: featureColors,
@@ -101,7 +99,7 @@ export function renderBoxFeatureCanvasBlock({
101
99
  : undefined),
102
100
  labelDrawn: band => labelled?.add(band.annotation.accession),
103
101
  contrastText: contrastTextFn(theme),
104
- xMin: offsetX - cull,
105
- xMax: offsetX + bx + cull,
102
+ xMin: offsetX,
103
+ xMax: offsetX + bx,
106
104
  })
107
105
  }
@@ -113,19 +113,18 @@ function drawTilesAndText({
113
113
  columns,
114
114
  colWidth,
115
115
  rowHeight,
116
+ fontSize,
116
117
  relativeTo,
117
118
  showMsaLetters,
118
119
  subFeatureRows,
119
120
  domainUnderline,
120
121
  domainBandsByStart,
122
+ featureColors,
121
123
  } = model
122
124
 
123
125
  const tiles = drawTiles && bgColor
124
126
  const paintTiles = tiles && !rasterTiles
125
127
  if (paintTiles || showMsaLetters) {
126
- // a letter takes its color from the cell it lands on, so the scheme lookup
127
- // is only waste when nothing draws letters
128
- const needsColor = paintTiles || showMsaLetters
129
128
  const tileColor = tileColorFn(model)
130
129
  // Domain fills come from a categorical palette with no fixed lightness, and
131
130
  // a dynamic scheme colors a cell from its column, so a readable letter color
@@ -133,8 +132,10 @@ function drawTilesAndText({
133
132
  const contrastText = contrastTextFn(theme)
134
133
  const offsetXAligned = xStart * colWidth
135
134
  const halfColWidth = colWidth / 2
136
- // note: -rowHeight/4 matches +rowHeight/4 in tree
137
- const quarterRowHeight = rowHeight / 4
135
+ // the context is translated by rowHeight/2, so `y` is the row's bottom
136
+ // edge and the baseline lands fontSize/4 below the row center, where the
137
+ // tree puts its tip labels
138
+ const baselineOffset = rowHeight / 2 - fontSize / 4
138
139
  // with the tiles coming from the domain overlay instead, letters sit either
139
140
  // on a domain box or on the plain background; sub-row layout stacks the boxes
140
141
  // above the letters and letter-color mode shrinks them to a bar under the
@@ -148,7 +149,7 @@ function drawTilesAndText({
148
149
  const str = columns.get(name)?.slice(xStart, xEnd)
149
150
  if (str) {
150
151
  const tileY = y - rowHeight
151
- const textY = y - quarterRowHeight
152
+ const textY = y - baselineOffset
152
153
  const bandAt = domainBandCursor(
153
154
  overDomains ? domainBandsByStart.get(name) : undefined,
154
155
  )
@@ -159,7 +160,7 @@ function drawTilesAndText({
159
160
  const x = j * colWidth + offsetXAligned
160
161
  const isMatchingReference =
161
162
  referenceSeq && name !== relativeTo && letter === referenceSeq[j]
162
- const color = needsColor ? tileColor(col, letter) : undefined
163
+ const color = tileColor(col, letter)
163
164
 
164
165
  if (paintTiles) {
165
166
  ctx.fillStyle = isMatchingReference
@@ -172,7 +173,7 @@ function drawTilesAndText({
172
173
  const covering = bandAt(col)
173
174
  ctx.fillStyle = covering
174
175
  ? // on top of a domain box: contrast against the box fill
175
- contrastText(model.fillPalette[covering.annotation.accession])
176
+ contrastText(featureColors.get(covering.annotation)?.fill)
176
177
  : isMatchingReference
177
178
  ? // the dot sits on the faint reference-match wash, which is
178
179
  // the theme's background with a little of its text color in it
@@ -309,18 +309,9 @@ export function drawTrackBlock({
309
309
  blockSizeXOverride?: number
310
310
  highResScaleFactorOverride?: number
311
311
  }) {
312
- const {
313
- blockSize,
314
- bgColor,
315
- colWidth,
316
- colStats,
317
- colorScheme: modelColorScheme,
318
- fontSize,
319
- rowHeight,
320
- showMsaLetters,
321
- alphabetMaxBits,
322
- highResScaleFactor,
323
- } = model
312
+ // an autorun tracks every model read here, so each kind reads only what it
313
+ // draws and a vertical zoom leaves the bar and arc canvases alone
314
+ const { blockSize, colWidth, highResScaleFactor } = model
324
315
  const {
325
316
  id,
326
317
  kind,
@@ -367,9 +358,9 @@ export function drawTrackBlock({
367
358
  case 'logo': {
368
359
  drawSequenceLogo({
369
360
  ctx,
370
- colStats,
371
- colorScheme: modelColorScheme,
372
- maxBits: alphabetMaxBits,
361
+ colStats: model.colStats,
362
+ colorScheme: model.colorScheme,
363
+ maxBits: model.alphabetMaxBits,
373
364
  textColor,
374
365
  colWidth,
375
366
  trackHeight,
@@ -399,13 +390,14 @@ export function drawTrackBlock({
399
390
  break
400
391
  }
401
392
  case 'text': {
393
+ const { bgColor, fontSize, rowHeight, showMsaLetters } = model
402
394
  ctx.textAlign = 'center'
403
395
  ctx.textBaseline = 'middle'
404
396
  setFontSize(ctx, fontSize)
405
397
  drawTextTrackContent({
406
398
  ctx,
407
399
  data,
408
- colorScheme: customColorScheme ?? modelColorScheme,
400
+ colorScheme: customColorScheme ?? model.colorScheme,
409
401
  contrastText: contrastTextFn(theme),
410
402
  bgColor,
411
403
  drawLetters: showMsaLetters,
@@ -29,8 +29,7 @@ const TreeBranchMenu = observer(function ({
29
29
  top: node.y,
30
30
  }}
31
31
  transitionDuration={0}
32
- keepMounted
33
- open={Boolean(node)}
32
+ open
34
33
  onClose={onClose}
35
34
  >
36
35
  <MenuItem dense disabled>