react-msaview 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) 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/expandSpec.d.ts +46 -0
  51. package/dist/expandSpec.js +127 -0
  52. package/dist/expandSpec.js.map +1 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/model.d.ts +52 -33
  57. package/dist/model.js +232 -142
  58. package/dist/model.js.map +1 -1
  59. package/dist/svgTestUtil.d.ts +28 -24
  60. package/dist/types.d.ts +2 -0
  61. package/dist/useWheelScroll.js +0 -4
  62. package/dist/useWheelScroll.js.map +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +3 -3
  66. package/src/calculateBlocks.ts +5 -3
  67. package/src/components/Loading.tsx +2 -30
  68. package/src/components/SequenceTextArea.tsx +0 -1
  69. package/src/components/Track.tsx +2 -1
  70. package/src/components/dialogs/AboutDialog.tsx +35 -6
  71. package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
  72. package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
  73. package/src/components/dialogs/FeatureDialog.tsx +11 -3
  74. package/src/components/header/Header.tsx +1 -1
  75. package/src/components/header/LoadWarnings.tsx +8 -21
  76. package/src/components/header/settingsMenuItems.ts +3 -3
  77. package/src/components/import/ImportForm.tsx +3 -5
  78. package/src/components/msa/MSACanvas.tsx +0 -3
  79. package/src/components/msa/drawFeatureSpans.ts +80 -25
  80. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
  81. package/src/components/msa/renderMSABlock.ts +9 -8
  82. package/src/components/tracks/drawTracks.ts +8 -16
  83. package/src/components/tree/TreeBranchMenu.tsx +1 -2
  84. package/src/components/tree/TreeCanvas.tsx +0 -3
  85. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  86. package/src/components/tree/TreeNodeMenu.tsx +1 -2
  87. package/src/components/tree/renderTreeCanvas.ts +2 -1
  88. package/src/constants.ts +5 -0
  89. package/src/expandSpec.ts +196 -0
  90. package/src/index.ts +8 -0
  91. package/src/model.ts +289 -162
  92. package/src/types.ts +2 -0
  93. package/src/useWheelScroll.ts +0 -4
  94. package/src/version.ts +1 -1
@@ -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>
@@ -71,9 +71,6 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
71
71
  ref={ref}
72
72
  onMouseDown={onMouseDown}
73
73
  onMouseUp={onMouseUp}
