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
@@ -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
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
@@ -1064,10 +1108,12 @@ function stateModelFactory() {
1064
1108
  /**
1065
1109
  * #action
1066
1110
  * record a non-fatal load problem: a layer that failed to load, a file
1067
- * that failed to parse
1111
+ * that failed to parse. A message already on the list is not added again
1068
1112
  */
1069
1113
  addWarning(warning: string) {
1070
- self.warnings = [...self.warnings, warning]
1114
+ if (!self.warnings.includes(warning)) {
1115
+ self.warnings = [...self.warnings, warning]
1116
+ }
1071
1117
  },
1072
1118
 
1073
1119
  /**
@@ -1776,7 +1822,7 @@ function stateModelFactory() {
1776
1822
  */
1777
1823
  get insertionPositions() {
1778
1824
  const { blanks, rows } = this
1779
- if (blanks.length === 0 || !self.hideGapsEffective) {
1825
+ if (blanks.length === 0) {
1780
1826
  return new Map<string, { pos: number; letters: string }[]>()
1781
1827
  }
1782
1828
  const result = new Map<string, { pos: number; letters: string }[]>()
@@ -2039,7 +2085,7 @@ function stateModelFactory() {
2039
2085
  * cladogram mode. The tree's scale bar uses it.
2040
2086
  */
2041
2087
  get pxPerBranchLength() {
2042
- const max = maxLength(this.root)
2088
+ const max = this.rootToTipLength
2043
2089
  return this.showBranchLenEffective && max ? self.treeWidth / max : 0
2044
2090
  },
2045
2091
 
@@ -2094,6 +2140,140 @@ function stateModelFactory() {
2094
2140
  return self.drawLabels && self.rowHeight >= minLetterRowHeight
2095
2141
  },
2096
2142
  }))
2143
+ .views(self => {
2144
+ // a spec is a frozen value, so it keys its computed until setColumnTracks
2145
+ // replaces it
2146
+ const columnTrackModels = new WeakMap<
2147
+ ColumnTrackSpec,
2148
+ IComputedValue<BasicTrack>
2149
+ >()
2150
+ return {
2151
+ /**
2152
+ * #getter
2153
+ * a data track's values or string, projected from its row's residues
2154
+ * onto alignment columns when it names a row
2155
+ */
2156
+ get columnTrackContent() {
2157
+ const { MSA, blanks, hideGapsEffective } = self
2158
+ const width = MSA?.getWidth() ?? 0
2159
+ const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
2160
+ if (!track.row) {
2161
+ return items
2162
+ }
2163
+ const out = Array.from({ length: width }, () => fill)
2164
+ const index = self.seqPosIndex(track.row)
2165
+ items.forEach((item, seqPos) => {
2166
+ const col = index?.[seqPos]
2167
+ if (col !== undefined) {
2168
+ out[col] = item
2169
+ }
2170
+ })
2171
+ return out
2172
+ }
2173
+ const skip = <T>(items: T[]) =>
2174
+ hideGapsEffective ? dropBlanks(blanks, items) : items
2175
+ // an arc endpoint maps row residue -> column -> visible column.
2176
+ // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
2177
+ // hidden column moves to the neighboring visible one and the arc stays
2178
+ const resolve = (track: ColumnTrackSpec, pos: number) => {
2179
+ const col = track.row
2180
+ ? self.seqPosIndex(track.row)?.[pos - 1]
2181
+ : pos - 1
2182
+ if (col === undefined || col < 0 || col >= width) {
2183
+ return undefined
2184
+ }
2185
+ return hideGapsEffective ? visibleColsBefore(blanks, col) : col
2186
+ }
2187
+ return new Map<
2188
+ string,
2189
+ { values?: number[]; data?: string; arcs?: Arc[] }
2190
+ >(
2191
+ self.columnTracks.map(track => {
2192
+ if (track.kind === 'arc') {
2193
+ const arcs = (track.arcs ?? [])
2194
+ .map(arc => {
2195
+ const start = resolve(track, Math.min(arc.start, arc.end))
2196
+ const end = resolve(track, Math.max(arc.start, arc.end))
2197
+ return start !== undefined &&
2198
+ end !== undefined &&
2199
+ start < end
2200
+ ? { start, end, color: arc.color }
2201
+ : undefined
2202
+ })
2203
+ .filter(notEmpty)
2204
+ return [track.id, { arcs }] as const
2205
+ }
2206
+ if (track.kind === 'bar') {
2207
+ const max = track.max ?? 1
2208
+ const values = skip(project(track, track.values ?? [], 0)).map(
2209
+ v => Math.min(1, Math.max(0, v / max)),
2210
+ )
2211
+ return [track.id, { values }] as const
2212
+ }
2213
+ const data = skip(
2214
+ project(track, (track.data ?? '').split(''), ' '),
2215
+ )
2216
+ return [track.id, { data: data.join('') }] as const
2217
+ }),
2218
+ )
2219
+ },
2220
+ /**
2221
+ * #method
2222
+ * the height a track draws at: what the user dragged its divider to,
2223
+ * then the height its snapshot asked for, then its kind's default. Only
2224
+ * a text track falls through to rowHeight
2225
+ */
2226
+ trackHeight(kind: TrackKind, heightKey: string = kind, given?: number) {
2227
+ return (
2228
+ self.trackHeights.get(heightKey) ??
2229
+ given ??
2230
+ defaultTrackHeights[kind] ??
2231
+ self.rowHeight
2232
+ )
2233
+ },
2234
+ /**
2235
+ * #method
2236
+ * the track a column track spec draws as, computed once per spec. A
2237
+ * text track's height falls through to rowHeight, and its own computed
2238
+ * confines the vertical zoom to it, so a sibling keeps its object and
2239
+ * its canvas skips the redraw
2240
+ */
2241
+ columnTrackModel(track: ColumnTrackSpec): BasicTrack {
2242
+ let computedModel = columnTrackModels.get(track)
2243
+ if (!computedModel) {
2244
+ computedModel = computed(() => {
2245
+ const heightKey = resizableKinds.has(track.kind)
2246
+ ? ownHeightKey(track.id)
2247
+ : undefined
2248
+ const content = this.columnTrackContent.get(track.id)
2249
+ return {
2250
+ model: {
2251
+ id: track.id,
2252
+ name: track.name,
2253
+ kind: track.kind,
2254
+ heightKey,
2255
+ height: this.trackHeight(track.kind, heightKey, track.height),
2256
+ barColor: track.color,
2257
+ arcColor: track.color,
2258
+ customColorScheme: track.colors,
2259
+ data: content?.data,
2260
+ arcs: content?.arcs,
2261
+ },
2262
+ ReactComponent: TrackBlocks,
2263
+ }
2264
+ })
2265
+ columnTrackModels.set(track, computedModel)
2266
+ }
2267
+ return computedModel.get()
2268
+ },
2269
+ /**
2270
+ * #getter
2271
+ */
2272
+ get columnTrackModels(): BasicTrack[] {
2273
+ return self.columnTracks.map(track => this.columnTrackModel(track))
2274
+ },
2275
+ }
2276
+ })
2097
2277
  .views(self => ({
2098
2278
  /**
2099
2279
  * #getter
@@ -2199,111 +2379,6 @@ function stateModelFactory() {
2199
2379
  }))
2200
2380
  },
2201
2381
 
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
2382
  /**
2308
2383
  * #getter
2309
2384
  * the consensus secondary structure as a track, when there is one. A
@@ -2320,7 +2395,7 @@ function stateModelFactory() {
2320
2395
  name: 'Base pairs',
2321
2396
  kind: 'arc' as const,
2322
2397
  heightKey: 'arc',
2323
- height: this.trackHeight('arc'),
2398
+ height: self.trackHeight('arc'),
2324
2399
  arcs,
2325
2400
  },
2326
2401
  ReactComponent: TrackBlocks,
@@ -2369,7 +2444,7 @@ function stateModelFactory() {
2369
2444
  model: {
2370
2445
  ...model,
2371
2446
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2372
- height: this.trackHeight(model.kind),
2447
+ height: self.trackHeight(model.kind),
2373
2448
  },
2374
2449
  ReactComponent: TrackBlocks,
2375
2450
  }))
@@ -2379,7 +2454,7 @@ function stateModelFactory() {
2379
2454
  return [
2380
2455
  ...this.adapterTrackModels,
2381
2456
  ...this.basePairTrackModels,
2382
- ...this.columnTrackModels,
2457
+ ...self.columnTrackModels,
2383
2458
  // every computed track reads the alignment's columns, and a tree, a
2384
2459
  // GFF and a features panel draw a figure with none
2385
2460
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
@@ -3275,13 +3350,6 @@ function stateModelFactory() {
3275
3350
  )
3276
3351
  return { ...segments, ...categorical }
3277
3352
  },
3278
- get strokePalette() {
3279
- return transform(this.fillPalette, ([key, val]) => [
3280
- key,
3281
- outlineColor(val),
3282
- ])
3283
- },
3284
-
3285
3353
  /**
3286
3354
  * #getter
3287
3355
  * the encoding coloring the overlay's spans, undefined when none does,
@@ -3600,23 +3668,6 @@ function stateModelFactory() {
3600
3668
  })
3601
3669
  },
3602
3670
 
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
3671
  /**
3621
3672
  * #method
3622
3673
  */
@@ -3683,7 +3734,6 @@ function stateModelFactory() {
3683
3734
  spans: featurePanelSpans({
3684
3735
  panel,
3685
3736
  width,
3686
- rowHeight: self.rowHeight,
3687
3737
  colWidth: self.colWidth,
3688
3738
  domainBands: this.domainBands,
3689
3739
  annotationsByRow: self.annotationsByRow,
@@ -4002,7 +4052,9 @@ function stateModelFactory() {
4002
4052
  // alignment all read. The tree arrives with the model for inline data
4003
4053
  // and later for a filehandle, so the seeding waits for it and then runs
4004
4054
  // once: expanding a seeded clade sticks, and the record collapses it
4005
- // again only on reload.
4055
+ // again only on reload. `dataInitialized` is true once the MSA alone
4056
+ // has loaded, when the tree is still the flat stub, so a tree
4057
+ // filehandle holds the seeding until its text lands.
4006
4058
  let cladesSeeded = false
4007
4059
  addDisposer(
4008
4060
  self,
@@ -4010,6 +4062,7 @@ function stateModelFactory() {
4010
4062
  if (
4011
4063
  cladesSeeded ||
4012
4064
  !self.dataInitialized ||
4065
+ (self.treeFilehandle && !self.data.tree) ||
4013
4066
  self.clades.length === 0
4014
4067
  ) {
4015
4068
  return
@@ -4193,6 +4246,24 @@ function stateModelFactory() {
4193
4246
  }),
4194
4247
  )
4195
4248
 
4249
+ // a GFF whose first column names rows of some other alignment parses
4250
+ // without error and draws nothing
4251
+ addDisposer(
4252
+ self,
4253
+ autorun(() => {
4254
+ const { annotations, rowNamesSet } = self
4255
+ if (
4256
+ annotations.length > 0 &&
4257
+ rowNamesSet.size > 0 &&
4258
+ !annotations.some(a => rowNamesSet.has(a.id))
4259
+ ) {
4260
+ self.addWarning(
4261
+ `0 of ${annotations.length} annotations name a row in this alignment`,
4262
+ )
4263
+ }
4264
+ }),
4265
+ )
4266
+
4196
4267
  // gffFilehandle loads into data.gff, which the autorun above parses
4197
4268
  loadOnFilehandleChange({
4198
4269
  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.1.0'