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
package/src/model.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isAlive,
10
10
  types,
11
11
  } from '@jbrowse/mobx-state-tree'
12
- import { autorun, transaction } from 'mobx'
12
+ import { autorun, computed, transaction } from 'mobx'
13
13
  import {
14
14
  generateNodeIds,
15
15
  gffToAnnotations,
@@ -111,7 +111,6 @@ import {
111
111
  len,
112
112
  outlineColor,
113
113
  skipBlanks,
114
- transform,
115
114
  withAlpha,
116
115
  } from './util.ts'
117
116
  import { saveAs } from './vendor/fileSaver.ts'
@@ -156,6 +155,7 @@ import type {
156
155
  } from './types.ts'
157
156
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
158
157
  import type { Instance } from '@jbrowse/mobx-state-tree'
158
+ import type { IComputedValue } from 'mobx'
159
159
  import type { InterProScanResults } from 'msa-parsers'
160
160
 
161
161
  function parseTreeText(text: string) {
@@ -257,6 +257,56 @@ function featureLabelMap(annotations: Annotation[], field: string) {
257
257
  return labels
258
258
  }
259
259
 
260
+ /** what `packDomainLanes` needs to lay a row out, before it has its lane */
261
+ type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
262
+ startCol: number
263
+ endCol: number
264
+ }
265
+
266
+ /**
267
+ * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
268
+ * forward features stack above the line and the reverse below it, each strand
269
+ * packing on its own. The deepest row on each side sets the grid every row lays
270
+ * out on, so the line between the strands sits at one height down the panel and
271
+ * a reader can scan it. A feature with no strand piles with the forward ones.
272
+ */
273
+ function strandpileLanes(
274
+ rows: [string, Unlaned[]][],
275
+ ): Map<string, RowPanelSpan[]> {
276
+ const piled = rows.map(([name, bands]) => {
277
+ const reverse = packDomainLanes(
278
+ bands.filter(b => b.annotation.strand === -1),
279
+ )
280
+ const forward = packDomainLanes(
281
+ bands.filter(b => b.annotation.strand !== -1),
282
+ )
283
+ return { name, forward, reverse }
284
+ })
285
+ const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
286
+ const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
287
+ const laneCount = up + down
288
+ return new Map(
289
+ piled.map(({ name, forward, reverse }) => [
290
+ name,
291
+ [
292
+ // forward level 0 is the lane just above the line, stacking upward
293
+ ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
294
+ ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
295
+ ],
296
+ ]),
297
+ )
298
+ }
299
+
300
+ /** one row's spans laid out by the panel's `position` */
301
+ function panelLanes(
302
+ panel: RowFeaturesSpec,
303
+ rows: [string, Unlaned[]][],
304
+ ): Map<string, RowPanelSpan[]> {
305
+ return panel.position === 'strandpile'
306
+ ? strandpileLanes(rows)
307
+ : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
308
+ }
309
+
260
310
  /**
261
311
  * The spans a `features` panel draws, keyed by row name and measured in the
262
312
  * panel's own pixels. `column` takes the bands the overlay draws, at the
@@ -268,7 +318,6 @@ function featureLabelMap(annotations: Annotation[], field: string) {
268
318
  function featurePanelSpans({
269
319
  panel,
270
320
  width,
271
- rowHeight,
272
321
  colWidth,
273
322
  domainBands,
274
323
  annotationsByRow,
@@ -276,14 +325,14 @@ function featurePanelSpans({
276
325
  }: {
277
326
  panel: RowFeaturesSpec
278
327
  width: number
279
- rowHeight: number
280
328
  colWidth: number
281
329
  domainBands: Map<string, DomainBand[]>
282
330
  annotationsByRow: Record<string, Annotation[]>
283
331
  shifts: Map<string, number> | undefined
284
332
  }): Map<string, RowPanelSpan[]> {
285
333
  if (panel.x === 'column') {
286
- return new Map(
334
+ return panelLanes(
335
+ panel,
287
336
  [...domainBands].map(([name, bands]) => [
288
337
  name,
289
338
  bands.map(band => ({
@@ -315,26 +364,25 @@ function featurePanelSpans({
315
364
  max = Math.max(max, end)
316
365
  }
317
366
  }
318
- // the arrowhead on the rightmost feature reaches a row height past its end,
319
- // so the extent maps onto the panel less that much
320
- const drawable = Math.max(1, width - rowHeight)
367
+ // a gene arrow ends where its feature does, so the extent maps onto the whole
368
+ // panel less the pixel the rightmost stroke needs
369
+ const drawable = Math.max(1, width - 1)
321
370
  const scale = max > min ? drawable / (max - min) : 0
322
371
  // a lane opens where a span covers more than a tenth of the one before it,
323
372
  // which keeps the genes of an operon on one lane: adjacent genes commonly
324
373
  // share a few bases, and a stop codon overlapping the next start reads as a
325
374
  // second lane over the whole row
326
- return new Map(
375
+ return panelLanes(
376
+ panel,
327
377
  shifted.map(([name, features]) => [
328
378
  name,
329
- packDomainLanes(
330
- features.map(({ annotation, start, end }) => ({
331
- annotation,
332
- xStart: (start - min) * scale,
333
- xEnd: (end - min) * scale,
334
- startCol: start,
335
- endCol: end - (end - start) * laneOverlap,
336
- })),
337
- ),
379
+ features.map(({ annotation, start, end }) => ({
380
+ annotation,
381
+ xStart: (start - min) * scale,
382
+ xEnd: (end - min) * scale,
383
+ startCol: start,
384
+ endCol: end - (end - start) * laneOverlap,
385
+ })),
338
386
  ]),
339
387
  )
340
388
  }
@@ -798,11 +846,7 @@ function stateModelFactory() {
798
846
  * data from the loaded tree/msa/treeMetadata, generally loaded by
799
847
  * autorun
800
848
  */
801
- data: types.optional(DataModelF(), {
802
- tree: '',
803
- msa: '',
804
- treeMetadata: '',
805
- }),
849
+ data: types.optional(DataModelF(), {}),
806
850
 
807
851
  /**
808
852
  * #property
@@ -833,6 +877,14 @@ function stateModelFactory() {
833
877
  * Persists in the snapshot and the URL.
834
878
  */
835
879
  highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
880
+ /**
881
+ * #property
882
+ * where the view opens, in `highlights` coordinates: `{row, start,
883
+ * end}` zooms onto residues of that row, `{start, end}` onto alignment
884
+ * columns. Applied once the alignment and any tree file have loaded,
885
+ * then cleared, so a reloaded session keeps the reader's own scroll.
886
+ */
887
+ region: types.frozen<Region | undefined>(),
836
888
  /**
837
889
  * #property
838
890
  * clades of the tree with a mark drawn over them. `mrca` names tips
@@ -1064,10 +1116,12 @@ function stateModelFactory() {
1064
1116
  /**
1065
1117
  * #action
1066
1118
  * record a non-fatal load problem: a layer that failed to load, a file
1067
- * that failed to parse
1119
+ * that failed to parse. A message already on the list is not added again
1068
1120
  */
1069
1121
  addWarning(warning: string) {
1070
- self.warnings = [...self.warnings, warning]
1122
+ if (!self.warnings.includes(warning)) {
1123
+ self.warnings = [...self.warnings, warning]
1124
+ }
1071
1125
  },
1072
1126
 
1073
1127
  /**
@@ -1110,6 +1164,12 @@ function stateModelFactory() {
1110
1164
  setHighlightedColumns(columns?: number[]) {
1111
1165
  self.highlightedColumns = columns
1112
1166
  },
1167
+ /**
1168
+ * #action
1169
+ */
1170
+ setRegion(region?: Region) {
1171
+ self.region = region
1172
+ },
1113
1173
  /**
1114
1174
  * #action
1115
1175
  */
@@ -1776,7 +1836,7 @@ function stateModelFactory() {
1776
1836
  */
1777
1837
  get insertionPositions() {
1778
1838
  const { blanks, rows } = this
1779
- if (blanks.length === 0 || !self.hideGapsEffective) {
1839
+ if (blanks.length === 0) {
1780
1840
  return new Map<string, { pos: number; letters: string }[]>()
1781
1841
  }
1782
1842
  const result = new Map<string, { pos: number; letters: string }[]>()
@@ -2039,7 +2099,7 @@ function stateModelFactory() {
2039
2099
  * cladogram mode. The tree's scale bar uses it.
2040
2100
  */
2041
2101
  get pxPerBranchLength() {
2042
- const max = maxLength(this.root)
2102
+ const max = this.rootToTipLength
2043
2103
  return this.showBranchLenEffective && max ? self.treeWidth / max : 0
2044
2104
  },
2045
2105
 
@@ -2094,6 +2154,140 @@ function stateModelFactory() {
2094
2154
  return self.drawLabels && self.rowHeight >= minLetterRowHeight
2095
2155
  },
2096
2156
  }))
2157
+ .views(self => {
2158
+ // a spec is a frozen value, so it keys its computed until setColumnTracks
2159
+ // replaces it
2160
+ const columnTrackModels = new WeakMap<
2161
+ ColumnTrackSpec,
2162
+ IComputedValue<BasicTrack>
2163
+ >()
2164
+ return {
2165
+ /**
2166
+ * #getter
2167
+ * a data track's values or string, projected from its row's residues
2168
+ * onto alignment columns when it names a row
2169
+ */
2170
+ get columnTrackContent() {
2171
+ const { MSA, blanks, hideGapsEffective } = self
2172
+ const width = MSA?.getWidth() ?? 0
2173
+ const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
2174
+ if (!track.row) {
2175
+ return items
2176
+ }
2177
+ const out = Array.from({ length: width }, () => fill)
2178
+ const index = self.seqPosIndex(track.row)
2179
+ items.forEach((item, seqPos) => {
2180
+ const col = index?.[seqPos]
2181
+ if (col !== undefined) {
2182
+ out[col] = item
2183
+ }
2184
+ })
2185
+ return out
2186
+ }
2187
+ const skip = <T>(items: T[]) =>
2188
+ hideGapsEffective ? dropBlanks(blanks, items) : items
2189
+ // an arc endpoint maps row residue -> column -> visible column.
2190
+ // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
2191
+ // hidden column moves to the neighboring visible one and the arc stays
2192
+ const resolve = (track: ColumnTrackSpec, pos: number) => {
2193
+ const col = track.row
2194
+ ? self.seqPosIndex(track.row)?.[pos - 1]
2195
+ : pos - 1
2196
+ if (col === undefined || col < 0 || col >= width) {
2197
+ return undefined
2198
+ }
2199
+ return hideGapsEffective ? visibleColsBefore(blanks, col) : col
2200
+ }
2201
+ return new Map<
2202
+ string,
2203
+ { values?: number[]; data?: string; arcs?: Arc[] }
2204
+ >(
2205
+ self.columnTracks.map(track => {
2206
+ if (track.kind === 'arc') {
2207
+ const arcs = (track.arcs ?? [])
2208
+ .map(arc => {
2209
+ const start = resolve(track, Math.min(arc.start, arc.end))
2210
+ const end = resolve(track, Math.max(arc.start, arc.end))
2211
+ return start !== undefined &&
2212
+ end !== undefined &&
2213
+ start < end
2214
+ ? { start, end, color: arc.color }
2215
+ : undefined
2216
+ })
2217
+ .filter(notEmpty)
2218
+ return [track.id, { arcs }] as const
2219
+ }
2220
+ if (track.kind === 'bar') {
2221
+ const max = track.max ?? 1
2222
+ const values = skip(project(track, track.values ?? [], 0)).map(
2223
+ v => Math.min(1, Math.max(0, v / max)),
2224
+ )
2225
+ return [track.id, { values }] as const
2226
+ }
2227
+ const data = skip(
2228
+ project(track, (track.data ?? '').split(''), ' '),
2229
+ )
2230
+ return [track.id, { data: data.join('') }] as const
2231
+ }),
2232
+ )
2233
+ },
2234
+ /**
2235
+ * #method
2236
+ * the height a track draws at: what the user dragged its divider to,
2237
+ * then the height its snapshot asked for, then its kind's default. Only
2238
+ * a text track falls through to rowHeight
2239
+ */
2240
+ trackHeight(kind: TrackKind, heightKey: string = kind, given?: number) {
2241
+ return (
2242
+ self.trackHeights.get(heightKey) ??
2243
+ given ??
2244
+ defaultTrackHeights[kind] ??
2245
+ self.rowHeight
2246
+ )
2247
+ },
2248
+ /**
2249
+ * #method
2250
+ * the track a column track spec draws as, computed once per spec. A
2251
+ * text track's height falls through to rowHeight, and its own computed
2252
+ * confines the vertical zoom to it, so a sibling keeps its object and
2253
+ * its canvas skips the redraw
2254
+ */
2255
+ columnTrackModel(track: ColumnTrackSpec): BasicTrack {
2256
+ let computedModel = columnTrackModels.get(track)
2257
+ if (!computedModel) {
2258
+ computedModel = computed(() => {
2259
+ const heightKey = resizableKinds.has(track.kind)
2260
+ ? ownHeightKey(track.id)
2261
+ : undefined
2262
+ const content = this.columnTrackContent.get(track.id)
2263
+ return {
2264
+ model: {
2265
+ id: track.id,
2266
+ name: track.name,
2267
+ kind: track.kind,
2268
+ heightKey,
2269
+ height: this.trackHeight(track.kind, heightKey, track.height),
2270
+ barColor: track.color,
2271
+ arcColor: track.color,
2272
+ customColorScheme: track.colors,
2273
+ data: content?.data,
2274
+ arcs: content?.arcs,
2275
+ },
2276
+ ReactComponent: TrackBlocks,
2277
+ }
2278
+ })
2279
+ columnTrackModels.set(track, computedModel)
2280
+ }
2281
+ return computedModel.get()
2282
+ },
2283
+ /**
2284
+ * #getter
2285
+ */
2286
+ get columnTrackModels(): BasicTrack[] {
2287
+ return self.columnTracks.map(track => this.columnTrackModel(track))
2288
+ },
2289
+ }
2290
+ })
2097
2291
  .views(self => ({
2098
2292
  /**
2099
2293
  * #getter
@@ -2199,111 +2393,6 @@ function stateModelFactory() {
2199
2393
  }))
2200
2394
  },
2201
2395
 
2202
- /**
2203
- * #getter
2204
- * a data track's values or string, projected from its row's residues
2205
- * onto alignment columns when it names a row
2206
- */
2207
- get columnTrackContent() {
2208
- const { MSA, blanks, hideGapsEffective } = self
2209
- const width = MSA?.getWidth() ?? 0
2210
- const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
2211
- if (!track.row) {
2212
- return items
2213
- }
2214
- const out = Array.from({ length: width }, () => fill)
2215
- const index = self.seqPosIndex(track.row)
2216
- items.forEach((item, seqPos) => {
2217
- const col = index?.[seqPos]
2218
- if (col !== undefined) {
2219
- out[col] = item
2220
- }
2221
- })
2222
- return out
2223
- }
2224
- const skip = <T>(items: T[]) =>
2225
- hideGapsEffective ? dropBlanks(blanks, items) : items
2226
- // an arc endpoint maps row residue -> column -> visible column.
2227
- // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
2228
- // hidden column moves to the neighboring visible one and the arc stays
2229
- const resolve = (track: ColumnTrackSpec, pos: number) => {
2230
- const col = track.row
2231
- ? self.seqPosIndex(track.row)?.[pos - 1]
2232
- : pos - 1
2233
- if (col === undefined || col < 0 || col >= width) {
2234
- return undefined
2235
- }
2236
- return hideGapsEffective ? visibleColsBefore(blanks, col) : col
2237
- }
2238
- return new Map<
2239
- string,
2240
- { values?: number[]; data?: string; arcs?: Arc[] }
2241
- >(
2242
- self.columnTracks.map(track => {
2243
- if (track.kind === 'arc') {
2244
- const arcs = (track.arcs ?? [])
2245
- .map(arc => {
2246
- const start = resolve(track, Math.min(arc.start, arc.end))
2247
- const end = resolve(track, Math.max(arc.start, arc.end))
2248
- return start !== undefined && end !== undefined && start < end
2249
- ? { start, end, color: arc.color }
2250
- : undefined
2251
- })
2252
- .filter(notEmpty)
2253
- return [track.id, { arcs }] as const
2254
- }
2255
- if (track.kind === 'bar') {
2256
- const max = track.max ?? 1
2257
- const values = skip(project(track, track.values ?? [], 0)).map(
2258
- v => Math.min(1, Math.max(0, v / max)),
2259
- )
2260
- return [track.id, { values }] as const
2261
- }
2262
- const data = skip(project(track, (track.data ?? '').split(''), ' '))
2263
- return [track.id, { data: data.join('') }] as const
2264
- }),
2265
- )
2266
- },
2267
- /**
2268
- * #method
2269
- * the height a track draws at: what the user dragged its divider to,
2270
- * then the height its snapshot asked for, then its kind's default. Only
2271
- * a text track falls through to rowHeight, and `??` short-circuits
2272
- * before reading it, so vertical zoom does not rebuild the other tracks
2273
- */
2274
- trackHeight(kind: TrackKind, heightKey = kind as string, given?: number) {
2275
- return (
2276
- self.trackHeights.get(heightKey) ??
2277
- given ??
2278
- defaultTrackHeights[kind] ??
2279
- self.rowHeight
2280
- )
2281
- },
2282
- /**
2283
- * #getter
2284
- */
2285
- get columnTrackModels(): BasicTrack[] {
2286
- return self.columnTracks.map(track => {
2287
- const heightKey = resizableKinds.has(track.kind)
2288
- ? ownHeightKey(track.id)
2289
- : undefined
2290
- return {
2291
- model: {
2292
- id: track.id,
2293
- name: track.name,
2294
- kind: track.kind,
2295
- heightKey,
2296
- height: this.trackHeight(track.kind, heightKey, track.height),
2297
- barColor: track.color,
2298
- arcColor: track.color,
2299
- customColorScheme: track.colors,
2300
- data: this.columnTrackContent.get(track.id)?.data,
2301
- arcs: this.columnTrackContent.get(track.id)?.arcs,
2302
- },
2303
- ReactComponent: TrackBlocks,
2304
- }
2305
- })
2306
- },
2307
2396
  /**
2308
2397
  * #getter
2309
2398
  * the consensus secondary structure as a track, when there is one. A
@@ -2320,7 +2409,7 @@ function stateModelFactory() {
2320
2409
  name: 'Base pairs',
2321
2410
  kind: 'arc' as const,
2322
2411
  heightKey: 'arc',
2323
- height: this.trackHeight('arc'),
2412
+ height: self.trackHeight('arc'),
2324
2413
  arcs,
2325
2414
  },
2326
2415
  ReactComponent: TrackBlocks,
@@ -2369,7 +2458,7 @@ function stateModelFactory() {
2369
2458
  model: {
2370
2459
  ...model,
2371
2460
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2372
- height: this.trackHeight(model.kind),
2461
+ height: self.trackHeight(model.kind),
2373
2462
  },
2374
2463
  ReactComponent: TrackBlocks,
2375
2464
  }))
@@ -2379,7 +2468,7 @@ function stateModelFactory() {
2379
2468
  return [
2380
2469
  ...this.adapterTrackModels,
2381
2470
  ...this.basePairTrackModels,
2382
- ...this.columnTrackModels,
2471
+ ...self.columnTrackModels,
2383
2472
  // every computed track reads the alignment's columns, and a tree, a
2384
2473
  // GFF and a features panel draw a figure with none
2385
2474
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
@@ -3275,13 +3364,6 @@ function stateModelFactory() {
3275
3364
  )
3276
3365
  return { ...segments, ...categorical }
3277
3366
  },
3278
- get strokePalette() {
3279
- return transform(this.fillPalette, ([key, val]) => [
3280
- key,
3281
- outlineColor(val),
3282
- ])
3283
- },
3284
-
3285
3367
  /**
3286
3368
  * #getter
3287
3369
  * the encoding coloring the overlay's spans, undefined when none does,
@@ -3553,6 +3635,26 @@ function stateModelFactory() {
3553
3635
  return runs
3554
3636
  },
3555
3637
 
3638
+ /**
3639
+ * #method
3640
+ * a highlight label with `{residue}` and `{position}` filled in from the
3641
+ * row's letter at `start`, which is how the `175` shorthand draws "R175"
3642
+ */
3643
+ fillHighlightLabel(label: string, row?: string, start?: number) {
3644
+ if (!label.includes('{')) {
3645
+ return label
3646
+ }
3647
+ const col =
3648
+ row && start !== undefined
3649
+ ? self.seqPosIndex(row)?.[start - 1]
3650
+ : undefined
3651
+ const residue =
3652
+ row && col !== undefined ? (self.MSA?.getRow(row)[col] ?? '') : ''
3653
+ return label
3654
+ .replaceAll('{residue}', residue)
3655
+ .replaceAll('{position}', start === undefined ? '' : String(start))
3656
+ },
3657
+
3556
3658
  /**
3557
3659
  * #getter
3558
3660
  * `highlights` projected onto what is on screen: residue spans go
@@ -3568,7 +3670,10 @@ function stateModelFactory() {
3568
3670
  ...Object.values(transientHighlights).flat(),
3569
3671
  ]
3570
3672
  return all.flatMap(({ row, rows, start, end, label, color }) => {
3571
- const base = { label, color }
3673
+ const base = {
3674
+ label: label && this.fillHighlightLabel(label, row, start),
3675
+ color,
3676
+ }
3572
3677
  if (rows) {
3573
3678
  const rowIndices = rows
3574
3679
  .map(name => rowNamesSet.get(name))
@@ -3600,23 +3705,6 @@ function stateModelFactory() {
3600
3705
  })
3601
3706
  },
3602
3707
 
3603
- /**
3604
- * #getter
3605
- * `columnStatsAt` for the hovered column, undefined when nothing is
3606
- * hovered
3607
- */
3608
- get mouseOverColumnStats(): ColumnStats | undefined {
3609
- const { mouseCol, colStats, conservation, propertyConservation } = self
3610
- return mouseCol === undefined
3611
- ? undefined
3612
- : columnStats({
3613
- col: mouseCol,
3614
- colStats,
3615
- conservation,
3616
- propertyConservation,
3617
- })
3618
- },
3619
-
3620
3708
  /**
3621
3709
  * #method
3622
3710
  */
@@ -3683,7 +3771,6 @@ function stateModelFactory() {
3683
3771
  spans: featurePanelSpans({
3684
3772
  panel,
3685
3773
  width,
3686
- rowHeight: self.rowHeight,
3687
3774
  colWidth: self.colWidth,
3688
3775
  domainBands: this.domainBands,
3689
3776
  annotationsByRow: self.annotationsByRow,
@@ -4002,7 +4089,9 @@ function stateModelFactory() {
4002
4089
  // alignment all read. The tree arrives with the model for inline data
4003
4090
  // and later for a filehandle, so the seeding waits for it and then runs
4004
4091
  // once: expanding a seeded clade sticks, and the record collapses it
4005
- // again only on reload.
4092
+ // again only on reload. `dataInitialized` is true once the MSA alone
4093
+ // has loaded, when the tree is still the flat stub, so a tree
4094
+ // filehandle holds the seeding until its text lands.
4006
4095
  let cladesSeeded = false
4007
4096
  addDisposer(
4008
4097
  self,
@@ -4010,6 +4099,7 @@ function stateModelFactory() {
4010
4099
  if (
4011
4100
  cladesSeeded ||
4012
4101
  !self.dataInitialized ||
4102
+ (self.treeFilehandle && !self.data.tree) ||
4013
4103
  self.clades.length === 0
4014
4104
  ) {
4015
4105
  return
@@ -4028,6 +4118,25 @@ function stateModelFactory() {
4028
4118
  }),
4029
4119
  )
4030
4120
 
4121
+ // zoomToRegion needs a width and resolves residues through the visible
4122
+ // columns, so this waits for both, and for a tree file whose collapsed
4123
+ // clades would hide columns and move the target after the zoom
4124
+ addDisposer(
4125
+ self,
4126
+ autorun(() => {
4127
+ const { region } = self
4128
+ if (
4129
+ region &&
4130
+ self.viewInitialized &&
4131
+ self.numColumns > 0 &&
4132
+ !(self.treeFilehandle && !self.data.tree)
4133
+ ) {
4134
+ this.zoomToRegion(region)
4135
+ self.setRegion(undefined)
4136
+ }
4137
+ }),
4138
+ )
4139
+
4031
4140
  // the matchMedia query is pinned to the current device pixel ratio, so
4032
4141
  // each change re-registers against the new one
4033
4142
  if (
@@ -4193,6 +4302,24 @@ function stateModelFactory() {
4193
4302
  }),
4194
4303
  )
4195
4304
 
4305
+ // a GFF whose first column names rows of some other alignment parses
4306
+ // without error and draws nothing
4307
+ addDisposer(
4308
+ self,
4309
+ autorun(() => {
4310
+ const { annotations, rowNamesSet } = self
4311
+ if (
4312
+ annotations.length > 0 &&
4313
+ rowNamesSet.size > 0 &&
4314
+ !annotations.some(a => rowNamesSet.has(a.id))
4315
+ ) {
4316
+ self.addWarning(
4317
+ `0 of ${annotations.length} annotations name a row in this alignment`,
4318
+ )
4319
+ }
4320
+ }),
4321
+ )
4322
+
4196
4323
  // gffFilehandle loads into data.gff, which the autorun above parses
4197
4324
  loadOnFilehandleChange({
4198
4325
  what: 'annotations',
package/src/types.ts CHANGED
@@ -131,6 +131,8 @@ export interface RowFeaturesSpec {
131
131
  header?: string
132
132
  encoding?: FeatureEncodingSpec
133
133
  transform?: AlignTransform[]
134
+ /** how overlapping features share the row's height; see docs/layers.md */
135
+ position?: 'identity' | 'strandpile'
134
136
  }
135
137
 
136
138
  /**
@@ -147,10 +147,6 @@ export function useWheelScroll({
147
147
  }, [mouseDragging, onScrollX, onScrollY])
148
148
 
149
149
  function onMouseDown(event: React.MouseEvent) {
150
- const target = event.target as HTMLElement
151
- if (target.draggable || target.dataset.resizer) {
152
- return
153
- }
154
150
  if (event.button === 0) {
155
151
  prevX.current = event.clientX
156
152
  prevY.current = event.clientY
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '8.0.0'
1
+ export const version = '8.2.0'