74
- onMouseLeave={event => {
75
- event.preventDefault()
76
- }}
77
74
  style={{
78
75
  height,
79
76
  position: 'relative',
@@ -13,12 +13,12 @@ import { useTreeHover } from './useTreeHover.ts'
13
13
 
14
14
  import type { MsaViewModel } from '../../model.ts'
15
15
 
16
- const useStyles = makeStyles()(() => ({
16
+ const useStyles = makeStyles()(theme => ({
17
17
  hover: {
18
18
  position: 'absolute',
19
19
  pointerEvents: 'none',
20
20
  zIndex: 100,
21
- background: 'rgba(0,0,0,0.1)',
21
+ background: theme.palette.action.hover,
22
22
  },
23
23
  }))
24
24
 
@@ -35,8 +35,7 @@ const TreeMenu = observer(function ({
35
35
  left: node.x,
36
36
  }}
37
37
  transitionDuration={0}
38
- keepMounted
39
- open={Boolean(node)}
38
+ open
40
39
  onClose={onClose}
41
40
  >
42
41
  <MenuItem dense disabled>
@@ -402,7 +402,8 @@ function renderTreeLabels({
402
402
  // label when the "name" is just the auto-generated internal-node id
403
403
  const isAnonymousCollapsed = collapsedSet.has(id) && name === id
404
404
  if (!isAnonymousCollapsed && inYBlock(y, offsetY, by, pad)) {
405
- // note: +rowHeight/4 matches with -rowHeight/4 in msa
405
+ // fontSize/4 below the row center, the baseline the alignment letters
406
+ // share
406
407
  const yp = y + fontSize / 4
407
408
  let xp = 0
408
409
  if (!noTree) {
package/src/constants.ts CHANGED
@@ -48,6 +48,11 @@ export const defaultScrollZoomAxis: ScrollZoomAxis = 'both'
48
48
  export const minLetterRowHeight = 8
49
49
  export const minLetterColWidth = 5
50
50
 
51
+ // A feature span draws its label at two pixels under its height, capped at 11,
52
+ // so a span shorter than this holds no readable text: an underline bar, a
53
+ // sub-row band or a features panel over small rows
54
+ export const minFeatureLabelHeight = 9
55
+
51
56
  // Tree labels are measured once at this size and scaled to the current font
52
57
  // size, so a vertical zoom never re-measures the tree.
53
58
  export const labelReferenceFontSize = 16
@@ -0,0 +1,196 @@
1
+ import type { ColumnTrackSpec, Highlight, Region } from './types.ts'
2
+
3
+ /**
4
+ * `row: null` opts one entry out of the spec's `query` row, back to alignment
5
+ * columns.
6
+ */
7
+ type RowChoice = { row?: string | null }
8
+
9
+ export type HighlightShorthand =
10
+ | number
11
+ | string
12
+ | (Omit<Highlight, 'row'> & RowChoice)
13
+
14
+ export type RegionShorthand = string | (Omit<Region, 'row'> & RowChoice)
15
+
16
+ export type ColumnTrackShorthand = Omit<
17
+ ColumnTrackSpec,
18
+ 'id' | 'kind' | 'row'
19
+ > &
20
+ RowChoice & {
21
+ id?: string
22
+ kind?: ColumnTrackSpec['kind']
23
+ /** the 1-based position `values` or `data` begins at */
24
+ start?: number
25
+ }
26
+
27
+ /**
28
+ * The short forms a hand-written or generated link can use. Every long form
29
+ * passes through unchanged, so a snapshot is also a valid spec.
30
+ */
31
+ export interface MsaSpec {
32
+ /**
33
+ * The row the spec is about: sets `relativeTo`, and is the `row` of every
34
+ * highlight, region and column track that names none
35
+ */
36
+ query?: string
37
+ /** a url, or the alignment itself when it spans more than one line */
38
+ msa?: string
39
+ /** a url, or a newick string when it starts with "(" */
40
+ tree?: string
41
+ highlights?: HighlightShorthand[]
42
+ region?: RegionShorthand
43
+ columnTracks?: ColumnTrackShorthand[]
44
+ [key: string]: unknown
45
+ }
46
+
47
+ /**
48
+ * The label a single-residue shorthand highlight gets, filled in from the
49
+ * sequence when drawn: `175` on a row reading R there becomes "R175".
50
+ */
51
+ export const residueLabel = '{residue}{position}'
52
+
53
+ const span = /^(\d+)(?:-(\d+))?(?:\s+(.+))?$/
54
+
55
+ function parseSpan(text: string, what: string) {
56
+ const match = span.exec(text.trim())
57
+ if (!match) {
58
+ throw new Error(
59
+ `${what} "${text}" is not "start", "start-end" or either followed by a label`,
60
+ )
61
+ }
62
+ const start = Number(match[1])
63
+ const end = match[2] === undefined ? start : Number(match[2])
64
+ return { start, end, label: match[3] }
65
+ }
66
+
67
+ function withRow<T extends RowChoice>(entry: T, query?: string) {
68
+ const { row, ...rest } = entry
69
+ const resolved = row === undefined ? query : (row ?? undefined)
70
+ return resolved === undefined ? rest : { ...rest, row: resolved }
71
+ }
72
+
73
+ function expandHighlight(entry: HighlightShorthand, query?: string) {
74
+ if (typeof entry === 'object') {
75
+ return (
76
+ entry.rows
77
+ ? entry
78
+ : withRow(entry, entry.start === undefined ? undefined : query)
79
+ ) as Highlight
80
+ }
81
+ const { start, end, label } = parseSpan(String(entry), 'highlight')
82
+ const single = start === end && query !== undefined
83
+ return {
84
+ ...(query === undefined ? {} : { row: query }),
85
+ start,
86
+ end,
87
+ ...(label ? { label } : single ? { label: residueLabel } : {}),
88
+ }
89
+ }
90
+
91
+ function expandRegion(region: RegionShorthand, query?: string) {
92
+ if (typeof region === 'string') {
93
+ const { start, end } = parseSpan(region, 'region')
94
+ const parsed: Omit<Region, 'row'> & RowChoice = { start, end }
95
+ return withRow(parsed, query) as Region
96
+ }
97
+ return withRow(region, query) as Region
98
+ }
99
+
100
+ function slug(name: string) {
101
+ return name
102
+ .toLowerCase()
103
+ .replaceAll(/[^a-z0-9]+/g, '-')
104
+ .replaceAll(/^-|-$/g, '')
105
+ }
106
+
107
+ function kindOf(track: ColumnTrackShorthand) {
108
+ if (track.kind) {
109
+ return track.kind
110
+ }
111
+ if (track.values) {
112
+ return 'bar'
113
+ }
114
+ if (track.data !== undefined) {
115
+ return 'text'
116
+ }
117
+ if (track.arcs) {
118
+ return 'arc'
119
+ }
120
+ throw new Error(
121
+ `column track "${track.name}" has no values, data or arcs to take its kind from`,
122
+ )
123
+ }
124
+
125
+ function expandTrack(
126
+ track: ColumnTrackShorthand,
127
+ query: string | undefined,
128
+ ids: Set<string>,
129
+ ): ColumnTrackSpec {
130
+ const { start, ...rest } = track
131
+ const pad = start && start > 1 ? start - 1 : 0
132
+ let id = track.id ?? (slug(track.name) || 'track')
133
+ for (let n = 2; !track.id && ids.has(id); n++) {
134
+ id = `${slug(track.name) || 'track'}-${n}`
135
+ }
136
+ ids.add(id)
137
+ return {
138
+ ...(withRow(rest, query) as Omit<ColumnTrackSpec, 'id' | 'kind'>),
139
+ id,
140
+ kind: kindOf(track),
141
+ ...(pad && track.values
142
+ ? { values: [...Array<number>(pad).fill(0), ...track.values] }
143
+ : {}),
144
+ ...(pad && track.data !== undefined
145
+ ? { data: ' '.repeat(pad) + track.data }
146
+ : {}),
147
+ }
148
+ }
149
+
150
+ function isInlineMsa(msa: string) {
151
+ return msa.includes('\n')
152
+ }
153
+
154
+ function isInlineTree(tree: string) {
155
+ return tree.trimStart().startsWith('(')
156
+ }
157
+
158
+ function uri(location: string) {
159
+ return { uri: location, locationType: 'UriLocation' as const }
160
+ }
161
+
162
+ /**
163
+ * Expand the short forms of an MsaView spec into the snapshot the model takes.
164
+ * Idempotent, and a no-op on a spec that uses none of them.
165
+ */
166
+ export function expandSpec(spec: MsaSpec): Record<string, unknown> {
167
+ const { query, msa, tree, highlights, region, columnTracks, ...rest } = spec
168
+ const data = {
169
+ ...(rest.data as Record<string, unknown> | undefined),
170
+ ...(msa !== undefined && isInlineMsa(msa) ? { msa } : {}),
171
+ ...(tree !== undefined && isInlineTree(tree) ? { tree } : {}),
172
+ }
173
+ const ids = new Set(
174
+ (columnTracks ?? []).map(t => t.id).filter(id => id !== undefined),
175
+ )
176
+ return {
177
+ ...rest,
178
+ ...(msa !== undefined && !isInlineMsa(msa)
179
+ ? { msaFilehandle: uri(msa) }
180
+ : {}),
181
+ ...(tree !== undefined && !isInlineTree(tree)
182
+ ? { treeFilehandle: uri(tree) }
183
+ : {}),
184
+ ...(Object.keys(data).length > 0 ? { data } : {}),
185
+ ...(query !== undefined && rest.relativeTo === undefined
186
+ ? { relativeTo: query }
187
+ : {}),
188
+ ...(highlights
189
+ ? { highlights: highlights.map(h => expandHighlight(h, query)) }
190
+ : {}),
191
+ ...(region === undefined ? {} : { region: expandRegion(region, query) }),
192
+ ...(columnTracks
193
+ ? { columnTracks: columnTracks.map(t => expandTrack(t, query, ids)) }
194
+ : {}),
195
+ }
196
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,14 @@ export {
18
18
  // the row count above which calculateNeighborJoiningTreeFromMSA throws, for
19
19
  // hosts gating their own menu item
20
20
  export { maxNeighborJoiningRows } from './constants.ts'
21
+ // the short forms a link or a script can write a view in; see docs/layers.md
22
+ export { expandSpec, residueLabel } from './expandSpec.ts'
23
+ export type {
24
+ ColumnTrackShorthand,
25
+ HighlightShorthand,
26
+ MsaSpec,
27
+ RegionShorthand,
28
+ } from './expandSpec.ts'
21
29
  export { default as MSAView } from './components/Loading.tsx'
22
30
  export { default as MSAViewer } from './components/MSAViewer.tsx'
23
31
  export type { MSAViewerProps } from './components/MSAViewer.tsx'