react-msaview 6.0.0 → 6.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 (118) hide show
  1. package/bundle/index.js +98 -98
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +7 -0
  4. package/dist/columnCounts.js +18 -0
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/DragHandle.d.ts +25 -0
  7. package/dist/components/DragHandle.js +49 -0
  8. package/dist/components/DragHandle.js.map +1 -0
  9. package/dist/components/ResizeHandles.d.ts +4 -2
  10. package/dist/components/ResizeHandles.js +29 -32
  11. package/dist/components/ResizeHandles.js.map +1 -1
  12. package/dist/components/VerticalScrollbar.js +2 -2
  13. package/dist/components/VerticalScrollbar.js.map +1 -1
  14. package/dist/components/dialogs/InterProScanDialog.js +52 -129
  15. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  16. package/dist/components/header/settingsMenuItems.js +15 -0
  17. package/dist/components/header/settingsMenuItems.js.map +1 -1
  18. package/dist/components/minimap/Minimap.js +32 -4
  19. package/dist/components/minimap/Minimap.js.map +1 -1
  20. package/dist/components/msa/MSACanvas.js +8 -2
  21. package/dist/components/msa/MSACanvas.js.map +1 -1
  22. package/dist/components/msa/MSACanvasBlock.js +12 -3
  23. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  24. package/dist/components/msa/msaRaster.d.ts +63 -0
  25. package/dist/components/msa/msaRaster.js +252 -0
  26. package/dist/components/msa/msaRaster.js.map +1 -0
  27. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  28. package/dist/components/msa/renderMSABlock.js +13 -21
  29. package/dist/components/msa/renderMSABlock.js.map +1 -1
  30. package/dist/components/msa/tileColor.d.ts +6 -0
  31. package/dist/components/msa/tileColor.js +35 -0
  32. package/dist/components/msa/tileColor.js.map +1 -0
  33. package/dist/components/renderCtx.d.ts +1 -1
  34. package/dist/components/tracks/TrackBlocks.d.ts +14 -0
  35. package/dist/components/tracks/TrackBlocks.js +65 -0
  36. package/dist/components/tracks/TrackBlocks.js.map +1 -0
  37. package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
  38. package/dist/components/tracks/drawTracks.js +193 -0
  39. package/dist/components/tracks/drawTracks.js.map +1 -0
  40. package/dist/components/tree/TreeCanvas.js +8 -2
  41. package/dist/components/tree/TreeCanvas.js.map +1 -1
  42. package/dist/components/tree/TreeCanvasBlock.js +7 -4
  43. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  44. package/dist/components/tree/renderTreeCanvas.js +5 -2
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +3 -0
  47. package/dist/constants.js +8 -0
  48. package/dist/constants.js.map +1 -1
  49. package/dist/model/DataModel.d.ts +1 -4
  50. package/dist/model/DataModel.js +8 -10
  51. package/dist/model/DataModel.js.map +1 -1
  52. package/dist/model.d.ts +36 -7
  53. package/dist/model.js +113 -58
  54. package/dist/model.js.map +1 -1
  55. package/dist/neighborJoining.js +34 -18
  56. package/dist/neighborJoining.js.map +1 -1
  57. package/dist/renderTestEnv.d.ts +43 -0
  58. package/dist/renderTestEnv.js +88 -0
  59. package/dist/renderTestEnv.js.map +1 -0
  60. package/dist/renderToSvg.js +1 -1
  61. package/dist/renderToSvg.js.map +1 -1
  62. package/dist/sequenceLogo.d.ts +28 -0
  63. package/dist/sequenceLogo.js +47 -0
  64. package/dist/sequenceLogo.js.map +1 -0
  65. package/dist/types.d.ts +2 -0
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +3 -3
  69. package/src/columnCounts.test.ts +15 -0
  70. package/src/columnCounts.ts +19 -0
  71. package/src/components/DragHandle.tsx +81 -0
  72. package/src/components/ResizeHandles.tsx +46 -54
  73. package/src/components/VerticalScrollbar.tsx +3 -2
  74. package/src/components/dialogs/InterProScanDialog.tsx +54 -131
  75. package/src/components/header/settingsMenuItems.ts +18 -0
  76. package/src/components/minimap/Minimap.tsx +47 -4
  77. package/src/components/msa/MSACanvas.tsx +30 -10
  78. package/src/components/msa/MSACanvasBlock.tsx +13 -3
  79. package/src/components/msa/msaRaster.test.ts +145 -0
  80. package/src/components/msa/msaRaster.ts +350 -0
  81. package/src/components/msa/renderMSABlock.ts +15 -20
  82. package/src/components/msa/tileColor.ts +37 -0
  83. package/src/components/renderCtx.ts +2 -0
  84. package/src/components/tracks/TrackBlocks.tsx +111 -0
  85. package/src/components/tracks/drawTracks.ts +323 -0
  86. package/src/components/tree/TreeCanvas.tsx +16 -4
  87. package/src/components/tree/TreeCanvasBlock.tsx +18 -11
  88. package/src/components/tree/renderTreeCanvas.ts +5 -1
  89. package/src/constants.ts +10 -0
  90. package/src/impgRender.test.tsx +2 -57
  91. package/src/largeTree.test.ts +16 -2
  92. package/src/model/DataModel.ts +11 -11
  93. package/src/model.ts +128 -60
  94. package/src/neighborJoining.ts +36 -18
  95. package/src/renderDomainsSvg.test.tsx +2 -57
  96. package/src/renderMinimapSvg.test.tsx +2 -56
  97. package/src/renderSequenceLogoSvg.test.tsx +176 -0
  98. package/src/renderTestEnv.ts +96 -0
  99. package/src/renderToSvg.tsx +1 -1
  100. package/src/sequenceLogo.test.ts +88 -0
  101. package/src/sequenceLogo.ts +63 -0
  102. package/src/types.ts +7 -1
  103. package/src/version.ts +1 -1
  104. package/dist/components/ConservationTrack.d.ts +0 -8
  105. package/dist/components/ConservationTrack.js +0 -49
  106. package/dist/components/ConservationTrack.js.map +0 -1
  107. package/dist/components/ScrollbarThumb.d.ts +0 -14
  108. package/dist/components/ScrollbarThumb.js +0 -28
  109. package/dist/components/ScrollbarThumb.js.map +0 -1
  110. package/dist/components/TextTrack.d.ts +0 -8
  111. package/dist/components/TextTrack.js +0 -58
  112. package/dist/components/TextTrack.js.map +0 -1
  113. package/dist/components/tracks/renderTracksSvg.js +0 -126
  114. package/dist/components/tracks/renderTracksSvg.js.map +0 -1
  115. package/src/components/ConservationTrack.tsx +0 -99
  116. package/src/components/ScrollbarThumb.tsx +0 -48
  117. package/src/components/TextTrack.tsx +0 -103
  118. package/src/components/tracks/renderTracksSvg.ts +0 -247
