react-msaview 5.4.0 → 5.5.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/LICENSE +21 -0
  2. package/bundle/index.js +465 -138
  3. package/bundle/index.js.map +7 -1
  4. package/dist/colorSchemes.js +11 -7
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/Checkbox2.js +3 -1
  7. package/dist/components/Checkbox2.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +7 -1
  9. package/dist/components/MSAViewer.js +11 -10
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/Track.js +8 -5
  12. package/dist/components/Track.js.map +1 -1
  13. package/dist/components/dialogs/FeatureDialog.js +27 -23
  14. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  15. package/dist/components/dialogs/InterProScanDialog.js +2 -1
  16. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  17. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  18. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  19. package/dist/components/msa/DomainLegend.d.ts +6 -0
  20. package/dist/components/msa/DomainLegend.js +41 -0
  21. package/dist/components/msa/DomainLegend.js.map +1 -0
  22. package/dist/components/msa/MSACanvasBlock.js +17 -5
  23. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  24. package/dist/components/msa/MSAPanel.js +3 -1
  25. package/dist/components/msa/MSAPanel.js.map +1 -1
  26. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +58 -14
  27. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  28. package/dist/components/msa/renderMSABlock.js +7 -9
  29. package/dist/components/msa/renderMSABlock.js.map +1 -1
  30. package/dist/components/msa/renderMSAMouseover.js +32 -4
  31. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  32. package/dist/components/renderCtx.d.ts +1 -1
  33. package/dist/components/tracks/renderTracksSvg.js +4 -4
  34. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  35. package/dist/components/tree/renderTreeCanvas.js +3 -0
  36. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  37. package/dist/constants.d.ts +3 -0
  38. package/dist/constants.js +16 -0
  39. package/dist/constants.js.map +1 -1
  40. package/dist/createPaletteMap.d.ts +5 -2
  41. package/dist/createPaletteMap.js +15 -5
  42. package/dist/createPaletteMap.js.map +1 -1
  43. package/dist/hierarchy.js +4 -1
  44. package/dist/hierarchy.js.map +1 -1
  45. package/dist/launchInterProScan.d.ts +2 -0
  46. package/dist/launchInterProScan.js.map +1 -1
  47. package/dist/model/msaModel.js.map +1 -1
  48. package/dist/model.d.ts +100 -3
  49. package/dist/model.js +121 -19
  50. package/dist/model.js.map +1 -1
  51. package/dist/neighborJoining.js +44 -30
  52. package/dist/neighborJoining.js.map +1 -1
  53. package/dist/renderToSvg.js +104 -74
  54. package/dist/renderToSvg.js.map +1 -1
  55. package/dist/{webpack.js → umd.js} +1 -1
  56. package/dist/umd.js.map +1 -0
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +8 -7
  60. package/src/colorSchemes.ts +12 -7
  61. package/src/components/Checkbox2.tsx +7 -1
  62. package/src/components/MSAViewer.tsx +31 -21
  63. package/src/components/Track.tsx +8 -5
  64. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  65. package/src/components/dialogs/InterProScanDialog.tsx +2 -1
  66. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  67. package/src/components/msa/DomainLegend.tsx +74 -0
  68. package/src/components/msa/MSACanvasBlock.tsx +22 -4
  69. package/src/components/msa/MSAPanel.tsx +2 -0
  70. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +73 -17
  71. package/src/components/msa/renderMSABlock.ts +5 -11
  72. package/src/components/msa/renderMSAMouseover.ts +32 -4
  73. package/src/components/renderCtx.ts +1 -0
  74. package/src/components/tracks/renderTracksSvg.ts +4 -4
  75. package/src/components/tree/renderTreeCanvas.ts +5 -1
  76. package/src/constants.ts +18 -0
  77. package/src/createPaletteMap.ts +18 -5
  78. package/src/hierarchy.ts +4 -1
  79. package/src/launchInterProScan.ts +3 -1
  80. package/src/model/msaModel.ts +3 -1
  81. package/src/model.ts +137 -36
  82. package/src/neighborJoining.ts +44 -31
  83. package/src/renderDomainLegendSvg.test.tsx +134 -0
  84. package/src/renderToSvg.tsx +210 -147
  85. package/src/segmentOverlay.test.ts +70 -0
  86. package/src/version.ts +1 -1
  87. package/bundle/index.js.LICENSE.txt +0 -100
  88. package/dist/components/util.d.ts +0 -1
  89. package/dist/components/util.js +0 -7
  90. package/dist/components/util.js.map +0 -1
  91. package/dist/webpack.js.map +0 -1
  92. package/src/components/util.ts +0 -9
  93. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  94. /package/src/{webpack.ts → umd.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { getVisibleLeaves } from './getVisibleLeaves.ts'
2
+ import { subFeatureRowHeight } from '../../constants.ts'
2
3
 
3
4
  import type { HierarchyNode } from '../../hierarchy.ts'
4
5
  import type { MsaViewModel } from '../../model.ts'
@@ -53,38 +54,93 @@ function drawTiles({
53
54
  rowHeight,
54
55
  fillPalette,
55
56
  strokePalette,
57
+ segmentLabels,
58
+ showMsaLetters,
56
59
  tidyFilteredGatheredInterProAnnotations,
57
60
  } = model
61
+ const h = subFeatureRows ? subFeatureRowHeight : rowHeight
62
+ // exon numbers label the bands only when residue letters aren't drawn (zoomed
63
+ // out); when letters show, the alternating shades alone mark the boundaries
64
+ // and a number would collide with the sequence
65
+ const drawSegmentLabels = !showMsaLetters && !subFeatureRows && h >= 9
58
66
 
59
67
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
60
68
  const node = visibleLeaves[i]!
61
- const {
62
- x,
63
- data: { name },
64
- } = node
65
- const y = x!
66
-
69
+ const { name } = node.data
70
+ const y = node.x!
67
71
  const entry = tidyFilteredGatheredInterProAnnotations[name]
68
72
 
69
73
  if (entry) {
70
74
  for (let j = 0, l2 = entry.length; j < l2; j++) {
71
- const { start, end, accession } = entry[j]!
75
+ const { start, end, accession, strand } = entry[j]!
72
76
  // Convert sequence positions to visible column positions
73
77
  // seqPos is 1-based from InterPro, so subtract 1 for 0-based
74
78
  const m1 = model.seqPosToVisibleCol(name, start - 1)
75
79
  const m2 = model.seqPosToVisibleCol(name, end)
76
- if (m1 === undefined || m2 === undefined) {
77
- continue // Skip if either position is hidden
80
+ if (m1 !== undefined && m2 !== undefined) {
81
+ const x = m1 * colWidth
82
+ const t = y - rowHeight + (subFeatureRows ? j * h : 0)
83
+ const lw = colWidth * (m2 - m1)
84
+ ctx.fillStyle = fillPalette[accession]!
85
+ ctx.strokeStyle = strokePalette[accession]!
86
+ if (strand === undefined) {
87
+ ctx.fillRect(x, t, lw, h)
88
+ ctx.strokeRect(x, t, lw, h)
89
+ // segment (exon) number drawn once per band, on the top visible row,
90
+ // so it reads as a column header for the whole band
91
+ const label = drawSegmentLabels ? segmentLabels.get(accession) : undefined
92
+ if (label !== undefined && i === 0) {
93
+ const fontSize = Math.min(h - 2, 11)
94
+ ctx.font = `${fontSize}px sans-serif`
95
+ if (ctx.measureText(label).width + 2 <= lw) {
96
+ ctx.fillStyle = '#222'
97
+ ctx.textAlign = 'center'
98
+ ctx.textBaseline = 'middle'
99
+ ctx.fillText(label, x + lw / 2, t + h / 2)
100
+ }
101
+ }
102
+ } else {
103
+ drawGeneArrow({ ctx, x, t, w: lw, h, strand })
104
+ }
78
105
  }
79
- const x = m1 * colWidth
80
- ctx.fillStyle = fillPalette[accession]!
81
- ctx.strokeStyle = strokePalette[accession]!
82
- const h = subFeatureRows ? 4 : rowHeight
83
- const t = y - rowHeight + (subFeatureRows ? j * h : 0)
84
- const lw = colWidth * (m2 - m1)
85
- ctx.fillRect(x, t, lw, h)
86
- ctx.strokeRect(x, t, lw, h)
87
106
  }
88
107
  }
89
108
  }
90
109
  }
110
+
111
+ // A gene arrow: a full-width rectangular body spanning the feature's
112
+ // start..end columns, plus a triangular head that points *beyond* that end in
113
+ // the strand direction (right for +, left for -). Keeping the body aligned to
114
+ // the exact start/end columns means + and - strand features read as having the
115
+ // same boundaries; the arrow lives outside them purely to show transcription
116
+ // direction, preserving the column-by-column homology down the rows.
117
+ function drawGeneArrow({
118
+ ctx,
119
+ x,
120
+ t,
121
+ w,
122
+ h,
123
+ strand,
124
+ }: {
125
+ ctx: RenderCtx
126
+ x: number
127
+ t: number
128
+ w: number
129
+ h: number
130
+ strand: number
131
+ }) {
132
+ // body spans bodyStart..bodyEnd (the exact columns); the head extends one
133
+ // row-height past bodyEnd in the strand direction
134
+ const dir = strand > 0 ? 1 : -1
135
+ const bodyStart = strand > 0 ? x : x + w
136
+ const bodyEnd = strand > 0 ? x + w : x
137
+ ctx.beginPath()
138
+ ctx.moveTo(bodyStart, t)
139
+ ctx.lineTo(bodyEnd, t)
140
+ ctx.lineTo(bodyEnd + dir * h, t + h / 2)
141
+ ctx.lineTo(bodyEnd, t + h)
142
+ ctx.lineTo(bodyStart, t + h)
143
+ ctx.closePath()
144
+ ctx.fill()
145
+ ctx.stroke()
146
+ }
@@ -46,6 +46,7 @@ export function renderMSABlock({
46
46
  ctx.scale(k, k)
47
47
  ctx.translate(-offsetX, rowHeight / 2 - offsetY)
48
48
  ctx.textAlign = 'center'
49
+ ctx.textBaseline = 'alphabetic'
49
50
  setFontSize(ctx, fontSize, !bgColor)
50
51
 
51
52
  const { xStart, xEnd } = visibleColRange({
@@ -65,7 +66,6 @@ export function renderMSABlock({
65
66
  ctx,
66
67
  theme,
67
68
  contrastScheme,
68
- offsetX,
69
69
  xStart,
70
70
  xEnd,
71
71
  visibleLeaves,
@@ -75,7 +75,6 @@ export function renderMSABlock({
75
75
  drawText({
76
76
  model,
77
77
  ctx,
78
- offsetX,
79
78
  xStart,
80
79
  xEnd,
81
80
  visibleLeaves,
@@ -94,7 +93,6 @@ export function renderMSABlock({
94
93
 
95
94
  function drawTilesAndText({
96
95
  model,
97
- offsetX,
98
96
  ctx,
99
97
  theme,
100
98
  contrastScheme,
@@ -104,7 +102,6 @@ function drawTilesAndText({
104
102
  referenceSeq,
105
103
  }: {
106
104
  model: MsaViewModel
107
- offsetX: number
108
105
  theme: Theme
109
106
  ctx: RenderCtx
110
107
  contrastScheme: Record<string, string>
@@ -128,11 +125,10 @@ function drawTilesAndText({
128
125
 
129
126
  const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
130
127
  const isPercentIdentity = colorSchemeName === 'percent_identity_dynamic'
131
- const drawBgTiles = bgColor || isClustalX || isPercentIdentity
132
- if (!drawBgTiles && !showMsaLetters) {
128
+ if (!bgColor && !showMsaLetters) {
133
129
  return
134
130
  }
135
- const offsetXAligned = offsetX - (offsetX % colWidth)
131
+ const offsetXAligned = xStart * colWidth
136
132
  const halfColWidth = colWidth / 2
137
133
  const quarterRowHeight = rowHeight / 4
138
134
 
@@ -163,7 +159,7 @@ function drawTilesAndText({
163
159
  color = colorScheme[letter]
164
160
  }
165
161
 
166
- if (drawBgTiles) {
162
+ if (bgColor) {
167
163
  ctx.fillStyle = isMatchingReference
168
164
  ? theme.palette.action.hover
169
165
  : color || theme.palette.background.default
@@ -190,14 +186,12 @@ function drawTilesAndText({
190
186
  // so we only need to draw text here.
191
187
  function drawText({
192
188
  model,
193
- offsetX,
194
189
  ctx,
195
190
  visibleLeaves,
196
191
  xStart,
197
192
  xEnd,
198
193
  referenceSeq,
199
194
  }: {
200
- offsetX: number
201
195
  model: MsaViewModel
202
196
  ctx: RenderCtx
203
197
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
@@ -210,7 +204,7 @@ function drawText({
210
204
  if (!showMsaLetters) {
211
205
  return
212
206
  }
213
- const offsetXAligned = offsetX - (offsetX % colWidth)
207
+ const offsetXAligned = xStart * colWidth
214
208
  const halfColWidth = colWidth / 2
215
209
  const quarterRowHeight = rowHeight / 4
216
210
  // note: -rowHeight/4 matches +rowHeight/4 in tree
@@ -2,9 +2,31 @@ import type { MsaViewModel } from '../../model.ts'
2
2
 
3
3
  const hoverColor = 'rgba(0,0,0,0.15)'
4
4
  const highlightColor = 'rgba(128,128,0,0.2)'
5
+ // the persistent highlightColumns overlay: a stronger fill plus a solid border
6
+ // so a domain/motif band reads clearly over the colored alignment cells (the
7
+ // faint hover-style wash alone is invisible against clustalx coloring)
8
+ const highlightColumnsFill = 'rgba(255,140,0,0.28)'
9
+ const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
5
10
  const referenceColor = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
6
11
  const multiRowHoverColor = 'rgba(255,165,0,0.15)' // Orange highlight for multi-row tree hover
7
12
 
13
+ // Collapse sorted column indices into contiguous [start,end] runs so a run of
14
+ // highlighted columns draws as one bordered band rather than per-column slices
15
+ // (which would draw internal borders).
16
+ function contiguousRuns(columns: number[]) {
17
+ const sorted = [...columns].sort((a, b) => a - b)
18
+ const runs: { start: number; end: number }[] = []
19
+ for (const col of sorted) {
20
+ const last = runs.at(-1)
21
+ if (last && col === last.end + 1) {
22
+ last.end = col
23
+ } else {
24
+ runs.push({ start: col, end: col })
25
+ }
26
+ }
27
+ return runs
28
+ }
29
+
8
30
  export function renderMouseover({
9
31
  ctx,
10
32
  model,
@@ -54,11 +76,17 @@ export function renderMouseover({
54
76
  }
55
77
  }
56
78
 
57
- // Highlight multiple columns
79
+ // Highlight multiple columns — draw each contiguous run as a filled, bordered
80
+ // band so a domain/motif highlight is clearly visible over the alignment
58
81
  if (highlightedColumns?.length) {
59
- ctx.fillStyle = highlightColor
60
- for (const col of highlightedColumns) {
61
- ctx.fillRect(col * colWidth + scrollX, 0, colWidth, height)
82
+ ctx.lineWidth = 2
83
+ for (const { start, end } of contiguousRuns(highlightedColumns)) {
84
+ const x = start * colWidth + scrollX
85
+ const w = (end - start + 1) * colWidth
86
+ ctx.fillStyle = highlightColumnsFill
87
+ ctx.fillRect(x, 0, w, height)
88
+ ctx.strokeStyle = highlightColumnsBorder
89
+ ctx.strokeRect(x, 0, w, height)
62
90
  }
63
91
  }
64
92
 
@@ -26,5 +26,6 @@ export type RenderCtx = Pick<
26
26
  | 'strokeRect'
27
27
  | 'strokeStyle'
28
28
  | 'textAlign'
29
+ | 'textBaseline'
29
30
  | 'translate'
30
31
  >
@@ -65,13 +65,13 @@ export function drawTextTrackContent({
65
65
 
66
66
  for (let i = 0; str && i < str.length; i++) {
67
67
  const letter = str[i]!
68
- const color = colorScheme[letter.toUpperCase()]
68
+ const upper = letter.toUpperCase()
69
69
  if (bgColor) {
70
- const x = i * colWidth + offsetX - (offsetX % colWidth)
71
- ctx.fillStyle = color || 'white'
70
+ const x = (xStart + i) * colWidth
71
+ ctx.fillStyle = colorScheme[upper] || 'white'
72
72
  ctx.fillRect(x, 0, colWidth, rowHeight)
73
73
  if (rowHeight >= 10 && colWidth >= rowHeight / 2) {
74
- ctx.fillStyle = contrastScheme[letter.toUpperCase()] || 'black'
74
+ ctx.fillStyle = contrastScheme[upper] || 'black'
75
75
  ctx.fillText(letter, x + colWidth / 2, rowHeight / 2 + 1)
76
76
  }
77
77
  }
@@ -29,6 +29,9 @@ function getNodeX(
29
29
  if (showBranchLen) {
30
30
  return node.len
31
31
  }
32
+ if (maxDepthToLeaf === 0) {
33
+ return 0
34
+ }
32
35
  const depthToLeaf = calcDepthToLeaf(node)
33
36
  return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
34
37
  }
@@ -134,7 +137,8 @@ export function renderCollapsedTriangles({
134
137
  if (collapsed.includes(id)) {
135
138
  const apexX = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
136
139
  const y = node.x!
137
- const inBlock = y > offsetY - extendBounds && y < offsetY + by + extendBounds
140
+ const inBlock =
141
+ y > offsetY - extendBounds && y < offsetY + by + extendBounds
138
142
  // in cladogram mode the hidden tips align at the right edge, otherwise use
139
143
  // the branch-length extent recorded in the model's hierarchy getter
140
144
  const baseX = showBranchLen
package/src/constants.ts CHANGED
@@ -9,6 +9,24 @@ export const defaultShowDomains = false
9
9
  export const defaultHideGaps = true
10
10
  export const defaultAllowedGappyness = 100
11
11
  export const defaultSubFeatureRows = false
12
+ // row height for each stacked feature when subFeatureRows is on
13
+ export const subFeatureRowHeight = 4
14
+
15
+ // Feature types that are ordinal *segments* of a single transcript (exons and
16
+ // the like) rather than categorical domains. Their identity is their position,
17
+ // so the overlay alternates two shades to mark boundaries and labels them by
18
+ // number, instead of assigning every one a distinct hue + legend row.
19
+ export const segmentFeatureTypes = new Set([
20
+ 'exon',
21
+ 'CDS',
22
+ 'five_prime_UTR',
23
+ 'three_prime_UTR',
24
+ 'UTR',
25
+ 'intron',
26
+ ])
27
+
28
+ // the two shades alternated across adjacent segments (exons)
29
+ export const segmentShades = ['#9fb6d4', '#d4dcea']
12
30
  export const defaultDrawMsaLetters = true
13
31
  export const defaultScrollZoom = false
14
32
 
@@ -1,11 +1,24 @@
1
+ import { colord } from 'colord'
2
+
1
3
  import palettes from './ggplotPalettes.ts'
2
4
 
3
5
  /**
4
- * Creates a map from keys to colors using ggplot-style palettes.
5
- * The palette is selected based on the number of keys.
6
+ * Creates a map from keys to colors. Up to the largest ggplot palette (8 keys)
7
+ * we use the hand-picked ggplot colors; beyond that e.g. a gene-structure
8
+ * overlay with one feature per exon — we generate evenly spaced HSL hues so
9
+ * every key still gets a distinct, stable color instead of running off the end
10
+ * of the palette.
6
11
  */
7
12
  export function createPaletteMap(keys: string[]) {
8
- const k = Math.min(keys.length - 1, palettes.length - 1)
9
- const palette = palettes[k]!
10
- return Object.fromEntries(keys.map((key, i) => [key, palette[i]!]))
13
+ const n = keys.length
14
+ if (n <= palettes.length) {
15
+ const palette = palettes[n - 1]!
16
+ return Object.fromEntries(keys.map((key, i) => [key, palette[i]!]))
17
+ }
18
+ return Object.fromEntries(
19
+ keys.map((key, i) => [
20
+ key,
21
+ colord({ h: (i * 360) / n, s: 60, l: 60 }).toHex(),
22
+ ]),
23
+ )
11
24
  }
package/src/hierarchy.ts CHANGED
@@ -269,7 +269,10 @@ export function collapsedSubtreeLengthExtent<T extends { length?: number }>(
269
269
  const kids = n.children ?? n._children
270
270
  if (kids?.length) {
271
271
  for (const child of kids) {
272
- stack.push({ node: child, acc: acc + Math.max(child.data.length ?? 0, 0) })
272
+ stack.push({
273
+ node: child,
274
+ acc: acc + Math.max(child.data.length ?? 0, 0),
275
+ })
273
276
  }
274
277
  } else {
275
278
  min = Math.min(min, acc)
@@ -13,9 +13,11 @@ export interface InterProScanResults {
13
13
  name: string
14
14
  description: string
15
15
  accession: string
16
+ // original GFF feature type (exon, CDS, ...) when sourced from GFF
17
+ featureType?: string
16
18
  }
17
19
  }
18
- locations: { start: number; end: number }[]
20
+ locations: { start: number; end: number; strand?: number }[]
19
21
  }[]
20
22
  xref: { id: string }[]
21
23
  }
@@ -29,7 +29,9 @@ export function MSAModelF() {
29
29
  * force the MSA data to be parsed as a specific format instead of relying
30
30
  * on auto-detection (which is ambiguous between e.g. fasta and a3m)
31
31
  */
32
- msaFormat: types.maybe(types.enumeration<MSAFormat>('MSAFormat', msaFormats)),
32
+ msaFormat: types.maybe(
33
+ types.enumeration<MSAFormat>('MSAFormat', msaFormats),
34
+ ),
33
35
  })
34
36
  .actions(self => ({
35
37
  /**