package/src/model.ts CHANGED
@@ -18,8 +18,7 @@ import { calculateBlocks } from './calculateBlocks.ts'
18
18
  import { clustalXColumnColors } from './clustalX.ts'
19
19
  import colorSchemes from './colorSchemes.ts'
20
20
  import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
21
- import ConservationTrack from './components/ConservationTrack.tsx'
22
- import TextTrack from './components/TextTrack.tsx'
21
+ import TrackBlocks from './components/tracks/TrackBlocks.tsx'
23
22
  import {
24
23
  defaultAllowedGappyness,
25
24
  defaultColWidth,
@@ -35,8 +34,11 @@ import {
35
34
  defaultShowDomainLegend,
36
35
  defaultShowDomains,
37
36
  defaultSubFeatureRows,
37
+ labelReferenceFontSize,
38
38
  maxCellSize,
39
39
  minColWidth,
40
+ minLetterColWidth,
41
+ minLetterRowHeight,
40
42
  minRowHeight,
41
43
  segmentFeatureTypes,
42
44
  segmentShades,
@@ -75,6 +77,7 @@ import {
75
77
  visibleColsBefore,
76
78
  } from './rowCoordinateCalculations.ts'
77
79
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
80
+ import { maxBitsFor } from './sequenceLogo.ts'
78
81
  import { stripDefault } from './stripDefault.ts'
79
82
  import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
80
83
  import { saveAs } from './vendor/fileSaver.ts'
@@ -99,6 +102,22 @@ function parseTreeText(text: string) {
99
102
  return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
100
103
  }
101
104
 
105
+ // Tracks that start hidden. The sequence logo answers a narrower question than
106
+ // conservation does and costs three times the vertical space, so it waits to be
107
+ // asked for.
108
+ const defaultOffTracks = new Set(['sequence-logo'])
109
+
110
+ // `turnedOffTracks` records the user's explicit choices only: an id is absent
111
+ // until they touch that track, and then its value is whether the track is OFF.
112
+ // Reading the default through this is what lets a track ship hidden without
113
+ // writing an entry into every snapshot and shared URL.
114
+ function trackIsOff(
115
+ turnedOffTracks: { get: (id: string) => boolean | undefined },
116
+ id: string,
117
+ ) {
118
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
119
+ }
120
+
102
121
  /**
103
122
  * #stateModel MsaView
104
123
  *
@@ -249,7 +268,10 @@ function stateModelFactory() {
249
268
  showOnly: types.maybe(types.string),
250
269
  /**
251
270
  * #property
252
- * turned off tracks
271
+ * the user's explicit show/hide choice per track id, keyed by id with
272
+ * the value meaning "off". A track the user has never touched is absent
273
+ * and falls back to its own default (see `defaultOffTracks`), so a
274
+ * hidden-by-default track adds nothing to the shared URL.
253
275
  */
254
276
  turnedOffTracks: stripDefault(types.map(types.boolean), {}),
255
277
 
@@ -375,6 +397,14 @@ function stateModelFactory() {
375
397
  */
376
398
  conservationTrackHeight: 40,
377
399
 
400
+ /**
401
+ * #volatile
402
+ * taller than the conservation track by default: the logo spends its
403
+ * height on stacked glyphs, and a 40px stack of four residues leaves each
404
+ * one too short to identify
405
+ */
406
+ sequenceLogoTrackHeight: 80,
407
+
378
408
  /**
379
409
  * #volatile
380
410
  */
@@ -1036,27 +1066,25 @@ function stateModelFactory() {
1036
1066
  * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
1037
1067
  */
1038
1068
  get conservation() {
1039
- const { colStats, sequenceType } = this
1040
- const alphabetSize = sequenceType === 'amino' ? 20 : 4
1041
- const maxEntropy = Math.log2(alphabetSize)
1042
-
1069
+ const { colStats, alphabetMaxBits } = this
1043
1070
  return Array.from({ length: colStats.numColumns }, (_, col) => {
1044
1071
  const total = colStats.total(col)
1045
1072
  const gapCount = colStats.gapCount(col)
1046
- const nonGapTotal = total - gapCount
1047
- let score = 0
1048
- if (nonGapTotal > 0) {
1049
- let entropy = 0
1050
- colStats.forEachResidue(col, (_slot, count) => {
1051
- const freq = count / nonGapTotal
1052
- entropy -= freq * Math.log2(freq)
1053
- })
1054
- score =
1055
- Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total)
1056
- }
1057
- return score
1073
+ return total > gapCount
1074
+ ? Math.max(0, 1 - colStats.entropy(col) / alphabetMaxBits) *
1075
+ (1 - gapCount / total)
1076
+ : 0
1058
1077
  })
1059
1078
  },
1079
+ /**
1080
+ * #getter
1081
+ * The information content of a fully conserved column, in bits, which
1082
+ * depends on the alphabet. Both the entropy ceiling `conservation`
1083
+ * normalizes against and the y-axis ceiling of the sequence logo track.
1084
+ */
1085
+ get alphabetMaxBits() {
1086
+ return maxBitsFor(this.sequenceType)
1087
+ },
1060
1088
  /**
1061
1089
  * #getter
1062
1090
  * Per-column conservation of physicochemical property class (amino acids
@@ -1168,12 +1196,24 @@ function stateModelFactory() {
1168
1196
  */
1169
1197
  get blocksY() {
1170
1198
  return calculateBlocks({
1171
- viewportSize: self.height,
1199
+ viewportSize: this.visibleMsaHeight,
1172
1200
  viewportPos: -self.scrollY,
1173
1201
  blockSize: self.blockSize,
1174
1202
  mapSize: self.totalHeight,
1175
1203
  })
1176
1204
  },
1205
+ /**
1206
+ * #getter
1207
+ * height of the alignment viewport, px. The same subtraction as
1208
+ * msaAreaHeight, which is defined later in the views chain
1209
+ */
1210
+ get visibleMsaHeight() {
1211
+ return (
1212
+ self.height -
1213
+ self.headerHeight -
1214
+ (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
1215
+ )
1216
+ },
1177
1217
  }))
1178
1218
  .views(self => ({
1179
1219
  /**
@@ -1200,15 +1240,10 @@ function stateModelFactory() {
1200
1240
  /**
1201
1241
  * #getter
1202
1242
  * most-negative allowed scrollY, keeping the last row in view rather than
1203
- * letting the whole alignment scroll off the top. visible MSA height is
1204
- * computed inline here because msaAreaHeight is defined later in the chain.
1243
+ * letting the whole alignment scroll off the top.
1205
1244
  */
1206
1245
  get maxScrollY() {
1207
- const visibleHeight =
1208
- self.height -
1209
- self.headerHeight -
1210
- (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
1211
- return Math.min(-self.totalHeight + visibleHeight, 0)
1246
+ return Math.min(-self.totalHeight + self.visibleMsaHeight, 0)
1212
1247
  },
1213
1248
  /**
1214
1249
  * #getter
@@ -1216,7 +1251,8 @@ function stateModelFactory() {
1216
1251
  get showMsaLetters() {
1217
1252
  return (
1218
1253
  self.drawMsaLetters &&
1219
- self.rowHeight >= 5 &&
1254
+ self.rowHeight >= minLetterRowHeight &&
1255
+ self.colWidth >= minLetterColWidth &&
1220
1256
  self.colWidth > self.rowHeight / 2
1221
1257
  )
1222
1258
  },
@@ -1224,7 +1260,7 @@ function stateModelFactory() {
1224
1260
  * #getter
1225
1261
  */
1226
1262
  get showTreeText() {
1227
- return self.drawLabels && self.rowHeight >= 5
1263
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight
1228
1264
  },
1229
1265
  }))
1230
1266
  .actions(self => ({
@@ -1437,11 +1473,9 @@ function stateModelFactory() {
1437
1473
  * #action
1438
1474
  */
1439
1475
  toggleTrack(id: string) {
1440
- if (self.turnedOffTracks.has(id)) {
1441
- self.turnedOffTracks.delete(id)
1442
- } else {
1443
- self.turnedOffTracks.set(id, true)
1444
- }
1476
+ // the stored value is "is off", so the current shown state is exactly
1477
+ // what the flipped entry should hold
1478
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
1445
1479
  },
1446
1480
  /**
1447
1481
  * #action
@@ -1455,10 +1489,13 @@ function stateModelFactory() {
1455
1489
  * #getter
1456
1490
  */
1457
1491
  get labelWidthMap() {
1458
- const { showTreeText, leaves, treeMetadata, fontSize } = self
1492
+ const { showTreeText, leaves, treeMetadata } = self
1459
1493
  // gated on the same condition the renderer draws labels under, so the
1460
1494
  // gutter labelsWidth reserves and the labels actually drawn cannot
1461
- // disagree -- and so turning labels off hands their space to the tree
1495
+ // disagree -- and so turning labels off hands their space to the tree.
1496
+ // Measured at a fixed reference size and scaled by labelWidthScale:
1497
+ // re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
1498
+ // 50k-leaf tree
1462
1499
  return showTreeText
1463
1500
  ? new Map(
1464
1501
  leaves.map(node => {
@@ -1467,12 +1504,24 @@ function stateModelFactory() {
1467
1504
  // to the row name, and measuring '' would size the gutter (and
1468
1505
  // the label's click target) to nothing
1469
1506
  const displayName = treeMetadata[name]?.genome || name
1470
- return [name, measureTextCanvas(displayName, fontSize)] as const
1507
+ return [
1508
+ name,
1509
+ measureTextCanvas(displayName, labelReferenceFontSize),
1510
+ ] as const
1471
1511
  }),
1472
1512
  )
1473
1513
  : new Map<string, number>()
1474
1514
  },
1475
1515
 
1516
+ /**
1517
+ * #getter
1518
+ * factor turning a labelWidthMap entry into its width at the current
1519
+ * font size
1520
+ */
1521
+ get labelWidthScale() {
1522
+ return self.fontSize / labelReferenceFontSize
1523
+ },
1524
+
1476
1525
  get labelsWidth() {
1477
1526
  // a loop, not Math.max(...widths.values()): spreading a map of every
1478
1527
  // leaf passes one argument per row, and the argument limit is somewhere
@@ -1485,7 +1534,7 @@ function stateModelFactory() {
1485
1534
  max = width
1486
1535
  }
1487
1536
  }
1488
- return max
1537
+ return max * this.labelWidthScale
1489
1538
  },
1490
1539
 
1491
1540
  /**
@@ -1513,10 +1562,11 @@ function stateModelFactory() {
1513
1562
  .map(t => ({
1514
1563
  model: {
1515
1564
  ...t,
1565
+ kind: 'text' as const,
1516
1566
  data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
1517
1567
  height: rowHeight,
1518
1568
  },
1519
- ReactComponent: TextTrack,
1569
+ ReactComponent: TrackBlocks,
1520
1570
  }))
1521
1571
  },
1522
1572
 
@@ -1524,28 +1574,35 @@ function stateModelFactory() {
1524
1574
  * #getter
1525
1575
  */
1526
1576
  get tracks(): BasicTrack[] {
1527
- const conservationTrack: BasicTrack = {
1528
- model: {
1529
- id: 'conservation',
1530
- name: 'Conservation',
1531
- height: self.conservationTrackHeight,
1532
- barColor: 'gray',
1533
- },
1534
- ReactComponent: ConservationTrack,
1577
+ const conservationTrack = {
1578
+ id: 'conservation',
1579
+ name: 'Conservation',
1580
+ kind: 'bar' as const,
1581
+ height: self.conservationTrackHeight,
1582
+ barColor: 'gray',
1535
1583
  }
1536
- const propertyConservationTrack: BasicTrack = {
1537
- model: {
1538
- id: 'property-conservation',
1539
- name: 'Property conservation',
1540
- height: self.conservationTrackHeight,
1541
- barColor: '#6a51a3',
1542
- },
1543
- ReactComponent: ConservationTrack,
1584
+ const propertyConservationTrack = {
1585
+ id: 'property-conservation',
1586
+ name: 'Property conservation',
1587
+ kind: 'bar' as const,
1588
+ height: self.conservationTrackHeight,
1589
+ barColor: '#6a51a3',
1590
+ }
1591
+ const sequenceLogoTrack = {
1592
+ id: 'sequence-logo',
1593
+ name: 'Sequence logo',
1594
+ kind: 'logo' as const,
1595
+ height: self.sequenceLogoTrackHeight,
1544
1596
  }
1545
1597
  return [
1546
1598
  ...this.adapterTrackModels,
1547
- conservationTrack,
1548
- ...(self.sequenceType === 'amino' ? [propertyConservationTrack] : []),
1599
+ ...[
1600
+ conservationTrack,
1601
+ ...(self.sequenceType === 'amino'
1602
+ ? [propertyConservationTrack]
1603
+ : []),
1604
+ sequenceLogoTrack,
1605
+ ].map(model => ({ model, ReactComponent: TrackBlocks })),
1549
1606
  ]
1550
1607
  },
1551
1608
 
@@ -1553,7 +1610,9 @@ function stateModelFactory() {
1553
1610
  * #getter
1554
1611
  */
1555
1612
  get turnedOnTracks() {
1556
- return this.tracks.filter(f => !self.turnedOffTracks.has(f.model.id))
1613
+ return this.tracks.filter(
1614
+ f => !trackIsOff(self.turnedOffTracks, f.model.id),
1615
+ )
1557
1616
  },
1558
1617
 
1559
1618
  /**
@@ -1810,11 +1869,11 @@ function stateModelFactory() {
1810
1869
  * needs to keep residues readable on top of the boxes.
1811
1870
  */
1812
1871
  get domainBands() {
1872
+ const { blanks } = self
1813
1873
  const bands = new Map<string, DomainBand[]>()
1814
1874
  for (const [name, annotations] of Object.entries(
1815
1875
  self.annotationsByRow,
1816
1876
  )) {
1817
- const { blanks } = self
1818
1877
  const rowBands = annotations
1819
1878
  .toSorted((a, b) => len(b) - len(a))
1820
1879
  .map(annotation => {
@@ -1942,8 +2001,11 @@ function stateModelFactory() {
1942
2001
  return undefined
1943
2002
  }
1944
2003
  const { colStats } = self
2004
+ if (mouseCol >= colStats.numColumns) {
2005
+ return undefined
2006
+ }
1945
2007
  const total = colStats.total(mouseCol)
1946
- if (mouseCol >= colStats.numColumns || !total) {
2008
+ if (!total) {
1947
2009
  return undefined
1948
2010
  }
1949
2011
  const gaps = colStats.gapCount(mouseCol)
@@ -1988,6 +2050,12 @@ function stateModelFactory() {
1988
2050
  setConservationTrackHeight(arg: number) {
1989
2051
  self.conservationTrackHeight = arg
1990
2052
  },
2053
+ /**
2054
+ * #action
2055
+ */
2056
+ setSequenceLogoTrackHeight(arg: number) {
2057
+ self.sequenceLogoTrackHeight = arg
2058
+ },
1991
2059
  /**
1992
2060
  * #action
1993
2061
  */
@@ -31,25 +31,43 @@ const BLOSUM62_DATA = [
31
31
  [-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4, 1],
32
32
  ]
33
33
 
34
- const BLOSUM62 = new Map<string, Map<string, number>>()
35
- for (let i = 0; i < BLOSUM62_KEYS.length; i++) {
36
- const row = new Map<string, number>()
37
- for (let j = 0; j < BLOSUM62_KEYS.length; j++) {
38
- row.set(BLOSUM62_KEYS[j]!, BLOSUM62_DATA[i]![j]!)
39
- }
40
- BLOSUM62.set(BLOSUM62_KEYS[i]!, row)
34
+ const NUM_SYMBOLS = BLOSUM62_KEYS.length
35
+ const BLOSUM62 = Int8Array.from(BLOSUM62_DATA.flat())
36
+
37
+ // charCode -> row/column in BLOSUM62, -1 for anything the matrix does not name.
38
+ // A flat table rather than a Map of Maps keyed by an upper-cased character, for
39
+ // the reason columnCounts.ts gives: the distance matrix is O(rows^2 * columns)
40
+ // lookups, and on a 120x800 alignment the Map form spent nearly all of its time
41
+ // allocating one-character strings in toUpperCase: 2.6s to build a tree, versus
42
+ // 0.34s here for a byte-identical newick.
43
+ const symbolOfCode = new Int8Array(128).fill(-1)
44
+ for (let i = 0; i < NUM_SYMBOLS; i++) {
45
+ const key = BLOSUM62_KEYS[i]!
46
+ symbolOfCode[key.charCodeAt(0)] = i
47
+ symbolOfCode[key.toLowerCase().charCodeAt(0)] = i
41
48
  }
42
49
 
43
- function getBlosum62Score(a: string, b: string) {
44
- return BLOSUM62.get(a.toUpperCase())?.get(b.toUpperCase()) ?? -4
45
- }
50
+ // gap, or past the end of a row shorter than the alignment
51
+ const GAP = -2
46
52
 
47
53
  // stockholm and a3m write gaps as '.', not just '-'. Scoring a '.' as a residue
48
54
  // charges the pair the unknown-symbol penalty on every padded column, so two
49
55
  // sequences that differ only in where an unrelated row forced padding come out
50
56
  // as divergent.
51
- function isGap(c: string) {
52
- return c === '-' || c === '.'
57
+ function symbolAt(seq: string, i: number) {
58
+ if (i >= seq.length) {
59
+ return GAP
60
+ }
61
+ const code = seq.charCodeAt(i)
62
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
63
+ if ((code - 45) >>> 0 <= 1) {
64
+ return GAP
65
+ }
66
+ return code < 128 ? symbolOfCode[code]! : -1
67
+ }
68
+
69
+ function blosumScore(a: number, b: number) {
70
+ return a < 0 || b < 0 ? -4 : BLOSUM62[a * NUM_SYMBOLS + b]!
53
71
  }
54
72
 
55
73
  function computePairwiseDistance(seq1: string, seq2: string) {
@@ -63,11 +81,11 @@ function computePairwiseDistance(seq1: string, seq2: string) {
63
81
  let maxPossibleScore = 0
64
82
 
65
83
  for (let i = 0; i < len; i++) {
66
- const a = seq1[i] ?? '-'
67
- const b = seq2[i] ?? '-'
84
+ const a = symbolAt(seq1, i)
85
+ const b = symbolAt(seq2, i)
68
86
 
69
- const aGap = isGap(a)
70
- const bGap = isGap(b)
87
+ const aGap = a === GAP
88
+ const bGap = b === GAP
71
89
  if (aGap && bGap) {
72
90
  continue
73
91
  }
@@ -77,8 +95,8 @@ function computePairwiseDistance(seq1: string, seq2: string) {
77
95
  continue
78
96
  }
79
97
 
80
- totalScore += getBlosum62Score(a, b)
81
- maxPossibleScore += Math.max(getBlosum62Score(a, a), getBlosum62Score(b, b))
98
+ totalScore += blosumScore(a, b)
99
+ maxPossibleScore += Math.max(blosumScore(a, a), blosumScore(b, b))
82
100
  }
83
101
 
84
102
  if (compared === 0) {
@@ -9,67 +9,12 @@ import { enableStaticRendering } from 'mobx-react'
9
9
  import { beforeAll, expect, test } from 'vitest'
10
10
 
11
11
  import MSAModelF from './model.ts'
12
+ import { installRenderTestEnv } from './renderTestEnv.ts'
12
13
  import { renderToSvg } from './renderToSvg.tsx'
13
14
 
14
- class Mat {
15
- constructor(
16
- public a = 1,
17
- public b = 0,
18
- public c = 0,
19
- public d = 1,
20
- public e = 0,
21
- public f = 0,
22
- ) {}
23
- multiply(o: Mat) {
24
- return new Mat(
25
- this.a * o.a + this.c * o.b,
26
- this.b * o.a + this.d * o.b,
27
- this.a * o.c + this.c * o.d,
28
- this.b * o.c + this.d * o.d,
29
- this.a * o.e + this.c * o.f + this.e,
30
- this.b * o.e + this.d * o.f + this.f,
31
- )
32
- }
33
- translate(x: number, y = 0) {
34
- return this.multiply(new Mat(1, 0, 0, 1, x, y))
35
- }
36
- scale(x: number, y = x) {
37
- return this.multiply(new Mat(x, 0, 0, y, 0, 0))
38
- }
39
- }
40
- class Pt {
41
- constructor(
42
- public x = 0,
43
- public y = 0,
44
- ) {}
45
- matrixTransform(m: Mat) {
46
- return new Pt(
47
- m.a * this.x + m.c * this.y + m.e,
48
- m.b * this.x + m.d * this.y + m.f,
49
- )
50
- }
51
- }
52
-
53
15
  beforeAll(() => {
54
16
  enableStaticRendering(true)
55
- const g = globalThis as Record<string, unknown>
56
- g.DOMMatrix = Mat
57
- g.DOMPoint = Pt
58
- HTMLCanvasElement.prototype.getContext = function () {
59
- let font = '10px sans-serif'
60
- return {
61
- get font() {
62
- return font
63
- },
64
- set font(v: string) {
65
- font = v
66
- },
67
- measureText(t: string) {
68
- const size = Number.parseFloat(font) || 10
69
- return { width: t.length * size * 0.6 } as TextMetrics
70
- },
71
- } as unknown as CanvasRenderingContext2D
72
- } as unknown as typeof HTMLCanvasElement.prototype.getContext
17
+ installRenderTestEnv()
73
18
  })
74
19
 
75
20
  const msa = `>seq1
@@ -9,66 +9,12 @@ import { enableStaticRendering } from 'mobx-react'
9
9
  import { beforeAll, expect, test } from 'vitest'
10
10
 
11
11
  import MSAModelF from './model.ts'
12
+ import { installRenderTestEnv } from './renderTestEnv.ts'
12
13
  import { renderToSvg } from './renderToSvg.tsx'
13
14
 
14
- class Mat {
15
- constructor(
16
- public a = 1,
17
- public b = 0,
18
- public c = 0,
19
- public d = 1,
20
- public e = 0,
21
- public f = 0,
22
- ) {}
23
- multiply(o: Mat) {
24
- return new Mat(
25
- this.a * o.a + this.c * o.b,
26
- this.b * o.a + this.d * o.b,
27
- this.a * o.c + this.c * o.d,
28
- this.b * o.c + this.d * o.d,
29
- this.a * o.e + this.c * o.f + this.e,
30
- this.b * o.e + this.d * o.f + this.f,
31
- )
32
- }
33
- translate(x: number, y = 0) {
34
- return this.multiply(new Mat(1, 0, 0, 1, x, y))
35
- }
36
- scale(x: number, y = x) {
37
- return this.multiply(new Mat(x, 0, 0, y, 0, 0))
38
- }
39
- }
40
- class Pt {
41
- constructor(
42
- public x = 0,
43
- public y = 0,
44
- ) {}
45
- matrixTransform(m: Mat) {
46
- return new Pt(
47
- m.a * this.x + m.c * this.y + m.e,
48
- m.b * this.x + m.d * this.y + m.f,
49
- )
50
- }
51
- }
52
-
53
15
  beforeAll(() => {
54
16
  enableStaticRendering(true)
55
- const g = globalThis as Record<string, unknown>
56
- g.DOMMatrix = Mat
57
- g.DOMPoint = Pt
58
- HTMLCanvasElement.prototype.getContext = function () {
59
- let font = '10px sans-serif'
60
- return {
61
- get font() {
62
- return font
63
- },
64
- set font(v: string) {
65
- font = v
66
- },
67
- measureText: (t: string) => ({
68
- width: t.length * (Number.parseFloat(font) || 10) * 0.6,
69
- }),
70
- } as unknown as CanvasRenderingContext2D
71
- } as unknown as typeof HTMLCanvasElement.prototype.getContext
17
+ installRenderTestEnv()
72
18
  })
73
19
 
74
20
  const width = 1000